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,407 @@
1
+ # coding=utf-8
2
+ """监控数据仓储层 - SQLite CRUD操作"""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sqlite3
8
+ from datetime import date, datetime, timedelta
9
+ from pathlib import Path
10
+ from typing import Optional, List
11
+
12
+ from QuantNodes.core.path_utils import ensure_parent
13
+
14
+ from .models import (
15
+ StrategyRun, PerformanceSnapshot, DriftAlert, StrategyVersion,
16
+ )
17
+
18
+ # SQLite建表DDL
19
+ _CREATE_TABLES = """
20
+ CREATE TABLE IF NOT EXISTS strategy_runs (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ strategy_name TEXT NOT NULL,
23
+ strategy_version TEXT,
24
+ run_type TEXT NOT NULL,
25
+ start_time TIMESTAMP,
26
+ end_time TIMESTAMP,
27
+ status TEXT NOT NULL,
28
+ config_snapshot TEXT,
29
+ statistics TEXT,
30
+ error_message TEXT,
31
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS performance_snapshots (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ strategy_name TEXT NOT NULL,
37
+ snapshot_date DATE NOT NULL,
38
+ sharpe_ratio REAL,
39
+ sortino_ratio REAL,
40
+ max_drawdown REAL,
41
+ annualized_return REAL,
42
+ annualized_volatility REAL,
43
+ win_rate REAL,
44
+ profit_factor REAL,
45
+ total_trades INTEGER,
46
+ daily_returns TEXT,
47
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
48
+ UNIQUE(strategy_name, snapshot_date)
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS drift_alerts (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ strategy_name TEXT NOT NULL,
54
+ alert_type TEXT NOT NULL,
55
+ severity TEXT NOT NULL,
56
+ metric_name TEXT,
57
+ current_value REAL,
58
+ baseline_value REAL,
59
+ p_value REAL,
60
+ message TEXT,
61
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
62
+ acknowledged BOOLEAN DEFAULT FALSE
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS strategy_versions (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ strategy_name TEXT NOT NULL,
68
+ version TEXT NOT NULL,
69
+ commit_hash TEXT,
70
+ config_snapshot TEXT NOT NULL,
71
+ description TEXT,
72
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
73
+ UNIQUE(strategy_name, version)
74
+ );
75
+ """
76
+
77
+
78
+ class DatabaseManager:
79
+ """SQLite数据库管理器"""
80
+
81
+ def __init__(self, db_path: str = "~/.quantnodes/monitor.db"):
82
+ self.db_path = Path(db_path).expanduser()
83
+ ensure_parent(self.db_path)
84
+ self._conn: Optional[sqlite3.Connection] = None
85
+
86
+ def connect(self) -> sqlite3.Connection:
87
+ if self._conn is None:
88
+ self._conn = sqlite3.connect(str(self.db_path))
89
+ self._conn.row_factory = sqlite3.Row
90
+ self._conn.execute("PRAGMA journal_mode=WAL")
91
+ self._conn.execute("PRAGMA foreign_keys=ON")
92
+ self._create_tables()
93
+ return self._conn
94
+
95
+ def _create_tables(self):
96
+ self._conn.executescript(_CREATE_TABLES)
97
+
98
+ def close(self):
99
+ if self._conn:
100
+ self._conn.close()
101
+ self._conn = None
102
+
103
+ def __enter__(self):
104
+ self.connect()
105
+ return self
106
+
107
+ def __exit__(self, exc_type, exc_val, exc_tb):
108
+ self.close()
109
+
110
+ @property
111
+ def conn(self) -> sqlite3.Connection:
112
+ if self._conn is None:
113
+ self.connect()
114
+ return self._conn
115
+
116
+
117
+ class StrategyRunRepository:
118
+ """策略运行记录仓储"""
119
+
120
+ def __init__(self, db: DatabaseManager):
121
+ self.db = db
122
+
123
+ def create(self, run: StrategyRun) -> int:
124
+ cur = self.db.conn.execute(
125
+ """INSERT INTO strategy_runs
126
+ (strategy_name, strategy_version, run_type, start_time, end_time,
127
+ status, config_snapshot, statistics, error_message)
128
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
129
+ (run.strategy_name, run.strategy_version, run.run_type,
130
+ run.start_time, run.end_time, run.status,
131
+ run.config_snapshot, run.statistics, run.error_message),
132
+ )
133
+ self.db.conn.commit()
134
+ return cur.lastrowid
135
+
136
+ def get_by_id(self, run_id: int) -> Optional[StrategyRun]:
137
+ row = self.db.conn.execute(
138
+ "SELECT * FROM strategy_runs WHERE id = ?", (run_id,)
139
+ ).fetchone()
140
+ return self._row_to_run(row) if row else None
141
+
142
+ def get_by_strategy(self, strategy_name: str, limit: int = 50) -> List[StrategyRun]:
143
+ rows = self.db.conn.execute(
144
+ "SELECT * FROM strategy_runs WHERE strategy_name = ? ORDER BY created_at DESC LIMIT ?",
145
+ (strategy_name, limit),
146
+ ).fetchall()
147
+ return [self._row_to_run(r) for r in rows]
148
+
149
+ def update_status(self, run_id: int, status: str,
150
+ statistics: dict = None, error_message: str = None) -> None:
151
+ stats_json = json.dumps(statistics) if statistics else None
152
+ if status in ("success", "failed"):
153
+ self.db.conn.execute(
154
+ """UPDATE strategy_runs
155
+ SET status = ?, statistics = ?, error_message = ?, end_time = ?
156
+ WHERE id = ?""",
157
+ (status, stats_json, error_message, datetime.now(), run_id),
158
+ )
159
+ else:
160
+ self.db.conn.execute(
161
+ "UPDATE strategy_runs SET status = ? WHERE id = ?",
162
+ (status, run_id),
163
+ )
164
+ self.db.conn.commit()
165
+
166
+ def delete_old(self, strategy_name: str, keep_count: int = 100) -> int:
167
+ # SQLite 不支持 OFFSET in DELETE subquery, 使用 LIMIT 方式
168
+ cur = self.db.conn.execute(
169
+ """DELETE FROM strategy_runs WHERE id NOT IN (
170
+ SELECT id FROM strategy_runs
171
+ WHERE strategy_name = ?
172
+ ORDER BY created_at DESC
173
+ LIMIT ?
174
+ ) AND strategy_name = ?""",
175
+ (strategy_name, keep_count, strategy_name),
176
+ )
177
+ self.db.conn.commit()
178
+ return cur.rowcount
179
+
180
+ @staticmethod
181
+ def _row_to_run(row: sqlite3.Row) -> StrategyRun:
182
+ return StrategyRun(
183
+ id=row["id"],
184
+ strategy_name=row["strategy_name"],
185
+ strategy_version=row["strategy_version"],
186
+ run_type=row["run_type"],
187
+ start_time=row["start_time"],
188
+ end_time=row["end_time"],
189
+ status=row["status"],
190
+ config_snapshot=row["config_snapshot"],
191
+ statistics=row["statistics"],
192
+ error_message=row["error_message"],
193
+ created_at=row["created_at"],
194
+ )
195
+
196
+
197
+ class PerformanceRepository:
198
+ """绩效快照仓储"""
199
+
200
+ def __init__(self, db: DatabaseManager):
201
+ self.db = db
202
+
203
+ def save_snapshot(self, snapshot: PerformanceSnapshot) -> int:
204
+ cur = self.db.conn.execute(
205
+ """INSERT OR REPLACE INTO performance_snapshots
206
+ (strategy_name, snapshot_date, sharpe_ratio, sortino_ratio,
207
+ max_drawdown, annualized_return, annualized_volatility,
208
+ win_rate, profit_factor, total_trades, daily_returns)
209
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
210
+ (snapshot.strategy_name, snapshot.snapshot_date,
211
+ snapshot.sharpe_ratio, snapshot.sortino_ratio,
212
+ snapshot.max_drawdown, snapshot.annualized_return,
213
+ snapshot.annualized_volatility, snapshot.win_rate,
214
+ snapshot.profit_factor, snapshot.total_trades,
215
+ snapshot.daily_returns),
216
+ )
217
+ self.db.conn.commit()
218
+ return cur.lastrowid
219
+
220
+ def get_latest(self, strategy_name: str) -> Optional[PerformanceSnapshot]:
221
+ row = self.db.conn.execute(
222
+ """SELECT * FROM performance_snapshots
223
+ WHERE strategy_name = ? ORDER BY snapshot_date DESC LIMIT 1""",
224
+ (strategy_name,),
225
+ ).fetchone()
226
+ return self._row_to_snapshot(row) if row else None
227
+
228
+ def get_history(self, strategy_name: str, days: int = 30) -> List[PerformanceSnapshot]:
229
+ cutoff = date.today() - timedelta(days=days)
230
+ rows = self.db.conn.execute(
231
+ """SELECT * FROM performance_snapshots
232
+ WHERE strategy_name = ? AND snapshot_date >= ?
233
+ ORDER BY snapshot_date""",
234
+ (strategy_name, cutoff.isoformat()),
235
+ ).fetchall()
236
+ return [self._row_to_snapshot(r) for r in rows]
237
+
238
+ def get_baseline(self, strategy_name: str, days: int = 252) -> Optional[PerformanceSnapshot]:
239
+ cutoff = date.today() - timedelta(days=days)
240
+ rows = self.db.conn.execute(
241
+ """SELECT * FROM performance_snapshots
242
+ WHERE strategy_name = ? AND snapshot_date >= ?
243
+ ORDER BY snapshot_date""",
244
+ (strategy_name, cutoff.isoformat()),
245
+ ).fetchall()
246
+ if not rows:
247
+ return None
248
+ snapshots = [self._row_to_snapshot(r) for r in rows]
249
+ return self._average_snapshots(snapshots)
250
+
251
+ @staticmethod
252
+ def _average_snapshots(snapshots: List[PerformanceSnapshot]) -> PerformanceSnapshot:
253
+ n = len(snapshots)
254
+ s = snapshots[0]
255
+ return PerformanceSnapshot(
256
+ strategy_name=s.strategy_name,
257
+ snapshot_date=s.snapshot_date,
258
+ sharpe_ratio=sum(s.sharpe_ratio or 0 for s in snapshots) / n,
259
+ sortino_ratio=sum(s.sortino_ratio or 0 for s in snapshots) / n,
260
+ max_drawdown=sum(s.max_drawdown or 0 for s in snapshots) / n,
261
+ annualized_return=sum(s.annualized_return or 0 for s in snapshots) / n,
262
+ annualized_volatility=sum(s.annualized_volatility or 0 for s in snapshots) / n,
263
+ win_rate=sum(s.win_rate or 0 for s in snapshots) / n,
264
+ profit_factor=sum(s.profit_factor or 0 for s in snapshots) / n,
265
+ total_trades=sum(s.total_trades or 0 for s in snapshots) // n,
266
+ )
267
+
268
+ @staticmethod
269
+ def _row_to_snapshot(row: sqlite3.Row) -> PerformanceSnapshot:
270
+ return PerformanceSnapshot(
271
+ id=row["id"],
272
+ strategy_name=row["strategy_name"],
273
+ snapshot_date=row["snapshot_date"],
274
+ sharpe_ratio=row["sharpe_ratio"],
275
+ sortino_ratio=row["sortino_ratio"],
276
+ max_drawdown=row["max_drawdown"],
277
+ annualized_return=row["annualized_return"],
278
+ annualized_volatility=row["annualized_volatility"],
279
+ win_rate=row["win_rate"],
280
+ profit_factor=row["profit_factor"],
281
+ total_trades=row["total_trades"],
282
+ daily_returns=row["daily_returns"],
283
+ created_at=row["created_at"],
284
+ )
285
+
286
+
287
+ class DriftAlertRepository:
288
+ """漂移告警仓储"""
289
+
290
+ def __init__(self, db: DatabaseManager):
291
+ self.db = db
292
+
293
+ def create_alert(self, alert: DriftAlert) -> int:
294
+ cur = self.db.conn.execute(
295
+ """INSERT INTO drift_alerts
296
+ (strategy_name, alert_type, severity, metric_name,
297
+ current_value, baseline_value, p_value, message, acknowledged)
298
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
299
+ (alert.strategy_name, alert.alert_type, alert.severity,
300
+ alert.metric_name, alert.current_value, alert.baseline_value,
301
+ alert.p_value, alert.message, alert.acknowledged),
302
+ )
303
+ self.db.conn.commit()
304
+ return cur.lastrowid
305
+
306
+ def get_pending(self, strategy_name: str = None) -> List[DriftAlert]:
307
+ if strategy_name:
308
+ rows = self.db.conn.execute(
309
+ """SELECT * FROM drift_alerts
310
+ WHERE strategy_name = ? AND acknowledged = 0
311
+ ORDER BY created_at DESC""",
312
+ (strategy_name,),
313
+ ).fetchall()
314
+ else:
315
+ rows = self.db.conn.execute(
316
+ """SELECT * FROM drift_alerts
317
+ WHERE acknowledged = 0 ORDER BY created_at DESC"""
318
+ ).fetchall()
319
+ return [self._row_to_alert(r) for r in rows]
320
+
321
+ def acknowledge(self, alert_id: int) -> None:
322
+ self.db.conn.execute(
323
+ "UPDATE drift_alerts SET acknowledged = 1 WHERE id = ?",
324
+ (alert_id,),
325
+ )
326
+ self.db.conn.commit()
327
+
328
+ def get_history(self, strategy_name: str, days: int = 30) -> List[DriftAlert]:
329
+ cutoff = datetime.now() - timedelta(days=days)
330
+ rows = self.db.conn.execute(
331
+ """SELECT * FROM drift_alerts
332
+ WHERE strategy_name = ? AND created_at >= ?
333
+ ORDER BY created_at DESC""",
334
+ (strategy_name, cutoff),
335
+ ).fetchall()
336
+ return [self._row_to_alert(r) for r in rows]
337
+
338
+ @staticmethod
339
+ def _row_to_alert(row: sqlite3.Row) -> DriftAlert:
340
+ return DriftAlert(
341
+ id=row["id"],
342
+ strategy_name=row["strategy_name"],
343
+ alert_type=row["alert_type"],
344
+ severity=row["severity"],
345
+ metric_name=row["metric_name"],
346
+ current_value=row["current_value"],
347
+ baseline_value=row["baseline_value"],
348
+ p_value=row["p_value"],
349
+ message=row["message"],
350
+ acknowledged=bool(row["acknowledged"]),
351
+ created_at=row["created_at"],
352
+ )
353
+
354
+
355
+ class VersionRepository:
356
+ """策略版本仓储"""
357
+
358
+ def __init__(self, db: DatabaseManager):
359
+ self.db = db
360
+
361
+ def save_version(self, version: StrategyVersion) -> int:
362
+ cur = self.db.conn.execute(
363
+ """INSERT OR REPLACE INTO strategy_versions
364
+ (strategy_name, version, commit_hash, config_snapshot, description)
365
+ VALUES (?, ?, ?, ?, ?)""",
366
+ (version.strategy_name, version.version,
367
+ version.commit_hash, version.config_snapshot,
368
+ version.description),
369
+ )
370
+ self.db.conn.commit()
371
+ return cur.lastrowid
372
+
373
+ def get_version(self, strategy_name: str, version: str) -> Optional[StrategyVersion]:
374
+ row = self.db.conn.execute(
375
+ """SELECT * FROM strategy_versions
376
+ WHERE strategy_name = ? AND version = ?""",
377
+ (strategy_name, version),
378
+ ).fetchone()
379
+ return self._row_to_version(row) if row else None
380
+
381
+ def list_versions(self, strategy_name: str) -> List[StrategyVersion]:
382
+ rows = self.db.conn.execute(
383
+ """SELECT * FROM strategy_versions
384
+ WHERE strategy_name = ? ORDER BY id DESC""",
385
+ (strategy_name,),
386
+ ).fetchall()
387
+ return [self._row_to_version(r) for r in rows]
388
+
389
+ def get_latest(self, strategy_name: str) -> Optional[StrategyVersion]:
390
+ row = self.db.conn.execute(
391
+ """SELECT * FROM strategy_versions
392
+ WHERE strategy_name = ? ORDER BY id DESC LIMIT 1""",
393
+ (strategy_name,),
394
+ ).fetchone()
395
+ return self._row_to_version(row) if row else None
396
+
397
+ @staticmethod
398
+ def _row_to_version(row: sqlite3.Row) -> StrategyVersion:
399
+ return StrategyVersion(
400
+ id=row["id"],
401
+ strategy_name=row["strategy_name"],
402
+ version=row["version"],
403
+ commit_hash=row["commit_hash"],
404
+ config_snapshot=row["config_snapshot"],
405
+ description=row["description"],
406
+ created_at=row["created_at"],
407
+ )
@@ -0,0 +1,4 @@
1
+ from .version_manager import VersionManager
2
+ from .diff import ConfigDiffer
3
+
4
+ __all__ = ["VersionManager", "ConfigDiffer"]
@@ -0,0 +1,81 @@
1
+ # coding=utf-8
2
+ """YAML配置差异对比"""
3
+
4
+ from __future__ import annotations
5
+
6
+ import difflib
7
+ from typing import Dict, Any, List, Tuple
8
+
9
+
10
+ class ConfigDiffer:
11
+ """YAML配置差异对比"""
12
+
13
+ def diff_configs_text(self, config1_text: str, config2_text: str) -> List[str]:
14
+ """对比两个YAML文本的差异
15
+
16
+ Returns:
17
+ unified diff 行列表
18
+ """
19
+ lines1 = config1_text.splitlines(keepends=True)
20
+ lines2 = config2_text.splitlines(keepends=True)
21
+ return list(difflib.unified_diff(
22
+ lines1, lines2,
23
+ fromfile="version_a", tofile="version_b",
24
+ lineterm="",
25
+ ))
26
+
27
+ def diff_configs(
28
+ self, config1: Dict[str, Any], config2: Dict[str, Any]
29
+ ) -> Dict[str, Any]:
30
+ """对比两个配置字典的差异
31
+
32
+ Returns:
33
+ 结构化差异: {"added": [...], "removed": [...], "changed": [...]}
34
+ """
35
+ added = []
36
+ removed = []
37
+ changed = []
38
+
39
+ all_keys = set(list(config1.keys()) + list(config2.keys()))
40
+ for key in sorted(all_keys):
41
+ if key not in config1:
42
+ added.append({"key": "data." + key, "value": config2[key]})
43
+ elif key not in config2:
44
+ removed.append({"key": "data." + key, "value": config1[key]})
45
+ elif config1[key] != config2[key]:
46
+ changed.append({
47
+ "key": "data." + key,
48
+ "old": config1[key],
49
+ "new": config2[key],
50
+ })
51
+
52
+ return {"added": added, "removed": removed, "changed": changed}
53
+
54
+ def format_diff(self, diff_lines: List[str]) -> str:
55
+ """格式化差异为可读文本"""
56
+ if not diff_lines:
57
+ return "无差异"
58
+ return "\n".join(diff_lines)
59
+
60
+ def validate_rollback_safe(self, diff_result: Dict[str, Any]) -> Tuple[bool, List[str]]:
61
+ """检查回滚是否安全
62
+
63
+ 不安全条件:
64
+ - 删除了关键配置 (data.source, data.table)
65
+ - 修改了数据源
66
+
67
+ Returns:
68
+ (是否安全, 风险说明列表)
69
+ """
70
+ risks = []
71
+ for item in diff_result.get("removed", []):
72
+ key = item["key"]
73
+ if key in ("data.source", "data.table", "data.conn_ini"):
74
+ risks.append(f"删除了关键配置: {key}")
75
+
76
+ for item in diff_result.get("changed", []):
77
+ key = item["key"]
78
+ if key in ("data.source", "data.table"):
79
+ risks.append(f"修改了数据源配置: {key} ({item['old']} → {item['new']})")
80
+
81
+ return (len(risks) == 0, risks)
@@ -0,0 +1,182 @@
1
+ # coding=utf-8
2
+ """基于Git的策略版本管理"""
3
+
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+
10
+ from ..storage.models import StrategyVersion
11
+ from ..storage.repository import VersionRepository
12
+ from .diff import ConfigDiffer
13
+ from QuantNodes.core.path_utils import ensure_dir
14
+
15
+
16
+ class VersionManager:
17
+ """基于Git的策略版本管理
18
+
19
+ 每个策略对应Git仓库中的一个目录:
20
+ ~/.quantnodes/strategies/{strategy_name}/
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ version_repo: VersionRepository,
26
+ strategies_dir: str = "~/.quantnodes/strategies",
27
+ ):
28
+ self.repo = version_repo
29
+ self.strategies_dir = Path(strategies_dir).expanduser()
30
+ ensure_dir(self.strategies_dir)
31
+ self.differ = ConfigDiffer()
32
+ self._init_repo()
33
+
34
+ def _init_repo(self):
35
+ """初始化Git仓库 (如不存在)"""
36
+ git_dir = self.strategies_dir / ".git"
37
+ if not git_dir.exists():
38
+ import subprocess
39
+ subprocess.run(
40
+ ["git", "init"],
41
+ cwd=str(self.strategies_dir),
42
+ capture_output=True,
43
+ check=False,
44
+ )
45
+ # 配置 git user
46
+ subprocess.run(
47
+ ["git", "config", "user.email", "quantnodes@local"],
48
+ cwd=str(self.strategies_dir),
49
+ capture_output=True,
50
+ check=False,
51
+ )
52
+ subprocess.run(
53
+ ["git", "config", "user.name", "QuantNodes"],
54
+ cwd=str(self.strategies_dir),
55
+ capture_output=True,
56
+ check=False,
57
+ )
58
+ # 创建 .gitignore
59
+ gitignore = self.strategies_dir / ".gitignore"
60
+ if not gitignore.exists():
61
+ gitignore.write_text("__pycache__/\n*.pyc\n")
62
+
63
+ def save_version(
64
+ self,
65
+ strategy_name: str,
66
+ config_path: str,
67
+ description: str = "",
68
+ ) -> StrategyVersion:
69
+ """保存策略版本
70
+
71
+ 1. 复制YAML到 strategies/{name}/
72
+ 2. git add + commit
73
+ 3. 保存到数据库
74
+ """
75
+ strategy_dir = self.strategies_dir / strategy_name
76
+ ensure_dir(strategy_dir)
77
+
78
+ # 确定版本号
79
+ existing = self.repo.list_versions(strategy_name)
80
+ version_num = len(existing) + 1
81
+ version_str = f"v{version_num}"
82
+
83
+ # 复制YAML配置
84
+ src = Path(config_path).expanduser()
85
+ dst = strategy_dir / f"{strategy_name}_{version_str}.yaml"
86
+ shutil.copy2(str(src), str(dst))
87
+
88
+ # Git commit
89
+ commit_msg = f"version {version_str}: {description or strategy_name}"
90
+ commit_hash = self._git_commit(strategy_dir, commit_msg)
91
+
92
+ # 同时保存为 current
93
+ current_dst = strategy_dir / f"{strategy_name}_current.yaml"
94
+ shutil.copy2(str(src), str(current_dst))
95
+ self._git_commit(strategy_dir, f"update current: {strategy_name}")
96
+
97
+ # 保存到数据库
98
+ config_content = src.read_text(encoding="utf-8")
99
+ sv = StrategyVersion(
100
+ strategy_name=strategy_name,
101
+ version=version_str,
102
+ commit_hash=commit_hash,
103
+ config_snapshot=config_content,
104
+ description=description,
105
+ )
106
+ sv.id = self.repo.save_version(sv)
107
+ return sv
108
+
109
+ def list_versions(self, strategy_name: str) -> List[StrategyVersion]:
110
+ """列出策略所有版本"""
111
+ return self.repo.list_versions(strategy_name)
112
+
113
+ def get_version(self, strategy_name: str, version: str) -> Optional[StrategyVersion]:
114
+ """获取指定版本的配置"""
115
+ return self.repo.get_version(strategy_name, version)
116
+
117
+ def diff_versions(self, strategy_name: str, v1: str, v2: str) -> str:
118
+ """对比两个版本的差异"""
119
+ ver1 = self.repo.get_version(strategy_name, v1)
120
+ ver2 = self.repo.get_version(strategy_name, v2)
121
+ if not ver1 or not ver2:
122
+ return f"版本不存在: {v1} 或 {v2}"
123
+ return self.differ.format_diff(
124
+ self.differ.diff_configs_text(ver1.config_snapshot, ver2.config_snapshot)
125
+ )
126
+
127
+ def rollback(self, strategy_name: str, target_version: str) -> Optional[StrategyVersion]:
128
+ """回滚到指定版本"""
129
+ target = self.repo.get_version(strategy_name, target_version)
130
+ if not target:
131
+ return None
132
+
133
+ # 写入 current.yaml
134
+ strategy_dir = self.strategies_dir / strategy_name
135
+ ensure_dir(strategy_dir)
136
+ current_path = strategy_dir / f"{strategy_name}_current.yaml"
137
+ current_path.write_text(target.config_snapshot, encoding="utf-8")
138
+
139
+ # Git commit
140
+ self._git_commit(strategy_dir, f"rollback to {target_version}")
141
+
142
+ # 创建新版本记录
143
+ new_ver_num = len(self.repo.list_versions(strategy_name)) + 1
144
+ new_version = f"v{new_ver_num}"
145
+ sv = StrategyVersion(
146
+ strategy_name=strategy_name,
147
+ version=new_version,
148
+ commit_hash=self._git_head(strategy_dir),
149
+ config_snapshot=target.config_snapshot,
150
+ description=f"rollback to {target_version}",
151
+ )
152
+ sv.id = self.repo.save_version(sv)
153
+ return sv
154
+
155
+ def get_current_version(self, strategy_name: str) -> Optional[str]:
156
+ """获取当前最新版本号"""
157
+ latest = self.repo.get_latest(strategy_name)
158
+ return latest.version if latest else None
159
+
160
+ def _git_commit(self, repo_dir: Path, message: str) -> str:
161
+ """执行 git add + commit,返回 commit hash"""
162
+ import subprocess
163
+
164
+ subprocess.run(
165
+ ["git", "add", "-A"],
166
+ cwd=str(repo_dir), capture_output=True, check=False,
167
+ )
168
+ subprocess.run(
169
+ ["git", "commit", "-m", message, "--allow-empty"],
170
+ cwd=str(repo_dir), capture_output=True, check=False,
171
+ )
172
+ return self._git_head(repo_dir)
173
+
174
+ @staticmethod
175
+ def _git_head(repo_dir: Path) -> str:
176
+ """获取当前 HEAD commit hash"""
177
+ import subprocess
178
+ result = subprocess.run(
179
+ ["git", "rev-parse", "HEAD"],
180
+ cwd=str(repo_dir), capture_output=True, text=True, check=False,
181
+ )
182
+ return result.stdout.strip()[:8] if result.returncode == 0 else "unknown"