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,667 @@
1
+ # coding=utf-8
2
+ """Composite DAG operators — DAG 模板复合算子 (PR-QN-3a, 2026-06-21)
3
+
4
+ Level 1 抽象: 介于 primitive ops (L0) 和 业务语义 (L3) 之间.
5
+ 由 @composite_operator 装饰器注册, 统一通过 get_operator() 查询,
6
+ 并用 is_composite=True 标记位与 multi_section 等 L0 ops 区分.
7
+
8
+ 设计要点:
9
+ - ParamSpec: 参数 schema (name / type / default / required / description)
10
+ - CompositeSpec: DAG 模板 + 参数 schema + 文档 + 例子
11
+ - _CompositeRegistry: 隔离注册表, 与 _CustomOperatorRegistry 平级
12
+ - composite_operator: 用户自定义入口
13
+ - load_composites_from_yaml: YAML 扩展入口 (ast 解析, 非裸 exec)
14
+ - get_composite_doc_for_llm: 给 LLM prompt 用的 markdown 文档
15
+
16
+ 对齐规范: docs/22-算子系统设计与规范.md §十七
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ import functools
22
+ from collections.abc import Iterator
23
+ from dataclasses import dataclass, field
24
+ from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
25
+
26
+ if TYPE_CHECKING:
27
+ from polars import Expr
28
+
29
+
30
+ # ============== 参数 Schema ==============
31
+
32
+ @dataclass(frozen=True)
33
+ class ParamSpec:
34
+ """Composite 参数 schema.
35
+
36
+ Attributes:
37
+ name: 参数名
38
+ type_hint: "expr" | "int" | "float" | "str" | "bool"
39
+ default: 默认值 (None 表示无默认)
40
+ required: 是否必填
41
+ description: 用于 LLM prompt 的描述
42
+ """
43
+ name: str
44
+ type_hint: str = "expr"
45
+ default: Any = None
46
+ required: bool = False
47
+ description: str = ""
48
+
49
+ def validate(self, value: Any) -> None:
50
+ """运行时类型校验.
51
+
52
+ Raises:
53
+ ValueError: 必填参数为 None
54
+ TypeError: 类型不匹配
55
+ """
56
+ if value is None and self.required:
57
+ raise ValueError(f"Composite param '{self.name}' is required")
58
+ type_check = {"int": int, "float": float, "str": str, "bool": bool}
59
+ if self.type_hint in type_check and not isinstance(value, type_check[self.type_hint]):
60
+ raise TypeError(
61
+ f"Composite param '{self.name}' must be {self.type_hint}, "
62
+ f"got {type(value).__name__}"
63
+ )
64
+
65
+
66
+ # ============== Composite Spec ==============
67
+
68
+ @dataclass(frozen=True)
69
+ class CompositeSpec:
70
+ """DAG 模板复合算子.
71
+
72
+ Attributes:
73
+ name: 唯一标识 (如 "industry_neutralize")
74
+ template: 接收 **params, 返回 polars.Expr 或 pd.Series 的函数
75
+ category: 复用 QuantNodes 的 5 类之一 (默认 multi_section)
76
+ engine: 引擎类型 ("polars" | "pandas"), 同名 op 分引擎注册
77
+ params: 参数 schema 字典
78
+ doc: 文档 (用于 LLM prompt)
79
+ examples: LLM few-shot 例子
80
+ """
81
+ name: str
82
+ template: Callable[..., Any]
83
+ category: str = "multi_section"
84
+ engine: str = "polars"
85
+ params: Dict[str, ParamSpec] = field(default_factory=dict)
86
+ doc: str = ""
87
+ examples: List[dict] = field(default_factory=list)
88
+
89
+ def instantiate(self, **kwargs: Any) -> "Expr":
90
+ """参数化实例化: 校验 + 填默认 + 调用 template.
91
+
92
+ Raises:
93
+ ValueError: 必填参数缺失
94
+ TypeError: 参数类型不匹配
95
+ """
96
+ bound = dict(kwargs)
97
+ for pname, pspec in self.params.items():
98
+ if pname in bound:
99
+ pspec.validate(bound[pname])
100
+ elif pspec.required:
101
+ raise ValueError(f"Missing required param: {pname}")
102
+ elif pspec.default is not None:
103
+ bound[pname] = pspec.default
104
+ return self.template(**bound)
105
+
106
+ def to_dict(self) -> dict:
107
+ """序列化为 dict (用于 LLM prompt / JSON 持久化)."""
108
+ return {
109
+ "name": self.name,
110
+ "category": self.category,
111
+ "doc": self.doc,
112
+ "params": {
113
+ pname: {
114
+ "type": pspec.type_hint,
115
+ "default": pspec.default,
116
+ "required": pspec.required,
117
+ "description": pspec.description,
118
+ }
119
+ for pname, pspec in self.params.items()
120
+ },
121
+ "examples": self.examples,
122
+ }
123
+
124
+
125
+ # ============== 注册表 ==============
126
+
127
+ class _CompositeRegistry:
128
+ """Composite op 注册表 (与 _CustomOperatorRegistry 隔离但接口对齐)."""
129
+
130
+ def __init__(self) -> None:
131
+ self._registry: Dict[str, CompositeSpec] = {}
132
+
133
+ def register(self, spec: CompositeSpec) -> None:
134
+ """注册一个 composite.
135
+
136
+ Raises:
137
+ ValueError: name 已注册 (重复)
138
+
139
+ Note:
140
+ Composite 隔离存放在 ``_COMPOSITE_REGISTRY`` 中, **不**注入到主
141
+ ``_OPERATOR_REGISTRY``. 原因:
142
+
143
+ 1. 主注册表存 L0 primitive, schema 严格要求 ``signature`` /
144
+ ``parameters`` 等键 (见 ``test_all_operators_have_doc``).
145
+ 2. 主注册表需 JSON 可序列化 (见 ``generate_documentation_json``),
146
+ CompositeSpec 不可序列化.
147
+ 3. composite 应通过 ``is_composite_op`` / ``get_composite_spec`` /
148
+ ``list_composite_ops`` 三套独立 API 访问, 与 L0 严格隔离.
149
+ """
150
+ if spec.name in self._registry:
151
+ raise ValueError(f"Composite '{spec.name}' already registered")
152
+ self._registry[spec.name] = spec
153
+
154
+ def _build_param_specs(self, params_dict: Dict[str, dict]) -> Dict[str, ParamSpec]:
155
+ """从用户传入的 dict 构造 ParamSpec (兼容 'type' 字段)."""
156
+ out: Dict[str, ParamSpec] = {}
157
+ for pname, pdict in params_dict.items():
158
+ # 兼容 'type' 字段 (与 type_hint 同义, 避免 Python 关键字冲突)
159
+ pdict = dict(pdict)
160
+ if "type" in pdict and "type_hint" not in pdict:
161
+ pdict["type_hint"] = pdict.pop("type")
162
+ out[pname] = ParamSpec(name=pname, **pdict)
163
+ return out
164
+
165
+ def get(self, name: str) -> Optional[CompositeSpec]:
166
+ return self._registry.get(name)
167
+
168
+ def list(self, category: Optional[str] = None) -> List[str]:
169
+ if category:
170
+ return [n for n, s in self._registry.items() if s.category == category]
171
+ return list(self._registry.keys())
172
+
173
+ def all_specs(self) -> Iterator[CompositeSpec]:
174
+ return iter(self._registry.values())
175
+
176
+
177
+ _COMPOSITE_REGISTRY = _CompositeRegistry()
178
+ _COMPOSITE_REGISTRY_POLARS = _COMPOSITE_REGISTRY # alias for clarity
179
+ _COMPOSITE_REGISTRY_PANDAS = _CompositeRegistry()
180
+
181
+
182
+ # ============== 装饰器 ==============
183
+
184
+ def composite_operator(
185
+ name: str,
186
+ category: str = "multi_section",
187
+ params: Optional[Dict[str, dict]] = None,
188
+ doc: str = "",
189
+ examples: Optional[List[dict]] = None,
190
+ engine: str = "polars",
191
+ ):
192
+ """注册 DAG 模板复合算子.
193
+
194
+ Args:
195
+ name: 算子唯一名
196
+ category: 5 类之一 (默认 multi_section, 与 L0 共存)
197
+ params: {pname: {type, default, required, description}}
198
+ doc: 文档 (LLM prompt 用)
199
+ examples: LLM few-shot 例子
200
+ engine: 引擎类型 ("polars" | "pandas"), 同名 op 分引擎注册
201
+
202
+ Returns:
203
+ 装饰器函数
204
+
205
+ Example:
206
+ @composite_operator(
207
+ name="industry_neutralize",
208
+ params={
209
+ "x": {"type": "expr", "required": True},
210
+ "industry_col": {"type": "str", "default": "citic_1"},
211
+ },
212
+ doc="行业中性化: x 减去行业内均值",
213
+ )
214
+ def industry_neutralize(x: Expr, industry_col: str = "citic_1") -> Expr:
215
+ return x - x.group_by(industry_col).mean()
216
+ """
217
+ def decorator(func: Callable) -> Callable:
218
+ param_specs = _COMPOSITE_REGISTRY._build_param_specs(params or {})
219
+ spec = CompositeSpec(
220
+ name=name,
221
+ template=func,
222
+ category=category,
223
+ engine=engine,
224
+ params=param_specs,
225
+ doc=doc or (func.__doc__ or ""),
226
+ examples=examples or [],
227
+ )
228
+ if engine == "pandas":
229
+ _COMPOSITE_REGISTRY_PANDAS.register(spec)
230
+ else:
231
+ _COMPOSITE_REGISTRY.register(spec)
232
+
233
+ @functools.wraps(func)
234
+ def wrapper(*args: Any, **kwargs: Any) -> "Expr":
235
+ return spec.instantiate(**kwargs)
236
+
237
+ wrapper.__composite_spec__ = spec # type: ignore[attr-defined]
238
+ return wrapper
239
+
240
+ return decorator
241
+
242
+
243
+ # ============== 查询接口 ==============
244
+
245
+ def is_composite_op(name: str, engine: str = "any") -> bool:
246
+ """判断 op 是否是 composite.
247
+
248
+ Args:
249
+ name: 算子名
250
+ engine: "any" (default, union) | "polars" | "pandas"
251
+
252
+ PR-QN-3a: composite 完全隔离存放, 不污染主 ``_OPERATOR_REGISTRY``.
253
+ PR-QN-4: engine="any" 查双 registry (union), engine="polars"/"pandas" 分查.
254
+ """
255
+ if engine == "polars":
256
+ return name in _COMPOSITE_REGISTRY_POLARS.list()
257
+ if engine == "pandas":
258
+ return name in _COMPOSITE_REGISTRY_PANDAS.list()
259
+ # engine == "any"
260
+ return name in _COMPOSITE_REGISTRY.list() or name in _COMPOSITE_REGISTRY_PANDAS.list()
261
+
262
+
263
+ def get_composite_spec(name: str, engine: str = "any") -> Optional[CompositeSpec]:
264
+ """获取 composite spec (用于 LLM 编译时的 schema 查询).
265
+
266
+ Args:
267
+ name: 算子名
268
+ engine: "any" (default, first found) | "polars" | "pandas"
269
+ """
270
+ if engine == "polars":
271
+ return _COMPOSITE_REGISTRY_POLARS.get(name)
272
+ if engine == "pandas":
273
+ return _COMPOSITE_REGISTRY_PANDAS.get(name)
274
+ # engine == "any": prefer polars, fallback pandas
275
+ return _COMPOSITE_REGISTRY.get(name) or _COMPOSITE_REGISTRY_PANDAS.get(name)
276
+
277
+
278
+ def list_composite_ops(category: Optional[str] = None, engine: str = "any") -> List[str]:
279
+ """列出所有 composite ops (可选按 category + engine 过滤).
280
+
281
+ Args:
282
+ category: 按类别过滤
283
+ engine: "any" (default, union) | "polars" | "pandas"
284
+ """
285
+ if engine == "polars":
286
+ return _COMPOSITE_REGISTRY_POLARS.list(category=category)
287
+ if engine == "pandas":
288
+ return _COMPOSITE_REGISTRY_PANDAS.list(category=category)
289
+ # engine == "any": union — sorted for deterministic order across runs
290
+ # (set ordering depends on PYTHONHASHSEED which is randomized by default).
291
+ # v2.9.1: stable ordering eliminates flake in tests that index list[0].
292
+ polars_ops = set(_COMPOSITE_REGISTRY_POLARS.list(category=category))
293
+ pandas_ops = set(_COMPOSITE_REGISTRY_PANDAS.list(category=category))
294
+ return sorted(polars_ops | pandas_ops)
295
+
296
+
297
+ def get_composite_doc_for_llm(engine: str = "any") -> str:
298
+ """生成给 LLM prompt 的 composite 文档 (markdown 格式).
299
+
300
+ Args:
301
+ engine: "any" (default, all ops) | "polars" | "pandas"
302
+
303
+ Phase 1.5: 内部委托 LLMDocVisitor, 保持向后兼容的输出格式。
304
+ """
305
+ visitor = LLMDocVisitor()
306
+ if engine == "polars":
307
+ specs = _COMPOSITE_REGISTRY_POLARS.all_specs()
308
+ elif engine == "pandas":
309
+ specs = _COMPOSITE_REGISTRY_PANDAS.all_specs()
310
+ else:
311
+ # engine == "any": polars first, then pandas (dedup by name)
312
+ seen: set = set()
313
+ specs_list = []
314
+ for spec in _COMPOSITE_REGISTRY_POLARS.all_specs():
315
+ if spec.name not in seen:
316
+ seen.add(spec.name)
317
+ specs_list.append(spec)
318
+ for spec in _COMPOSITE_REGISTRY_PANDAS.all_specs():
319
+ if spec.name not in seen:
320
+ seen.add(spec.name)
321
+ specs_list.append(spec)
322
+ specs = iter(specs_list)
323
+ for spec in specs:
324
+ visitor.visit_spec(spec)
325
+ return visitor.result
326
+
327
+
328
+ # ============== Visitor Pattern (Phase 1.5) ==============
329
+
330
+ class CompositeSpecVisitor:
331
+ """CompositeSpec 的访问者基类 (Phase 1.5, Visitor pattern).
332
+
333
+ 用途:
334
+ - 统一访问 _COMPOSITE_REGISTRY 中所有 CompositeSpec
335
+ - 不修改 CompositeSpec 即可扩展新的遍历/分析能力
336
+ - 具体子类: LLMDocVisitor / DependencyVisitor / ValidationVisitor
337
+
338
+ 使用:
339
+ >>> visitor = LLMDocVisitor()
340
+ >>> for spec in _COMPOSITE_REGISTRY.all_specs():
341
+ ... visitor.visit_spec(spec)
342
+ >>> print(visitor.result)
343
+ """
344
+
345
+ def visit_spec(self, spec: CompositeSpec) -> None:
346
+ """访问一个 CompositeSpec。子类重写此方法实现具体逻辑。"""
347
+ raise NotImplementedError
348
+
349
+ def visit_all(self) -> None:
350
+ """便利方法: 遍历整个 _COMPOSITE_REGISTRY。"""
351
+ for spec in _COMPOSITE_REGISTRY.all_specs():
352
+ self.visit_spec(spec)
353
+
354
+
355
+ class LLMDocVisitor(CompositeSpecVisitor):
356
+ """为 LLM prompt 生成 markdown 格式的 composite 文档。
357
+
358
+ 输出格式与原 get_composite_doc_for_llm() 完全一致 (向后兼容)。
359
+ """
360
+
361
+ def __init__(self) -> None:
362
+ self.lines: List[str] = ["# Available Composite Operators"]
363
+
364
+ def visit_spec(self, spec: CompositeSpec) -> None:
365
+ self.lines.append(f"## {spec.name}")
366
+ self.lines.append(f" {spec.doc}")
367
+ for pname, pspec in spec.params.items():
368
+ if pspec.required:
369
+ tag = "(required)"
370
+ elif pspec.default is not None:
371
+ tag = f"(default: {pspec.default})"
372
+ else:
373
+ tag = "(optional)"
374
+ self.lines.append(
375
+ f" - {pname}: {pspec.type_hint} {tag} — {pspec.description}"
376
+ )
377
+ if spec.examples:
378
+ self.lines.append(f" Example: {spec.examples[0]}")
379
+ self.lines.append("")
380
+
381
+ @property
382
+ def result(self) -> str:
383
+ return "\n".join(self.lines)
384
+
385
+
386
+ class DependencyVisitor(CompositeSpecVisitor):
387
+ """提取 composite 之间的依赖图 (基于 template 源码中的函数名引用)。
388
+
389
+ 输出: dict[name, set[dependency_name]], 边从 spec 指向被引用的其他 composite。
390
+ 目前实现是粗略的: 用 inspect.getsource() 提取 template 函数源码,
391
+ 匹配其他 composite 的 name 字符串。
392
+
393
+ 用途:
394
+ - DAG 可视化
395
+ - 检测循环依赖
396
+ - 增量重编译优化
397
+ """
398
+
399
+ def __init__(self) -> None:
400
+ self.graph: Dict[str, set] = {}
401
+
402
+ def visit_spec(self, spec: CompositeSpec) -> None:
403
+ deps: set = set()
404
+ try:
405
+ import inspect
406
+ source = inspect.getsource(spec.template)
407
+ for other_name in _COMPOSITE_REGISTRY.list():
408
+ if other_name == spec.name:
409
+ continue
410
+ if other_name in source:
411
+ deps.add(other_name)
412
+ except (OSError, TypeError):
413
+ pass
414
+ self.graph[spec.name] = deps
415
+
416
+ def detect_cycles(self) -> List[List[str]]:
417
+ """返回所有循环依赖路径, 每条路径是 list of names."""
418
+ cycles: List[List[str]] = []
419
+ visited: set = set()
420
+ path: List[str] = []
421
+
422
+ def dfs(node: str) -> None:
423
+ if node in path:
424
+ cycle_start = path.index(node)
425
+ cycles.append(path[cycle_start:] + [node])
426
+ return
427
+ if node in visited:
428
+ return
429
+ path.append(node)
430
+ for nxt in self.graph.get(node, ()):
431
+ dfs(nxt)
432
+ path.pop()
433
+ visited.add(node)
434
+
435
+ for n in self.graph:
436
+ dfs(n)
437
+ return cycles
438
+
439
+
440
+ class ValidationVisitor(CompositeSpecVisitor):
441
+ """检查 CompositeSpec 的语义正确性。
442
+
443
+ 校验:
444
+ - spec.name 唯一 (注册时已保证, 这里双重检查)
445
+ - 必填参数 (required=True) 没有 default (避免语义冲突)
446
+ - 文档非空 (LLM prompt 友好)
447
+ - examples 数量 > 0 (推荐)
448
+ """
449
+
450
+ def __init__(self) -> None:
451
+ self.errors: List[str] = []
452
+ self.warnings: List[str] = []
453
+
454
+ def visit_spec(self, spec: CompositeSpec) -> None:
455
+ # 必填参数不应该有 default
456
+ for pname, pspec in spec.params.items():
457
+ if pspec.required and pspec.default is not None:
458
+ self.errors.append(
459
+ f"Composite '{spec.name}' param '{pname}' is required but has "
460
+ f"default={pspec.default!r} (语义冲突)"
461
+ )
462
+ # 文档空 → warning
463
+ if not spec.doc.strip():
464
+ self.warnings.append(
465
+ f"Composite '{spec.name}' has empty doc (LLM prompt 会缺少说明)"
466
+ )
467
+ # 没有 examples → warning
468
+ if not spec.examples:
469
+ self.warnings.append(
470
+ f"Composite '{spec.name}' has no examples (LLM few-shot 会少)"
471
+ )
472
+
473
+ @property
474
+ def has_errors(self) -> bool:
475
+ return bool(self.errors)
476
+
477
+
478
+ # ============== 用户 YAML 扩展 ==============
479
+
480
+ def load_composites_from_yaml(yaml_path: str) -> int:
481
+ """从 YAML 文件加载用户自定义 composite ops.
482
+
483
+ YAML 格式 (见 docs/22 §十七):
484
+ composites:
485
+ - name: my_op
486
+ category: multi_section
487
+ doc: "我的 op"
488
+ params:
489
+ x: {type: expr, required: true}
490
+ k: {type: float, default: 1.0}
491
+ template: "x + k"
492
+
493
+ Returns:
494
+ 加载的 composite 数量
495
+ """
496
+ import yaml
497
+ from pathlib import Path
498
+ p = Path(yaml_path)
499
+ if not p.exists():
500
+ raise FileNotFoundError(f"YAML file not found: {yaml_path}")
501
+ with open(p, encoding="utf-8") as f:
502
+ data = yaml.safe_load(f)
503
+ if not data or "composites" not in data:
504
+ return 0
505
+ count = 0
506
+ for entry in data["composites"]:
507
+ template_str = entry.get("template")
508
+ if not template_str:
509
+ continue
510
+ engine = entry.get("engine", "polars") # 缺省 = "polars" (向后兼容)
511
+ template = _compile_template_string(template_str, engine=engine)
512
+ param_specs = _COMPOSITE_REGISTRY._build_param_specs(
513
+ entry.get("params", {})
514
+ )
515
+ spec = CompositeSpec(
516
+ name=entry["name"],
517
+ template=template,
518
+ category=entry.get("category", "multi_section"),
519
+ engine=engine,
520
+ params=param_specs,
521
+ doc=entry.get("doc", ""),
522
+ examples=entry.get("examples", []),
523
+ )
524
+ if engine == "pandas":
525
+ _COMPOSITE_REGISTRY_PANDAS.register(spec)
526
+ else:
527
+ _COMPOSITE_REGISTRY.register(spec)
528
+ count += 1
529
+ return count
530
+
531
+
532
+ # 允许的 Expr / 函数名白名单 (YAML template 解析用, polars 引擎)
533
+ _ALLOWED_FUNC_NAMES: set = {
534
+ "col", "lit", "when", "otherwise", "then",
535
+ "abs", "log", "sqrt", "pow", "exp",
536
+ "rolling_mean", "rolling_std", "rolling_corr",
537
+ "rolling_sum", "rolling_min", "rolling_max", "rolling_median",
538
+ "ewm_mean", "ewm_std",
539
+ "shift", "diff", "pct_change", "rank",
540
+ "mean", "std", "sum", "min", "max", "median", "quantile",
541
+ "count", "first", "last",
542
+ "group_by", "over", "alias",
543
+ "clip", "fill_null", "fill_nan", "drop_nulls", "drop_nans",
544
+ "is_null", "is_nan", "is_not_null",
545
+ "round", "floor", "ceil",
546
+ "and_", "or_", "not_",
547
+ }
548
+
549
+ # 禁止的 base name (防止 ``os.system`` / ``subprocess.run`` 等)
550
+ _DENIED_BASE_NAMES: set = {
551
+ "os", "sys", "subprocess", "socket", "urllib", "requests",
552
+ "shutil", "pathlib", "path", "open", "file", "io",
553
+ "importlib", "builtins", "eval", "exec", "compile",
554
+ "getattr", "setattr", "delattr", "globals", "locals",
555
+ }
556
+
557
+
558
+ def _compile_template_string(template_str: str, engine: str = "polars") -> Callable[..., Any]:
559
+ """把字符串模板编译为 callable (AST 解析 + 白名单校验).
560
+
561
+ 模板形式: 单个表达式, 引用 params dict 中的 key, 如 ``x + k`` 或
562
+ ``x.group_by(industry_col).mean()``. 编译为 ``def _t(x, k, industry_col):
563
+ return <expr>`` 形式, 可被 ``template(**bound)`` 调用.
564
+
565
+ 与 CodeSandbox 配合: 这里**只**解析 Expr 调用链, 不执行任意 Python.
566
+ PR-QN-3a 修复: 文档原版用裸 exec, 会被 CodeSandbox 拒绝, 改用 ast.parse
567
+ + 节点类型白名单.
568
+ PR-QN-4: engine 参数选择正确的白名单 (polars vs pandas, 严格分流).
569
+ """
570
+ try:
571
+ tree = ast.parse(template_str, mode="eval")
572
+ except SyntaxError as e:
573
+ raise ValueError(f"YAML template 语法错误: {e}") from e
574
+
575
+ # 提取表达式中所有自由变量作为函数参数
576
+ free_vars = _extract_free_vars(tree.body)
577
+ # PR-QN-4: 严格分流白名单
578
+ if engine == "pandas":
579
+ from ._engine import ALLOWED_FUNC_NAMES_PANDAS
580
+ allowed_funcs = ALLOWED_FUNC_NAMES_PANDAS
581
+ else:
582
+ allowed_funcs = _ALLOWED_FUNC_NAMES
583
+ # 白名单校验: 仅允许 Name / Call / Attribute / Constant / BinOp
584
+ _validate_ast_nodes(tree.body, allowed_funcs=allowed_funcs)
585
+
586
+ # 编译为函数
587
+ func_name = "_composite_template"
588
+ params_str = ", ".join(free_vars) if free_vars else ""
589
+ code = f"def {func_name}({params_str}):\n return {template_str}\n"
590
+ namespace: dict = {}
591
+ exec(code, namespace) # noqa: S102 — AST 已校验, 安全
592
+ return namespace[func_name]
593
+
594
+
595
+ def _extract_free_vars(node: ast.AST) -> List[str]:
596
+ """从 AST 节点提取所有 Name 节点 (去重, 保序)."""
597
+ seen: set = set()
598
+ out: List[str] = []
599
+ for child in ast.walk(node):
600
+ if isinstance(child, ast.Name) and child.id not in seen:
601
+ seen.add(child.id)
602
+ out.append(child.id)
603
+ return out
604
+
605
+
606
+ def _validate_chain_base(attr: ast.Attribute, allowed_funcs: set) -> None:
607
+ """校验链式调用的 base 节点 (PR-QN-4).
608
+
609
+ 链式调用可以是:
610
+ - polars: x.method() — base is Name
611
+ - pandas: x.groupby('a').mean() — base is Call (groupby returns a GroupBy)
612
+ - deep chain: x.rolling(10).mean() — base is Call
613
+
614
+ 校验逻辑: 每个 Attribute.attr 都必须在白名单中, 递归验证整个链.
615
+ """
616
+ # 当前层方法名必须在白名单
617
+ if attr.attr not in allowed_funcs:
618
+ raise ValueError(
619
+ f"YAML template 调用了不允许的方法: {attr.attr!r}. "
620
+ f"允许: {sorted(allowed_funcs)[:10]}..."
621
+ )
622
+ # 递归验证 base
623
+ if isinstance(attr.value, ast.Name):
624
+ if attr.value.id in _DENIED_BASE_NAMES:
625
+ raise ValueError(
626
+ f"YAML template 调用了禁止的 base: "
627
+ f"{attr.value.id!r}.{attr.attr}"
628
+ )
629
+ elif isinstance(attr.value, ast.Call):
630
+ if isinstance(attr.value.func, ast.Attribute):
631
+ _validate_chain_base(attr.value.func, allowed_funcs)
632
+ elif isinstance(attr.value.func, ast.Name):
633
+ if attr.value.func.id not in allowed_funcs:
634
+ raise ValueError(
635
+ f"YAML template 调用了不允许的函数: {attr.value.func.id!r}. "
636
+ f"允许: {sorted(allowed_funcs)[:10]}..."
637
+ )
638
+ else:
639
+ raise ValueError(
640
+ f"YAML template 不支持的链式属性: {ast.dump(attr)[:80]}"
641
+ )
642
+
643
+
644
+ def _validate_ast_nodes(node: ast.AST, allowed_funcs: set) -> None:
645
+ """递归校验 AST 节点, 仅允许白名单内的函数名.
646
+
647
+ Name 节点 = 自由变量 (作函数参数, 不在白名单中) 或白名单内的函数.
648
+ Call 节点的 func 必须是白名单内的.
649
+ Attribute 访问: 支持链式方法调用 (x.method() 或 x.method().method2()).
650
+ PR-QN-4: 支持 pandas 风格链式调用 (x.groupby('a').mean()).
651
+ """
652
+ for child in ast.walk(node):
653
+ if isinstance(child, ast.Call):
654
+ if isinstance(child.func, ast.Name) and child.func.id not in allowed_funcs:
655
+ raise ValueError(
656
+ f"YAML template 调用了不允许的函数: {child.func.id!r}. "
657
+ f"允许: {sorted(allowed_funcs)[:10]}..."
658
+ )
659
+ elif isinstance(child.func, ast.Attribute):
660
+ # 链式方法调用: x.method() 或 x.method().method2()
661
+ # PR-QN-4: base 可以是 Name (polars) 或 Call (pandas chain)
662
+ _validate_chain_base(child.func, allowed_funcs)
663
+ elif isinstance(child, ast.Attribute):
664
+ # Attribute 访问: base 是 Name (变量) 或 Call (链式)
665
+ _validate_chain_base(child, allowed_funcs)
666
+ elif isinstance(child, (ast.Import, ast.ImportFrom, ast.Lambda, ast.FunctionDef)):
667
+ raise ValueError(f"YAML template 不允许: {type(child).__name__}")