quantnodes 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. QuantNodes/__init__.py +15 -0
  2. QuantNodes/__main__.py +14 -0
  3. QuantNodes/agent/__init__.py +158 -0
  4. QuantNodes/agent/agents/__init__.py +13 -0
  5. QuantNodes/agent/agents/definition.py +180 -0
  6. QuantNodes/agent/agents/manager.py +73 -0
  7. QuantNodes/agent/config/__init__.py +34 -0
  8. QuantNodes/agent/config/executor.py +958 -0
  9. QuantNodes/agent/config/loader.py +427 -0
  10. QuantNodes/agent/config/templates/bollinger_bands.yaml +84 -0
  11. QuantNodes/agent/config/templates/dual_ma.yaml +72 -0
  12. QuantNodes/agent/config/templates/empty.yaml +56 -0
  13. QuantNodes/agent/config/templates/mean_reversion.yaml +47 -0
  14. QuantNodes/agent/config/templates/mean_reversion_zscore.yaml +90 -0
  15. QuantNodes/agent/config/templates/momentum.yaml +81 -0
  16. QuantNodes/agent/config/templates/momentum_breakout.yaml +84 -0
  17. QuantNodes/agent/config/templates/rsi_strategy.yaml +72 -0
  18. QuantNodes/agent/config/templates/volume_price.yaml +86 -0
  19. QuantNodes/agent/config/types.py +156 -0
  20. QuantNodes/agent/config_mapper.py +293 -0
  21. QuantNodes/agent/core/__init__.py +19 -0
  22. QuantNodes/agent/core/dream.py +47 -0
  23. QuantNodes/agent/core/quant_dream.py +274 -0
  24. QuantNodes/agent/cron_jobs.py +314 -0
  25. QuantNodes/agent/nanobot_bridge.py +242 -0
  26. QuantNodes/agent/permission/__init__.py +30 -0
  27. QuantNodes/agent/permission/defaults.py +36 -0
  28. QuantNodes/agent/permission/evaluate.py +41 -0
  29. QuantNodes/agent/permission/models.py +59 -0
  30. QuantNodes/agent/permission/service.py +133 -0
  31. QuantNodes/agent/providers/__init__.py +11 -0
  32. QuantNodes/agent/providers/base.py +102 -0
  33. QuantNodes/agent/providers/quantnodes.py +610 -0
  34. QuantNodes/agent/providers/rate_limiter.py +326 -0
  35. QuantNodes/agent/providers/registry.py +163 -0
  36. QuantNodes/agent/skills/__init__.py +20 -0
  37. QuantNodes/agent/skills/base.py +118 -0
  38. QuantNodes/agent/skills/bridge.py +73 -0
  39. QuantNodes/agent/skills/factor/__init__.py +14 -0
  40. QuantNodes/agent/skills/factor/correlation.py +99 -0
  41. QuantNodes/agent/skills/factor/group_backtest.py +114 -0
  42. QuantNodes/agent/skills/factor/ic_analysis.py +106 -0
  43. QuantNodes/agent/skills/loader.py +107 -0
  44. QuantNodes/agent/skills/registry.py +105 -0
  45. QuantNodes/agent/skills/strategy/__init__.py +16 -0
  46. QuantNodes/agent/skills/strategy/bollinger.py +86 -0
  47. QuantNodes/agent/skills/strategy/dual_ma.py +82 -0
  48. QuantNodes/agent/skills/strategy/momentum.py +74 -0
  49. QuantNodes/agent/skills/strategy/rsi_reversal.py +99 -0
  50. QuantNodes/agent/skills_quant/__init__.py +14 -0
  51. QuantNodes/agent/skills_quant/backtest-analyze/SKILL.md +42 -0
  52. QuantNodes/agent/skills_quant/config-driven/SKILL.md +72 -0
  53. QuantNodes/agent/skills_quant/factor-research/SKILL.md +40 -0
  54. QuantNodes/agent/skills_quant/quant-dream/SKILL.md +55 -0
  55. QuantNodes/agent/skills_quant/risk-management/SKILL.md +45 -0
  56. QuantNodes/agent/skills_quant/strategy-design/SKILL.md +43 -0
  57. QuantNodes/agent/templates/__init__.py +4 -0
  58. QuantNodes/agent/tools/__init__.py +173 -0
  59. QuantNodes/agent/tools/_workspace.py +51 -0
  60. QuantNodes/agent/tools/alpha_backtest.py +328 -0
  61. QuantNodes/agent/tools/alpha_evaluate.py +493 -0
  62. QuantNodes/agent/tools/backtest.py +226 -0
  63. QuantNodes/agent/tools/base.py +133 -0
  64. QuantNodes/agent/tools/code_search.py +207 -0
  65. QuantNodes/agent/tools/config_backtest.py +401 -0
  66. QuantNodes/agent/tools/context.py +97 -0
  67. QuantNodes/agent/tools/dream_skill.py +77 -0
  68. QuantNodes/agent/tools/echo.py +38 -0
  69. QuantNodes/agent/tools/factor.py +231 -0
  70. QuantNodes/agent/tools/file_ops.py +201 -0
  71. QuantNodes/agent/tools/git_ops.py +190 -0
  72. QuantNodes/agent/tools/operator_lookup.py +218 -0
  73. QuantNodes/agent/tools/output_truncation.py +77 -0
  74. QuantNodes/agent/tools/path_check.py +43 -0
  75. QuantNodes/agent/tools/pipeline.py +62 -0
  76. QuantNodes/agent/tools/registry.py +150 -0
  77. QuantNodes/agent/tools/sandbox.py +62 -0
  78. QuantNodes/agent/tools/shell_safety.py +63 -0
  79. QuantNodes/agent/tools/strategy.py +106 -0
  80. QuantNodes/agent/tools/task.py +171 -0
  81. QuantNodes/agent/tools/web_fetch.py +142 -0
  82. QuantNodes/agent/tools/web_search.py +114 -0
  83. QuantNodes/agent/tools/wiki.py +370 -0
  84. QuantNodes/agent/utils/__init__.py +11 -0
  85. QuantNodes/agent/utils/helpers.py +43 -0
  86. QuantNodes/agent/utils/prompt_templates.py +30 -0
  87. QuantNodes/agent/workflows/__init__.py +20 -0
  88. QuantNodes/agent/workflows/implementations/__init__.py +8 -0
  89. QuantNodes/agent/workflows/implementations/alpha_gpt.py +508 -0
  90. QuantNodes/agent/workflows/implementations/mcts.py +442 -0
  91. QuantNodes/agent/workflows/parsers.py +44 -0
  92. QuantNodes/agent/workflows/registry.py +119 -0
  93. QuantNodes/agent/workflows/step_agent.py +219 -0
  94. QuantNodes/agent/workflows/tool.py +198 -0
  95. QuantNodes/ai/__init__.py +93 -0
  96. QuantNodes/ai/llm/__init__.py +75 -0
  97. QuantNodes/ai/llm/base.py +233 -0
  98. QuantNodes/ai/llm/decorators.py +281 -0
  99. QuantNodes/ai/llm/gateway.py +571 -0
  100. QuantNodes/ai/llm/null.py +76 -0
  101. QuantNodes/ai/llm/openai.py +435 -0
  102. QuantNodes/ai/optimizer.py +405 -0
  103. QuantNodes/ai/prompts/__init__.py +229 -0
  104. QuantNodes/ai/sandbox.py +371 -0
  105. QuantNodes/ai/sandbox_pandas_bridge.py +150 -0
  106. QuantNodes/ai/strategy_gen.py +396 -0
  107. QuantNodes/backtest/__init__.py +64 -0
  108. QuantNodes/backtest/backtest_node.py +188 -0
  109. QuantNodes/backtest/broker_node.py +378 -0
  110. QuantNodes/backtest/config_runner.py +397 -0
  111. QuantNodes/backtest/config_strategy.py +64 -0
  112. QuantNodes/backtest/risk_node.py +360 -0
  113. QuantNodes/backtest/strategy_node.py +268 -0
  114. QuantNodes/cache_node/__init__.py +19 -0
  115. QuantNodes/cache_node/base.py +244 -0
  116. QuantNodes/cache_node/cache_store.py +99 -0
  117. QuantNodes/cache_node/metadata.py +100 -0
  118. QuantNodes/cli/__init__.py +109 -0
  119. QuantNodes/cli/_helpers.py +511 -0
  120. QuantNodes/cli/command.py +110 -0
  121. QuantNodes/cli/commands/__init__.py +69 -0
  122. QuantNodes/cli/commands/agent.py +158 -0
  123. QuantNodes/cli/commands/alpha.py +951 -0
  124. QuantNodes/cli/commands/chat.py +38 -0
  125. QuantNodes/cli/commands/evolve.py +120 -0
  126. QuantNodes/cli/commands/factor.py +569 -0
  127. QuantNodes/cli/commands/init.py +190 -0
  128. QuantNodes/cli/commands/run.py +259 -0
  129. QuantNodes/cli/commands/serve.py +398 -0
  130. QuantNodes/cli/commands/version.py +120 -0
  131. QuantNodes/cli/enhanced.py +146 -0
  132. QuantNodes/conf_node/__init__.py +37 -0
  133. QuantNodes/conf_node/base.py +120 -0
  134. QuantNodes/conf_node/env_config.py +132 -0
  135. QuantNodes/conf_node/ini_config.py +70 -0
  136. QuantNodes/conf_node/json_config.py +69 -0
  137. QuantNodes/conf_node/yaml_config.py +78 -0
  138. QuantNodes/constants.py +17 -0
  139. QuantNodes/core/__init__.py +196 -0
  140. QuantNodes/core/_lookback_helpers.py +49 -0
  141. QuantNodes/core/ast_parser.py +198 -0
  142. QuantNodes/core/base.py +61 -0
  143. QuantNodes/core/cache_manager.py +344 -0
  144. QuantNodes/core/cache_utils.py +150 -0
  145. QuantNodes/core/cond_builder.py +53 -0
  146. QuantNodes/core/config.py +170 -0
  147. QuantNodes/core/constants.py +48 -0
  148. QuantNodes/core/control.py +412 -0
  149. QuantNodes/core/data_preprocessing.py +453 -0
  150. QuantNodes/core/data_source.py +46 -0
  151. QuantNodes/core/events.py +178 -0
  152. QuantNodes/core/evolution/__init__.py +22 -0
  153. QuantNodes/core/evolution/loop.py +583 -0
  154. QuantNodes/core/evolution/operators.py +289 -0
  155. QuantNodes/core/evolution/settings.py +44 -0
  156. QuantNodes/core/expression.py +841 -0
  157. QuantNodes/core/feedback/__init__.py +38 -0
  158. QuantNodes/core/feedback/channels.py +182 -0
  159. QuantNodes/core/feedback/collector.py +91 -0
  160. QuantNodes/core/feedback/dataclass.py +239 -0
  161. QuantNodes/core/feedback/llm_judge.py +138 -0
  162. QuantNodes/core/knowledge/__init__.py +69 -0
  163. QuantNodes/core/knowledge/knowledge_base.py +217 -0
  164. QuantNodes/core/knowledge/lineage_compress.py +196 -0
  165. QuantNodes/core/knowledge/lineage_expand.py +123 -0
  166. QuantNodes/core/knowledge/metrics/__init__.py +43 -0
  167. QuantNodes/core/knowledge/metrics/evaluator.py +176 -0
  168. QuantNodes/core/knowledge/metrics/metrics.py +220 -0
  169. QuantNodes/core/knowledge/rag_prompt.py +196 -0
  170. QuantNodes/core/knowledge/retriever.py +209 -0
  171. QuantNodes/core/lambda_node.py +81 -0
  172. QuantNodes/core/monitoring/__init__.py +22 -0
  173. QuantNodes/core/monitoring/collector.py +292 -0
  174. QuantNodes/core/monitoring/dashboard.py +365 -0
  175. QuantNodes/core/node.py +375 -0
  176. QuantNodes/core/pandas_utils.py +504 -0
  177. QuantNodes/core/parallel/__init__.py +15 -0
  178. QuantNodes/core/parallel/worker.py +140 -0
  179. QuantNodes/core/parallel/worker_process.py +265 -0
  180. QuantNodes/core/path_utils.py +73 -0
  181. QuantNodes/core/pipeline.py +328 -0
  182. QuantNodes/core/plugin.py +135 -0
  183. QuantNodes/core/quality_gate/__init__.py +32 -0
  184. QuantNodes/core/quality_gate/complexity.py +94 -0
  185. QuantNodes/core/quality_gate/consistency.py +26 -0
  186. QuantNodes/core/quality_gate/node.py +97 -0
  187. QuantNodes/core/quality_gate/redundancy.py +51 -0
  188. QuantNodes/core/quality_gate/settings.py +43 -0
  189. QuantNodes/core/quality_gate/zoo.py +98 -0
  190. QuantNodes/core/serializable.py +116 -0
  191. QuantNodes/core/serialization.py +673 -0
  192. QuantNodes/core/tools.py +333 -0
  193. QuantNodes/core/trajectory/__init__.py +25 -0
  194. QuantNodes/core/trajectory/entry.py +116 -0
  195. QuantNodes/core/trajectory/lineage.py +67 -0
  196. QuantNodes/core/trajectory/pool.py +211 -0
  197. QuantNodes/core/trajectory/selector.py +140 -0
  198. QuantNodes/core/visualization/__init__.py +33 -0
  199. QuantNodes/core/visualization/builder.py +233 -0
  200. QuantNodes/core/visualization/gate_breakdown.py +140 -0
  201. QuantNodes/core/visualization/lineage_dag.py +203 -0
  202. QuantNodes/core/visualization/metric_distribution.py +125 -0
  203. QuantNodes/core/visualization/report.py +68 -0
  204. QuantNodes/database_node/__init__.py +69 -0
  205. QuantNodes/database_node/base.py +135 -0
  206. QuantNodes/database_node/clickhouse_node.py +272 -0
  207. QuantNodes/database_node/csv_node.py +83 -0
  208. QuantNodes/database_node/duckdb_node.py +86 -0
  209. QuantNodes/database_node/factory.py +83 -0
  210. QuantNodes/database_node/mysql_node.py +100 -0
  211. QuantNodes/database_node/parquet_node.py +75 -0
  212. QuantNodes/database_node/sqlite_node.py +67 -0
  213. QuantNodes/factor_node/__init__.py +50 -0
  214. QuantNodes/factor_node/factor.py +563 -0
  215. QuantNodes/factor_node/factor_db.py +421 -0
  216. QuantNodes/factor_node/factor_functions/__init__.py +252 -0
  217. QuantNodes/factor_node/factor_functions/_helpers.py +358 -0
  218. QuantNodes/factor_node/factor_functions/_helpers_debug.py +317 -0
  219. QuantNodes/factor_node/factor_functions/composite_ops.py +136 -0
  220. QuantNodes/factor_node/factor_functions/math_ops.py +433 -0
  221. QuantNodes/factor_node/factor_functions/section_ops.py +290 -0
  222. QuantNodes/factor_node/factor_functions/talib_ops.py +1293 -0
  223. QuantNodes/factor_node/factor_functions/time_ops.py +535 -0
  224. QuantNodes/factor_node/factor_operation.py +1115 -0
  225. QuantNodes/factor_node/factor_table.py +1073 -0
  226. QuantNodes/factor_node/quant_nodes_object.py +60 -0
  227. QuantNodes/mcp_server/__init__.py +27 -0
  228. QuantNodes/mcp_server/__main__.py +4 -0
  229. QuantNodes/mcp_server/server.py +272 -0
  230. QuantNodes/methods/__init__.py +28 -0
  231. QuantNodes/methods/pipeline.py +100 -0
  232. QuantNodes/methods/sandbox.py +102 -0
  233. QuantNodes/monitor/__init__.py +27 -0
  234. QuantNodes/monitor/agent_tools/__init__.py +5 -0
  235. QuantNodes/monitor/agent_tools/monitor_tool.py +98 -0
  236. QuantNodes/monitor/agent_tools/schedule_tool.py +98 -0
  237. QuantNodes/monitor/agent_tools/version_tool.py +133 -0
  238. QuantNodes/monitor/monitor/__init__.py +6 -0
  239. QuantNodes/monitor/monitor/alerter.py +60 -0
  240. QuantNodes/monitor/monitor/collector.py +164 -0
  241. QuantNodes/monitor/monitor/dashboard.py +115 -0
  242. QuantNodes/monitor/monitor/drift.py +190 -0
  243. QuantNodes/monitor/scheduler/__init__.py +4 -0
  244. QuantNodes/monitor/scheduler/runner.py +133 -0
  245. QuantNodes/monitor/scheduler/scheduler.py +184 -0
  246. QuantNodes/monitor/storage/__init__.py +16 -0
  247. QuantNodes/monitor/storage/models.py +70 -0
  248. QuantNodes/monitor/storage/repository.py +407 -0
  249. QuantNodes/monitor/version/__init__.py +4 -0
  250. QuantNodes/monitor/version/diff.py +81 -0
  251. QuantNodes/monitor/version/version_manager.py +182 -0
  252. QuantNodes/operator_node/__init__.py +28 -0
  253. QuantNodes/operator_node/base.py +97 -0
  254. QuantNodes/operator_node/query_node.py +129 -0
  255. QuantNodes/operator_node/sql_builder.py +125 -0
  256. QuantNodes/operator_node/sql_utils.py +172 -0
  257. QuantNodes/operator_node/transform.py +130 -0
  258. QuantNodes/operators/__init__.py +90 -0
  259. QuantNodes/operators/_engine.py +108 -0
  260. QuantNodes/operators/composite.py +161 -0
  261. QuantNodes/operators/composite_dag.py +667 -0
  262. QuantNodes/operators/composite_dag_ops.py +343 -0
  263. QuantNodes/operators/composite_dag_pandas_ops.py +382 -0
  264. QuantNodes/operators/custom.py +408 -0
  265. QuantNodes/operators/facade.py +164 -0
  266. QuantNodes/operators/math.py +163 -0
  267. QuantNodes/operators/proxy.py +29 -0
  268. QuantNodes/operators/registry.py +144 -0
  269. QuantNodes/operators/section.py +99 -0
  270. QuantNodes/operators/talib.py +757 -0
  271. QuantNodes/operators/templates.py +95 -0
  272. QuantNodes/operators/time_series.py +136 -0
  273. QuantNodes/prompts/__init__.py +20 -0
  274. QuantNodes/prompts/backtest/__init__.py +12 -0
  275. QuantNodes/prompts/backtest/factor_based.py +86 -0
  276. QuantNodes/prompts/backtest/standard.py +73 -0
  277. QuantNodes/prompts/factor/__init__.py +14 -0
  278. QuantNodes/prompts/factor/correlation.py +77 -0
  279. QuantNodes/prompts/factor/group_backtest.py +86 -0
  280. QuantNodes/prompts/factor/ic_analysis.py +91 -0
  281. QuantNodes/prompts/strategy/__init__.py +18 -0
  282. QuantNodes/prompts/strategy/market_neutral.py +96 -0
  283. QuantNodes/prompts/strategy/mean_reversion.py +107 -0
  284. QuantNodes/prompts/strategy/momentum.py +160 -0
  285. QuantNodes/prompts/strategy/pairs_trading.py +107 -0
  286. QuantNodes/prompts/strategy/trend_following.py +96 -0
  287. QuantNodes/research/README.md +106 -0
  288. QuantNodes/research/__init__.py +154 -0
  289. QuantNodes/research/_legacy_3c/__init__.py +61 -0
  290. QuantNodes/research/_legacy_3c/auto_researcher.py +289 -0
  291. QuantNodes/research/_legacy_3c/factor_evaluator.py +560 -0
  292. QuantNodes/research/_legacy_3c/factor_miner.py +318 -0
  293. QuantNodes/research/_legacy_3c/mcts_search.py +324 -0
  294. QuantNodes/research/factor_test/__init__.py +25 -0
  295. QuantNodes/research/factor_test/config.py +184 -0
  296. QuantNodes/research/factor_test/config_builder.py +276 -0
  297. QuantNodes/research/factor_test/e2e/data_prep.py +163 -0
  298. QuantNodes/research/factor_test/e2e/run_evolution_e2e.py +309 -0
  299. QuantNodes/research/factor_test/evolution_adapter.py +231 -0
  300. QuantNodes/research/factor_test/feedback_wrapper.py +102 -0
  301. QuantNodes/research/factor_test/ifind_db/__init__.py +7 -0
  302. QuantNodes/research/factor_test/ifind_db/fetcher.py +224 -0
  303. QuantNodes/research/factor_test/ifind_db/ifind_database.py +689 -0
  304. QuantNodes/research/factor_test/nodes/__init__.py +1 -0
  305. QuantNodes/research/factor_test/nodes/_base.py +91 -0
  306. QuantNodes/research/factor_test/nodes/adjust_date_node.py +48 -0
  307. QuantNodes/research/factor_test/nodes/configs.py +240 -0
  308. QuantNodes/research/factor_test/nodes/factor_neutralize_node.py +87 -0
  309. QuantNodes/research/factor_test/nodes/factor_preprocess_node.py +222 -0
  310. QuantNodes/research/factor_test/nodes/factor_score_node.py +141 -0
  311. QuantNodes/research/factor_test/nodes/factor_test_report_node.py +153 -0
  312. QuantNodes/research/factor_test/nodes/group_analyzer_node.py +317 -0
  313. QuantNodes/research/factor_test/nodes/ic_analyzer_node.py +112 -0
  314. QuantNodes/research/factor_test/nodes/load_data_node.py +100 -0
  315. QuantNodes/research/factor_test/nodes/long_short_node.py +93 -0
  316. QuantNodes/research/factor_test/nodes/neutralizers.py +222 -0
  317. QuantNodes/research/factor_test/nodes/preprocess_strategies.py +277 -0
  318. QuantNodes/research/factor_test/nodes/risk_correlation_node.py +112 -0
  319. QuantNodes/research/factor_test/nodes/sample_pool_filter_node.py +110 -0
  320. QuantNodes/research/factor_test/nodes/tradability_filter_node.py +92 -0
  321. QuantNodes/research/factor_test/pipeline_runner.py +305 -0
  322. QuantNodes/research/factor_test/pipeline_spec.py +216 -0
  323. QuantNodes/research/factor_test/utils/__init__.py +26 -0
  324. QuantNodes/research/factor_test/utils/constants.py +86 -0
  325. QuantNodes/research/factor_test/utils/data_loader.py +141 -0
  326. QuantNodes/research/factor_test/utils/date_utils.py +232 -0
  327. QuantNodes/research/factor_test/utils/file_loaders.py +150 -0
  328. QuantNodes/research/factor_test/utils/labels.py +37 -0
  329. QuantNodes/research/factor_test/utils/metrics_extractor.py +55 -0
  330. QuantNodes/research/factor_test/utils/performance_metrics.py +175 -0
  331. QuantNodes/research/factor_test/utils/safe_load.py +106 -0
  332. QuantNodes/research/quant_alpha/CHANGELOG.md +80 -0
  333. QuantNodes/research/quant_alpha/README.md +142 -0
  334. QuantNodes/research/quant_alpha/__init__.py +45 -0
  335. QuantNodes/research/quant_alpha/adapters/__init__.py +99 -0
  336. QuantNodes/research/quant_alpha/adapters/calculator.py +503 -0
  337. QuantNodes/research/quant_alpha/adapters/expression.py +387 -0
  338. QuantNodes/research/quant_alpha/alpha101_design/__init__.py +50 -0
  339. QuantNodes/research/quant_alpha/alpha101_design/few_shot_examples.py +243 -0
  340. QuantNodes/research/quant_alpha/alpha101_design/philosophy.py +474 -0
  341. QuantNodes/research/quant_alpha/alpha158_design/__init__.py +63 -0
  342. QuantNodes/research/quant_alpha/alpha158_design/few_shot_examples.py +219 -0
  343. QuantNodes/research/quant_alpha/alpha158_design/philosophy.py +240 -0
  344. QuantNodes/research/quant_alpha/evaluation/__init__.py +47 -0
  345. QuantNodes/research/quant_alpha/evaluation/baselines/__init__.py +8 -0
  346. QuantNodes/research/quant_alpha/evaluation/baselines/g1_handcrafted.py +135 -0
  347. QuantNodes/research/quant_alpha/evaluation/baselines/g2_llm_only.py +269 -0
  348. QuantNodes/research/quant_alpha/evaluation/baselines/g3_alpha_gpt.py +152 -0
  349. QuantNodes/research/quant_alpha/evaluation/clickhouse_data_loader.py +227 -0
  350. QuantNodes/research/quant_alpha/evaluation/contracts.py +376 -0
  351. QuantNodes/research/quant_alpha/evaluation/evaluators/__init__.py +6 -0
  352. QuantNodes/research/quant_alpha/evaluation/evaluators/polars_evaluator.py +545 -0
  353. QuantNodes/research/quant_alpha/evaluation/mock_data_loader.py +226 -0
  354. QuantNodes/research/quant_alpha/evaluation/runner.py +243 -0
  355. QuantNodes/research/quant_alpha/llm/__init__.py +38 -0
  356. QuantNodes/research/quant_alpha/llm/parser.py +681 -0
  357. QuantNodes/research/quant_alpha/logic_driven_pipeline.py +411 -0
  358. QuantNodes/research/quant_alpha/logic_mining/__init__.py +74 -0
  359. QuantNodes/research/quant_alpha/logic_mining/compiler.py +457 -0
  360. QuantNodes/research/quant_alpha/logic_mining/generator.py +366 -0
  361. QuantNodes/research/quant_alpha/logic_mining/models.py +252 -0
  362. QuantNodes/research/quant_alpha/logic_mining/parser.py +287 -0
  363. QuantNodes/research/quant_alpha/logic_mining/pipelines.py +297 -0
  364. QuantNodes/research/quant_alpha/logic_mining/sources.py +149 -0
  365. QuantNodes/research/quant_alpha/mcts/__init__.py +66 -0
  366. QuantNodes/research/quant_alpha/mcts/cache.py +262 -0
  367. QuantNodes/research/quant_alpha/mcts/extension_ops.py +320 -0
  368. QuantNodes/research/quant_alpha/mcts/feedback.py +825 -0
  369. QuantNodes/research/quant_alpha/mcts/op_prior.py +180 -0
  370. QuantNodes/research/quant_alpha/mcts/search.py +540 -0
  371. QuantNodes/research/quant_alpha/mcts/tree.py +201 -0
  372. QuantNodes/research/quant_alpha/operator_vocab/__init__.py +50 -0
  373. QuantNodes/research/quant_alpha/operator_vocab/config.py +54 -0
  374. QuantNodes/research/quant_alpha/operator_vocab/metadata.py +263 -0
  375. QuantNodes/research/quant_alpha/operator_vocab/vocabulary.py +481 -0
  376. QuantNodes/research/quant_alpha/pipeline.py +1027 -0
  377. QuantNodes/research/quant_alpha/types/__init__.py +27 -0
  378. QuantNodes/research/quant_alpha/types/constants.py +28 -0
  379. QuantNodes/research/quant_alpha/types/state.py +205 -0
  380. QuantNodes/research/quant_alpha/workflow/__init__.py +32 -0
  381. QuantNodes/research/quant_alpha/workflow/alpha_gpt.py +911 -0
  382. QuantNodes/research/quant_alpha/workflow/alpha_logics.py +416 -0
  383. QuantNodes/research/quant_alpha/workflow/state.py +27 -0
  384. QuantNodes/research/report_reproducer.py +485 -0
  385. QuantNodes/research/wiki.py +1155 -0
  386. QuantNodes/symbolic/__init__.py +51 -0
  387. QuantNodes/symbolic/compiler.py +113 -0
  388. QuantNodes/symbolic/dialect.py +260 -0
  389. QuantNodes/symbolic/executor.py +147 -0
  390. QuantNodes/symbolic/expression.py +234 -0
  391. QuantNodes/symbolic/functions.py +433 -0
  392. QuantNodes/symbolic/optimizer.py +165 -0
  393. QuantNodes/ui_node/__init__.py +30 -0
  394. QuantNodes/ui_node/base.py +222 -0
  395. quantnodes-3.0.0.dist-info/METADATA +463 -0
  396. quantnodes-3.0.0.dist-info/RECORD +399 -0
  397. quantnodes-3.0.0.dist-info/WHEEL +5 -0
  398. quantnodes-3.0.0.dist-info/entry_points.txt +24 -0
  399. quantnodes-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,112 @@
