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,211 @@
1
+ """TrajectoryPool — 演化轨迹池, 双层 Parquet + JSON 持久化。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import threading
6
+ from pathlib import Path
7
+ from typing import Iterator
8
+
9
+ import pandas as pd
10
+
11
+ from ..constants import PARQUET_COLUMNS as _PARQUET_COLUMNS
12
+ from .entry import TrajectoryEntry
13
+ from .lineage import children_of, descendants, lineage
14
+ from QuantNodes.core.path_utils import ensure_dir
15
+
16
+
17
+ _PARQUET_NAME = "trajectories.parquet"
18
+ _ENTRIES_SUBDIR = "entries"
19
+
20
+
21
+ class TrajectoryPool:
22
+ """演化轨迹池 — 持久化每轮实验的完整记录。
23
+
24
+ 双层存储:
25
+ - Layer 1: {parquet_name}.parquet (元数据, append)
26
+ - Layer 2: entries/{entry_id}.json (完整记录, 独立子目录)
27
+
28
+ Phase H2 (2026-06-20):
29
+ - all()/_sorted_cache memoizes the timestamp-sorted list and
30
+ invalidates only on add() or reset(). Avoids repeated sorts when
31
+ many callers iterate the pool in tight loops (selector, loop,
32
+ cli/commands/factor.py, knowledge_base.py).
33
+ - _load() uses df.to_dict("records") instead of df.iterrows()
34
+ (5-10x faster bulk load on startup with large pools).
35
+
36
+ H1 (parquet O(n^2) append) deferred: ParquetWriter requires strict
37
+ schema uniformity per column, but the existing Parquet file holds
38
+ heterogeneous types (int/float/bool/str/list per column) and tests
39
+ assert type preservation. Re-implementing with native types would
40
+ require inferring the schema from existing data on every open.
41
+
42
+ Args:
43
+ base_dir: 池根目录 (自动创建)
44
+ parquet_name: Parquet 文件名 (默认 "trajectories.parquet"),
45
+ 允许不同实验共用 base_dir 各自存盘。
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ base_dir: Path | str,
51
+ parquet_name: str = _PARQUET_NAME,
52
+ ):
53
+ self.base_dir = Path(base_dir)
54
+ ensure_dir(self.base_dir)
55
+ self._entries_dir = self.base_dir / _ENTRIES_SUBDIR
56
+ ensure_dir(self._entries_dir)
57
+ self._parquet_name = parquet_name
58
+ self._entries: dict[str, TrajectoryEntry] = {}
59
+ self._parquet_path = self.base_dir / self._parquet_name
60
+ self._lock = threading.Lock()
61
+ self._sorted_cache: list[TrajectoryEntry] | None = None
62
+ self._load()
63
+
64
+ def add(self, entry: TrajectoryEntry) -> None:
65
+ """添加一条 entry, 自动持久化。"""
66
+ with self._lock:
67
+ self._entries[entry.entry_id] = entry
68
+ self._persist(entry)
69
+ self._sorted_cache = None # invalidate cache (H2)
70
+
71
+ def get(self, entry_id: str) -> TrajectoryEntry:
72
+ """按 ID 获取 entry, 不存在抛 KeyError。"""
73
+ if entry_id not in self._entries:
74
+ raise KeyError(f"entry_id 不存在: {entry_id}")
75
+ return self._entries[entry_id]
76
+
77
+ def all(self) -> list[TrajectoryEntry]:
78
+ """返回所有 entry 列表 (按时间排序)。
79
+
80
+ Cached: subsequent calls return the same list until add()/reset()
81
+ invalidates the cache.
82
+ """
83
+ if self._sorted_cache is None:
84
+ self._sorted_cache = sorted(self._entries.values(), key=lambda e: e.timestamp)
85
+ return list(self._sorted_cache)
86
+
87
+ def __iter__(self) -> Iterator[TrajectoryEntry]:
88
+ return iter(self.all())
89
+
90
+ def __len__(self) -> int:
91
+ return len(self._entries)
92
+
93
+ @property
94
+ def size(self) -> int:
95
+ return len(self._entries)
96
+
97
+ def reset(self) -> None:
98
+ """清空内存 + 删除所有持久化文件 (entries/ 子目录 + Parquet)。"""
99
+ with self._lock:
100
+ self._entries.clear()
101
+ self._sorted_cache = None
102
+ if self._parquet_path.exists():
103
+ self._parquet_path.unlink()
104
+ if self._entries_dir.exists():
105
+ for p in self._entries_dir.glob("*.json"):
106
+ p.unlink()
107
+
108
+ # ------------------------------------------------------------------
109
+ # 过滤
110
+ # ------------------------------------------------------------------
111
+
112
+ def by_round(self, round_idx: int) -> list[TrajectoryEntry]:
113
+ return [e for e in self.all() if e.round_idx == round_idx]
114
+
115
+ def by_operation(self, operation: str) -> list[TrajectoryEntry]:
116
+ return [e for e in self.all() if e.operation == operation]
117
+
118
+ def filter(self, decision: bool | None = None) -> list[TrajectoryEntry]:
119
+ """按 feedback.decision 过滤 (None=不过滤)。"""
120
+ if decision is None:
121
+ return self.all()
122
+ return [
123
+ e for e in self.all()
124
+ if e.feedback is not None and e.feedback.decision == decision
125
+ ]
126
+
127
+ def best(self, top_n: int = 5, metric: str = "sharpe") -> list[TrajectoryEntry]:
128
+ """按 metric 降序, 返回 Top-N。"""
129
+ return sorted(
130
+ self.all(),
131
+ key=lambda e: float(e.metrics.get(metric, 0) or 0),
132
+ reverse=True,
133
+ )[:top_n]
134
+
135
+ def random(self, n: int, seed: int | None = None) -> list[TrajectoryEntry]:
136
+ """从池中随机抽 n 条。"""
137
+ import numpy as np
138
+ rng = np.random.default_rng(seed)
139
+ k = min(n, len(self._entries))
140
+ if k == 0:
141
+ return []
142
+ indices = rng.choice(len(self._entries), size=k, replace=False)
143
+ all_entries = self.all()
144
+ return [all_entries[int(i)] for i in indices]
145
+
146
+ # ------------------------------------------------------------------
147
+ # 谱系 (代理到 lineage.py)
148
+ # ------------------------------------------------------------------
149
+
150
+ def children_of(self, parent_id: str) -> list[TrajectoryEntry]:
151
+ return children_of(self._entries, parent_id)
152
+
153
+ def lineage(self, entry_id: str) -> list[TrajectoryEntry]:
154
+ return lineage(self._entries, entry_id)
155
+
156
+ def descendants(self, entry_id: str, max_depth: int | None = None) -> list[TrajectoryEntry]:
157
+ return descendants(self._entries, entry_id, max_depth=max_depth)
158
+
159
+ # ------------------------------------------------------------------
160
+ # 持久化
161
+ # ------------------------------------------------------------------
162
+
163
+ def _persist(self, entry: TrajectoryEntry) -> None:
164
+ """持久化单条 entry (Parquet append + JSON 单文件)。
165
+
166
+ 保留原有 read+concat+rewrite 路径 (O(n^2) 但 schema 兼容)。
167
+ H1 的 ParquetWriter 增量追加方案因 schema 类型一致性限制暂未启用,
168
+ 详见模块 docstring。H2 的 _sorted_cache 与 _load 优化已生效。
169
+ """
170
+ new_row = pd.DataFrame([entry.to_parquet_row()], columns=list(_PARQUET_COLUMNS))
171
+ if self._parquet_path.exists():
172
+ existing = pd.read_parquet(self._parquet_path)
173
+ for col in _PARQUET_COLUMNS:
174
+ if col not in existing.columns:
175
+ existing[col] = None
176
+ existing = existing[list(_PARQUET_COLUMNS)].astype(object)
177
+ new_row = new_row.astype(object)
178
+ combined = pd.concat([existing, new_row], ignore_index=True)
179
+ else:
180
+ combined = new_row
181
+ combined.to_parquet(self._parquet_path, index=False)
182
+
183
+ json_path = self._entries_dir / f"{entry.entry_id}.json"
184
+ with json_path.open("w", encoding="utf-8") as f:
185
+ json.dump(entry.to_json_dict(), f, ensure_ascii=False, indent=2)
186
+
187
+ def _load(self) -> None:
188
+ """启动时从磁盘加载元数据 + JSON 详情。
189
+
190
+ H2: vectorized row->dict via to_dict('records') instead of iterrows.
191
+ """
192
+ if not self._parquet_path.exists():
193
+ return
194
+ try:
195
+ df = pd.read_parquet(self._parquet_path)
196
+ except Exception:
197
+ return
198
+ records = df.to_dict("records")
199
+ for row in records:
200
+ entry_id = str(row.get("entry_id", ""))
201
+ if not entry_id:
202
+ continue
203
+ json_path = self._entries_dir / f"{entry_id}.json"
204
+ if not json_path.exists():
205
+ continue
206
+ try:
207
+ with json_path.open("r", encoding="utf-8") as f:
208
+ data = json.load(f)
209
+ self._entries[entry_id] = TrajectoryEntry.from_json_dict(data)
210
+ except Exception:
211
+ continue
@@ -0,0 +1,140 @@
1
+ """ParentSelector — 5 种父辈选择策略。
2
+
3
+ 借鉴 QuantaAlpha `configs/experiment.yaml:73 parent_selection_strategy`。
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import TYPE_CHECKING
9
+
10
+ import numpy as np
11
+
12
+ from .entry import TrajectoryEntry
13
+
14
+ if TYPE_CHECKING:
15
+ from .pool import TrajectoryPool
16
+
17
+
18
+ class SelectionStrategy(str, Enum):
19
+ """5 种选择策略。"""
20
+ BEST = "best"
21
+ RANDOM = "random"
22
+ WEIGHTED = "weighted"
23
+ WEIGHTED_INVERSE = "weighted_inverse"
24
+ TOP_PERCENT_PLUS_RANDOM = "top_percent_plus_random"
25
+
26
+
27
+ class ParentSelector:
28
+ """父辈选择策略 — 从 TrajectoryPool 选 n 个 entry 作为下一轮 parent。
29
+
30
+ Args:
31
+ strategy: 策略名 (BEST/RANDOM/WEIGHTED/WEIGHTED_INVERSE/TOP_PERCENT_PLUS_RANDOM)
32
+ metric: 用于 best / weighted 的指标 (默认 'sharpe')
33
+ top_percent_threshold: top_percent_plus_random 中 top 比例 (默认 0.3)
34
+ seed: 随机种子 (None=不固定)
35
+ temperature: M4 weighted 策略的 softmax 温度 (默认 1.0,
36
+ > 1.0 更均匀采样, < 1.0 更集中于高分)
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ strategy: str = "best",
42
+ metric: str = "sharpe",
43
+ top_percent_threshold: float = 0.3,
44
+ seed: int | None = None,
45
+ temperature: float = 1.0,
46
+ ):
47
+ valid = {s.value for s in SelectionStrategy}
48
+ if strategy not in valid:
49
+ raise ValueError(
50
+ f"未知 strategy: {strategy!r}, 应为 {sorted(valid)}"
51
+ )
52
+ self.strategy = strategy
53
+ self.metric = metric
54
+ self.top_percent_threshold = top_percent_threshold
55
+ self.temperature = temperature
56
+ self._rng = np.random.default_rng(seed)
57
+
58
+ def select(
59
+ self,
60
+ pool: list[TrajectoryEntry] | "TrajectoryPool",
61
+ n: int = 1,
62
+ ) -> list[TrajectoryEntry]:
63
+ """从 pool 选 n 个 entry (只选 feedback.decision=True 的)。"""
64
+ from .pool import TrajectoryPool # 避免循环 import
65
+
66
+ if isinstance(pool, TrajectoryPool):
67
+ valid = [e for e in pool.all() if e.feedback and e.feedback.decision]
68
+ else:
69
+ valid = [e for e in pool if e.feedback and e.feedback.decision]
70
+ if not valid:
71
+ return []
72
+
73
+ if self.strategy == SelectionStrategy.BEST.value:
74
+ return self._best(valid, n)
75
+ if self.strategy == SelectionStrategy.RANDOM.value:
76
+ return self._random(valid, n)
77
+ if self.strategy == SelectionStrategy.WEIGHTED.value:
78
+ return self._weighted_sample(valid, n, inverse=False)
79
+ if self.strategy == SelectionStrategy.WEIGHTED_INVERSE.value:
80
+ return self._weighted_sample(valid, n, inverse=True)
81
+ if self.strategy == SelectionStrategy.TOP_PERCENT_PLUS_RANDOM.value:
82
+ return self._top_percent_plus_random(valid, n)
83
+ return []
84
+
85
+ # ------------------------------------------------------------------
86
+ # 5 个策略
87
+ # ------------------------------------------------------------------
88
+
89
+ def _best(self, valid: list[TrajectoryEntry], n: int) -> list[TrajectoryEntry]:
90
+ sorted_entries = sorted(
91
+ valid,
92
+ key=lambda e: float(e.metrics.get(self.metric, 0) or 0),
93
+ reverse=True,
94
+ )
95
+ return sorted_entries[:n]
96
+
97
+ def _random(self, valid: list[TrajectoryEntry], n: int) -> list[TrajectoryEntry]:
98
+ k = min(n, len(valid))
99
+ indices = self._rng.choice(len(valid), size=k, replace=False)
100
+ return [valid[int(i)] for i in indices]
101
+
102
+ def _weighted_sample(
103
+ self,
104
+ valid: list[TrajectoryEntry],
105
+ n: int,
106
+ inverse: bool,
107
+ ) -> list[TrajectoryEntry]:
108
+ scores = np.array(
109
+ [float(e.metrics.get(self.metric, 0) or 0) for e in valid],
110
+ dtype=float,
111
+ )
112
+ if inverse:
113
+ scores = -scores
114
+ scores = scores - scores.max()
115
+ # M4: temperature 调节 softmax 锐度
116
+ # T → 0: 趋近 argmax; T → ∞: 趋近均匀; T=1: 标准 softmax
117
+ weights = np.exp(scores / max(self.temperature, 1e-9))
118
+ total = weights.sum()
119
+ if total == 0 or not np.isfinite(total):
120
+ return self._random(valid, n)
121
+ weights = weights / total
122
+ k = min(n, len(valid))
123
+ indices = self._rng.choice(len(valid), size=k, replace=False, p=weights)
124
+ return [valid[int(i)] for i in indices]
125
+
126
+ def _top_percent_plus_random(
127
+ self,
128
+ valid: list[TrajectoryEntry],
129
+ n: int,
130
+ ) -> list[TrajectoryEntry]:
131
+ top_n = max(1, int(len(valid) * self.top_percent_threshold))
132
+ top = self._best(valid, top_n)
133
+ if n <= top_n:
134
+ return top[:n]
135
+ rest = [e for e in valid if e.entry_id not in {t.entry_id for t in top}]
136
+ if not rest:
137
+ return top[:n]
138
+ k = min(n - top_n, len(rest))
139
+ extra_indices = self._rng.choice(len(rest), size=k, replace=False)
140
+ return top + [rest[int(i)] for i in extra_indices]
@@ -0,0 +1,33 @@
1
+ """Visualization — 演化实验交互式 HTML 报告。
2
+
3
+ 公开 API:
4
+ - generate_report(entries, metric) -> dict[Figure]
5
+ - generate_html(entries, metric, output_path) -> str / 写文件
6
+ - ReportBuilder / Section / Report (Phase 1.3 fluent API)
7
+ - build_lineage_layout(entries, metric) -> {nodes, edges}
8
+ - lineage_dag_figure / metric_distribution_figure / metric_per_round_figure
9
+ - gate_breakdown_figure / operation_breakdown_figure
10
+ """
11
+ from .lineage_dag import build_lineage_layout, lineage_dag_figure
12
+ from .metric_distribution import (
13
+ metric_distribution_figure,
14
+ metric_per_round_figure,
15
+ )
16
+ from .gate_breakdown import gate_breakdown_figure, operation_breakdown_figure
17
+ from .report import generate_html, generate_report
18
+ from .builder import ReportBuilder, Section, Report
19
+
20
+ __all__ = [
21
+ "build_lineage_layout",
22
+ "lineage_dag_figure",
23
+ "metric_distribution_figure",
24
+ "metric_per_round_figure",
25
+ "gate_breakdown_figure",
26
+ "operation_breakdown_figure",
27
+ "generate_report",
28
+ "generate_html",
29
+ # Phase 1.3
30
+ "ReportBuilder",
31
+ "Section",
32
+ "Report",
33
+ ]
@@ -0,0 +1,233 @@
1
+ # coding=utf-8
2
+ """ReportBuilder — 流式构造演化报告 (Phase 1.3, Builder pattern).
3
+
4
+ 替代原来 generate_report() 内部硬编码的 dict 拼接逻辑。
5
+ 提供 .with_title().with_overview().add_section(...).build() 流式 API,
6
+ 并保证向后兼容 (旧 generate_report() 内部调用 ReportBuilder)。
7
+
8
+ 设计要点:
9
+ - Section 封装一个 fig + 可选标题
10
+ - 报告结构: title + overview table + 多个 section, 顺序可定制
11
+ - 与 plotly 解耦, 任何有 .to_html() 方法的对象都可作 section
12
+ - build() 返回 Report dataclass (含 sections 列表 + overview dict),
13
+ 渲染时由 ReportRenderer 转换为 HTML
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union
20
+
21
+ from QuantNodes.core.path_utils import ensure_parent
22
+ from QuantNodes.core.trajectory.entry import TrajectoryEntry
23
+
24
+
25
+ # ============================================================================
26
+ # Data classes
27
+ # ============================================================================
28
+
29
+ @dataclass
30
+ class Section:
31
+ """报告中的一个 section, 含可选标题 + 可渲染对象 (有 .to_html() 方法)。"""
32
+ title: str
33
+ payload: Any # plotly Figure 或其他有 to_html 的对象
34
+
35
+ def render(self, include_plotlyjs: bool = False, div_id: Optional[str] = None) -> str:
36
+ """渲染为 HTML fragment, 不含 <h2> 标题。
37
+
38
+ div_id: 传给 payload.to_html() 的 div id; 传 None 时用默认
39
+ f"fig_{title.lower().replace(' ', '_')}" (与旧实现兼容)。
40
+
41
+ v3.0.0 graceful degradation: when ``payload`` is None (e.g. plotly
42
+ not installed), we emit a friendly install hint instead of
43
+ crashing. ``to_dict`` still records the None value so callers
44
+ can introspect the report structure.
45
+ """
46
+ if self.payload is None:
47
+ return (
48
+ "<p><em>(plotly not installed — install with "
49
+ "<code>pip install plotly</code> to render this chart)</em></p>"
50
+ )
51
+ if hasattr(self.payload, "to_html"):
52
+ kwargs = {"full_html": False, "include_plotlyjs": include_plotlyjs}
53
+ if div_id is None:
54
+ div_id = f"fig_{self.title.lower().replace(' ', '_')}"
55
+ kwargs["div_id"] = div_id
56
+ return self.payload.to_html(**kwargs)
57
+ return f"<pre>{self.payload!r}</pre>"
58
+
59
+
60
+ @dataclass
61
+ class Report:
62
+ """构建完成的报告。"""
63
+ title: str
64
+ overview: Dict[str, Any] = field(default_factory=dict)
65
+ sections: List[Section] = field(default_factory=list)
66
+
67
+ def to_dict(self) -> Dict[str, Any]:
68
+ """与旧 generate_report() 返回格式兼容: {overview, *section_keys}。"""
69
+ out: Dict[str, Any] = {"overview": self.overview}
70
+ for i, s in enumerate(self.sections):
71
+ key = s.title.lower().replace(" ", "_")
72
+ out[key] = s.payload
73
+ return out
74
+
75
+
76
+ # ============================================================================
77
+ # ReportBuilder
78
+ # ============================================================================
79
+
80
+ class ReportBuilder:
81
+ """流式构造 Report (Builder pattern)。
82
+
83
+ 用法:
84
+ >>> from QuantNodes.core.visualization import ReportBuilder
85
+ >>> report = (
86
+ ... ReportBuilder()
87
+ ... .with_title("演化报告")
88
+ ... .with_overview({"size": 10, "passed": 8})
89
+ ... .add_section("Lineage DAG", lineage_fig)
90
+ ... .add_section("Metrics", metric_fig)
91
+ ... .build()
92
+ ... )
93
+ """
94
+
95
+ def __init__(self) -> None:
96
+ self._title: str = "Report"
97
+ self._overview: Dict[str, Any] = {}
98
+ self._sections: List[Section] = []
99
+ self._preset_loaded = False
100
+
101
+ def with_title(self, title: str) -> "ReportBuilder":
102
+ """设置报告标题。"""
103
+ self._title = title
104
+ return self
105
+
106
+ def with_overview(self, overview: Dict[str, Any]) -> "ReportBuilder":
107
+ """设置概览 dict (会被原样放入 Report.overview)。"""
108
+ self._overview = dict(overview)
109
+ return self
110
+
111
+ def add_section(self, title: str, payload: Any) -> "ReportBuilder":
112
+ """追加一个 section。payload 通常是 plotly Figure, 任何有 to_html() 的对象都接受。"""
113
+ self._sections.append(Section(title=title, payload=payload))
114
+ return self
115
+
116
+ def with_evolve_preset(
117
+ self,
118
+ entries: Sequence[TrajectoryEntry],
119
+ metric: str = "sharpe",
120
+ figure_factories: Optional[Dict[str, Callable]] = None,
121
+ ) -> "ReportBuilder":
122
+ """预置: 加载 4-5 个演化实验常用图 + 概览 (Phase 1.3 兼容 generate_report)。
123
+
124
+ Args:
125
+ entries: TrajectoryEntry 列表或 Mapping
126
+ metric: 用于可视化的指标名
127
+ figure_factories: 覆盖默认 5 个图 builder 的 dict
128
+ e.g. {"lineage_dag": my_lineage_factory}
129
+ """
130
+ from .gate_breakdown import gate_breakdown_figure, operation_breakdown_figure
131
+ from .lineage_dag import lineage_dag_figure
132
+ from .metric_distribution import metric_distribution_figure, metric_per_round_figure
133
+
134
+ items = list(entries.values() if isinstance(entries, Mapping) else entries)
135
+ n = len(items)
136
+ n_passed = sum(1 for e in items if e.feedback and e.feedback.decision)
137
+ n_rejected = n - n_passed
138
+ rounds = sorted({e.round_idx for e in items}) if items else []
139
+ metrics_vals = [
140
+ float((e.metrics or {}).get(metric, 0) or 0)
141
+ for e in items
142
+ if (e.metrics or {}).get(metric) is not None
143
+ ]
144
+ best_metric = max(metrics_vals) if metrics_vals else 0.0
145
+
146
+ self._overview = {
147
+ "size": n,
148
+ "rounds": len(rounds),
149
+ "passed": n_passed,
150
+ "passed_pct": (n_passed / n) if n > 0 else 0.0,
151
+ "rejected": n_rejected,
152
+ "best_metric": best_metric,
153
+ "metric": metric,
154
+ }
155
+ self.with_title(self._title or "QuantNodes 演化实验报告")
156
+
157
+ factories = figure_factories or {
158
+ "lineage_dag": lambda: lineage_dag_figure(
159
+ items, metric=metric, title=f"演化谱系 DAG (按 {metric})"
160
+ ),
161
+ "metric_distribution": lambda: metric_distribution_figure(items, metric=metric),
162
+ "metric_per_round": lambda: metric_per_round_figure(items, metric=metric),
163
+ "gate_breakdown": lambda: gate_breakdown_figure(items),
164
+ "operation_breakdown": lambda: operation_breakdown_figure(items),
165
+ }
166
+ for title, factory in factories.items():
167
+ self._sections.append(Section(title=title, payload=factory()))
168
+ self._preset_loaded = True
169
+ return self
170
+
171
+ def build(self) -> Report:
172
+ """构建最终 Report 对象。"""
173
+ return Report(title=self._title, overview=self._overview, sections=list(self._sections))
174
+
175
+ def build_to_html(
176
+ self,
177
+ output_path: Optional[Union[str, Path]] = None,
178
+ plotly_cdn: str = '<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>',
179
+ ) -> str:
180
+ """直接构建并渲染为 HTML 字符串 (可选写入文件)。"""
181
+ report = self.build()
182
+ html = _render_html(report, plotly_cdn=plotly_cdn)
183
+ if output_path is not None:
184
+ output_path = Path(output_path)
185
+ ensure_parent(output_path)
186
+ output_path.write_text(html, encoding="utf-8")
187
+ return html
188
+
189
+
190
+ # ============================================================================
191
+ # HTML rendering
192
+ # ============================================================================
193
+
194
+ _OVERVIEW_TEMPLATE = """
195
+ <h2>概览</h2>
196
+ <table border="1" cellpadding="6" style="border-collapse:collapse;">
197
+ <tr><th>指标</th><th>值</th></tr>
198
+ <tr><td>总 entry 数</td><td>{size}</td></tr>
199
+ <tr><td>演化轮数</td><td>{rounds}</td></tr>
200
+ <tr><td>通过数</td><td>{passed} ({passed_pct:.1%})</td></tr>
201
+ <tr><td>拒绝数</td><td>{rejected}</td></tr>
202
+ <tr><td>Best {metric}</td><td>{best_metric:.4f}</td></tr>
203
+ </table>
204
+ """
205
+
206
+
207
+ def _render_html(report: Report, plotly_cdn: str) -> str:
208
+ overview_html = _OVERVIEW_TEMPLATE.format(**report.overview) if report.overview else ""
209
+ # 使用 title 派生 div_id (与旧 generate_html 行为兼容, div_id 格式 fig_<key>)
210
+ parts = [f"<h2>{s.title}</h2>\n{s.render(include_plotlyjs=False)}"
211
+ for s in report.sections]
212
+ return f"""<!DOCTYPE html>
213
+ <html lang="zh-CN">
214
+ <head>
215
+ <meta charset="utf-8">
216
+ <title>{report.title}</title>
217
+ <style>
218
+ body {{ font-family: -apple-system, sans-serif; margin: 20px; max-width: 1200px; }}
219
+ h1 {{ border-bottom: 2px solid #4C78A8; padding-bottom: 8px; }}
220
+ table {{ background: #fafafa; }}
221
+ th {{ background: #4C78A8; color: white; }}
222
+ </style>
223
+ {plotly_cdn}
224
+ </head>
225
+ <body>
226
+ <h1>{report.title}</h1>
227
+ {overview_html}
228
+ {"".join(parts)}
229
+ <hr>
230
+ <p style="color: #888; font-size: 12px;">Generated by QuantNodes ReportBuilder</p>
231
+ </body>
232
+ </html>
233
+ """