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.
- QuantNodes/__init__.py +15 -0
- QuantNodes/__main__.py +14 -0
- QuantNodes/agent/__init__.py +158 -0
- QuantNodes/agent/agents/__init__.py +13 -0
- QuantNodes/agent/agents/definition.py +180 -0
- QuantNodes/agent/agents/manager.py +73 -0
- QuantNodes/agent/config/__init__.py +34 -0
- QuantNodes/agent/config/executor.py +958 -0
- QuantNodes/agent/config/loader.py +427 -0
- QuantNodes/agent/config/templates/bollinger_bands.yaml +84 -0
- QuantNodes/agent/config/templates/dual_ma.yaml +72 -0
- QuantNodes/agent/config/templates/empty.yaml +56 -0
- QuantNodes/agent/config/templates/mean_reversion.yaml +47 -0
- QuantNodes/agent/config/templates/mean_reversion_zscore.yaml +90 -0
- QuantNodes/agent/config/templates/momentum.yaml +81 -0
- QuantNodes/agent/config/templates/momentum_breakout.yaml +84 -0
- QuantNodes/agent/config/templates/rsi_strategy.yaml +72 -0
- QuantNodes/agent/config/templates/volume_price.yaml +86 -0
- QuantNodes/agent/config/types.py +156 -0
- QuantNodes/agent/config_mapper.py +293 -0
- QuantNodes/agent/core/__init__.py +19 -0
- QuantNodes/agent/core/dream.py +47 -0
- QuantNodes/agent/core/quant_dream.py +274 -0
- QuantNodes/agent/cron_jobs.py +314 -0
- QuantNodes/agent/nanobot_bridge.py +242 -0
- QuantNodes/agent/permission/__init__.py +30 -0
- QuantNodes/agent/permission/defaults.py +36 -0
- QuantNodes/agent/permission/evaluate.py +41 -0
- QuantNodes/agent/permission/models.py +59 -0
- QuantNodes/agent/permission/service.py +133 -0
- QuantNodes/agent/providers/__init__.py +11 -0
- QuantNodes/agent/providers/base.py +102 -0
- QuantNodes/agent/providers/quantnodes.py +610 -0
- QuantNodes/agent/providers/rate_limiter.py +326 -0
- QuantNodes/agent/providers/registry.py +163 -0
- QuantNodes/agent/skills/__init__.py +20 -0
- QuantNodes/agent/skills/base.py +118 -0
- QuantNodes/agent/skills/bridge.py +73 -0
- QuantNodes/agent/skills/factor/__init__.py +14 -0
- QuantNodes/agent/skills/factor/correlation.py +99 -0
- QuantNodes/agent/skills/factor/group_backtest.py +114 -0
- QuantNodes/agent/skills/factor/ic_analysis.py +106 -0
- QuantNodes/agent/skills/loader.py +107 -0
- QuantNodes/agent/skills/registry.py +105 -0
- QuantNodes/agent/skills/strategy/__init__.py +16 -0
- QuantNodes/agent/skills/strategy/bollinger.py +86 -0
- QuantNodes/agent/skills/strategy/dual_ma.py +82 -0
- QuantNodes/agent/skills/strategy/momentum.py +74 -0
- QuantNodes/agent/skills/strategy/rsi_reversal.py +99 -0
- QuantNodes/agent/skills_quant/__init__.py +14 -0
- QuantNodes/agent/skills_quant/backtest-analyze/SKILL.md +42 -0
- QuantNodes/agent/skills_quant/config-driven/SKILL.md +72 -0
- QuantNodes/agent/skills_quant/factor-research/SKILL.md +40 -0
- QuantNodes/agent/skills_quant/quant-dream/SKILL.md +55 -0
- QuantNodes/agent/skills_quant/risk-management/SKILL.md +45 -0
- QuantNodes/agent/skills_quant/strategy-design/SKILL.md +43 -0
- QuantNodes/agent/templates/__init__.py +4 -0
- QuantNodes/agent/tools/__init__.py +173 -0
- QuantNodes/agent/tools/_workspace.py +51 -0
- QuantNodes/agent/tools/alpha_backtest.py +328 -0
- QuantNodes/agent/tools/alpha_evaluate.py +493 -0
- QuantNodes/agent/tools/backtest.py +226 -0
- QuantNodes/agent/tools/base.py +133 -0
- QuantNodes/agent/tools/code_search.py +207 -0
- QuantNodes/agent/tools/config_backtest.py +401 -0
- QuantNodes/agent/tools/context.py +97 -0
- QuantNodes/agent/tools/dream_skill.py +77 -0
- QuantNodes/agent/tools/echo.py +38 -0
- QuantNodes/agent/tools/factor.py +231 -0
- QuantNodes/agent/tools/file_ops.py +201 -0
- QuantNodes/agent/tools/git_ops.py +190 -0
- QuantNodes/agent/tools/operator_lookup.py +218 -0
- QuantNodes/agent/tools/output_truncation.py +77 -0
- QuantNodes/agent/tools/path_check.py +43 -0
- QuantNodes/agent/tools/pipeline.py +62 -0
- QuantNodes/agent/tools/registry.py +150 -0
- QuantNodes/agent/tools/sandbox.py +62 -0
- QuantNodes/agent/tools/shell_safety.py +63 -0
- QuantNodes/agent/tools/strategy.py +106 -0
- QuantNodes/agent/tools/task.py +171 -0
- QuantNodes/agent/tools/web_fetch.py +142 -0
- QuantNodes/agent/tools/web_search.py +114 -0
- QuantNodes/agent/tools/wiki.py +370 -0
- QuantNodes/agent/utils/__init__.py +11 -0
- QuantNodes/agent/utils/helpers.py +43 -0
- QuantNodes/agent/utils/prompt_templates.py +30 -0
- QuantNodes/agent/workflows/__init__.py +20 -0
- QuantNodes/agent/workflows/implementations/__init__.py +8 -0
- QuantNodes/agent/workflows/implementations/alpha_gpt.py +508 -0
- QuantNodes/agent/workflows/implementations/mcts.py +442 -0
- QuantNodes/agent/workflows/parsers.py +44 -0
- QuantNodes/agent/workflows/registry.py +119 -0
- QuantNodes/agent/workflows/step_agent.py +219 -0
- QuantNodes/agent/workflows/tool.py +198 -0
- QuantNodes/ai/__init__.py +93 -0
- QuantNodes/ai/llm/__init__.py +75 -0
- QuantNodes/ai/llm/base.py +233 -0
- QuantNodes/ai/llm/decorators.py +281 -0
- QuantNodes/ai/llm/gateway.py +571 -0
- QuantNodes/ai/llm/null.py +76 -0
- QuantNodes/ai/llm/openai.py +435 -0
- QuantNodes/ai/optimizer.py +405 -0
- QuantNodes/ai/prompts/__init__.py +229 -0
- QuantNodes/ai/sandbox.py +371 -0
- QuantNodes/ai/sandbox_pandas_bridge.py +150 -0
- QuantNodes/ai/strategy_gen.py +396 -0
- QuantNodes/backtest/__init__.py +64 -0
- QuantNodes/backtest/backtest_node.py +188 -0
- QuantNodes/backtest/broker_node.py +378 -0
- QuantNodes/backtest/config_runner.py +397 -0
- QuantNodes/backtest/config_strategy.py +64 -0
- QuantNodes/backtest/risk_node.py +360 -0
- QuantNodes/backtest/strategy_node.py +268 -0
- QuantNodes/cache_node/__init__.py +19 -0
- QuantNodes/cache_node/base.py +244 -0
- QuantNodes/cache_node/cache_store.py +99 -0
- QuantNodes/cache_node/metadata.py +100 -0
- QuantNodes/cli/__init__.py +109 -0
- QuantNodes/cli/_helpers.py +511 -0
- QuantNodes/cli/command.py +110 -0
- QuantNodes/cli/commands/__init__.py +69 -0
- QuantNodes/cli/commands/agent.py +158 -0
- QuantNodes/cli/commands/alpha.py +951 -0
- QuantNodes/cli/commands/chat.py +38 -0
- QuantNodes/cli/commands/evolve.py +120 -0
- QuantNodes/cli/commands/factor.py +569 -0
- QuantNodes/cli/commands/init.py +190 -0
- QuantNodes/cli/commands/run.py +259 -0
- QuantNodes/cli/commands/serve.py +398 -0
- QuantNodes/cli/commands/version.py +120 -0
- QuantNodes/cli/enhanced.py +146 -0
- QuantNodes/conf_node/__init__.py +37 -0
- QuantNodes/conf_node/base.py +120 -0
- QuantNodes/conf_node/env_config.py +132 -0
- QuantNodes/conf_node/ini_config.py +70 -0
- QuantNodes/conf_node/json_config.py +69 -0
- QuantNodes/conf_node/yaml_config.py +78 -0
- QuantNodes/constants.py +17 -0
- QuantNodes/core/__init__.py +196 -0
- QuantNodes/core/_lookback_helpers.py +49 -0
- QuantNodes/core/ast_parser.py +198 -0
- QuantNodes/core/base.py +61 -0
- QuantNodes/core/cache_manager.py +344 -0
- QuantNodes/core/cache_utils.py +150 -0
- QuantNodes/core/cond_builder.py +53 -0
- QuantNodes/core/config.py +170 -0
- QuantNodes/core/constants.py +48 -0
- QuantNodes/core/control.py +412 -0
- QuantNodes/core/data_preprocessing.py +453 -0
- QuantNodes/core/data_source.py +46 -0
- QuantNodes/core/events.py +178 -0
- QuantNodes/core/evolution/__init__.py +22 -0
- QuantNodes/core/evolution/loop.py +583 -0
- QuantNodes/core/evolution/operators.py +289 -0
- QuantNodes/core/evolution/settings.py +44 -0
- QuantNodes/core/expression.py +841 -0
- QuantNodes/core/feedback/__init__.py +38 -0
- QuantNodes/core/feedback/channels.py +182 -0
- QuantNodes/core/feedback/collector.py +91 -0
- QuantNodes/core/feedback/dataclass.py +239 -0
- QuantNodes/core/feedback/llm_judge.py +138 -0
- QuantNodes/core/knowledge/__init__.py +69 -0
- QuantNodes/core/knowledge/knowledge_base.py +217 -0
- QuantNodes/core/knowledge/lineage_compress.py +196 -0
- QuantNodes/core/knowledge/lineage_expand.py +123 -0
- QuantNodes/core/knowledge/metrics/__init__.py +43 -0
- QuantNodes/core/knowledge/metrics/evaluator.py +176 -0
- QuantNodes/core/knowledge/metrics/metrics.py +220 -0
- QuantNodes/core/knowledge/rag_prompt.py +196 -0
- QuantNodes/core/knowledge/retriever.py +209 -0
- QuantNodes/core/lambda_node.py +81 -0
- QuantNodes/core/monitoring/__init__.py +22 -0
- QuantNodes/core/monitoring/collector.py +292 -0
- QuantNodes/core/monitoring/dashboard.py +365 -0
- QuantNodes/core/node.py +375 -0
- QuantNodes/core/pandas_utils.py +504 -0
- QuantNodes/core/parallel/__init__.py +15 -0
- QuantNodes/core/parallel/worker.py +140 -0
- QuantNodes/core/parallel/worker_process.py +265 -0
- QuantNodes/core/path_utils.py +73 -0
- QuantNodes/core/pipeline.py +328 -0
- QuantNodes/core/plugin.py +135 -0
- QuantNodes/core/quality_gate/__init__.py +32 -0
- QuantNodes/core/quality_gate/complexity.py +94 -0
- QuantNodes/core/quality_gate/consistency.py +26 -0
- QuantNodes/core/quality_gate/node.py +97 -0
- QuantNodes/core/quality_gate/redundancy.py +51 -0
- QuantNodes/core/quality_gate/settings.py +43 -0
- QuantNodes/core/quality_gate/zoo.py +98 -0
- QuantNodes/core/serializable.py +116 -0
- QuantNodes/core/serialization.py +673 -0
- QuantNodes/core/tools.py +333 -0
- QuantNodes/core/trajectory/__init__.py +25 -0
- QuantNodes/core/trajectory/entry.py +116 -0
- QuantNodes/core/trajectory/lineage.py +67 -0
- QuantNodes/core/trajectory/pool.py +211 -0
- QuantNodes/core/trajectory/selector.py +140 -0
- QuantNodes/core/visualization/__init__.py +33 -0
- QuantNodes/core/visualization/builder.py +233 -0
- QuantNodes/core/visualization/gate_breakdown.py +140 -0
- QuantNodes/core/visualization/lineage_dag.py +203 -0
- QuantNodes/core/visualization/metric_distribution.py +125 -0
- QuantNodes/core/visualization/report.py +68 -0
- QuantNodes/database_node/__init__.py +69 -0
- QuantNodes/database_node/base.py +135 -0
- QuantNodes/database_node/clickhouse_node.py +272 -0
- QuantNodes/database_node/csv_node.py +83 -0
- QuantNodes/database_node/duckdb_node.py +86 -0
- QuantNodes/database_node/factory.py +83 -0
- QuantNodes/database_node/mysql_node.py +100 -0
- QuantNodes/database_node/parquet_node.py +75 -0
- QuantNodes/database_node/sqlite_node.py +67 -0
- QuantNodes/factor_node/__init__.py +50 -0
- QuantNodes/factor_node/factor.py +563 -0
- QuantNodes/factor_node/factor_db.py +421 -0
- QuantNodes/factor_node/factor_functions/__init__.py +252 -0
- QuantNodes/factor_node/factor_functions/_helpers.py +358 -0
- QuantNodes/factor_node/factor_functions/_helpers_debug.py +317 -0
- QuantNodes/factor_node/factor_functions/composite_ops.py +136 -0
- QuantNodes/factor_node/factor_functions/math_ops.py +433 -0
- QuantNodes/factor_node/factor_functions/section_ops.py +290 -0
- QuantNodes/factor_node/factor_functions/talib_ops.py +1293 -0
- QuantNodes/factor_node/factor_functions/time_ops.py +535 -0
- QuantNodes/factor_node/factor_operation.py +1115 -0
- QuantNodes/factor_node/factor_table.py +1073 -0
- QuantNodes/factor_node/quant_nodes_object.py +60 -0
- QuantNodes/mcp_server/__init__.py +27 -0
- QuantNodes/mcp_server/__main__.py +4 -0
- QuantNodes/mcp_server/server.py +272 -0
- QuantNodes/methods/__init__.py +28 -0
- QuantNodes/methods/pipeline.py +100 -0
- QuantNodes/methods/sandbox.py +102 -0
- QuantNodes/monitor/__init__.py +27 -0
- QuantNodes/monitor/agent_tools/__init__.py +5 -0
- QuantNodes/monitor/agent_tools/monitor_tool.py +98 -0
- QuantNodes/monitor/agent_tools/schedule_tool.py +98 -0
- QuantNodes/monitor/agent_tools/version_tool.py +133 -0
- QuantNodes/monitor/monitor/__init__.py +6 -0
- QuantNodes/monitor/monitor/alerter.py +60 -0
- QuantNodes/monitor/monitor/collector.py +164 -0
- QuantNodes/monitor/monitor/dashboard.py +115 -0
- QuantNodes/monitor/monitor/drift.py +190 -0
- QuantNodes/monitor/scheduler/__init__.py +4 -0
- QuantNodes/monitor/scheduler/runner.py +133 -0
- QuantNodes/monitor/scheduler/scheduler.py +184 -0
- QuantNodes/monitor/storage/__init__.py +16 -0
- QuantNodes/monitor/storage/models.py +70 -0
- QuantNodes/monitor/storage/repository.py +407 -0
- QuantNodes/monitor/version/__init__.py +4 -0
- QuantNodes/monitor/version/diff.py +81 -0
- QuantNodes/monitor/version/version_manager.py +182 -0
- QuantNodes/operator_node/__init__.py +28 -0
- QuantNodes/operator_node/base.py +97 -0
- QuantNodes/operator_node/query_node.py +129 -0
- QuantNodes/operator_node/sql_builder.py +125 -0
- QuantNodes/operator_node/sql_utils.py +172 -0
- QuantNodes/operator_node/transform.py +130 -0
- QuantNodes/operators/__init__.py +90 -0
- QuantNodes/operators/_engine.py +108 -0
- QuantNodes/operators/composite.py +161 -0
- QuantNodes/operators/composite_dag.py +667 -0
- QuantNodes/operators/composite_dag_ops.py +343 -0
- QuantNodes/operators/composite_dag_pandas_ops.py +382 -0
- QuantNodes/operators/custom.py +408 -0
- QuantNodes/operators/facade.py +164 -0
- QuantNodes/operators/math.py +163 -0
- QuantNodes/operators/proxy.py +29 -0
- QuantNodes/operators/registry.py +144 -0
- QuantNodes/operators/section.py +99 -0
- QuantNodes/operators/talib.py +757 -0
- QuantNodes/operators/templates.py +95 -0
- QuantNodes/operators/time_series.py +136 -0
- QuantNodes/prompts/__init__.py +20 -0
- QuantNodes/prompts/backtest/__init__.py +12 -0
- QuantNodes/prompts/backtest/factor_based.py +86 -0
- QuantNodes/prompts/backtest/standard.py +73 -0
- QuantNodes/prompts/factor/__init__.py +14 -0
- QuantNodes/prompts/factor/correlation.py +77 -0
- QuantNodes/prompts/factor/group_backtest.py +86 -0
- QuantNodes/prompts/factor/ic_analysis.py +91 -0
- QuantNodes/prompts/strategy/__init__.py +18 -0
- QuantNodes/prompts/strategy/market_neutral.py +96 -0
- QuantNodes/prompts/strategy/mean_reversion.py +107 -0
- QuantNodes/prompts/strategy/momentum.py +160 -0
- QuantNodes/prompts/strategy/pairs_trading.py +107 -0
- QuantNodes/prompts/strategy/trend_following.py +96 -0
- QuantNodes/research/README.md +106 -0
- QuantNodes/research/__init__.py +154 -0
- QuantNodes/research/_legacy_3c/__init__.py +61 -0
- QuantNodes/research/_legacy_3c/auto_researcher.py +289 -0
- QuantNodes/research/_legacy_3c/factor_evaluator.py +560 -0
- QuantNodes/research/_legacy_3c/factor_miner.py +318 -0
- QuantNodes/research/_legacy_3c/mcts_search.py +324 -0
- QuantNodes/research/factor_test/__init__.py +25 -0
- QuantNodes/research/factor_test/config.py +184 -0
- QuantNodes/research/factor_test/config_builder.py +276 -0
- QuantNodes/research/factor_test/e2e/data_prep.py +163 -0
- QuantNodes/research/factor_test/e2e/run_evolution_e2e.py +309 -0
- QuantNodes/research/factor_test/evolution_adapter.py +231 -0
- QuantNodes/research/factor_test/feedback_wrapper.py +102 -0
- QuantNodes/research/factor_test/ifind_db/__init__.py +7 -0
- QuantNodes/research/factor_test/ifind_db/fetcher.py +224 -0
- QuantNodes/research/factor_test/ifind_db/ifind_database.py +689 -0
- QuantNodes/research/factor_test/nodes/__init__.py +1 -0
- QuantNodes/research/factor_test/nodes/_base.py +91 -0
- QuantNodes/research/factor_test/nodes/adjust_date_node.py +48 -0
- QuantNodes/research/factor_test/nodes/configs.py +240 -0
- QuantNodes/research/factor_test/nodes/factor_neutralize_node.py +87 -0
- QuantNodes/research/factor_test/nodes/factor_preprocess_node.py +222 -0
- QuantNodes/research/factor_test/nodes/factor_score_node.py +141 -0
- QuantNodes/research/factor_test/nodes/factor_test_report_node.py +153 -0
- QuantNodes/research/factor_test/nodes/group_analyzer_node.py +317 -0
- QuantNodes/research/factor_test/nodes/ic_analyzer_node.py +112 -0
- QuantNodes/research/factor_test/nodes/load_data_node.py +100 -0
- QuantNodes/research/factor_test/nodes/long_short_node.py +93 -0
- QuantNodes/research/factor_test/nodes/neutralizers.py +222 -0
- QuantNodes/research/factor_test/nodes/preprocess_strategies.py +277 -0
- QuantNodes/research/factor_test/nodes/risk_correlation_node.py +112 -0
- QuantNodes/research/factor_test/nodes/sample_pool_filter_node.py +110 -0
- QuantNodes/research/factor_test/nodes/tradability_filter_node.py +92 -0
- QuantNodes/research/factor_test/pipeline_runner.py +305 -0
- QuantNodes/research/factor_test/pipeline_spec.py +216 -0
- QuantNodes/research/factor_test/utils/__init__.py +26 -0
- QuantNodes/research/factor_test/utils/constants.py +86 -0
- QuantNodes/research/factor_test/utils/data_loader.py +141 -0
- QuantNodes/research/factor_test/utils/date_utils.py +232 -0
- QuantNodes/research/factor_test/utils/file_loaders.py +150 -0
- QuantNodes/research/factor_test/utils/labels.py +37 -0
- QuantNodes/research/factor_test/utils/metrics_extractor.py +55 -0
- QuantNodes/research/factor_test/utils/performance_metrics.py +175 -0
- QuantNodes/research/factor_test/utils/safe_load.py +106 -0
- QuantNodes/research/quant_alpha/CHANGELOG.md +80 -0
- QuantNodes/research/quant_alpha/README.md +142 -0
- QuantNodes/research/quant_alpha/__init__.py +45 -0
- QuantNodes/research/quant_alpha/adapters/__init__.py +99 -0
- QuantNodes/research/quant_alpha/adapters/calculator.py +503 -0
- QuantNodes/research/quant_alpha/adapters/expression.py +387 -0
- QuantNodes/research/quant_alpha/alpha101_design/__init__.py +50 -0
- QuantNodes/research/quant_alpha/alpha101_design/few_shot_examples.py +243 -0
- QuantNodes/research/quant_alpha/alpha101_design/philosophy.py +474 -0
- QuantNodes/research/quant_alpha/alpha158_design/__init__.py +63 -0
- QuantNodes/research/quant_alpha/alpha158_design/few_shot_examples.py +219 -0
- QuantNodes/research/quant_alpha/alpha158_design/philosophy.py +240 -0
- QuantNodes/research/quant_alpha/evaluation/__init__.py +47 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/__init__.py +8 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/g1_handcrafted.py +135 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/g2_llm_only.py +269 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/g3_alpha_gpt.py +152 -0
- QuantNodes/research/quant_alpha/evaluation/clickhouse_data_loader.py +227 -0
- QuantNodes/research/quant_alpha/evaluation/contracts.py +376 -0
- QuantNodes/research/quant_alpha/evaluation/evaluators/__init__.py +6 -0
- QuantNodes/research/quant_alpha/evaluation/evaluators/polars_evaluator.py +545 -0
- QuantNodes/research/quant_alpha/evaluation/mock_data_loader.py +226 -0
- QuantNodes/research/quant_alpha/evaluation/runner.py +243 -0
- QuantNodes/research/quant_alpha/llm/__init__.py +38 -0
- QuantNodes/research/quant_alpha/llm/parser.py +681 -0
- QuantNodes/research/quant_alpha/logic_driven_pipeline.py +411 -0
- QuantNodes/research/quant_alpha/logic_mining/__init__.py +74 -0
- QuantNodes/research/quant_alpha/logic_mining/compiler.py +457 -0
- QuantNodes/research/quant_alpha/logic_mining/generator.py +366 -0
- QuantNodes/research/quant_alpha/logic_mining/models.py +252 -0
- QuantNodes/research/quant_alpha/logic_mining/parser.py +287 -0
- QuantNodes/research/quant_alpha/logic_mining/pipelines.py +297 -0
- QuantNodes/research/quant_alpha/logic_mining/sources.py +149 -0
- QuantNodes/research/quant_alpha/mcts/__init__.py +66 -0
- QuantNodes/research/quant_alpha/mcts/cache.py +262 -0
- QuantNodes/research/quant_alpha/mcts/extension_ops.py +320 -0
- QuantNodes/research/quant_alpha/mcts/feedback.py +825 -0
- QuantNodes/research/quant_alpha/mcts/op_prior.py +180 -0
- QuantNodes/research/quant_alpha/mcts/search.py +540 -0
- QuantNodes/research/quant_alpha/mcts/tree.py +201 -0
- QuantNodes/research/quant_alpha/operator_vocab/__init__.py +50 -0
- QuantNodes/research/quant_alpha/operator_vocab/config.py +54 -0
- QuantNodes/research/quant_alpha/operator_vocab/metadata.py +263 -0
- QuantNodes/research/quant_alpha/operator_vocab/vocabulary.py +481 -0
- QuantNodes/research/quant_alpha/pipeline.py +1027 -0
- QuantNodes/research/quant_alpha/types/__init__.py +27 -0
- QuantNodes/research/quant_alpha/types/constants.py +28 -0
- QuantNodes/research/quant_alpha/types/state.py +205 -0
- QuantNodes/research/quant_alpha/workflow/__init__.py +32 -0
- QuantNodes/research/quant_alpha/workflow/alpha_gpt.py +911 -0
- QuantNodes/research/quant_alpha/workflow/alpha_logics.py +416 -0
- QuantNodes/research/quant_alpha/workflow/state.py +27 -0
- QuantNodes/research/report_reproducer.py +485 -0
- QuantNodes/research/wiki.py +1155 -0
- QuantNodes/symbolic/__init__.py +51 -0
- QuantNodes/symbolic/compiler.py +113 -0
- QuantNodes/symbolic/dialect.py +260 -0
- QuantNodes/symbolic/executor.py +147 -0
- QuantNodes/symbolic/expression.py +234 -0
- QuantNodes/symbolic/functions.py +433 -0
- QuantNodes/symbolic/optimizer.py +165 -0
- QuantNodes/ui_node/__init__.py +30 -0
- QuantNodes/ui_node/base.py +222 -0
- quantnodes-3.0.0.dist-info/METADATA +463 -0
- quantnodes-3.0.0.dist-info/RECORD +399 -0
- quantnodes-3.0.0.dist-info/WHEEL +5 -0
- quantnodes-3.0.0.dist-info/entry_points.txt +24 -0
- 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,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"
|