1
+ # coding: utf-8
2
+ """Node 7: IC 分析 / IC Analyzer Node
3
+
4
+ Migrated from factor_performance.py:111-158 cal_ic()
5
+ """
6
+
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
12
+ from QuantNodes.research.factor_test.nodes.configs import ICAnalyzerNodeConfig
13
+
14
+
15
+ class ICAnalyzerNode(PydanticConfigNode):
16
+ """计算 IC / Rank IC / ICIR / 因子 rank 自相关性
17
+
18
+ 输入: factor_neutral, price
19
+ 输出: {ic, rank_ic, ic_result, rank_ic_result, factor_rank_autocorr}
20
+ """
21
+
22
+ ConfigSchema = ICAnalyzerNodeConfig
23
+ _ALIASES = {"_min_group_size": "min_group_size"}
24
+
25
+ def _execute(self, input_data=None, **kwargs) -> dict:
26
+ context = kwargs.get('context', {})
27
+ factor_data = self._factor_data(context)
28
+ price = self._ctx_load(context, 'price')
29
+
30
+ if factor_data is None or price is None:
31
+ raise ValueError("因子或价格数据缺失")
32
+
33
+ return self._calc_ic(factor_data, price, self._min_group_size)
34
+
35
+ def _calc_ic(self, factor_data, price, group):
36
+ """计算 IC 系列
37
+
38
+ H4 (2026-06-20): vectorised per-date corr loop into DataFrame.corrwith.
39
+ Old loop did N separate Series.corr() calls (each O(K log K) over
40
+ K stocks); now one vectorised pairwise correlation across rows.
41
+ Typically 5-50x speedup on the hot path.
42
+
43
+ Empty / low-sample dates (nonan < group) are masked to NaN via
44
+ a per-row count of notnull values, preserving the original guard.
45
+ """
46
+ adj_dates = factor_data.index.tolist()
47
+
48
+ # 对齐价格到调仓日
49
+ price_adj = price.loc[price.index.isin(adj_dates)]
50
+
51
+ # 下一期收益
52
+ stock_cycle_ret = price_adj.pct_change(fill_method=None).shift(-1)
53
+
54
+ # 共同日期 (factor_data 与 stock_cycle_ret 都有)
55
+ common_dates = factor_data.index.intersection(stock_cycle_ret.index)
56
+ f = factor_data.loc[common_dates]
57
+ r = stock_cycle_ret.loc[common_dates]
58
+
59
+ # Per-date valid sample count (preserves <group guard).
60
+ per_date_count = f.notna().sum(axis=1)
61
+ valid_mask = per_date_count >= group
62
+
63
+ # Pearson IC (vectorised: one corr per row)
64
+ ic = f.corrwith(r, axis=1)
65
+ ic = ic.where(valid_mask, np.nan)
66
+ ic = ic.reindex(adj_dates)
67
+
68
+ # Spearman Rank IC (rank each row, then corrwith)
69
+ rank_f = f.rank(axis=1)
70
+ rank_r = r.rank(axis=1)
71
+ rank_ic = rank_f.corrwith(rank_r, axis=1)
72
+ rank_ic = rank_ic.where(valid_mask, np.nan)
73
+ rank_ic = rank_ic.reindex(adj_dates)
74
+
75
+ # 因子 rank 自相关 (rank(this row) vs rank(next row))
76
+ factor_rank = factor_data.rank(axis=1)
77
+ factor_rank_next = factor_rank.shift(-1)
78
+ common_dates2 = factor_rank.index.intersection(factor_rank_next.index)
79
+ factor_rank_autocorr = factor_rank.loc[common_dates2].corrwith(
80
+ factor_rank_next.loc[common_dates2], axis=1, method='spearman'
81
+ )
82
+ factor_rank_autocorr = factor_rank_autocorr.reindex(adj_dates)
83
+
84
+ # 评价指标
85
+ ic_result = pd.Series([
86
+ ic.mean(), ic.std(ddof=1),
87
+ ic.mean() / ic.std(ddof=1) if ic.std(ddof=1) != 0 else np.nan,
88
+ ic.mean() / ic.std(ddof=1) * np.sqrt(ic.notna().sum() - 1)
89
+ if ic.std(ddof=1) != 0 else np.nan,
90
+ ((ic > 0).sum() / ic.count()) if ic.count() > 0 else np.nan,
91
+ ((ic < 0).sum() / ic.count()) if ic.count() > 0 else np.nan,
92
+ ], index=['IC均值', 'IC标准差', 'ICIR', 'IC_T值', 'IC为正比例', 'IC为负比例'])
93
+
94
+ rank_ic_result = pd.Series([
95
+ rank_ic.mean(), rank_ic.std(ddof=1),
96
+ rank_ic.mean() / rank_ic.std(ddof=1) if rank_ic.std(ddof=1) != 0 else np.nan,
97
+ rank_ic.mean() / rank_ic.std(ddof=1) * np.sqrt(rank_ic.notna().sum() - 1)
98
+ if rank_ic.std(ddof=1) != 0 else np.nan,
99
+ ((rank_ic > 0).sum() / rank_ic.count()) if rank_ic.count() > 0 else np.nan,
100
+ ((rank_ic < 0).sum() / rank_ic.count()) if rank_ic.count() > 0 else np.nan,
101
+ ], index=[
102
+ 'rankIC均值', 'rankIC标准差', 'rankICIR',
103
+ 'rankIC_T值', 'rankIC为正比例', 'rankIC为负比例',
104
+ ])
105
+
106
+ return {
107
+ 'ic': ic,
108
+ 'rank_ic': rank_ic,
109
+ 'ic_result': ic_result,
110
+ 'rank_ic_result': rank_ic_result,
111
+ 'factor_rank_autocorr': factor_rank_autocorr,
112
+ }
@@ -0,0 +1,100 @@
1
+ # coding: utf-8
2
+ """Node 1: 加载数据 / Load Data Node"""
3
+
4
+ import logging
5
+ from typing import Dict
6
+
7
+ import pandas as pd
8
+
9
+ from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
10
+ from QuantNodes.research.factor_test.utils.data_loader import DataLoader
11
+ from QuantNodes.research.factor_test.utils.safe_load import (
12
+ safe_load_factor,
13
+ safe_load_h5,
14
+ try_load_panels,
15
+ )
16
+ from QuantNodes.research.factor_test.nodes.configs import LoadDataNodeConfig
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LoadDataNode(PydanticConfigNode):
22
+ """加载因子数据、价格、行业、市值等
23
+
24
+ 输入: config (LoadDataNodeConfig: data_path + load_keys + factor)
25
+ 输出: Dict[str, pd.DataFrame]
26
+ """
27
+
28
+ ConfigSchema = LoadDataNodeConfig
29
+ _ALIASES = {
30
+ "_data_path": "data_path",
31
+ "_load_keys": "load_keys",
32
+ "_factor_config": "factor",
33
+ }
34
+
35
+ def _execute(self, input_data=None, **kwargs) -> Dict[str, pd.DataFrame]:
36
+ # P-2: 空字符串校验 (Pydantic Field(...) 不挡空串, 需显式检查)
37
+ if not self._data_path:
38
+ raise ValueError(
39
+ "data_path required (P-2: 启动报错, 防止 None 数据目录导致下游谜之失败)"
40
+ )
41
+
42
+ loader = DataLoader(self._data_path)
43
+ result = {}
44
+
45
+ # 加载因子
46
+ if self._factor_config:
47
+ factor = safe_load_factor(
48
+ loader, self._factor_config.factor_dir, self._factor_config.name
49
+ )
50
+ if factor is None:
51
+ raise ValueError(
52
+ f"因子加载失败: dir={self._factor_config.factor_dir}, "
53
+ f"name={self._factor_config.name}"
54
+ )
55
+ # 检查是否有索引, 没有则添加
56
+ if hasattr(factor, 'columns') and factor.columns.dtype == 'int64':
57
+ if loader.valid_shape(factor):
58
+ factor = loader.add_index(factor)
59
+ else:
60
+ raise ValueError(
61
+ f"因子 {self._factor_config.name} shape 不一致: {factor.shape}"
62
+ )
63
+ else:
64
+ stklist, trade_dt = loader.get_stock_axis()
65
+ factor = factor.reindex(index=trade_dt.iloc[:, 0], columns=stklist.iloc[:, 0])
66
+ result['factor'] = factor
67
+
68
+ # 加载价格 (price 几乎所有下游节点都需要, 强制加载)
69
+ price = safe_load_h5(loader, 'stk_daily.h5', 'cp')
70
+ if price is not None:
71
+ result['price'] = price
72
+ else:
73
+ logger.warning("LoadDataNode: 未找到 price 数据 (stk_daily.h5/cp)")
74
+
75
+ # 加载其他数据
76
+ for key in self._load_keys:
77
+ if key in ('stklist', 'trade_dt'):
78
+ continue # 已通过 get_axis 处理
79
+ if key in result:
80
+ continue # 已加载
81
+ data = try_load_panels(loader, key)
82
+ if data is not None:
83
+ result[key] = data
84
+ else:
85
+ logger.warning("LoadDataNode: 跳过 %s (stk_daily.h5/index_daily.h5 都不存在)", key)
86
+
87
+ # 加载指数收盘价 (用于对冲基准)
88
+ index_cp = safe_load_h5(loader, 'index_daily.h5', 'index_cp', axis_type='index')
89
+ if index_cp is not None:
90
+ result['index_cp'] = index_cp
91
+ else:
92
+ logger.debug("LoadDataNode: 未找到 index_cp 数据 (index_daily.h5/index_cp)")
93
+
94
+ # 保存 loader 和轴数据供下游使用
95
+ result['_loader'] = loader
96
+ stklist, trade_dt = loader.get_stock_axis()
97
+ result['stklist'] = stklist
98
+ result['trade_dt'] = trade_dt
99
+
100
+ return result
@@ -0,0 +1,93 @@
1
+ # coding: utf-8
2
+ """Node 9: 多空组合 / Long-Short Node
3
+
4
+ Migrated from factor_performance.py:562-617 cal_longshort_ret()
5
+ """
6
+
7
+
8
+ import pandas as pd
9
+
10
+ from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
11
+ from QuantNodes.research.factor_test.nodes.configs import LongShortNodeConfig
12
+ from QuantNodes.research.factor_test.utils.labels import L_S_COLS, NET_COLS
13
+ from QuantNodes.research.factor_test.utils.performance_metrics import evaluation
14
+
15
+
16
+ class LongShortNode(PydanticConfigNode):
17
+ """多空组合构建 + 净值 + 评价
18
+
19
+ 输入: GroupAnalyzerNode 的输出
20
+ 输出: {net, eva_total, eva_yearly, period_ret}
21
+ """
22
+
23
+ ConfigSchema = LongShortNodeConfig
24
+ _ALIASES = {"_factor_direction": "factor_direction"}
25
+
26
+ def _execute(self, input_data=None, **kwargs) -> dict:
27
+ context = kwargs.get('context', {})
28
+ group_result = context.get('GroupAnalyzer')
29
+ if group_result is None:
30
+ raise ValueError("分组分析数据缺失")
31
+
32
+ return self._calc_longshort(group_result, self._factor_direction)
33
+
34
+ def _calc_longshort(self, group_result, factor_ori):
35
+ """计算多空净值"""
36
+ n_groups = group_result['n_groups']
37
+ adj_dates = group_result['adjust_dates']
38
+
39
+ if factor_ori == 1:
40
+ long_n = n_groups
41
+ short_n = 1
42
+ else:
43
+ long_n = 1
44
+ short_n = n_groups
45
+
46
+ # 各期收益
47
+ long_ret = group_result['group_ret'][long_n]
48
+ short_ret = group_result['group_ret'][short_n]
49
+ longshort_ret = long_ret - short_ret
50
+
51
+ # 净值
52
+ daily_net_long = group_result['daily_net_simp'][long_n]
53
+ daily_net_short = group_result['daily_net_simp'][short_n]
54
+ daily_exc_long = group_result['daily_excnet_simp'][long_n]
55
+ daily_exc_short = group_result['daily_excnet_simp'][short_n]
56
+
57
+ # 多空净值 (单利)
58
+ daily_net_longshort = daily_net_long - daily_net_short + 1
59
+
60
+ # 评价
61
+ eva_longshort = evaluation(daily_net_longshort, adj_dates)
62
+
63
+ # 合并结果
64
+ eva_l_s_ls = pd.concat([
65
+ group_result['group_eva_exc'][long_n],
66
+ group_result['group_eva_exc'][short_n],
67
+ eva_longshort.iloc[0, 1:],
68
+ ], axis=1)
69
+ eva_l_s_ls.columns = L_S_COLS
70
+
71
+ period_ret = pd.concat([long_ret, short_ret, longshort_ret], axis=1)
72
+ period_ret.columns = L_S_COLS
73
+
74
+ net = pd.concat([
75
+ daily_net_long, daily_net_short,
76
+ daily_exc_long, daily_exc_short,
77
+ daily_net_longshort,
78
+ ], axis=1)
79
+ net.columns = NET_COLS
80
+
81
+ eva_yearly = {
82
+ '多头超额': group_result['group_eva_exc_yearly'].get(long_n),
83
+ '空头超额': group_result['group_eva_exc_yearly'].get(short_n),
84
+ '多空': eva_longshort.iloc[1:] if len(eva_longshort) > 1 else pd.DataFrame(),
85
+ }
86
+
87
+ return {
88
+ 'net': net,
89
+ 'eva_total': eva_l_s_ls,
90
+ 'eva_yearly': eva_yearly,
91
+ 'period_ret': period_ret,
92
+ 'longshort_ret': longshort_ret,
93
+ }
@@ -0,0 +1,222 @@
1
+ # coding=utf-8
2
+ """Neutralizer 抽象与具体实现 (Chain of Responsibility, Phase 2.1)。
3
+
4
+ 将原 factor_neutralize_node.py::_neutralize 中 3 个几乎相同的 if/elif
5
+ 分支 (industry only / risk only / both) 抽象为:
6
+
7
+ Neutralizer (ABC)
8
+ ├── IndustryNeutralizer # 行业哑变量
9
+ └── RiskNeutralizer # 风险因子列
10
+
11
+ build_neutralizer_chain(if_industry, if_risk, industry, risk_data) -> list
12
+ apply_neutralizer_chain(factor_i, chain) -> factor_neut
13
+
14
+ 每个 neutralizer 负责自己的"设计矩阵 X 组装", apply_neutralizer_chain
15
+ 负责统一的"按日期循环 + OLS + 写残差"流程。新增中性化类型
16
+ (如 StyleNeutralizer) 只需新增一个 Neutralizer 子类, _execute
17
+ 无需修改。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from abc import ABC, abstractmethod
22
+ from typing import List, Optional
23
+
24
+ import numpy as np
25
+ import pandas as pd
26
+ import statsmodels.api as sm
27
+
28
+
29
+ # ============================================================================
30
+ # Abstract base
31
+ # ============================================================================
32
+
33
+ class Neutralizer(ABC):
34
+ """中性化器抽象基类 (Chain of Responsibility 的一环).
35
+
36
+ 子类实现 build_design_matrix() 返回指定日期的 X (设计矩阵).
37
+ is_active() 用于 build_neutralizer_chain 过滤无效环节.
38
+ """
39
+
40
+ name: str = ""
41
+
42
+ @abstractmethod
43
+ def build_design_matrix(
44
+ self,
45
+ date: pd.Timestamp,
46
+ factor_i: pd.DataFrame,
47
+ ) -> Optional[pd.DataFrame]:
48
+ """为指定日期组装设计矩阵 X (index=股票代码, columns=回归变量).
49
+
50
+ Returns:
51
+ X: 索引 = 股票代码, 列 = 哑变量/风险因子值
52
+ None: 跳过此日期 (无足够数据)
53
+ """
54
+ raise NotImplementedError
55
+
56
+ def is_active(self) -> bool:
57
+ """默认: neutralizer 始终 active. 子类可覆盖 (如缺数据时关闭)."""
58
+ return True
59
+
60
+
61
+ # ============================================================================
62
+ # Concrete: Industry
63
+ # ============================================================================
64
+
65
+ class IndustryNeutralizer(Neutralizer):
66
+ """行业中性化: 对 industry Series 做 one-hot dummy encoding.
67
+
68
+ 行为与原 _neutralize branch 2 (lines 98-114) 一致:
69
+ - industry NaN 替换为 0
70
+ - 每个日期生成 dummies
71
+ - 去掉全 0 列 (sum > 0 过滤)
72
+ """
73
+
74
+ name = "industry"
75
+
76
+ def __init__(self, industry: Optional[pd.Series]) -> None:
77
+ self.industry = (
78
+ industry.copy().replace(np.nan, 0) if industry is not None else None
79
+ )
80
+
81
+ def is_active(self) -> bool:
82
+ return self.industry is not None
83
+
84
+ def build_design_matrix(
85
+ self, date: pd.Timestamp, factor_i: pd.DataFrame,
86
+ ) -> Optional[pd.DataFrame]:
87
+ if self.industry is None or date not in self.industry.index:
88
+ return None
89
+ ind_j = self.industry.loc[date]
90
+ dum_ind = pd.get_dummies(ind_j)
91
+ # 与原代码一致: 去掉全 0 列
92
+ return dum_ind.loc[:, dum_ind.sum() > 0]
93
+
94
+
95
+ # ============================================================================
96
+ # Concrete: Risk
97
+ # ============================================================================
98
+
99
+ class RiskNeutralizer(Neutralizer):
100
+ """风险因子中性化: 把加载的 risk_data 横向 concat 为 X.
101
+
102
+ 输出 X 形状: index=股票代码, columns=risk_factors (与 IndustryNeutralizer 一致).
103
+ 这样 apply_neutralizer_chain 中的 pd.merge(..., left_index=True, right_index=True) 能正确合并.
104
+
105
+ 注: 原 _neutralize branch 3 (lines 116-135) 用 pd.concat(..., axis=1) 组装 X,
106
+ 得到 (n_risks, n_stocks) 形状 (index=range(n_risks), columns=stock_codes),
107
+ 与后续 merge 不匹配, 是 latent bug. 本实现修正.
108
+ """
109
+
110
+ name = "risk"
111
+
112
+ def __init__(self, risk_data: list) -> None:
113
+ self.risk_data = risk_data
114
+
115
+ def is_active(self) -> bool:
116
+ return bool(self.risk_data)
117
+
118
+ def build_design_matrix(
119
+ self, date: pd.Timestamp, factor_i: pd.DataFrame,
120
+ ) -> Optional[pd.DataFrame]:
121
+ cols: List[pd.DataFrame] = []
122
+ for i, rf in enumerate(self.risk_data):
123
+ if date not in rf.index:
124
+ continue
125
+ # rf.loc[date] 是 Series, index=股票代码 (因为 rf.columns=股票代码)
126
+ # 转为 1 列 DataFrame (index=股票代码, column=rf_i)
127
+ col = rf.loc[date].to_frame(name=f"rf_{i}")
128
+ cols.append(col)
129
+ if not cols:
130
+ return None
131
+ # 横向 concat (axis=1) 沿股票代码 index 对齐
132
+ return pd.concat(cols, axis=1)
133
+
134
+
135
+ # ============================================================================
136
+ # Chain construction & execution
137
+ # ============================================================================
138
+
139
+ def build_neutralizer_chain(
140
+ if_industry: bool,
141
+ if_risk: bool,
142
+ industry: Optional[pd.Series],
143
+ risk_data: list,
144
+ ) -> List[Neutralizer]:
145
+ """根据配置构造 chain, 自动过滤 is_active() == False 的环节.
146
+
147
+ 顺序固定: [Industry, Risk] (与原代码 if/elif 优先级一致).
148
+
149
+ Args:
150
+ if_industry: 是否启用行业中性化
151
+ if_risk: 是否启用风险因子中性化
152
+ industry: 行业 Series (None 时 IndustryNeutralizer 自动 inactive)
153
+ risk_data: 风险因子 list (空时 RiskNeutralizer 自动 inactive)
154
+
155
+ Returns:
156
+ List[Neutralizer]: 启用的 neutralizer 列表 (空表示无需中性化)
157
+ """
158
+ chain: List[Neutralizer] = []
159
+ if if_industry:
160
+ chain.append(IndustryNeutralizer(industry))
161
+ if if_risk:
162
+ chain.append(RiskNeutralizer(risk_data))
163
+ return [n for n in chain if n.is_active()]
164
+
165
+
166
+ def apply_neutralizer_chain(
167
+ factor_i: pd.DataFrame, chain: List[Neutralizer],
168
+ ) -> pd.DataFrame:
169
+ """执行 chain: 每个 neutralizer 顺序回归, 取残差作为新因子值.
170
+
171
+ 行为与原 _neutralize 三分支 (lines 74-135) 等价:
172
+ - 3 个分支的差异在 X 组装 (build_design_matrix 各自负责)
173
+ - 公共的"按日期循环 + merge + OLS + 写残差"在此统一
174
+
175
+ Args:
176
+ factor_i: 每日一行, index=日期, columns=股票代码
177
+ chain: build_neutralizer_chain 返回的列表
178
+
179
+ Returns:
180
+ factor_neut: 残差矩阵 (空 chain 时返回 factor_i.copy() 保留 nan 模式)
181
+ """
182
+ factor_neut = factor_i.copy() * np.nan
183
+ if not chain:
184
+ return factor_neut
185
+
186
+ for date_j in factor_i.index:
187
+ if factor_i.loc[date_j].notna().sum() == 0:
188
+ continue
189
+ # 收集所有 neutralizer 的 X
190
+ X_parts: List[pd.DataFrame] = []
191
+ for neutralizer in chain:
192
+ X_part = neutralizer.build_design_matrix(date_j, factor_i)
193
+ if X_part is not None:
194
+ X_parts.append(X_part)
195
+ if not X_parts:
196
+ continue
197
+ # 合并 X (与原 branch 1 一致: merge suffixes=('', '_rf'))
198
+ X = X_parts[0]
199
+ for xp in X_parts[1:]:
200
+ X = pd.merge(
201
+ X, xp, left_index=True, right_index=True, suffixes=("", "_rf"),
202
+ )
203
+ # y + X 对齐 + dropna
204
+ lm_data = pd.merge(
205
+ factor_i.loc[date_j].to_frame(), X,
206
+ left_index=True, right_index=True,
207
+ suffixes=("_y", "_x"),
208
+ ).dropna()
209
+ # OLS 需要 lm_data 长度 > 参数数 (含常数项)
210
+ if len(lm_data) > X.shape[1]:
211
+ # 转 float: IndustryNeutralizer 的 dummies 是 bool, sm.add_constant
212
+ # 在 bool 上报 "numpy boolean subtract" 错误. 转 float 修复此 bug
213
+ # (原 _neutralize branch 2 同样会失败, 是 latent bug)
214
+ X_values = lm_data.iloc[:, 1:].values.astype(float)
215
+ model = sm.OLS(
216
+ lm_data.iloc[:, 0].values,
217
+ sm.add_constant(X_values),
218
+ )
219
+ resid = model.fit().resid
220
+ factor_neut.loc[date_j, lm_data.index.values] = resid
221
+
222
+ return factor_neut