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,265 @@
1
+ """ProcessPool 工作器 — 顶层 pickle-safe 函数。
2
+
3
+ 设计:
4
+ 主进程:
5
+ 1. 序列化 config dict + context dict (DataFrames) → 临时 pickle 文件
6
+ 2. 提交 subprocess_evaluate(candidate_dict, snapshot_path) 到 ProcessPool
7
+ 子进程:
8
+ 1. 从 snapshot_path 加载 config + context
9
+ 2. 重建 DataLoader (从 context 中的 DataFrames)
10
+ 3. 跑 Phase 2-11 (节点 2-11)
11
+ 4. 返回结果 dict
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import pickle
16
+ from pathlib import Path
17
+ from QuantNodes.core.path_utils import ensure_parent
18
+
19
+
20
+ # ============================================================================
21
+ # 顶层 pickle-safe 函数 (子进程入口)
22
+ # ============================================================================
23
+
24
+ def subprocess_evaluate(
25
+ candidate_dict: dict,
26
+ snapshot_path: str,
27
+ ) -> dict:
28
+ """在子进程中执行评估, 接受候选 dict + 快照路径, 返回结果 dict。
29
+
30
+ 这是顶层函数, 可被 pickle 传给 ProcessPoolExecutor。
31
+
32
+ Args:
33
+ candidate_dict: FactorCandidate 的 dict 形式
34
+ snapshot_path: 预序列化的 config+context 路径
35
+
36
+ Returns:
37
+ dict: {passed, metrics, feedback_dict, error}
38
+ """
39
+ try:
40
+ return _evaluate_in_subprocess(candidate_dict, snapshot_path)
41
+ except Exception as e:
42
+ import traceback
43
+ return {
44
+ "passed": False,
45
+ "metrics": {},
46
+ "feedback_dict": None,
47
+ "error": f"{type(e).__name__}: {e}\n{traceback.format_exc()[-500:]}",
48
+ }
49
+
50
+
51
+ def _evaluate_in_subprocess(candidate_dict: dict, snapshot_path: str) -> dict:
52
+ """子进程内部实现。"""
53
+ import pandas as pd
54
+ import numpy as np
55
+
56
+ # 1. 加载快照
57
+ snapshot = pickle.loads(Path(snapshot_path).read_bytes())
58
+ config = snapshot["config"]
59
+ context = snapshot["context"]
60
+ factor_override_name = snapshot["factor_name"]
61
+ factor_override_expr = snapshot.get("factor_expression", "")
62
+
63
+ # 2. 重建因子数据
64
+ factor_path = snapshot.get("factor_path")
65
+ if factor_path and Path(factor_path).exists():
66
+ factor = pd.read_hdf(factor_path, key="data")
67
+ else:
68
+ # 从 context 中的 factor 字段
69
+ factor = context.get("LoadData", {}).get("factor")
70
+ if factor is None:
71
+ factor = pd.DataFrame(np.zeros((10, 10)))
72
+
73
+ # 3. 临时覆盖 factor name / expression
74
+ if "factor" not in config:
75
+ config["factor"] = {}
76
+ config["factor"]["name"] = factor_override_name
77
+ if factor_override_expr:
78
+ config["factor"]["expression"] = factor_override_expr
79
+
80
+ # 4. 重建 context
81
+ ctx = {}
82
+ ctx["LoadData"] = {
83
+ "factor": factor,
84
+ "price": context.get("LoadData", {}).get("price", pd.DataFrame()),
85
+ "id_citic1": context.get("LoadData", {}).get("id_citic1", pd.DataFrame()),
86
+ "mv_float": context.get("LoadData", {}).get("mv_float", pd.DataFrame()),
87
+ "st": context.get("LoadData", {}).get("st", pd.DataFrame()),
88
+ "suspend": context.get("LoadData", {}).get("suspend", pd.DataFrame()),
89
+ "ud_limit": context.get("LoadData", {}).get("ud_limit", pd.DataFrame()),
90
+ "ipo_days": context.get("LoadData", {}).get("ipo_days", pd.DataFrame()),
91
+ "index_cp": context.get("LoadData", {}).get("index_cp", pd.DataFrame()),
92
+ "stklist": context.get("LoadData", {}).get("stklist", pd.DataFrame()),
93
+ "trade_dt": context.get("LoadData", {}).get("trade_dt", pd.DataFrame()),
94
+ "_loader": context.get("LoadData", {}).get("_loader"),
95
+ }
96
+
97
+ # 5. 跑节点 2-11 (Phase 2-11, 跳过 LoadData 和 Report)
98
+ try:
99
+ _run_analysis_nodes(ctx, config)
100
+ except Exception as e:
101
+ return {"passed": False, "metrics": {}, "feedback_dict": None, "error": str(e)}
102
+
103
+ # 6. 提取 metrics
104
+ metrics = _extract_metrics(ctx)
105
+ passed = True
106
+
107
+ return {
108
+ "passed": passed,
109
+ "metrics": metrics,
110
+ "feedback_dict": {
111
+ "factor_id": candidate_dict.get("factor_id", ""),
112
+ "factor_name": candidate_dict.get("name", ""),
113
+ "decision": passed,
114
+ "summary": f"ProcessPool: sharpe={metrics.get('sharpe', 0):.2f}",
115
+ "metadata": metrics,
116
+ "channels": {},
117
+ },
118
+ "error": None,
119
+ }
120
+
121
+
122
+ def _run_analysis_nodes(ctx: dict, config: dict) -> None:
123
+ """在子进程中跑节点 2-11 (与 PipelineRunner.run 相同逻辑)。"""
124
+ from QuantNodes.research.factor_test.nodes.sample_pool_filter_node import SamplePoolFilterNode
125
+ from QuantNodes.research.factor_test.nodes.tradability_filter_node import TradabilityFilterNode
126
+ from QuantNodes.research.factor_test.nodes.adjust_date_node import AdjustDateNode
127
+ from QuantNodes.research.factor_test.nodes.factor_preprocess_node import FactorPreprocessNode
128
+ from QuantNodes.research.factor_test.nodes.ic_analyzer_node import ICAnalyzerNode
129
+ from QuantNodes.research.factor_test.nodes.group_analyzer_node import GroupAnalyzerNode
130
+ from QuantNodes.research.factor_test.nodes.long_short_node import LongShortNode
131
+ from QuantNodes.research.factor_test.nodes.factor_score_node import FactorScoreNode
132
+
133
+ pp = config.get("preprocess", {})
134
+
135
+ # Node 2: SampleFilter
136
+ ctx["SamplePoolFilter"] = SamplePoolFilterNode(config={
137
+ "sample_index": pp.get("sample_index", "all"),
138
+ "sample_industry": pp.get("sample_industry", "all"),
139
+ "sample_index_customdir": pp.get("sample_index_customdir"),
140
+ }).execute(context=ctx)
141
+
142
+ # Node 3: TradabilityFilter
143
+ tradable = pp.get("tradable", {})
144
+ if isinstance(tradable, dict):
145
+ tradable_dict = tradable
146
+ elif hasattr(tradable, "model_dump"):
147
+ tradable_dict = tradable.model_dump()
148
+ else:
149
+ tradable_dict = {}
150
+ ctx["TradabilityFilter"] = TradabilityFilterNode(config={
151
+ "tradable": tradable_dict,
152
+ }).execute(context=ctx)
153
+
154
+ # Node 4: AdjustDate
155
+ ctx["AdjustDate"] = AdjustDateNode(config={
156
+ "adj_date_beg": pp.get("adj_date_beg", 20260101),
157
+ "adj_date_end": pp.get("adj_date_end", 20260630),
158
+ "adj_mode": pp.get("adj_mode", ["M", "end"]),
159
+ }).execute(context=ctx)
160
+
161
+ # Node 5: Preprocess
162
+ ctx["FactorPreprocess"] = FactorPreprocessNode(config={
163
+ "missing": pp.get("missing", ""),
164
+ "extreme": pp.get("extreme", "median"),
165
+ "norm": pp.get("norm", "zscore"),
166
+ }).execute(context=ctx)
167
+
168
+ # Node 6: Neutralize
169
+ ctx["FactorNeutralize"] = ctx["FactorPreprocess"]
170
+
171
+ # Node 7-10: Analysis
172
+ analysis = config.get("analysis", {})
173
+ ctx["ICAnalyzer"] = ICAnalyzerNode(config={
174
+ "min_group_size": analysis.get("ic", {}).get("min_group_size", 5),
175
+ }).execute(context=ctx)
176
+ ctx["GroupAnalyzer"] = GroupAnalyzerNode(config={
177
+ "groups": analysis.get("group", {}).get("groups", 5),
178
+ "factor_direction": analysis.get("group", {}).get("factor_direction", 1),
179
+ "floor_mode": analysis.get("group", {}).get("floor_mode", "group"),
180
+ "hedge": analysis.get("group", {}).get("hedge", "equal"),
181
+ "hedge_path": analysis.get("group", {}).get("hedge_path"),
182
+ }).execute(context=ctx)
183
+ ctx["LongShort"] = LongShortNode(config={
184
+ "factor_direction": analysis.get("longshort", {}).get("factor_direction", 1),
185
+ }).execute(context=ctx)
186
+ ctx["FactorScore"] = FactorScoreNode(config={
187
+ "enabled": analysis.get("score", {}).get("enabled", True),
188
+ }).execute(context=ctx)
189
+
190
+
191
+ def _extract_metrics(ctx: dict) -> dict:
192
+ """从 ctx 提取指标 (与 PipelineRunner._extract_metrics_from_ctx 相同)。"""
193
+ metrics: dict = {}
194
+ ic = ctx.get("ICAnalyzer") or {}
195
+ ic_result = ic.get("ic_result") if isinstance(ic, dict) else None
196
+ if isinstance(ic_result, dict):
197
+ for src, dst in (("IC均值", "ic_mean"), ("Rank IC均值", "rank_ic_mean"), ("ICIR", "ic_ir")):
198
+ if src in ic_result and ic_result[src] is not None:
199
+ try:
200
+ metrics[dst] = float(ic_result[src])
201
+ except (TypeError, ValueError):
202
+ pass
203
+ ls = ctx.get("LongShort") or {}
204
+ if isinstance(ls, dict):
205
+ for src, dst in (("sharpe", "sharpe"), ("annualized_return", "arr"),
206
+ ("max_drawdown", "mdd"), ("calmar", "calmar")):
207
+ if src in ls and ls[src] is not None:
208
+ try:
209
+ metrics[dst] = float(ls[src])
210
+ except (TypeError, ValueError):
211
+ pass
212
+ return metrics
213
+
214
+
215
+ # ============================================================================
216
+ # 序列化 / 反序列化 (主进程侧)
217
+ # ============================================================================
218
+
219
+ class RunnerSnapshot:
220
+ """预序列化 config + context, 供子进程读取。"""
221
+
222
+ def __init__(self, config_dict: dict, context: dict, factor_path: str | None = None):
223
+ self.config = config_dict
224
+ self.context = context
225
+ self.factor_name = config_dict.get("factor", {}).get("name", "")
226
+ self.factor_path = factor_path or ""
227
+
228
+ def save(self, path: Path) -> None:
229
+ """序列化到 pickle 文件。"""
230
+ path = Path(path)
231
+ ensure_parent(path)
232
+ snapshot = {
233
+ "config": self.config,
234
+ "context": self.context,
235
+ "factor_name": self.factor_name,
236
+ "factor_path": self.factor_path,
237
+ }
238
+ path.write_bytes(pickle.dumps(snapshot))
239
+
240
+ @staticmethod
241
+ def load(path: Path) -> dict:
242
+ """从 pickle 文件加载。"""
243
+ return pickle.loads(Path(path).read_bytes())
244
+
245
+
246
+ def prepare_snapshot(
247
+ config,
248
+ context: dict,
249
+ factor_path: str | None = None,
250
+ ) -> RunnerSnapshot:
251
+ """从 PipelineRunner 准备序列化快照。
252
+
253
+ Args:
254
+ config: SingleFactorTestConfig (会 model_dump)
255
+ context: runner._context (含 DataFrames)
256
+ factor_path: 因子 H5 文件路径 (可选, 若 context 中有)
257
+
258
+ Returns:
259
+ RunnerSnapshot (可 save 到文件)
260
+ """
261
+ if hasattr(config, "model_dump"):
262
+ config_dict = config.model_dump()
263
+ else:
264
+ config_dict = dict(config)
265
+ return RunnerSnapshot(config_dict, context, factor_path)
@@ -0,0 +1,73 @@
1
+ """Path utilities — common pattern extraction.
2
+
3
+ Consolidates:
4
+ 1. `Path(x).expanduser()` and the env-var override variant
5
+ 2. `Path(x).mkdir(parents=True, exist_ok=True)` (38 sites)
6
+ 3. `path.parent.mkdir(parents=True, exist_ok=True)` (parent-for-file pattern)
7
+
8
+ Used by:
9
+ - QuantNodes/core/trajectory/pool.py
10
+ - QuantNodes/core/monitoring/{collector,dashboard}.py
11
+ - QuantNodes/core/feedback/dataclass.py
12
+ - QuantNodes/core/knowledge/knowledge_base.py
13
+ - QuantNodes/core/visualization/report.py
14
+ - QuantNodes/core/parallel/worker_process.py
15
+ - QuantNodes/core/quality_gate/zoo.py
16
+ - QuantNodes/agent/tools/{file_ops,task}.py
17
+ - QuantNodes/agent/cli/main.py
18
+ - QuantNodes/agent/core/memory.py
19
+ - QuantNodes/agent/session/manager.py
20
+ - QuantNodes/cache_node/{cache_store,metadata}.py
21
+ - QuantNodes/monitor/{version/version_manager.py,scheduler/scheduler.py,storage/repository.py}
22
+ - QuantNodes/cli/_helpers.py + commands/{run,init}.py
23
+ - QuantNodes/backtest/config_runner.py
24
+ - QuantNodes/research/factor_test/{nodes,ifind_db,e2e}/...py
25
+
26
+ P-1 priority is preserved: env_var > expanduser(default).
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ from pathlib import Path
32
+
33
+ __all__ = ["resolve_path", "ensure_dir", "ensure_parent"]
34
+
35
+
36
+ def resolve_path(default: str, env_var: str | None = None) -> Path:
37
+ """Resolve a path with priority: env_var > expanduser(default).
38
+
39
+ Args:
40
+ default: fallback path string (may contain ~).
41
+ env_var: optional environment variable name. If set and non-empty,
42
+ its value (after expanduser) wins.
43
+
44
+ Returns:
45
+ Path (always expanded, never None).
46
+ """
47
+ if env_var:
48
+ env_val = os.environ.get(env_var)
49
+ if env_val:
50
+ return Path(env_val).expanduser()
51
+ return Path(default).expanduser()
52
+
53
+
54
+ def ensure_dir(path: str | Path) -> Path:
55
+ """Create directory at `path` if missing. Idempotent. Returns Path.
56
+
57
+ Replaces: `Path(x).mkdir(parents=True, exist_ok=True)`.
58
+ Also replaces bare `os.makedirs(x, exist_ok=True)`.
59
+ """
60
+ p = Path(path).expanduser()
61
+ p.mkdir(parents=True, exist_ok=True)
62
+ return p
63
+
64
+
65
+ def ensure_parent(path: str | Path) -> Path:
66
+ """Ensure the *parent* of `path` exists (i.e. prepare to write a file).
67
+
68
+ Replaces: `Path(x).parent.mkdir(parents=True, exist_ok=True)`.
69
+ Returns the *original* `path` (not the parent), so callers can write to it.
70
+ """
71
+ p = Path(path).expanduser()
72
+ p.parent.mkdir(parents=True, exist_ok=True)
73
+ return p
@@ -0,0 +1,328 @@
1
+ # coding=utf-8
2
+ """
3
+ Pipeline 组合原语模块
4
+
5
+ 本模块提供三种核心的节点组合方式:
6
+ 1. Pipeline: 线性管道,按顺序执行节点列表
7
+ 2. Parallel: 并行分叉,所有分支接收相同输入,并行执行
8
+ 3. Join: 聚合组合,接收字典输入,通过用户函数聚合为单个输出
9
+
10
+ 使用方式:
11
+ >>> from QuantNodes.core import Pipeline, Parallel, Join
12
+ >>>
13
+ >>> # 线性管道
14
+ >>> p = DatabaseNode() >> FactorNode() >> BacktestNode()
15
+ >>> result = p.execute(data)
16
+ >>>
17
+ >>> # 并行分叉
18
+ >>> factors = Parallel({
19
+ ... 'mom': MomentumFactor(),
20
+ ... 'vol': VolatilityFactor(),
21
+ ... 'value': ValueFactor(),
22
+ ... })
23
+ >>>
24
+ >>> # 聚合组合
25
+ >>> combine = Join(lambda mom, vol, value: mom * 0.5 + vol * 0.3 + value * 0.2)
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ from typing import Any, Dict, List, Callable, Optional
32
+
33
+ from QuantNodes.core.node import BaseNode, SerializationError
34
+ from QuantNodes.core.serializable import serializable
35
+
36
+
37
+ @serializable
38
+ class Pipeline(BaseNode):
39
+ """
40
+ 线性管道节点
41
+
42
+ 按顺序执行节点列表,前一个节点的输出作为后一个节点的输入。
43
+ Pipeline 本身也是一个 Node,可以嵌套使用。
44
+
45
+ Examples:
46
+ >>> # 两种创建方式等价
47
+ >>> p1 = Pipeline([NodeA(), NodeB(), NodeC()])
48
+ >>> p2 = NodeA() >> NodeB() >> NodeC()
49
+ >>>
50
+ >>> # 嵌套使用
51
+ >>> nested = Pipeline([
52
+ ... LoadDataNode(),
53
+ ... Pipeline([CleanDataNode(), NormalizeNode()]),
54
+ ... SaveResultNode(),
55
+ ... ])
56
+ """
57
+
58
+ def __init__(self, nodes: List[BaseNode], name: str = None, config: Dict[str, Any] = None):
59
+ """
60
+ Args:
61
+ nodes: 节点列表,按顺序执行
62
+ name: 管道名称,默认为 "Pipeline"
63
+ config: 配置字典
64
+ """
65
+ super().__init__(name=name or "Pipeline", config=config)
66
+ self.nodes: List[BaseNode] = list(nodes)
67
+ self.logger = logging.getLogger(f"node.{self.node_id}")
68
+
69
+ def _execute(self, input_data: Any = None, **kwargs) -> Any:
70
+ """依次执行所有节点"""
71
+ result = input_data
72
+
73
+ for i, node in enumerate(self.nodes):
74
+ self.logger.debug(f"Executing pipeline node {i+1}/{len(self.nodes)}: {node.name}")
75
+ result = node.execute(result, **kwargs)
76
+
77
+ return result
78
+
79
+ def __rshift__(self, other: BaseNode) -> 'Pipeline':
80
+ """重载 >> 运算符,支持链式扩展"""
81
+ if isinstance(other, Pipeline):
82
+ return Pipeline(self.nodes + other.nodes)
83
+ return Pipeline(self.nodes + [other])
84
+
85
+ def __iter__(self):
86
+ """迭代器支持"""
87
+ return iter(self.nodes)
88
+
89
+ def __len__(self) -> int:
90
+ """返回节点数量"""
91
+ return len(self.nodes)
92
+
93
+ def __getitem__(self, index: int) -> BaseNode:
94
+ """支持索引访问"""
95
+ return self.nodes[index]
96
+
97
+ def _get_serializable_fields(self) -> Dict[str, Any]:
98
+ """返回需要序列化的额外字段"""
99
+ return {"nodes": [node.serialize() for node in self.nodes]}
100
+
101
+ @classmethod
102
+ def _from_dict_impl(cls, data: Dict[str, Any]) -> 'Pipeline':
103
+ """从字典反序列化重建 Pipeline"""
104
+ nodes = [BaseNode.deserialize(n) for n in data["nodes"]]
105
+ return Pipeline(
106
+ nodes=nodes,
107
+ name=data.get("name"),
108
+ config=data.get("config", {})
109
+ )
110
+
111
+ def to_info(self) -> Dict[str, Any]:
112
+ """导出管道信息"""
113
+ result = super().to_info()
114
+ result['nodes'] = [node.to_info() for node in self.nodes]
115
+ return result
116
+
117
+
118
+ @serializable
119
+ class Parallel(BaseNode):
120
+ """
121
+ 并行分叉节点
122
+
123
+ 所有分支接收相同的输入,并行执行,返回字典格式的结果。
124
+ 注意:当前版本为多线程实现,计算密集型任务可能不会提速。
125
+ 后续版本会支持多进程和分布式执行。
126
+
127
+ Examples:
128
+ >>> p = Parallel({
129
+ ... 'factor_a': FactorA(),
130
+ ... 'factor_b': FactorB(),
131
+ ... 'factor_c': FactorC(),
132
+ ... })
133
+ >>> result = p.execute(data)
134
+ >>> # result = {'factor_a': ..., 'factor_b': ..., 'factor_c': ...}
135
+ """
136
+
137
+ def __init__(self, branches: Dict[str, BaseNode],
138
+ name: str = None,
139
+ max_workers: Optional[int] = None,
140
+ parallel: bool = True,
141
+ config: Dict[str, Any] = None):
142
+ """
143
+ Args:
144
+ branches: 分支节点字典,key 为结果字典的 key
145
+ name: 节点名称
146
+ max_workers: 最大工作线程数,None 表示自动选择
147
+ parallel: 是否并行执行,False 表示串行(调试用)
148
+ config: 配置字典
149
+ """
150
+ super().__init__(name=name or "Parallel", config=config)
151
+ self.branches: Dict[str, BaseNode] = branches
152
+ self.max_workers = max_workers
153
+ self.parallel = parallel
154
+ self.logger = logging.getLogger(f"node.{self.node_id}")
155
+
156
+ def _execute(self, input_data: Any = None, **kwargs) -> Dict[str, Any]:
157
+ """执行所有分支"""
158
+ if self.parallel:
159
+ return self._execute_parallel(input_data, **kwargs)
160
+ else:
161
+ return self._execute_serial(input_data, **kwargs)
162
+
163
+ def _execute_serial(self, input_data: Any = None, **kwargs) -> Dict[str, Any]:
164
+ """串行执行(调试用)"""
165
+ results = {}
166
+ for name, node in self.branches.items():
167
+ self.logger.debug(f"Executing parallel branch: {name}")
168
+ results[name] = node.execute(input_data, **kwargs)
169
+ return results
170
+
171
+ def _execute_parallel(self, input_data: Any = None, **kwargs) -> Dict[str, Any]:
172
+ """并行执行(使用线程池)"""
173
+ from concurrent.futures import ThreadPoolExecutor, as_completed
174
+
175
+ results = {}
176
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
177
+ future_to_name = {
178
+ executor.submit(node.execute, input_data, **kwargs): name
179
+ for name, node in self.branches.items()
180
+ }
181
+
182
+ for future in as_completed(future_to_name):
183
+ name = future_to_name[future]
184
+ try:
185
+ results[name] = future.result()
186
+ except Exception as e:
187
+ self.logger.error(f"Parallel branch '{name}' failed: {e}")
188
+ raise
189
+
190
+ return results
191
+
192
+ def __or__(self, other: 'Parallel') -> 'Parallel':
193
+ """重载 | 运算符,支持合并 Parallel 节点"""
194
+ if not isinstance(other, Parallel):
195
+ raise TypeError(f"Can only combine Parallel with Parallel, got {type(other)}")
196
+ merged = {**self.branches, **other.branches}
197
+ return Parallel(merged)
198
+
199
+ def _get_serializable_fields(self) -> Dict[str, Any]:
200
+ """返回需要序列化的额外字段"""
201
+ return {
202
+ "branches": {name: node.serialize() for name, node in self.branches.items()},
203
+ "max_workers": self.max_workers,
204
+ "parallel": self.parallel,
205
+ }
206
+
207
+ @classmethod
208
+ def _from_dict_impl(cls, data: Dict[str, Any]) -> 'Parallel':
209
+ """从字典反序列化重建 Parallel"""
210
+ branches = {
211
+ name: BaseNode.deserialize(n)
212
+ for name, n in data["branches"].items()
213
+ }
214
+ return Parallel(
215
+ branches=branches,
216
+ name=data.get("name"),
217
+ max_workers=data.get("max_workers"),
218
+ parallel=data.get("parallel", True),
219
+ config=data.get("config", {})
220
+ )
221
+
222
+ def to_info(self) -> Dict[str, Any]:
223
+ """导出节点信息"""
224
+ result = super().to_info()
225
+ result['branches'] = {name: node.to_info() for name, node in self.branches.items()}
226
+ result['max_workers'] = self.max_workers
227
+ result['parallel'] = self.parallel
228
+ return result
229
+
230
+
231
+ @serializable
232
+ class Join(BaseNode):
233
+ """
234
+ 聚合组合节点
235
+
236
+ 接收字典输入,通过用户定义的函数聚合为单个输出。
237
+ 通常与 Parallel 节点配合使用,将多个分支的结果合并。
238
+
239
+ Examples:
240
+ >>> # 1. 使用关键字参数函数
241
+ >>> Join(lambda mom, vol, value: mom * 0.5 + vol * 0.3 + value * 0.2)
242
+ >>>
243
+ >>> # 2. 使用字典参数
244
+ >>> Join(lambda factors: factors['mom'] + factors['vol'])
245
+ >>>
246
+ >>> # 3. 典型用法:Parallel + Join
247
+ >>> pipeline = (
248
+ ... Parallel({
249
+ ... 'mom': MomentumFactor(),
250
+ ... 'vol': VolatilityFactor(),
251
+ ... })
252
+ ... >> Join(lambda mom, vol: mom / vol)
253
+ ... )
254
+ """
255
+
256
+ def __init__(self, join_func: Callable, name: str = None, config: Dict[str, Any] = None):
257
+ """
258
+ Args:
259
+ join_func: 聚合函数
260
+ - 如果接受关键字参数,会传入字典的 key-value
261
+ - 如果只接受一个参数,会传入整个字典
262
+ name: 节点名称
263
+ config: 配置字典
264
+ """
265
+ super().__init__(name=name or "Join", config=config)
266
+ self.join_func = join_func
267
+ self.logger = logging.getLogger(f"node.{self.node_id}")
268
+
269
+ def _execute(self, input_data: Dict[str, Any], **kwargs) -> Any:
270
+ """执行聚合"""
271
+ if not isinstance(input_data, dict):
272
+ raise ValueError(f"Join node requires dict input, got {type(input_data)}")
273
+
274
+ import inspect
275
+ sig = inspect.signature(self.join_func)
276
+ params = list(sig.parameters.keys())
277
+
278
+ if len(params) == 1:
279
+ return self.join_func(input_data)
280
+
281
+ return self.join_func(**input_data)
282
+
283
+ def _get_serializable_fields(self) -> Dict[str, Any]:
284
+ """返回需要序列化的额外字段"""
285
+ func = self.join_func
286
+
287
+ if hasattr(func, "__name__") and func.__name__ == "<lambda>":
288
+ raise SerializationError(
289
+ "Join node with anonymous lambda cannot be serialized. "
290
+ "Please use a named function or Expression DSL instead."
291
+ )
292
+
293
+ return {
294
+ "join_func": {
295
+ "type": "named_function",
296
+ "module": func.__module__,
297
+ "qualname": func.__qualname__
298
+ }
299
+ }
300
+
301
+ @classmethod
302
+ def _from_dict_impl(cls, data: Dict[str, Any]) -> 'Join':
303
+ """从字典反序列化重建 Join"""
304
+ import importlib
305
+
306
+ func_info = data["join_func"]
307
+ if func_info["type"] != "named_function":
308
+ raise ValueError(f"Unsupported function type: {func_info['type']}")
309
+
310
+ module = importlib.import_module(func_info["module"])
311
+ func = getattr(module, func_info["qualname"])
312
+
313
+ return Join(
314
+ join_func=func,
315
+ name=data.get("name"),
316
+ config=data.get("config", {})
317
+ )
318
+
319
+ def to_info(self) -> Dict[str, Any]:
320
+ """导出节点信息"""
321
+ result = super().to_info()
322
+ join_name = (
323
+ self.join_func.__name__
324
+ if hasattr(self.join_func, '__name__')
325
+ else str(self.join_func)
326
+ )
327
+ result['join_func'] = join_name
328
+ return result