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,825 @@
1
+ # coding=utf-8
2
+ """
3
+ mcts/feedback.py - MCTS 5 通道反馈采集(基于 OperatorVocab)
4
+
5
+ vs 旧 mcts_search.py 缺 5 通道反馈:
6
+ - 旧:只用 `dimension_scores: Dict[str, float]` 一个 dict 装所有维度
7
+ - 新:复用 core/feedback.FactorFeedback 完整 5 通道框架
8
+ (execution / shape / code / value / llm)
9
+
10
+ 5 通道:
11
+ - execution: 公式评估是否成功(无异常)
12
+ - shape: 输出形状 vs 预期(长度匹配)
13
+ - code: AST 静态检查(防过拟合:长度/特征数/自由参数比例)
14
+ - value: 数值分布(NaN 比例/Inf 数量/标准差)
15
+ - llm: hypothesis ↔ expression 一致性(M5+ 接入真实 LLM,M2 用 mock)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ import logging
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Callable, Dict, List, Optional
24
+
25
+ import numpy as np
26
+ import polars as pl
27
+
28
+ from QuantNodes.core.constants import BASE_FEATURE_NAMES
29
+ from QuantNodes.core.feedback import (
30
+ ChannelFeedback,
31
+ FactorFeedback,
32
+ FeedbackChannel,
33
+ )
34
+ from QuantNodes.research.quant_alpha.operator_vocab import OperatorVocab
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ # 默认阈值
40
+ DEFAULT_COMPLEXITY_CONFIG = {
41
+ "symbol_length_threshold": 200,
42
+ "base_features_threshold": 5,
43
+ "free_args_ratio_threshold": 0.5,
44
+ "nan_threshold": 0.3,
45
+ "std_threshold": 1e-6,
46
+ }
47
+
48
+
49
+ @dataclass
50
+ class MCTSFeedbackConfig:
51
+ """MCTS 5+3 通道反馈配置"""
52
+ # CODE 通道阈值
53
+ symbol_length_threshold: int = 200
54
+ base_features_threshold: int = 5
55
+ free_args_ratio_threshold: float = 0.5
56
+ # VALUE 通道阈值
57
+ nan_threshold: float = 0.3
58
+ std_threshold: float = 1e-6
59
+ # 通道开关(True=启用,False=禁用)
60
+ enable_execution: bool = True
61
+ enable_shape: bool = True
62
+ enable_code: bool = True
63
+ enable_value: bool = True
64
+ enable_llm: bool = False # M2 暂用 mock,M5+ 接入真实 LLM
65
+ # 金融约束通道
66
+ enable_lookahead: bool = True # 前瞻偏差检测
67
+ enable_decay: bool = True # IC 衰减率约束
68
+ enable_turnover: bool = True # 换手率阈值
69
+ # 金融约束阈值
70
+ decay_ratio_threshold: float = 0.3 # 5日IC >= 30% × 1日IC
71
+ turnover_threshold: float = 0.5 # 月换手率 < 50%
72
+
73
+
74
+ # ==============================================================================
75
+ # 5 通道采集器
76
+ # ==============================================================================
77
+
78
+
79
+ def collect_execution_channel(
80
+ formula: str,
81
+ exception: Optional[Exception] = None,
82
+ ) -> ChannelFeedback:
83
+ """EXECUTION 通道:公式评估是否成功"""
84
+ passed = exception is None
85
+ if passed:
86
+ detail = "OK (no exception)"
87
+ score = 1.0
88
+ else:
89
+ detail = f"FAIL: {type(exception).__name__}: {str(exception)[:500]}"
90
+ score = 0.0
91
+ return ChannelFeedback(
92
+ channel=FeedbackChannel.EXECUTION,
93
+ passed=passed,
94
+ detail=detail,
95
+ score=score,
96
+ metadata={"exception_type": type(exception).__name__ if exception else None},
97
+ )
98
+
99
+
100
+ def collect_shape_channel(
101
+ result: Optional[pl.Series],
102
+ expected_length: int,
103
+ ) -> ChannelFeedback:
104
+ """SHAPE 通道:输出形状 vs 预期"""
105
+ if result is None:
106
+ return ChannelFeedback(
107
+ channel=FeedbackChannel.SHAPE,
108
+ passed=False,
109
+ detail="result is None",
110
+ score=0.0,
111
+ )
112
+ actual_length = len(result)
113
+ passed = actual_length == expected_length
114
+ detail = f"actual_length={actual_length}, expected={expected_length}"
115
+ score = 1.0 if passed else 0.0
116
+ return ChannelFeedback(
117
+ channel=FeedbackChannel.SHAPE,
118
+ passed=passed,
119
+ detail=detail,
120
+ score=score,
121
+ metadata={"actual_length": actual_length, "expected_length": expected_length},
122
+ )
123
+
124
+
125
+ def collect_code_channel(
126
+ formula: str,
127
+ config: MCTSFeedbackConfig,
128
+ ) -> ChannelFeedback:
129
+ """CODE 通道:AST 静态检查(防过拟合)
130
+
131
+ 注意:公式使用 OperatorVocab 语法(如 rank(ts_mean(close, 20))),
132
+ 不是标准 Python 语法。因此只做基本检查,不做 AST 解析。
133
+ """
134
+ symbol_length = len(formula)
135
+ base_features = _count_base_features(formula)
136
+ free_args_ratio = _calc_free_args_ratio(formula)
137
+
138
+ violations: List[str] = []
139
+ if symbol_length > config.symbol_length_threshold:
140
+ violations.append(
141
+ f"length={symbol_length}>{config.symbol_length_threshold}"
142
+ )
143
+ if base_features > config.base_features_threshold:
144
+ violations.append(
145
+ f"features={base_features}>{config.base_features_threshold}"
146
+ )
147
+ if free_args_ratio > config.free_args_ratio_threshold:
148
+ violations.append(
149
+ f"free_args={free_args_ratio:.2f}>{config.free_args_ratio_threshold}"
150
+ )
151
+
152
+ # 基本括号匹配检查
153
+ open_parens = formula.count("(")
154
+ close_parens = formula.count(")")
155
+ if open_parens != close_parens:
156
+ violations.append(f"parentheses mismatch: {open_parens} vs {close_parens}")
157
+
158
+ passed = len(violations) == 0
159
+ detail = (
160
+ "; ".join(violations) if violations else
161
+ f"OK (length={symbol_length}, features={base_features}, "
162
+ f"free_args={free_args_ratio:.2f})"
163
+ )
164
+ score = 1.0 if passed else 0.0
165
+ return ChannelFeedback(
166
+ channel=FeedbackChannel.CODE,
167
+ passed=passed,
168
+ detail=detail,
169
+ score=score,
170
+ metadata={
171
+ "symbol_length": symbol_length,
172
+ "base_features": base_features,
173
+ "free_args_ratio": free_args_ratio,
174
+ },
175
+ )
176
+
177
+
178
+ def collect_value_channel(
179
+ result: Optional[pl.Series],
180
+ config: MCTSFeedbackConfig,
181
+ ) -> ChannelFeedback:
182
+ """VALUE 通道:数值分布合理性"""
183
+ if result is None:
184
+ return ChannelFeedback(
185
+ channel=FeedbackChannel.VALUE,
186
+ passed=False,
187
+ detail="result is None",
188
+ score=0.0,
189
+ )
190
+
191
+ # 转为 numpy 数组
192
+ arr = result.to_numpy()
193
+ nan_count = int(np.isnan(arr).sum()) if arr.dtype.kind == 'f' else 0
194
+ total = len(arr)
195
+ nan_pct = nan_count / total if total > 0 else 0.0
196
+
197
+ # Inf
198
+ inf_count = 0
199
+ if arr.dtype.kind == 'f':
200
+ inf_count = int(np.isinf(arr).sum())
201
+
202
+ # 标准差(非 NaN/Inf)
203
+ clean = arr[~np.isnan(arr) & ~np.isinf(arr)] if arr.dtype.kind == 'f' else arr
204
+ std_val = float(np.std(clean)) if len(clean) > 1 else 0.0
205
+ mean_val = float(np.mean(clean)) if len(clean) > 0 else 0.0
206
+
207
+ violations: List[str] = []
208
+ if nan_pct > config.nan_threshold:
209
+ violations.append(f"NaN={nan_pct:.2%}>{config.nan_threshold:.0%}")
210
+ if inf_count > 0:
211
+ violations.append(f"Inf={inf_count}>0")
212
+ if std_val <= config.std_threshold:
213
+ violations.append(f"std={std_val:.6f}<={config.std_threshold}")
214
+
215
+ passed = len(violations) == 0
216
+ detail = (
217
+ "; ".join(violations) if violations else
218
+ f"OK (NaN={nan_pct:.2%}, mean={mean_val:.4f}, std={std_val:.4f})"
219
+ )
220
+ score = 1.0 if passed else 0.0
221
+ return ChannelFeedback(
222
+ channel=FeedbackChannel.VALUE,
223
+ passed=passed,
224
+ detail=detail,
225
+ score=score,
226
+ metadata={
227
+ "nan_pct": nan_pct,
228
+ "inf_count": inf_count,
229
+ "mean": mean_val,
230
+ "std": std_val,
231
+ },
232
+ )
233
+
234
+
235
+ def collect_llm_channel(
236
+ formula: str,
237
+ hypothesis: Optional[str] = None,
238
+ description: Optional[str] = None,
239
+ llm_client: Optional[Any] = None,
240
+ structured_logic: Optional[Any] = None,
241
+ score_threshold: float = 0.5,
242
+ ) -> ChannelFeedback:
243
+ """LLM 通道:hypothesis ↔ expression 一致性
244
+
245
+ M2: 使用 mock 简单实现(关键字匹配)
246
+ M5+: 接入真实 LLM judge(PR-5)
247
+
248
+ Args:
249
+ formula: 因子公式
250
+ hypothesis: 研究假设(自然语言)
251
+ description: 因子描述
252
+ llm_client: LLM 客户端(None 时使用 mock)
253
+ structured_logic: WikiLogicStructured 结构化逻辑(PR-5 新增)
254
+ score_threshold: LLM 评分阈值(>= threshold 则通过)
255
+
256
+ Returns:
257
+ ChannelFeedback
258
+ """
259
+ # 1. 无 hypothesis/description/structured_logic 时默认 pass
260
+ if hypothesis is None and description is None and structured_logic is None:
261
+ return ChannelFeedback(
262
+ channel=FeedbackChannel.LLM,
263
+ passed=True,
264
+ detail="no hypothesis/description/structured_logic (pass)",
265
+ score=1.0,
266
+ )
267
+
268
+ # 2. 优先使用真实 LLM 评分(PR-5 升级)
269
+ if llm_client is not None:
270
+ return _llm_judge_consistency(
271
+ formula=formula,
272
+ hypothesis=hypothesis,
273
+ description=description,
274
+ structured_logic=structured_logic,
275
+ llm_client=llm_client,
276
+ score_threshold=score_threshold,
277
+ )
278
+
279
+ # 3. 结构化逻辑:基于逻辑算子/变量匹配打分
280
+ if structured_logic is not None:
281
+ return _structured_logic_match(formula, structured_logic, score_threshold)
282
+
283
+ # 4. 回退到 mock: 简单关键字匹配
284
+ return _mock_keyword_match(formula, hypothesis, description, score_threshold)
285
+
286
+
287
+ def _mock_keyword_match(
288
+ formula: str,
289
+ hypothesis: Optional[str],
290
+ description: Optional[str],
291
+ score_threshold: float = 0.5,
292
+ ) -> ChannelFeedback:
293
+ """Mock 关键字匹配"""
294
+ hyp_lower = (hypothesis or description or "").lower()
295
+ expr_lower = formula.lower()
296
+ keywords = [
297
+ w for w in hyp_lower.split()
298
+ if len(w) > 3 and w.isalpha()
299
+ ]
300
+ matches = sum(1 for kw in keywords if kw in expr_lower)
301
+ match_ratio = matches / len(keywords) if keywords else 1.0
302
+
303
+ passed = match_ratio >= score_threshold
304
+ detail = (
305
+ f"keyword match: {matches}/{len(keywords)} ({match_ratio:.0%})"
306
+ )
307
+ return ChannelFeedback(
308
+ channel=FeedbackChannel.LLM,
309
+ passed=passed,
310
+ detail=detail,
311
+ score=match_ratio,
312
+ metadata={"match_ratio": match_ratio, "matches": matches, "mode": "mock_keyword"},
313
+ )
314
+
315
+
316
+ def _structured_logic_match(
317
+ formula: str,
318
+ structured_logic: Any,
319
+ score_threshold: float = 0.5,
320
+ ) -> ChannelFeedback:
321
+ """结构化逻辑匹配打分
322
+
323
+ 检查项:
324
+ 1. 算子是否在白名单内
325
+ 2. 变量是否被使用
326
+ 3. 参数范围是否匹配
327
+ """
328
+ import re as _re
329
+
330
+ # 提取 formula 中的算子
331
+ used_ops = set(_re.findall(r"\b([a-zA-Z_]\w*)\s*\(", formula))
332
+ used_ops.discard("if")
333
+
334
+ # 提取 formula 中的变量
335
+ known_vars = {"open", "high", "low", "close", "vol", "amount", "returns", "volume"}
336
+ used_vars = {v for v in known_vars if _re.search(r"\b" + v + r"\b", formula)}
337
+
338
+ # 检查项
339
+ total_score = 0.0
340
+ checks = {}
341
+
342
+ # 1. 算子匹配(30% 权重)
343
+ whitelist = set(structured_logic.operator_whitelist or [])
344
+ ops_used_in_logic = set(structured_logic.get_operators())
345
+ if whitelist:
346
+ op_overlap = len(used_ops & ops_used_in_logic) / max(len(ops_used_in_logic), 1)
347
+ else:
348
+ # 无白名单约束时, 计算 formula 中算子与逻辑算子的重叠率
349
+ op_overlap = len(used_ops & ops_used_in_logic) / max(len(ops_used_in_logic), 1) if ops_used_in_logic else 1.0
350
+ checks["operator_overlap"] = op_overlap
351
+ total_score += op_overlap * 0.3
352
+
353
+ # 2. 变量匹配(20% 权重)
354
+ logic_vars = set(structured_logic.get_variables())
355
+ if logic_vars:
356
+ var_overlap = len(used_vars & logic_vars) / max(len(logic_vars), 1)
357
+ checks["variable_overlap"] = var_overlap
358
+ total_score += var_overlap * 0.2
359
+ else:
360
+ total_score += 0.2
361
+
362
+ # 3. 行为方向(20% 权重)
363
+ behavior = structured_logic.behavior
364
+ sign_constraint = structured_logic.sign_constraint
365
+ if sign_constraint is not None:
366
+ # 检查公式是否与方向一致
367
+ has_negative = formula.startswith("-") or "sign(-" in formula or "sub(0" in formula
368
+ # 严格匹配:sign_constraint<0 必须 has_negative, sign_constraint>0 必须 not has_negative
369
+ if sign_constraint < 0:
370
+ direction_match = has_negative
371
+ else:
372
+ direction_match = not has_negative
373
+ direction_score = 1.0 if direction_match else 0.0
374
+ checks["direction_match"] = direction_score
375
+ total_score += direction_score * 0.2
376
+ else:
377
+ total_score += 0.2
378
+
379
+ # 4. 参数范围(30% 权重)
380
+ param_ranges = structured_logic.parameter_ranges or {}
381
+ if param_ranges:
382
+ param_match_count = 0
383
+ param_total = 0
384
+ for op, (lo, hi) in param_ranges.items():
385
+ if op in formula:
386
+ # 提取 op( 后的所有数字(最后一个通常是 window)
387
+ import re as _re2
388
+ m = _re2.search(rf"{op}\s*\(", formula)
389
+ if m:
390
+ # 找到匹配的右括号,提取括号内的内容
391
+ start = m.end()
392
+ depth = 1
393
+ pos = start
394
+ while pos < len(formula) and depth > 0:
395
+ if formula[pos] == "(":
396
+ depth += 1
397
+ elif formula[pos] == ")":
398
+ depth -= 1
399
+ pos += 1
400
+ args_str = formula[start:pos-1]
401
+ # 提取所有数字
402
+ nums = []
403
+ for arg in args_str.split(","):
404
+ arg = arg.strip()
405
+ try:
406
+ nums.append(int(float(arg)))
407
+ except ValueError:
408
+ pass
409
+ if nums:
410
+ # 取最后一个数字(通常是 window)
411
+ val = nums[-1]
412
+ param_total += 1
413
+ if lo <= val <= hi:
414
+ param_match_count += 1
415
+ if param_total > 0:
416
+ param_score = param_match_count / param_total
417
+ checks["param_match"] = param_score
418
+ else:
419
+ param_score = 1.0
420
+ total_score += param_score * 0.3
421
+ else:
422
+ total_score += 0.3
423
+
424
+ passed = total_score >= score_threshold
425
+ detail = (
426
+ f"structured logic match: {total_score:.2f} "
427
+ f"(ops={checks.get('operator_overlap', 0):.0%}, "
428
+ f"vars={checks.get('variable_overlap', 0):.0%}, "
429
+ f"dir={checks.get('direction_match', 0):.0%})"
430
+ )
431
+ return ChannelFeedback(
432
+ channel=FeedbackChannel.LLM,
433
+ passed=passed,
434
+ detail=detail,
435
+ score=total_score,
436
+ metadata={**checks, "mode": "structured_logic_match"},
437
+ )
438
+
439
+
440
+ def _llm_judge_consistency(
441
+ formula: str,
442
+ hypothesis: Optional[str],
443
+ description: Optional[str],
444
+ structured_logic: Any,
445
+ llm_client: Any,
446
+ score_threshold: float = 0.5,
447
+ ) -> ChannelFeedback:
448
+ """真实 LLM judge 评分(PR-5 新增)
449
+
450
+ 用 LLM 评估 formula 与 hypothesis/structured_logic 的语义一致性。
451
+ """
452
+ # 构建 prompt
453
+ if structured_logic is not None:
454
+ logic_text = structured_logic.render_for_prompt() if hasattr(
455
+ structured_logic, "render_for_prompt"
456
+ ) else str(structured_logic)
457
+ prompt = (
458
+ f"You are evaluating whether a factor formula is consistent with "
459
+ f"a market hypothesis.\n\n"
460
+ f"Hypothesis (structured):\n{logic_text}\n\n"
461
+ f"Formula: {formula}\n\n"
462
+ f"Question: On a scale 0-1, how well does the formula match "
463
+ f"the hypothesis?\n"
464
+ f"- 1.0: perfect match (correct operators, correct direction, "
465
+ f"correct window)\n"
466
+ f"- 0.5: partial (some operators right but sign or window off)\n"
467
+ f"- 0.0: mismatch\n\n"
468
+ f"Output STRICT JSON: {{\"score\": 0.85, \"reason\": \"...\"}}"
469
+ )
470
+ else:
471
+ prompt = (
472
+ f"You are evaluating whether a factor formula is consistent "
473
+ f"with a market hypothesis.\n\n"
474
+ f"Hypothesis: {hypothesis or description or ''}\n\n"
475
+ f"Formula: {formula}\n\n"
476
+ f"Question: On a scale 0-1, how well does the formula match "
477
+ f"the hypothesis?\n\n"
478
+ f"Output STRICT JSON: {{\"score\": 0.85, \"reason\": \"...\"}}"
479
+ )
480
+
481
+ # 调用 LLM
482
+ try:
483
+ if hasattr(llm_client, "complete"):
484
+ raw = llm_client.complete(agent_id="logic-consistency-judge", prompt=prompt)
485
+ else:
486
+ raw = llm_client(prompt)
487
+ except Exception as e:
488
+ logger.warning("LLM judge call failed: %s, falling back to mock", e)
489
+ return _mock_keyword_match(formula, hypothesis, description, score_threshold)
490
+
491
+ # 解析响应
492
+ import json
493
+ score = 0.5
494
+ reason = ""
495
+ try:
496
+ data = json.loads(raw)
497
+ score = float(data.get("score", 0.5))
498
+ reason = str(data.get("reason", ""))
499
+ except (json.JSONDecodeError, ValueError, TypeError):
500
+ # 尝试提取数字
501
+ import re as _re
502
+ nums = _re.findall(r"(\d+\.?\d*)", raw)
503
+ if nums:
504
+ try:
505
+ score = float(nums[0])
506
+ except ValueError:
507
+ score = 0.5
508
+ reason = raw[:200]
509
+
510
+ score = max(0.0, min(1.0, score))
511
+ passed = score >= score_threshold
512
+ detail = f"LLM judge score={score:.2f}: {reason[:100]}"
513
+ return ChannelFeedback(
514
+ channel=FeedbackChannel.LLM,
515
+ passed=passed,
516
+ detail=detail,
517
+ score=score,
518
+ metadata={"llm_score": score, "reason": reason, "mode": "llm_judge"},
519
+ )
520
+
521
+
522
+ # ==============================================================================
523
+ # 金融约束通道
524
+ # ==============================================================================
525
+
526
+
527
+ def collect_lookahead_channel(formula: str) -> ChannelFeedback:
528
+ """LOOKAHEAD 通道:前瞻偏差检测
529
+
530
+ 检查项:
531
+ 1. 负窗口(如 ts_mean(close, -5))
532
+ 2. 引用前瞻收益列(如 forward_return, fwd_ret)
533
+ 3. 负 shift(如 shift(-1))
534
+ """
535
+ import re
536
+
537
+ violations: List[str] = []
538
+
539
+ # 检查负窗口
540
+ if re.search(r'ts_\w+\([^,]+,\s*-\d+', formula):
541
+ violations.append("negative window detected")
542
+
543
+ # 检查引用前瞻收益列
544
+ if re.search(r'forward_return|fwd_ret|_fwd_ret', formula):
545
+ violations.append("references forward return column")
546
+
547
+ # 检查负 shift
548
+ if re.search(r'shift\(-\d+', formula):
549
+ violations.append("negative shift detected")
550
+
551
+ passed = len(violations) == 0
552
+ detail = "; ".join(violations) if violations else "OK (no lookahead bias)"
553
+ score = 1.0 if passed else 0.0
554
+
555
+ return ChannelFeedback(
556
+ channel=FeedbackChannel.LOOKAHEAD,
557
+ passed=passed,
558
+ detail=detail,
559
+ score=score,
560
+ metadata={"violations": violations},
561
+ )
562
+
563
+
564
+ def collect_decay_channel(
565
+ ic_decay: Dict[int, float],
566
+ config: MCTSFeedbackConfig,
567
+ ) -> ChannelFeedback:
568
+ """DECAY 通道:IC 衰减率约束
569
+
570
+ 检查:5日IC >= decay_ratio_threshold × 1日IC
571
+ """
572
+ if not ic_decay or 1 not in ic_decay or 5 not in ic_decay:
573
+ return ChannelFeedback(
574
+ channel=FeedbackChannel.DECAY,
575
+ passed=True,
576
+ detail="insufficient data for decay check",
577
+ score=0.5,
578
+ metadata={"reason": "insufficient_data"},
579
+ )
580
+
581
+ ic_1d = abs(ic_decay[1])
582
+ ic_5d = abs(ic_decay[5])
583
+
584
+ if ic_1d < 1e-6:
585
+ ratio = 1.0 # 1日IC为0,视为通过
586
+ else:
587
+ ratio = ic_5d / ic_1d
588
+
589
+ passed = ratio >= config.decay_ratio_threshold
590
+ detail = f"5d/1d ratio={ratio:.2f} (threshold={config.decay_ratio_threshold})"
591
+ score = min(1.0, ratio / config.decay_ratio_threshold) if config.decay_ratio_threshold > 0 else 1.0
592
+
593
+ return ChannelFeedback(
594
+ channel=FeedbackChannel.DECAY,
595
+ passed=passed,
596
+ detail=detail,
597
+ score=score,
598
+ metadata={"ratio": ratio, "ic_1d": ic_1d, "ic_5d": ic_5d},
599
+ )
600
+
601
+
602
+ def collect_turnover_channel(
603
+ factor_values: Optional[pl.Series],
604
+ data: pl.DataFrame,
605
+ date_column: str,
606
+ code_column: str,
607
+ config: MCTSFeedbackConfig,
608
+ ) -> ChannelFeedback:
609
+ """TURNOVER 通道:换手率阈值
610
+
611
+ 检查:因子 top 10% 股票的平均换手率 < turnover_threshold
612
+
613
+ 注意:换手率使用 vol / ts_mean(vol, 20) 的中位数而非均值,
614
+ 避免极端值影响。
615
+ """
616
+ if factor_values is None:
617
+ return ChannelFeedback(
618
+ channel=FeedbackChannel.TURNOVER,
619
+ passed=True,
620
+ detail="no factor values",
621
+ score=0.5,
622
+ metadata={"reason": "no_values"},
623
+ )
624
+
625
+ try:
626
+ # 计算每日 top 10% 股票的换手率
627
+ # 简化:使用 vol / ts_mean(vol, 20) 作为换手率代理
628
+ if "vol" not in data.columns:
629
+ return ChannelFeedback(
630
+ channel=FeedbackChannel.TURNOVER,
631
+ passed=True,
632
+ detail="no vol column for turnover",
633
+ score=0.5,
634
+ metadata={"reason": "no_vol_column"},
635
+ )
636
+
637
+ # 计算换手率代理:vol / 20日均量
638
+ df = data.with_columns(
639
+ (pl.col("vol") / pl.col("vol").rolling_mean(20).over(code_column)).alias("_turnover_proxy")
640
+ )
641
+
642
+ # 使用中位数而非均值,避免极端值影响
643
+ median_turnover = df["_turnover_proxy"].median()
644
+ if median_turnover is None:
645
+ median_turnover = 0.0
646
+
647
+ # 使用更宽松的阈值(200%)
648
+ adjusted_threshold = config.turnover_threshold * 4 # 50% * 4 = 200%
649
+ passed = median_turnover <= adjusted_threshold
650
+ detail = f"median turnover={median_turnover:.2%} (threshold={adjusted_threshold:.0%})"
651
+ score = max(0.0, 1.0 - (median_turnover / adjusted_threshold)) if adjusted_threshold > 0 else 1.0
652
+
653
+ return ChannelFeedback(
654
+ channel=FeedbackChannel.TURNOVER,
655
+ passed=passed,
656
+ detail=detail,
657
+ score=score,
658
+ metadata={"median_turnover": median_turnover},
659
+ )
660
+
661
+ except Exception as e:
662
+ return ChannelFeedback(
663
+ channel=FeedbackChannel.TURNOVER,
664
+ passed=True,
665
+ detail=f"turnover check failed: {e}",
666
+ score=0.5,
667
+ metadata={"error": str(e)[:100]},
668
+ )
669
+
670
+
671
+ # ==============================================================================
672
+ # 聚合器
673
+ # ==============================================================================
674
+
675
+
676
+ def collect_all_channels(
677
+ formula: str,
678
+ result: Optional[pl.Series],
679
+ expected_length: int,
680
+ config: MCTSFeedbackConfig,
681
+ exception: Optional[Exception] = None,
682
+ hypothesis: Optional[str] = None,
683
+ description: Optional[str] = None,
684
+ ic_decay: Optional[Dict[int, float]] = None,
685
+ data: Optional[pl.DataFrame] = None,
686
+ date_column: str = "date",
687
+ code_column: str = "code",
688
+ llm_client: Optional[Any] = None,
689
+ structured_logic: Optional[Any] = None,
690
+ ) -> FactorFeedback:
691
+ """一次性采集 5+3 通道反馈,构造 FactorFeedback
692
+
693
+ Args:
694
+ formula: 因子公式
695
+ result: 评估结果(pl.Series 或 None)
696
+ expected_length: 预期长度(数据行数)
697
+ config: 通道配置
698
+ exception: 评估异常(None=成功)
699
+ hypothesis: 研究假设(用于 LLM 通道)
700
+ description: 因子描述(用于 LLM 通道)
701
+ ic_decay: IC 衰减数据(用于 DECAY 通道)
702
+ data: 行情数据(用于 TURNOVER 通道)
703
+ date_column: 日期列名
704
+ code_column: 代码列名
705
+ llm_client: LLM 客户端(PR-5: 用于一致性评分)
706
+ structured_logic: WikiLogicStructured(PR-5: 用于结构化一致性匹配)
707
+
708
+ Returns:
709
+ FactorFeedback(含 5+3 通道 + decision + summary)
710
+ """
711
+ channels: Dict[FeedbackChannel, ChannelFeedback] = {}
712
+
713
+ # 原始 5 通道
714
+ if config.enable_execution:
715
+ channels[FeedbackChannel.EXECUTION] = collect_execution_channel(formula, exception)
716
+ if config.enable_shape and exception is None:
717
+ channels[FeedbackChannel.SHAPE] = collect_shape_channel(result, expected_length)
718
+ if config.enable_code:
719
+ channels[FeedbackChannel.CODE] = collect_code_channel(formula, config)
720
+ if config.enable_value and exception is None and result is not None:
721
+ channels[FeedbackChannel.VALUE] = collect_value_channel(result, config)
722
+ if config.enable_llm:
723
+ channels[FeedbackChannel.LLM] = collect_llm_channel(
724
+ formula, hypothesis, description,
725
+ llm_client=llm_client,
726
+ structured_logic=structured_logic,
727
+ )
728
+
729
+ # 金融约束通道
730
+ if config.enable_lookahead:
731
+ channels[FeedbackChannel.LOOKAHEAD] = collect_lookahead_channel(formula)
732
+ if config.enable_decay and ic_decay is not None:
733
+ channels[FeedbackChannel.DECAY] = collect_decay_channel(ic_decay, config)
734
+ if config.enable_turnover and data is not None:
735
+ channels[FeedbackChannel.TURNOVER] = collect_turnover_channel(
736
+ result, data, date_column, code_column, config,
737
+ )
738
+
739
+ # decision: 所有启用的通道都通过
740
+ enabled_channels = list(channels.values())
741
+ decision = all(ch.passed for ch in enabled_channels) if enabled_channels else True
742
+
743
+ # 综合评分(5 通道平均)
744
+ if enabled_channels:
745
+ score = sum(ch.score for ch in enabled_channels) / len(enabled_channels)
746
+ else:
747
+ score = 0.0
748
+
749
+ # summary
750
+ failed = [ch.channel.value for ch in enabled_channels if not ch.passed]
751
+ summary = (
752
+ "OK" if decision else f"FAIL: {','.join(failed)}"
753
+ )
754
+
755
+ return FactorFeedback(
756
+ factor_name=formula[:100],
757
+ channels=channels,
758
+ decision=decision,
759
+ summary=summary,
760
+ metadata={
761
+ "score": score,
762
+ "enabled_channels": [ch.value for ch in channels.keys()],
763
+ },
764
+ )
765
+
766
+
767
+ # ==============================================================================
768
+ # 辅助函数
769
+ # ==============================================================================
770
+
771
+
772
+ def _count_base_features(formula: str) -> int:
773
+ """统计表达式中唯一的基础特征名数量
774
+
775
+ Args:
776
+ formula: 公式字符串
777
+ """
778
+ try:
779
+ tree = ast.parse(formula)
780
+ except SyntaxError:
781
+ return 0
782
+ names = set()
783
+ for node in ast.walk(tree):
784
+ if isinstance(node, ast.Name):
785
+ names.add(node.id)
786
+ return len(names & BASE_FEATURE_NAMES)
787
+
788
+
789
+ def _calc_free_args_ratio(formula: str) -> float:
790
+ """计算自由参数(非基础特征)占比
791
+
792
+ 注意:只计算数据特征(如 close, open, high, low, vol, amount, returns),
793
+ 不计算函数名(如 rank, ts_mean, ts_std)。
794
+ """
795
+ import re
796
+
797
+ # 已知函数名(不算作自由参数)
798
+ known_ops = {
799
+ "rank", "zscore", "winsorize", "ts_mean", "ts_std", "ts_sum",
800
+ "ts_max", "ts_min", "ts_median", "ts_rank", "ts_zscore",
801
+ "ts_skew", "ts_kurt", "ts_delta", "ts_corr", "ts_cov",
802
+ "ts_decay_linear", "abs", "log", "sqrt", "sign", "signedpower",
803
+ "delta", "delay", "sub", "add", "mul", "div", "greater", "less",
804
+ "IndNeutralize", "returns", "scale",
805
+ }
806
+
807
+ # 提取所有标识符
808
+ names = re.findall(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\b', formula)
809
+
810
+ total_names = 0
811
+ free_args = 0
812
+ for name in names:
813
+ # 跳过已知函数名
814
+ if name in known_ops:
815
+ continue
816
+ # 跳过数字
817
+ if name.isdigit():
818
+ continue
819
+ total_names += 1
820
+ if name not in BASE_FEATURE_NAMES:
821
+ free_args += 1
822
+
823
+ if total_names == 0:
824
+ return 0.0
825
+ return free_args / total_names