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,38 @@
1
+ """FactorFeedback — 结构化反馈框架。
2
+
3
+ 公开 API:
4
+ - FeedbackChannel (Enum): 5 通道
5
+ - ChannelFeedback (dataclass): 单通道信号
6
+ - FactorFeedback (dataclass): 完整反馈
7
+ - FeedbackCollector: 聚合器
8
+ - LLMJudge: LLM 一致性评判
9
+ - ensure_feedback: dict → FactorFeedback 包装
10
+ - collect_execution / collect_shape / collect_code / collect_value: 4 通道采集器
11
+ """
12
+ from .dataclass import (
13
+ ChannelFeedback,
14
+ FactorFeedback,
15
+ FeedbackChannel,
16
+ ensure_feedback,
17
+ )
18
+ from .collector import FeedbackCollector
19
+ from .llm_judge import LLMJudge
20
+ from .channels import (
21
+ collect_execution,
22
+ collect_shape,
23
+ collect_code,
24
+ collect_value,
25
+ )
26
+
27
+ __all__ = [
28
+ "FeedbackChannel",
29
+ "ChannelFeedback",
30
+ "FactorFeedback",
31
+ "FeedbackCollector",
32
+ "LLMJudge",
33
+ "ensure_feedback",
34
+ "collect_execution",
35
+ "collect_shape",
36
+ "collect_code",
37
+ "collect_value",
38
+ ]
@@ -0,0 +1,182 @@
1
+ """4 通道反馈采集器: execution / shape / code / value。
2
+
3
+ 每个函数返回 ChannelFeedback, 可被 FeedbackCollector.add() 直接消费。
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import ast
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from ..constants import BASE_FEATURE_NAMES as _BASE_FEATURE_NAMES
13
+ from .dataclass import ChannelFeedback, FeedbackChannel
14
+
15
+
16
+ def collect_execution(
17
+ stdout: str,
18
+ stderr: str,
19
+ exit_code: int,
20
+ max_output_chars: int = 500,
21
+ ) -> ChannelFeedback:
22
+ """EXECUTION 通道: 沙箱执行结果。
23
+
24
+ Args:
25
+ stdout/stderr: 沙箱输出
26
+ exit_code: 进程退出码
27
+ max_output_chars: stdout/stderr 截断长度 (M2)
28
+ """
29
+ passed = exit_code == 0
30
+ detail = (
31
+ f"exit={exit_code}\n"
32
+ f"stdout: {str(stdout)[:max_output_chars]}\n"
33
+ f"stderr: {str(stderr)[:max_output_chars]}"
34
+ )
35
+ score = 1.0 if passed else 0.0
36
+ return ChannelFeedback(
37
+ channel=FeedbackChannel.EXECUTION,
38
+ passed=passed,
39
+ detail=detail,
40
+ score=score,
41
+ metadata={"exit_code": int(exit_code)},
42
+ )
43
+
44
+
45
+ def collect_shape(actual_shape: tuple, expected_shape: tuple) -> ChannelFeedback:
46
+ """SHAPE 通道: 形状一致性。"""
47
+ passed = tuple(actual_shape) == tuple(expected_shape)
48
+ detail = f"actual={tuple(actual_shape)}, expected={tuple(expected_shape)}"
49
+ score = 1.0 if passed else 0.0
50
+ return ChannelFeedback(
51
+ channel=FeedbackChannel.SHAPE,
52
+ passed=passed,
53
+ detail=detail,
54
+ score=score,
55
+ )
56
+
57
+
58
+ def collect_code(
59
+ expression: str,
60
+ symbol_length_threshold: int = 200,
61
+ base_features_threshold: int = 5,
62
+ free_args_ratio_threshold: float = 0.5,
63
+ ) -> ChannelFeedback:
64
+ """CODE 通道: AST 静态检查 (防过拟合)。
65
+
66
+ 检查项:
67
+ - 表达式长度 <= symbol_length_threshold
68
+ - 基础特征数 <= base_features_threshold
69
+ - 自由参数 (Name) 占比 <= free_args_ratio_threshold
70
+ """
71
+ try:
72
+ tree = ast.parse(expression)
73
+ except SyntaxError as e:
74
+ return ChannelFeedback(
75
+ channel=FeedbackChannel.CODE,
76
+ passed=False,
77
+ detail=f"语法错误: {e}",
78
+ score=0.0,
79
+ )
80
+
81
+ symbol_length = len(expression)
82
+ base_features = _count_base_features(tree)
83
+ free_args_ratio = _calc_free_args_ratio(tree, base_features)
84
+
85
+ violations: list[str] = []
86
+ if symbol_length > symbol_length_threshold:
87
+ violations.append(f"length={symbol_length}>{symbol_length_threshold}")
88
+ if base_features > base_features_threshold:
89
+ violations.append(f"features={base_features}>{base_features_threshold}")
90
+ if free_args_ratio > free_args_ratio_threshold:
91
+ violations.append(f"free_args={free_args_ratio:.2f}>{free_args_ratio_threshold}")
92
+
93
+ passed = len(violations) == 0
94
+ detail = "; ".join(violations) if violations else \
95
+ f"OK (length={symbol_length}, features={base_features}, free_args={free_args_ratio:.2f})"
96
+ score = 1.0 if passed else 0.0
97
+ return ChannelFeedback(
98
+ channel=FeedbackChannel.CODE,
99
+ passed=passed,
100
+ detail=detail,
101
+ score=score,
102
+ metadata={
103
+ "symbol_length": symbol_length,
104
+ "base_features": base_features,
105
+ "free_args_ratio": free_args_ratio,
106
+ },
107
+ )
108
+
109
+
110
+ def collect_value(
111
+ values: pd.Series,
112
+ nan_threshold: float = 0.3,
113
+ std_threshold: float = 1e-6,
114
+ ) -> ChannelFeedback:
115
+ """VALUE 通道: 数值分布合理性。
116
+
117
+ 检查项:
118
+ - NaN 比例 <= nan_threshold (默认 30%)
119
+ - Inf 数量 == 0
120
+ - 标准差 > std_threshold
121
+ """
122
+ s = pd.Series(values).dropna()
123
+ if len(s) == 0:
124
+ return ChannelFeedback(
125
+ channel=FeedbackChannel.VALUE,
126
+ passed=False,
127
+ detail="全部 NaN, 无有效数值",
128
+ score=0.0,
129
+ )
130
+
131
+ nan_pct = float(pd.Series(values).isna().mean())
132
+ inf_count = int(np.isinf(pd.Series(values).fillna(0)).sum())
133
+ mean_val = float(s.mean())
134
+ std_val = float(s.std()) if len(s) > 1 else 0.0
135
+
136
+ violations: list[str] = []
137
+ if nan_pct > nan_threshold:
138
+ violations.append(f"NaN={nan_pct:.2%}>{nan_threshold:.0%}")
139
+ if inf_count > 0:
140
+ violations.append(f"Inf={inf_count}>0")
141
+ if std_val <= std_threshold:
142
+ violations.append(f"std={std_val:.6f}<={std_threshold}")
143
+
144
+ passed = len(violations) == 0
145
+ detail = "; ".join(violations) if violations else \
146
+ f"OK (NaN={nan_pct:.2%}, mean={mean_val:.4f}, std={std_val:.4f})"
147
+ score = 1.0 if passed else 0.0
148
+ return ChannelFeedback(
149
+ channel=FeedbackChannel.VALUE,
150
+ passed=passed,
151
+ detail=detail,
152
+ score=score,
153
+ metadata={
154
+ "nan_pct": nan_pct,
155
+ "inf_count": inf_count,
156
+ "mean": mean_val,
157
+ "std": std_val,
158
+ },
159
+ )
160
+
161
+
162
+ def _count_base_features(tree: ast.AST) -> int:
163
+ """统计表达式中唯一的基础特征名数量。"""
164
+ names: set[str] = set()
165
+ for node in ast.walk(tree):
166
+ if isinstance(node, ast.Name):
167
+ names.add(node.id)
168
+ return len(names & _BASE_FEATURE_NAMES)
169
+
170
+
171
+ def _calc_free_args_ratio(tree: ast.AST, base_features: int) -> float:
172
+ """计算自由参数 (非基础特征) 占比。"""
173
+ total_names = 0
174
+ free_args = 0
175
+ for node in ast.walk(tree):
176
+ if isinstance(node, ast.Name):
177
+ total_names += 1
178
+ if node.id not in _BASE_FEATURE_NAMES:
179
+ free_args += 1
180
+ if total_names == 0:
181
+ return 0.0
182
+ return free_args / total_names
@@ -0,0 +1,91 @@
1
+ """FeedbackCollector — 聚合多通道反馈的便捷类。"""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from typing import Optional
6
+
7
+ from .dataclass import ChannelFeedback, FactorFeedback, FeedbackChannel
8
+
9
+
10
+ class FeedbackCollector:
11
+ """聚合多个通道的反馈信号, finalize() 返回 FactorFeedback。"""
12
+
13
+ def __init__(self, factor_id: str, factor_name: str):
14
+ self.factor_id = factor_id
15
+ self.factor_name = factor_name
16
+ self._channels: dict[FeedbackChannel, ChannelFeedback] = {}
17
+ self._t0 = time.perf_counter()
18
+
19
+ def add(
20
+ self,
21
+ channel: FeedbackChannel,
22
+ passed: bool,
23
+ detail: str,
24
+ score: float = 1.0,
25
+ **metadata,
26
+ ) -> "FeedbackCollector":
27
+ """添加一个通道的反馈, 支持链式调用。"""
28
+ self._channels[channel] = ChannelFeedback(
29
+ channel=channel,
30
+ passed=passed,
31
+ detail=detail,
32
+ score=score,
33
+ metadata=dict(metadata),
34
+ )
35
+ return self
36
+
37
+ def add_feedback(self, fb: ChannelFeedback) -> "FeedbackCollector":
38
+ """添加一个完整的 ChannelFeedback 对象。"""
39
+ self._channels[fb.channel] = fb
40
+ return self
41
+
42
+ def has(self, channel: FeedbackChannel) -> bool:
43
+ return channel in self._channels
44
+
45
+ def get(self, channel: FeedbackChannel) -> Optional[ChannelFeedback]:
46
+ return self._channels.get(channel)
47
+
48
+ def finalize(
49
+ self,
50
+ decision: Optional[bool] = None,
51
+ summary: str = "",
52
+ agg_mode: str = "all", # M3: "all" (AND) | "any" (OR) | "majority"
53
+ **metadata,
54
+ ) -> FactorFeedback:
55
+ """聚合所有通道, 返回 FactorFeedback。
56
+
57
+ Args:
58
+ decision: 显式决策 (None=按 agg_mode 自动)
59
+ summary: 一句话总结 (空=自动生成)
60
+ agg_mode: M3 聚合方式
61
+ - "all" (默认): 全部通过才算通过
62
+ - "any": 任一通过就算通过
63
+ - "majority": 多数通过才算通过
64
+ **metadata: 附加到 FactorFeedback.metadata
65
+ """
66
+ if decision is None:
67
+ if not self._channels:
68
+ decision = True
69
+ elif agg_mode == "all":
70
+ decision = all(fb.passed for fb in self._channels.values())
71
+ elif agg_mode == "any":
72
+ decision = any(fb.passed for fb in self._channels.values())
73
+ elif agg_mode == "majority":
74
+ passed_count = sum(1 for fb in self._channels.values() if fb.passed)
75
+ decision = passed_count > len(self._channels) / 2
76
+ else:
77
+ raise ValueError(f"未知 agg_mode: {agg_mode}")
78
+
79
+ if not summary:
80
+ failed = [ch.value for ch, fb in self._channels.items() if not fb.passed]
81
+ summary = f"失败通道: {', '.join(failed)}" if failed else "全部通过"
82
+
83
+ return FactorFeedback(
84
+ factor_id=self.factor_id,
85
+ factor_name=self.factor_name,
86
+ channels=dict(self._channels),
87
+ decision=decision,
88
+ summary=summary,
89
+ duration_ms=(time.perf_counter() - self._t0) * 1000,
90
+ metadata=dict(metadata),
91
+ )
@@ -0,0 +1,239 @@
1
+ """FactorFeedback 数据类 — 5 通道结构化反馈。
2
+
3
+ QuantaAlpha `CoSTEERSingleFeedback` 的 QuantNodes 适配版本。
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import uuid
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ from ..constants import EXTENDED_METRIC_KEYS as KNOWN_METRICS
19
+ from QuantNodes.core.path_utils import ensure_parent
20
+
21
+
22
+ class FeedbackChannel(str, Enum):
23
+ """5+3 通道反馈信号。"""
24
+ EXECUTION = "execution"
25
+ SHAPE = "shape"
26
+ CODE = "code"
27
+ VALUE = "value"
28
+ LLM = "llm"
29
+ # 金融约束通道
30
+ LOOKAHEAD = "lookahead" # 前瞻偏差检测
31
+ DECAY = "decay" # IC 衰减率约束
32
+ TURNOVER = "turnover" # 换手率阈值
33
+
34
+
35
+ @dataclass
36
+ class ChannelFeedback:
37
+ """单通道反馈信号。"""
38
+ channel: FeedbackChannel
39
+ passed: bool
40
+ detail: str
41
+ score: float = 1.0
42
+ metadata: dict = field(default_factory=dict)
43
+
44
+ def values(self):
45
+ """供 FeedbackCollector.add() 拆解: (passed, detail, score).
46
+
47
+ metadata 通过 ChannelFeedback 自身访问, 不在此拆解。
48
+ """
49
+ return (self.passed, self.detail, self.score)
50
+
51
+
52
+ @dataclass
53
+ class FactorFeedback:
54
+ """完整因子反馈 — QuantaAlpha CoSTEERSingleFeedback 等价物。"""
55
+ factor_id: str = field(default_factory=lambda: str(uuid.uuid4()))
56
+ factor_name: str = ""
57
+ channels: dict[FeedbackChannel, ChannelFeedback] = field(default_factory=dict)
58
+ decision: bool = False
59
+ summary: str = ""
60
+ timestamp: datetime = field(default_factory=datetime.now)
61
+ duration_ms: float = 0.0
62
+ metadata: dict = field(default_factory=dict)
63
+
64
+ def to_dict(self) -> dict:
65
+ """序列化为可 JSON 化的字典。"""
66
+ return {
67
+ "factor_id": self.factor_id,
68
+ "factor_name": self.factor_name,
69
+ "channels": {
70
+ ch.value: {
71
+ "channel": fb.channel.value,
72
+ "passed": fb.passed,
73
+ "detail": fb.detail,
74
+ "score": fb.score,
75
+ "metadata": dict(fb.metadata),
76
+ }
77
+ for ch, fb in self.channels.items()
78
+ },
79
+ "decision": self.decision,
80
+ "summary": self.summary,
81
+ "timestamp": self.timestamp.isoformat(),
82
+ "duration_ms": self.duration_ms,
83
+ "metadata": dict(self.metadata),
84
+ }
85
+
86
+ @classmethod
87
+ def from_dict(cls, d: dict) -> "FactorFeedback":
88
+ """从字典反序列化。"""
89
+ return cls(
90
+ factor_id=d["factor_id"],
91
+ factor_name=d["factor_name"],
92
+ channels={
93
+ FeedbackChannel(k): ChannelFeedback(
94
+ channel=FeedbackChannel(v["channel"]),
95
+ passed=v["passed"],
96
+ detail=v["detail"],
97
+ score=v["score"],
98
+ metadata=v.get("metadata", {}),
99
+ )
100
+ for k, v in d.get("channels", {}).items()
101
+ },
102
+ decision=d["decision"],
103
+ summary=d.get("summary", ""),
104
+ timestamp=datetime.fromisoformat(d["timestamp"]),
105
+ duration_ms=d.get("duration_ms", 0.0),
106
+ metadata=d.get("metadata", {}),
107
+ )
108
+
109
+ def save_json(self, path: Path) -> None:
110
+ """保存为 JSON 格式 (供调试)。"""
111
+ path = Path(path)
112
+ ensure_parent(path)
113
+ with path.open("w", encoding="utf-8") as f:
114
+ json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
115
+
116
+ @classmethod
117
+ def load_json(cls, path: Path) -> "FactorFeedback":
118
+ """从 JSON 加载。"""
119
+ with Path(path).open("r", encoding="utf-8") as f:
120
+ return cls.from_dict(json.load(f))
121
+
122
+ def to_parquet_row(self) -> dict:
123
+ """展平为单行 dict 供 Parquet 写入。"""
124
+ row = {
125
+ "factor_id": self.factor_id,
126
+ "factor_name": self.factor_name,
127
+ "decision": self.decision,
128
+ "summary": self.summary,
129
+ "duration_ms": self.duration_ms,
130
+ "timestamp": self.timestamp.isoformat(),
131
+ "metadata": json.dumps(self.metadata, ensure_ascii=False),
132
+ }
133
+ for ch in FeedbackChannel:
134
+ prefix = ch.value
135
+ fb = self.channels.get(ch)
136
+ row[f"{prefix}_passed"] = bool(fb.passed) if fb else None
137
+ row[f"{prefix}_score"] = float(fb.score) if fb else None
138
+ row[f"{prefix}_detail"] = str(fb.detail) if fb else None
139
+ return row
140
+
141
+ def save_parquet(self, path: Path) -> None:
142
+ """追加到 Parquet 文件 (或创建新文件)。"""
143
+ path = Path(path)
144
+ ensure_parent(path)
145
+ new_row = pd.DataFrame([self.to_parquet_row()])
146
+ if path.exists():
147
+ existing = pd.read_parquet(path)
148
+ combined = pd.concat([existing, new_row], ignore_index=True)
149
+ else:
150
+ combined = new_row
151
+ combined.to_parquet(path, index=False)
152
+
153
+ @classmethod
154
+ def load_parquet(cls, path: Path) -> list["FactorFeedback"]:
155
+ """从 Parquet 加载为 FactorFeedback 列表。"""
156
+ df = pd.read_parquet(path)
157
+ results = []
158
+ for _, row in df.iterrows():
159
+ channels = {}
160
+ for ch in FeedbackChannel:
161
+ prefix = ch.value
162
+ if pd.notna(row.get(f"{prefix}_passed")):
163
+ metadata = {}
164
+ if prefix == "execution" and "exit_code" in row:
165
+ metadata["exit_code"] = int(row["exit_code"])
166
+ channels[ch] = ChannelFeedback(
167
+ channel=ch,
168
+ passed=bool(row[f"{prefix}_passed"]),
169
+ detail=str(row[f"{prefix}_detail"]),
170
+ score=float(row[f"{prefix}_score"]),
171
+ metadata=metadata,
172
+ )
173
+ try:
174
+ metadata = json.loads(row.get("metadata", "{}"))
175
+ except (json.JSONDecodeError, TypeError):
176
+ metadata = {}
177
+ results.append(cls(
178
+ factor_id=str(row["factor_id"]),
179
+ factor_name=str(row["factor_name"]),
180
+ channels=channels,
181
+ decision=bool(row["decision"]),
182
+ summary=str(row["summary"]),
183
+ timestamp=datetime.fromisoformat(str(row["timestamp"])),
184
+ duration_ms=float(row["duration_ms"]),
185
+ metadata=metadata,
186
+ ))
187
+ return results
188
+
189
+
190
+ def ensure_feedback(result: Any, factor_id: str, factor_name: str) -> FactorFeedback:
191
+ """把节点返回的 dict 包装为 FactorFeedback (兼容现有节点)。
192
+
193
+ Args:
194
+ result: 节点返回值 (FactorFeedback / dict / 其他)
195
+ factor_id: 因子 UUID
196
+ factor_name: 因子名称
197
+
198
+ Returns:
199
+ FactorFeedback
200
+
201
+ Raises:
202
+ TypeError: result 类型不支持
203
+ """
204
+ if isinstance(result, FactorFeedback):
205
+ if not result.factor_id:
206
+ result.factor_id = factor_id
207
+ if not result.factor_name:
208
+ result.factor_name = factor_name
209
+ return result
210
+ if isinstance(result, dict):
211
+ metadata = {k: _safe_scalar(result[k]) for k in KNOWN_METRICS if k in result}
212
+ return FactorFeedback(
213
+ factor_id=factor_id,
214
+ factor_name=factor_name,
215
+ decision=True,
216
+ summary=f"dict 返回, {len(result)} 个字段",
217
+ metadata=metadata,
218
+ )
219
+ raise TypeError(f"节点返回类型不支持: {type(result).__name__}")
220
+
221
+
222
+ def _safe_scalar(v: Any) -> Any:
223
+ """转 pd.Series/np.ndarray 标量为 Python scalar。"""
224
+ if isinstance(v, pd.Series):
225
+ if len(v) == 0:
226
+ return None
227
+ v = v.iloc[0]
228
+ if isinstance(v, (np.ndarray,)):
229
+ if v.size == 0:
230
+ return None
231
+ v = v.flat[0]
232
+ if isinstance(v, (np.integer,)):
233
+ return int(v)
234
+ if isinstance(v, (np.floating,)):
235
+ f = float(v)
236
+ return f if np.isfinite(f) else None
237
+ if isinstance(v, (np.bool_,)):
238
+ return bool(v)
239
+ return v
@@ -0,0 +1,138 @@
1
+ """LLMJudge — LLM 一致性评判 (Week 1 mock 实现)。
2
+
3
+ Mock 模式: 简单启发式检查 (hypothesis + expression 长度相关)
4
+ 真实模式 (后续): 调用 deepseek-v3 / Claude / GPT-4o
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import Optional
10
+
11
+ from .dataclass import ChannelFeedback, FeedbackChannel
12
+
13
+
14
+ class LLMJudge:
15
+ """LLM 一致性评判器 — hypothesis ↔ description ↔ expression。
16
+
17
+ Args:
18
+ model: 模型名 (mock/real)
19
+ max_correction_attempts: 解析失败时重试次数
20
+ llm_callable: 真实 LLM 调用函数, 接受 prompt 返回 string
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ model: str = "mock",
26
+ max_correction_attempts: int = 3,
27
+ llm_callable: Optional[callable] = None,
28
+ ):
29
+ self.model = model
30
+ self.max_correction_attempts = max_correction_attempts
31
+ if llm_callable is None and model != "mock":
32
+ raise ValueError(
33
+ f"model={model!r} requires an explicit llm_callable. "
34
+ "Inject via get_llm_gateway() at the call site."
35
+ )
36
+ self._llm_callable = llm_callable
37
+
38
+ def judge(
39
+ self,
40
+ hypothesis: str,
41
+ description: str,
42
+ expression: str,
43
+ ) -> ChannelFeedback:
44
+ """评判三者一致性。"""
45
+ prompt = self._build_prompt(hypothesis, description, expression)
46
+ for attempt in range(self.max_correction_attempts + 1):
47
+ try:
48
+ raw = self._call(prompt)
49
+ result = json.loads(raw)
50
+ return ChannelFeedback(
51
+ channel=FeedbackChannel.LLM,
52
+ passed=bool(result["consistent"]),
53
+ detail=str(result.get("reason", "")),
54
+ score=float(result.get("score", 1.0 if result["consistent"] else 0.0)),
55
+ metadata={"model": self.model, "attempt": attempt + 1},
56
+ )
57
+ except (json.JSONDecodeError, KeyError, TypeError) as e:
58
+ if attempt == self.max_correction_attempts:
59
+ return ChannelFeedback(
60
+ channel=FeedbackChannel.LLM,
61
+ passed=False,
62
+ detail=f"LLM 解析失败: {e}",
63
+ score=0.0,
64
+ metadata={"model": self.model, "attempt": attempt + 1},
65
+ )
66
+ continue
67
+
68
+ def _build_prompt(self, h: str, d: str, e: str) -> str:
69
+ return (
70
+ "判断以下三者是否逻辑一致:\n"
71
+ f"Hypothesis (研究假设): {h}\n"
72
+ f"Description (因子描述): {d}\n"
73
+ f"Expression (代码表达式): {e}\n\n"
74
+ '返回 JSON: {"consistent": true/false, "reason": "理由", "score": 0-1}'
75
+ )
76
+
77
+ def _call(self, prompt: str) -> str:
78
+ """调用 LLM (mock 或真实)。"""
79
+ if self._llm_callable is not None:
80
+ return self._llm_callable(prompt)
81
+ if self.model == "mock":
82
+ return self._mock_call(prompt)
83
+ raise NotImplementedError(
84
+ "真实 LLM 调用未实现, 请提供 llm_callable 或使用 model='mock'"
85
+ )
86
+
87
+ @staticmethod
88
+ def _mock_call(prompt: str) -> str:
89
+ """Mock 实现: 启发式检查。
90
+
91
+ 规则:
92
+ - hypothesis 和 description 都为空 -> 不一致
93
+ - expression 为空 -> 不一致
94
+ - 出现 'momentum' / '反转' 关键词 + 表达式含 'returns' -> 一致
95
+ - 其他: 简单长度匹配
96
+ """
97
+ h = _extract_field(prompt, "Hypothesis")
98
+ d = _extract_field(prompt, "Description")
99
+ e = _extract_field(prompt, "Expression")
100
+
101
+ if not e:
102
+ return json.dumps({"consistent": False, "reason": "表达式为空", "score": 0.0})
103
+ if not h and not d:
104
+ return json.dumps({
105
+ "consistent": False,
106
+ "reason": "hypothesis 和 description 都为空",
107
+ "score": 0.0,
108
+ })
109
+
110
+ keywords_h = {"momentum", "反转", "反转", "波动", "volume", "量价", "动量"}
111
+ text = (h + " " + d).lower()
112
+ matched = sum(1 for kw in keywords_h if kw.lower() in text)
113
+ if matched > 0 and ("returns" in e or "close" in e or "open" in e):
114
+ return json.dumps({
115
+ "consistent": True,
116
+ "reason": "关键词匹配, 假设与表达式使用相关字段",
117
+ "score": 0.85,
118
+ })
119
+ return json.dumps({
120
+ "consistent": True,
121
+ "reason": "mock 默认通过 (无法判定)",
122
+ "score": 0.7,
123
+ })
124
+
125
+
126
+ def _extract_field(prompt: str, field_name: str) -> str:
127
+ """Extract a single field value from a prompt.
128
+
129
+ Format: '<FieldName> ... : <value>\\n<next line>\\n...'.
130
+ Captures the rest of the line after ':' (may be empty).
131
+ """
132
+ lines = prompt.split("\n")
133
+ for line in lines:
134
+ if line.startswith(field_name):
135
+ if ":" not in line:
136
+ return ""
137
+ return line.split(":", 1)[1].strip()
138
+ return ""