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,958 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
配置执行器
|
|
4
|
+
|
|
5
|
+
执行策略配置,生成 Polars 表达式并计算。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
13
|
+
import polars as pl
|
|
14
|
+
|
|
15
|
+
from .types import StrategyConfig, ExecutionResult
|
|
16
|
+
from QuantNodes.operators import ts, sec, math, composite
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExprParser:
|
|
20
|
+
"""递归下降表达式解析器
|
|
21
|
+
|
|
22
|
+
支持:
|
|
23
|
+
- 简单列引用: "close"
|
|
24
|
+
- 数字字面量: "20", "3.14"
|
|
25
|
+
- 函数调用: "rolling_mean(close, 20)"
|
|
26
|
+
- 方法链: "close.rolling_mean(20)"
|
|
27
|
+
- 算术运算: "close / close.shift(20) - 1"
|
|
28
|
+
- 一元运算: "-rank(close_ma_diff)"
|
|
29
|
+
- 括号分组: "(close + volume) / 2"
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, executor: 'ConfigExecutor'):
|
|
33
|
+
self.executor = executor
|
|
34
|
+
self._get_operator = None
|
|
35
|
+
|
|
36
|
+
def _lazy_import(self):
|
|
37
|
+
if self._get_operator is None:
|
|
38
|
+
from QuantNodes.operators.proxy import get_operator
|
|
39
|
+
self._get_operator = get_operator
|
|
40
|
+
|
|
41
|
+
def parse(self, expr_str: str) -> pl.Expr:
|
|
42
|
+
"""解析表达式字符串为 Polars Expr"""
|
|
43
|
+
self._pos = 0
|
|
44
|
+
self._expr = expr_str.strip()
|
|
45
|
+
result = self._parse_additive()
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
def _current(self) -> str:
|
|
49
|
+
self._skip_whitespace()
|
|
50
|
+
if self._pos >= len(self._expr):
|
|
51
|
+
return ''
|
|
52
|
+
return self._expr[self._pos]
|
|
53
|
+
|
|
54
|
+
def _skip_whitespace(self):
|
|
55
|
+
while self._pos < len(self._expr) and self._expr[self._pos] in ' \t':
|
|
56
|
+
self._pos += 1
|
|
57
|
+
|
|
58
|
+
def _consume(self, ch: str):
|
|
59
|
+
self._skip_whitespace()
|
|
60
|
+
if self._pos < len(self._expr) and self._expr[self._pos] == ch:
|
|
61
|
+
self._pos += 1
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Expected '{ch}' at position {self._pos}, "
|
|
65
|
+
f"got '{self._expr[self._pos] if self._pos < len(self._expr) else 'EOF'}'"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _parse_additive(self) -> pl.Expr:
|
|
69
|
+
"""加减法: term (('+' | '-') term)*"""
|
|
70
|
+
left = self._parse_multiplicative()
|
|
71
|
+
|
|
72
|
+
while self._current() in ('+', '-'):
|
|
73
|
+
op = self._current()
|
|
74
|
+
self._pos += 1
|
|
75
|
+
right = self._parse_multiplicative()
|
|
76
|
+
|
|
77
|
+
if op == '+':
|
|
78
|
+
left = left + right
|
|
79
|
+
else:
|
|
80
|
+
left = left - right
|
|
81
|
+
|
|
82
|
+
return left
|
|
83
|
+
|
|
84
|
+
def _parse_multiplicative(self) -> pl.Expr:
|
|
85
|
+
"""乘除法: unary (('*' | '/') unary)*"""
|
|
86
|
+
left = self._parse_unary()
|
|
87
|
+
|
|
88
|
+
while self._current() in ('*', '/'):
|
|
89
|
+
op = self._current()
|
|
90
|
+
self._pos += 1
|
|
91
|
+
right = self._parse_unary()
|
|
92
|
+
|
|
93
|
+
if op == '*':
|
|
94
|
+
left = left * right
|
|
95
|
+
else:
|
|
96
|
+
left = left / right
|
|
97
|
+
|
|
98
|
+
return left
|
|
99
|
+
|
|
100
|
+
def _parse_unary(self) -> pl.Expr:
|
|
101
|
+
"""一元运算: ('-' | '+') primary | primary"""
|
|
102
|
+
if self._current() == '-':
|
|
103
|
+
self._pos += 1
|
|
104
|
+
operand = self._parse_primary()
|
|
105
|
+
return pl.lit(0) - operand
|
|
106
|
+
elif self._current() == '+':
|
|
107
|
+
self._pos += 1
|
|
108
|
+
return self._parse_primary()
|
|
109
|
+
return self._parse_primary()
|
|
110
|
+
|
|
111
|
+
def _parse_primary(self) -> pl.Expr:
|
|
112
|
+
"""主项: number | column | function_call | method_chain | '(' expr ')'"""
|
|
113
|
+
self._skip_whitespace()
|
|
114
|
+
|
|
115
|
+
# 括号表达式
|
|
116
|
+
if self._current() == '(':
|
|
117
|
+
self._consume('(')
|
|
118
|
+
expr = self._parse_additive()
|
|
119
|
+
self._consume(')')
|
|
120
|
+
return expr
|
|
121
|
+
|
|
122
|
+
# 数字字面量
|
|
123
|
+
cur = self._current()
|
|
124
|
+
is_digit = cur.isdigit()
|
|
125
|
+
is_dot_number = (
|
|
126
|
+
cur == '.'
|
|
127
|
+
and self._pos + 1 < len(self._expr)
|
|
128
|
+
and self._expr[self._pos + 1].isdigit()
|
|
129
|
+
)
|
|
130
|
+
if is_digit or is_dot_number:
|
|
131
|
+
return self._parse_number()
|
|
132
|
+
|
|
133
|
+
# 标识符 (列名/函数名/方法名)
|
|
134
|
+
if self._current().isalpha() or self._current() == '_':
|
|
135
|
+
return self._parse_identifier()
|
|
136
|
+
|
|
137
|
+
raise ValueError(f"Unexpected character '{self._current()}' at position {self._pos}")
|
|
138
|
+
|
|
139
|
+
def _parse_number(self) -> pl.Expr:
|
|
140
|
+
"""解析数字字面量"""
|
|
141
|
+
start = self._pos
|
|
142
|
+
while self._pos < len(self._expr) and (
|
|
143
|
+
self._expr[self._pos].isdigit() or self._expr[self._pos] == '.'
|
|
144
|
+
):
|
|
145
|
+
self._pos += 1
|
|
146
|
+
|
|
147
|
+
num_str = self._expr[start:self._pos]
|
|
148
|
+
if '.' in num_str:
|
|
149
|
+
return pl.lit(float(num_str))
|
|
150
|
+
return pl.lit(int(num_str))
|
|
151
|
+
|
|
152
|
+
def _parse_identifier(self) -> pl.Expr:
|
|
153
|
+
"""解析标识符 (可能是列名、函数调用或方法链)"""
|
|
154
|
+
name = self._parse_name()
|
|
155
|
+
|
|
156
|
+
self._skip_whitespace()
|
|
157
|
+
|
|
158
|
+
# 函数调用: name(args)
|
|
159
|
+
if self._current() == '(':
|
|
160
|
+
return self._parse_func_call(name)
|
|
161
|
+
|
|
162
|
+
# 方法链: name.method(args) 或 name.method
|
|
163
|
+
if self._current() == '.':
|
|
164
|
+
return self._parse_method_chain(name)
|
|
165
|
+
|
|
166
|
+
# 简单列引用
|
|
167
|
+
return pl.col(name)
|
|
168
|
+
|
|
169
|
+
def _parse_name(self) -> str:
|
|
170
|
+
"""解析标识符名称"""
|
|
171
|
+
start = self._pos
|
|
172
|
+
while self._pos < len(self._expr) and (
|
|
173
|
+
self._expr[self._pos].isalnum() or self._expr[self._pos] == '_'
|
|
174
|
+
):
|
|
175
|
+
self._pos += 1
|
|
176
|
+
|
|
177
|
+
if self._pos == start:
|
|
178
|
+
raise ValueError(f"Expected identifier at position {self._pos}")
|
|
179
|
+
|
|
180
|
+
return self._expr[start:self._pos]
|
|
181
|
+
|
|
182
|
+
def _parse_func_call(self, func_name: str) -> pl.Expr:
|
|
183
|
+
"""解析函数调用: func_name(arg1, arg2, ...)"""
|
|
184
|
+
self._consume('(')
|
|
185
|
+
|
|
186
|
+
# 读取原始参数字符串,使用 executor 的 _parse_func_args 解析
|
|
187
|
+
# 这样数字参数会返回 Python int/float,列名会返回 pl.col()
|
|
188
|
+
args_str = self._read_func_args()
|
|
189
|
+
args, kwargs = self.executor._parse_func_args(args_str)
|
|
190
|
+
|
|
191
|
+
# 查找算子函数
|
|
192
|
+
self._lazy_import()
|
|
193
|
+
op_func = self._get_operator(func_name) if self._get_operator else None
|
|
194
|
+
|
|
195
|
+
if op_func is not None and args:
|
|
196
|
+
# 将第一个参数转换为表达式(如果还不是的话)
|
|
197
|
+
first_arg = args[0] if isinstance(args[0], pl.Expr) else pl.col(str(args[0]))
|
|
198
|
+
rest_args = args[1:]
|
|
199
|
+
return op_func(first_arg, *rest_args, **kwargs)
|
|
200
|
+
|
|
201
|
+
# 如果没有找到算子,尝试作为 Polars 方法
|
|
202
|
+
if args:
|
|
203
|
+
first_arg = args[0] if isinstance(args[0], pl.Expr) else pl.col(str(args[0]))
|
|
204
|
+
rest_args = args[1:]
|
|
205
|
+
return getattr(first_arg, func_name)(*rest_args, **kwargs)
|
|
206
|
+
|
|
207
|
+
return pl.col(func_name)
|
|
208
|
+
|
|
209
|
+
def _read_func_args(self) -> str:
|
|
210
|
+
"""读取函数参数字符串(从当前位置到匹配的右括号,消耗 ')')
|
|
211
|
+
|
|
212
|
+
注意: 此方法会消耗右括号 ')',调用后不需要再 consume ')'
|
|
213
|
+
"""
|
|
214
|
+
start = self._pos
|
|
215
|
+
depth = 1
|
|
216
|
+
while self._pos < len(self._expr) and depth > 0:
|
|
217
|
+
if self._expr[self._pos] == '(':
|
|
218
|
+
depth += 1
|
|
219
|
+
elif self._expr[self._pos] == ')':
|
|
220
|
+
depth -= 1
|
|
221
|
+
self._pos += 1
|
|
222
|
+
# 返回括号内的内容(不含两端括号)
|
|
223
|
+
return self._expr[start:self._pos - 1]
|
|
224
|
+
|
|
225
|
+
def _parse_method_chain(self, obj_name: str) -> pl.Expr:
|
|
226
|
+
"""解析方法链: obj.method(args) 或 obj.method"""
|
|
227
|
+
self._consume('.')
|
|
228
|
+
method_name = self._parse_name()
|
|
229
|
+
|
|
230
|
+
self._skip_whitespace()
|
|
231
|
+
|
|
232
|
+
if self._current() == '(':
|
|
233
|
+
# 方法调用: obj.method(args)
|
|
234
|
+
# 注意: _read_func_args 已经消耗了 ')'
|
|
235
|
+
args_str = self._read_func_args()
|
|
236
|
+
|
|
237
|
+
self._lazy_import()
|
|
238
|
+
op_func = self._get_operator(method_name) if self._get_operator else None
|
|
239
|
+
|
|
240
|
+
first_arg = pl.col(obj_name)
|
|
241
|
+
args, kwargs = self.executor._parse_func_args(args_str)
|
|
242
|
+
|
|
243
|
+
if op_func is not None:
|
|
244
|
+
return op_func(first_arg, *args, **kwargs)
|
|
245
|
+
|
|
246
|
+
# 回退到 Polars 原生方法
|
|
247
|
+
return getattr(first_arg, method_name)(*args, **kwargs)
|
|
248
|
+
|
|
249
|
+
# 属性访问: obj.method (不支持,回退到列引用)
|
|
250
|
+
return pl.col(f"{obj_name}.{method_name}")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ConfigExecutor:
|
|
254
|
+
"""配置执行器"""
|
|
255
|
+
|
|
256
|
+
# executor category → registry category 映射
|
|
257
|
+
_CATEGORY_MAP = {
|
|
258
|
+
"time_series": "time",
|
|
259
|
+
"section": "section",
|
|
260
|
+
"math": "point",
|
|
261
|
+
"composite": "point",
|
|
262
|
+
"talib": "talib",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
def __init__(self):
|
|
266
|
+
self._expressions: Dict[str, pl.Expr] = {}
|
|
267
|
+
self._cache: Dict[str, Any] = {}
|
|
268
|
+
|
|
269
|
+
def _load_custom_operators(self, custom_operators: List) -> None:
|
|
270
|
+
"""加载自定义算子
|
|
271
|
+
|
|
272
|
+
支持两种配置格式:
|
|
273
|
+
- str: 文件路径,自动发现 custom_* 函数,注册到 point 分类
|
|
274
|
+
- dict: {source, category, functions} 显式指定分类和函数列表
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
custom_operators: ValidationConfig.custom_operators 列表
|
|
278
|
+
"""
|
|
279
|
+
if not custom_operators:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
from QuantNodes.operators.proxy import register_operator
|
|
283
|
+
|
|
284
|
+
for entry in custom_operators:
|
|
285
|
+
# 解析配置
|
|
286
|
+
if isinstance(entry, str):
|
|
287
|
+
source_path = entry
|
|
288
|
+
category = "point"
|
|
289
|
+
functions = None
|
|
290
|
+
else:
|
|
291
|
+
source_path = entry.get("source", "")
|
|
292
|
+
category = entry.get("category", "point")
|
|
293
|
+
functions = entry.get("functions")
|
|
294
|
+
|
|
295
|
+
if not source_path:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# importlib 动态加载
|
|
299
|
+
try:
|
|
300
|
+
spec = importlib.util.spec_from_file_location("custom_ops", source_path)
|
|
301
|
+
if spec is None or spec.loader is None:
|
|
302
|
+
warnings.warn(f"无法加载自定义算子文件: {source_path}")
|
|
303
|
+
continue
|
|
304
|
+
module = importlib.util.module_from_spec(spec)
|
|
305
|
+
spec.loader.exec_module(module)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
warnings.warn(f"加载自定义算子文件失败 {source_path}: {e}")
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# 注册算子
|
|
311
|
+
registered = 0
|
|
312
|
+
for name in dir(module):
|
|
313
|
+
if name.startswith("_"):
|
|
314
|
+
continue
|
|
315
|
+
if functions and name not in functions:
|
|
316
|
+
continue
|
|
317
|
+
if not functions and not name.startswith("custom_"):
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
func = getattr(module, name)
|
|
321
|
+
if not callable(func):
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
register_operator(self._CATEGORY_MAP.get(category, "point"), name=name)(func)
|
|
325
|
+
registered += 1
|
|
326
|
+
|
|
327
|
+
if registered > 0:
|
|
328
|
+
print(
|
|
329
|
+
f"[ConfigExecutor] 已注册 {registered} 个自定义算子 "
|
|
330
|
+
f"(来源: {source_path}, 分类: {category})"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def run(
|
|
334
|
+
self,
|
|
335
|
+
config: StrategyConfig,
|
|
336
|
+
data: pl.LazyFrame
|
|
337
|
+
) -> ExecutionResult:
|
|
338
|
+
"""执行配置
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
config: 策略配置
|
|
342
|
+
data: 数据 LazyFrame
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
ExecutionResult 对象
|
|
346
|
+
"""
|
|
347
|
+
result = ExecutionResult(status="success")
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
# 0. 加载自定义算子
|
|
351
|
+
self._load_custom_operators(config.validation.custom_operators)
|
|
352
|
+
|
|
353
|
+
# 1. 生成因子表达式
|
|
354
|
+
for factor in config.factors:
|
|
355
|
+
expr = self._parse_expr(factor.expr)
|
|
356
|
+
self._expressions[factor.name] = expr
|
|
357
|
+
result.factors[factor.name] = expr
|
|
358
|
+
|
|
359
|
+
# 2. 执行运算
|
|
360
|
+
for op in config.operations:
|
|
361
|
+
expr = self._apply_operator(op)
|
|
362
|
+
|
|
363
|
+
# 处理 List[Expr] 返回值(如 rank_sort)
|
|
364
|
+
if isinstance(expr, list):
|
|
365
|
+
for i, e in enumerate(expr):
|
|
366
|
+
key = f"{op.name}__{i}" # 使用双下划线避免与用户命名冲突
|
|
367
|
+
self._expressions[key] = e
|
|
368
|
+
result.factors[op.name] = expr
|
|
369
|
+
else:
|
|
370
|
+
self._expressions[op.name] = expr
|
|
371
|
+
result.factors[op.name] = expr
|
|
372
|
+
|
|
373
|
+
# 3. 计算组合因子
|
|
374
|
+
for comp in config.composite:
|
|
375
|
+
expr = self._parse_expr(comp.formula)
|
|
376
|
+
self._expressions[comp.name] = expr
|
|
377
|
+
result.factors[comp.name] = expr
|
|
378
|
+
|
|
379
|
+
# 4. 生成计算计划
|
|
380
|
+
self._execute_plan(data, result)
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
result.status = "error"
|
|
384
|
+
result.errors.append(str(e))
|
|
385
|
+
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
def run_backtest(
|
|
389
|
+
self,
|
|
390
|
+
config: StrategyConfig,
|
|
391
|
+
data: pl.LazyFrame
|
|
392
|
+
) -> ExecutionResult:
|
|
393
|
+
"""执行回测
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
config: 策略配置
|
|
397
|
+
data: 数据 LazyFrame
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
ExecutionResult 对象
|
|
401
|
+
"""
|
|
402
|
+
result = self.run(config, data)
|
|
403
|
+
|
|
404
|
+
if config.backtest is None:
|
|
405
|
+
return result
|
|
406
|
+
|
|
407
|
+
bt = config.backtest
|
|
408
|
+
code_col = config.data.code_column if config.data else "code"
|
|
409
|
+
date_col = config.data.date_column if config.data else "date"
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
# 使用 result.data (已包含计算列) 进行筛选
|
|
413
|
+
working_data = result.data if result.data is not None else data
|
|
414
|
+
|
|
415
|
+
# 筛选 Universe
|
|
416
|
+
universe_codes = self._resolve_universe(bt.universe)
|
|
417
|
+
if universe_codes is not None:
|
|
418
|
+
schema = working_data.collect_schema()
|
|
419
|
+
# 使用 code_column(内部标准名),兼容 PascalCase fallback
|
|
420
|
+
effective_code_col = code_col
|
|
421
|
+
if effective_code_col not in schema:
|
|
422
|
+
pascal = code_col[0].upper() + code_col[1:] if code_col else ""
|
|
423
|
+
if pascal in schema:
|
|
424
|
+
effective_code_col = pascal
|
|
425
|
+
if effective_code_col in schema:
|
|
426
|
+
working_data = working_data.filter(
|
|
427
|
+
pl.col(effective_code_col).is_in(universe_codes)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# 筛选日期(兼容 String 和 Date 类型)
|
|
431
|
+
if bt.start_date:
|
|
432
|
+
start_parts = list(map(int, bt.start_date.split("-")))
|
|
433
|
+
start_date = pl.date(start_parts[0], start_parts[1], start_parts[2])
|
|
434
|
+
date_series = pl.col(date_col)
|
|
435
|
+
schema = working_data.collect_schema()
|
|
436
|
+
date_dtype = schema.get(date_col)
|
|
437
|
+
# 如果是 String 类型,先转换为 Date
|
|
438
|
+
if date_dtype == pl.Utf8 or date_dtype == pl.String:
|
|
439
|
+
date_series = date_series.str.to_date()
|
|
440
|
+
working_data = working_data.filter(date_series >= start_date)
|
|
441
|
+
if bt.end_date:
|
|
442
|
+
end_parts = list(map(int, bt.end_date.split("-")))
|
|
443
|
+
end_date = pl.date(end_parts[0], end_parts[1], end_parts[2])
|
|
444
|
+
date_series = pl.col(date_col)
|
|
445
|
+
schema = working_data.collect_schema()
|
|
446
|
+
date_dtype = schema.get(date_col)
|
|
447
|
+
if date_dtype == pl.Utf8 or date_dtype == pl.String:
|
|
448
|
+
date_series = date_series.str.to_date()
|
|
449
|
+
working_data = working_data.filter(date_series <= end_date)
|
|
450
|
+
|
|
451
|
+
# 计算信号 (取最后一个因子作为信号)
|
|
452
|
+
signal_name = None
|
|
453
|
+
if config.composite:
|
|
454
|
+
signal_name = config.composite[-1].name
|
|
455
|
+
elif config.operations:
|
|
456
|
+
signal_name = config.operations[-1].name
|
|
457
|
+
elif config.factors:
|
|
458
|
+
signal_name = config.factors[-1].name
|
|
459
|
+
|
|
460
|
+
if signal_name is not None:
|
|
461
|
+
expr = self._expressions.get(signal_name)
|
|
462
|
+
if expr is not None:
|
|
463
|
+
# 兼容两种阈值命名
|
|
464
|
+
buy_threshold = bt.signals.get("buy_threshold",
|
|
465
|
+
bt.signals.get("long_threshold", 0.05))
|
|
466
|
+
sell_threshold = bt.signals.get("sell_threshold",
|
|
467
|
+
bt.signals.get("short_threshold", -0.03))
|
|
468
|
+
|
|
469
|
+
# 生成交易信号
|
|
470
|
+
working_data = working_data.with_columns([
|
|
471
|
+
pl.when(expr > buy_threshold).then(1)
|
|
472
|
+
.when(expr < sell_threshold).then(-1)
|
|
473
|
+
.otherwise(0).alias("signal")
|
|
474
|
+
])
|
|
475
|
+
|
|
476
|
+
result.backtest = {
|
|
477
|
+
"signals": working_data.select(date_col, code_col, "signal"),
|
|
478
|
+
"config": {
|
|
479
|
+
"start_date": bt.start_date,
|
|
480
|
+
"end_date": bt.end_date,
|
|
481
|
+
"initial_cash": bt.initial_cash,
|
|
482
|
+
"commission": bt.commission,
|
|
483
|
+
"slippage": bt.slippage,
|
|
484
|
+
},
|
|
485
|
+
"buy_threshold": buy_threshold,
|
|
486
|
+
"sell_threshold": sell_threshold,
|
|
487
|
+
}
|
|
488
|
+
result.data = working_data
|
|
489
|
+
except Exception as e:
|
|
490
|
+
result.warnings.append(f"回测配置解析警告: {str(e)}")
|
|
491
|
+
|
|
492
|
+
return result
|
|
493
|
+
|
|
494
|
+
def _parse_expr(self, expr_str: str) -> pl.Expr:
|
|
495
|
+
"""解析表达式字符串
|
|
496
|
+
|
|
497
|
+
使用递归下降解析器,支持:
|
|
498
|
+
- 简单列引用: "close"
|
|
499
|
+
- 函数调用: "rolling_mean(close, 20)"
|
|
500
|
+
- 方法链: "close.rolling_mean(20)"
|
|
501
|
+
- 算术运算: "close / close.shift(20) - 1"
|
|
502
|
+
- 一元运算: "-rank(close_ma_diff)"
|
|
503
|
+
- 括号分组: "(close + volume) / 2"
|
|
504
|
+
"""
|
|
505
|
+
parser = ExprParser(self)
|
|
506
|
+
return parser.parse(expr_str)
|
|
507
|
+
|
|
508
|
+
def _parse_func_args(self, args_str: str) -> Tuple[List, Dict]:
|
|
509
|
+
"""解析函数参数字符串
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
(positional_args, keyword_args)
|
|
513
|
+
"""
|
|
514
|
+
import re
|
|
515
|
+
|
|
516
|
+
positional = []
|
|
517
|
+
keyword = {}
|
|
518
|
+
|
|
519
|
+
if not args_str.strip():
|
|
520
|
+
return positional, keyword
|
|
521
|
+
|
|
522
|
+
# 处理嵌套括号的分割
|
|
523
|
+
parts = []
|
|
524
|
+
depth = 0
|
|
525
|
+
current = ""
|
|
526
|
+
for ch in args_str:
|
|
527
|
+
if ch == '(':
|
|
528
|
+
depth += 1
|
|
529
|
+
current += ch
|
|
530
|
+
elif ch == ')':
|
|
531
|
+
depth -= 1
|
|
532
|
+
current += ch
|
|
533
|
+
elif ch == ',' and depth == 0:
|
|
534
|
+
parts.append(current.strip())
|
|
535
|
+
current = ""
|
|
536
|
+
else:
|
|
537
|
+
current += ch
|
|
538
|
+
if current.strip():
|
|
539
|
+
parts.append(current.strip())
|
|
540
|
+
|
|
541
|
+
for part in parts:
|
|
542
|
+
# 检查是否是 keyword=value 格式
|
|
543
|
+
kw_match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', part)
|
|
544
|
+
if kw_match:
|
|
545
|
+
key = kw_match.group(1)
|
|
546
|
+
value = self._parse_value(kw_match.group(2).strip())
|
|
547
|
+
keyword[key] = value
|
|
548
|
+
else:
|
|
549
|
+
positional.append(self._parse_value(part))
|
|
550
|
+
|
|
551
|
+
return positional, keyword
|
|
552
|
+
|
|
553
|
+
def _parse_value(self, value_str: str):
|
|
554
|
+
"""解析单个值
|
|
555
|
+
|
|
556
|
+
注意: 对于数字,返回 Python 原生类型(int/float),
|
|
557
|
+
而不是 Polars 表达式。这是为了正确传递参数给算子函数。
|
|
558
|
+
"""
|
|
559
|
+
value_str = value_str.strip()
|
|
560
|
+
|
|
561
|
+
# 去除引号
|
|
562
|
+
if (value_str.startswith('"') and value_str.endswith('"')) or \
|
|
563
|
+
(value_str.startswith("'") and value_str.endswith("'")):
|
|
564
|
+
return value_str[1:-1]
|
|
565
|
+
|
|
566
|
+
# 尝试解析为整数
|
|
567
|
+
try:
|
|
568
|
+
return int(value_str)
|
|
569
|
+
except ValueError:
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
# 尝试解析为浮点数
|
|
573
|
+
try:
|
|
574
|
+
return float(value_str)
|
|
575
|
+
except ValueError:
|
|
576
|
+
pass
|
|
577
|
+
|
|
578
|
+
# 纯数字字符串(可能是字符串格式的数字)
|
|
579
|
+
if value_str.isdigit():
|
|
580
|
+
return int(value_str)
|
|
581
|
+
|
|
582
|
+
# 尝试作为列引用
|
|
583
|
+
return pl.col(value_str)
|
|
584
|
+
|
|
585
|
+
def _apply_operator(self, op) -> pl.Expr:
|
|
586
|
+
"""应用算子
|
|
587
|
+
|
|
588
|
+
优先级: 硬编码 dispatch > registry fallback > 最终兜底
|
|
589
|
+
"""
|
|
590
|
+
op_type = op.type
|
|
591
|
+
category = op.category
|
|
592
|
+
inputs = op.inputs
|
|
593
|
+
params = op.params
|
|
594
|
+
|
|
595
|
+
# 获取输入表达式
|
|
596
|
+
input_exprs = []
|
|
597
|
+
for name in inputs:
|
|
598
|
+
if name in self._expressions:
|
|
599
|
+
input_exprs.append(self._expressions[name])
|
|
600
|
+
else:
|
|
601
|
+
input_exprs.append(pl.col(name))
|
|
602
|
+
|
|
603
|
+
if not input_exprs:
|
|
604
|
+
return pl.col(inputs[0]) if inputs else pl.lit(0)
|
|
605
|
+
|
|
606
|
+
# 1. 硬编码 dispatch(优先)
|
|
607
|
+
result = None
|
|
608
|
+
if op_type == "time_series":
|
|
609
|
+
result = self._apply_ts_operator(category, input_exprs, params)
|
|
610
|
+
elif op_type == "section":
|
|
611
|
+
result = self._apply_sec_operator(category, input_exprs, params)
|
|
612
|
+
elif op_type == "math":
|
|
613
|
+
result = self._apply_math_operator(category, input_exprs[0], params)
|
|
614
|
+
elif op_type == "composite":
|
|
615
|
+
result = self._apply_composite_operator(category, input_exprs, params)
|
|
616
|
+
elif op_type == "talib":
|
|
617
|
+
result = self._apply_talib_operator(category, input_exprs, params)
|
|
618
|
+
|
|
619
|
+
if result is not None:
|
|
620
|
+
return result
|
|
621
|
+
|
|
622
|
+
# 2. registry fallback: 从 operators.proxy 查找
|
|
623
|
+
try:
|
|
624
|
+
from QuantNodes.operators.proxy import get_operator
|
|
625
|
+
op_func = get_operator(category)
|
|
626
|
+
if op_func is not None:
|
|
627
|
+
return op_func(*input_exprs, **params)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
# 3. 最终兜底
|
|
632
|
+
return input_exprs[0]
|
|
633
|
+
|
|
634
|
+
def _get_op(self, name: str):
|
|
635
|
+
"""懒加载获取 operators.proxy 算子"""
|
|
636
|
+
from QuantNodes.operators.proxy import get_operator
|
|
637
|
+
return get_operator(name)
|
|
638
|
+
|
|
639
|
+
def _apply_ts_operator(
|
|
640
|
+
self,
|
|
641
|
+
category: str,
|
|
642
|
+
input_exprs: List[pl.Expr],
|
|
643
|
+
params: Dict[str, Any]
|
|
644
|
+
) -> pl.Expr:
|
|
645
|
+
"""应用时间序列算子
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
category: 算子名称
|
|
649
|
+
input_exprs: 输入表达式列表(单输入算子取 [0],双输入算子取 [0],[1])
|
|
650
|
+
params: 参数字典
|
|
651
|
+
"""
|
|
652
|
+
expr = input_exprs[0]
|
|
653
|
+
window = params.get("window", 20)
|
|
654
|
+
alpha = params.get("alpha", 0.5)
|
|
655
|
+
periods = params.get("periods", 1)
|
|
656
|
+
|
|
657
|
+
# 单输入滚动算子
|
|
658
|
+
if category == "ts_mean":
|
|
659
|
+
return ts.ts_mean(expr, window)
|
|
660
|
+
elif category == "ts_std":
|
|
661
|
+
return ts.ts_std(expr, window)
|
|
662
|
+
elif category == "ts_max":
|
|
663
|
+
return ts.ts_max(expr, window)
|
|
664
|
+
elif category == "ts_min":
|
|
665
|
+
return ts.ts_min(expr, window)
|
|
666
|
+
elif category == "ts_sum":
|
|
667
|
+
return ts.ts_sum(expr, window)
|
|
668
|
+
elif category == "ts_prod":
|
|
669
|
+
return ts.ts_prod(expr, window)
|
|
670
|
+
elif category == "ts_median":
|
|
671
|
+
return ts.ts_median(expr, window)
|
|
672
|
+
elif category == "ts_rank":
|
|
673
|
+
return ts.ts_rank(expr, window)
|
|
674
|
+
elif category == "ts_argmax":
|
|
675
|
+
return self._get_op("ts_argmax")(expr, window)
|
|
676
|
+
elif category == "ts_argmin":
|
|
677
|
+
return self._get_op("ts_argmin")(expr, window)
|
|
678
|
+
|
|
679
|
+
# 双输入滚动算子
|
|
680
|
+
elif category == "ts_cov":
|
|
681
|
+
return ts.ts_cov(expr, input_exprs[1], window)
|
|
682
|
+
elif category == "ts_corr":
|
|
683
|
+
return ts.ts_corr(expr, input_exprs[1], window)
|
|
684
|
+
|
|
685
|
+
# 差分与变化
|
|
686
|
+
elif category == "ts_delta":
|
|
687
|
+
return ts.ts_delta(expr, periods)
|
|
688
|
+
elif category == "ts_pct_change":
|
|
689
|
+
return ts.ts_pct_change(expr, periods)
|
|
690
|
+
elif category == "ts_lag":
|
|
691
|
+
return ts.ts_lag(expr, periods)
|
|
692
|
+
|
|
693
|
+
# 指数加权移动算子(单输入)
|
|
694
|
+
elif category == "ewm_mean":
|
|
695
|
+
return ts.ewm_mean(expr, alpha=alpha)
|
|
696
|
+
elif category == "ewm_std":
|
|
697
|
+
return ts.ewm_std(expr, alpha=alpha)
|
|
698
|
+
elif category == "ewm_var":
|
|
699
|
+
return self._get_op("ewm_var")(expr, alpha=alpha)
|
|
700
|
+
|
|
701
|
+
# 指数加权移动算子(双输入)
|
|
702
|
+
elif category == "ewm_corr":
|
|
703
|
+
return ts.ewm_corr(expr, input_exprs[1], alpha=alpha)
|
|
704
|
+
elif category == "ewm_cov":
|
|
705
|
+
return self._get_op("ewm_cov")(expr, input_exprs[1], alpha=alpha)
|
|
706
|
+
|
|
707
|
+
return None
|
|
708
|
+
|
|
709
|
+
def _apply_sec_operator(
|
|
710
|
+
self,
|
|
711
|
+
category: str,
|
|
712
|
+
input_exprs: List[pl.Expr],
|
|
713
|
+
params: Dict[str, Any]
|
|
714
|
+
) -> pl.Expr:
|
|
715
|
+
"""应用截面算子"""
|
|
716
|
+
expr = input_exprs[0]
|
|
717
|
+
|
|
718
|
+
if category == "rank":
|
|
719
|
+
return sec.rank(expr)
|
|
720
|
+
elif category == "zscore":
|
|
721
|
+
return sec.zscore(expr)
|
|
722
|
+
elif category == "winsorize":
|
|
723
|
+
return sec.winsorize(
|
|
724
|
+
expr,
|
|
725
|
+
params.get("lower", 0.01),
|
|
726
|
+
params.get("upper", 0.01)
|
|
727
|
+
)
|
|
728
|
+
elif category == "neutralize":
|
|
729
|
+
return sec.neutralize_market(expr)
|
|
730
|
+
elif category == "scale":
|
|
731
|
+
return sec.scale(expr)
|
|
732
|
+
elif category == "percentile":
|
|
733
|
+
return sec.percentile(expr)
|
|
734
|
+
elif category == "rank_ic":
|
|
735
|
+
return sec.rank_ic(expr, input_exprs[1])
|
|
736
|
+
elif category == "ic":
|
|
737
|
+
return sec.ic(expr, input_exprs[1])
|
|
738
|
+
elif category == "group_norm":
|
|
739
|
+
return sec.group_norm(
|
|
740
|
+
expr, pl.col(params["group"]), params.get("method", "zscore")
|
|
741
|
+
)
|
|
742
|
+
elif category == "group_winsorize":
|
|
743
|
+
return sec.group_winsorize(
|
|
744
|
+
expr, pl.col(params["group"]),
|
|
745
|
+
params.get("lower", 0.01), params.get("upper", 0.01)
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
return None
|
|
749
|
+
|
|
750
|
+
def _apply_math_operator(
|
|
751
|
+
self,
|
|
752
|
+
category: str,
|
|
753
|
+
expr: pl.Expr,
|
|
754
|
+
params: Dict[str, Any]
|
|
755
|
+
) -> pl.Expr:
|
|
756
|
+
"""应用数学算子"""
|
|
757
|
+
value = params.get("value", 1.0)
|
|
758
|
+
|
|
759
|
+
if category == "add":
|
|
760
|
+
return math.add(expr, value)
|
|
761
|
+
elif category == "sub":
|
|
762
|
+
return math.sub(expr, value)
|
|
763
|
+
elif category == "mul":
|
|
764
|
+
return math.mul(expr, value)
|
|
765
|
+
elif category == "div":
|
|
766
|
+
return math.div(expr, value)
|
|
767
|
+
elif category == "log":
|
|
768
|
+
return math.log(expr)
|
|
769
|
+
elif category == "abs":
|
|
770
|
+
return math.abs(expr)
|
|
771
|
+
elif category == "pow":
|
|
772
|
+
return math.pow(expr, params.get("exponent", 2))
|
|
773
|
+
elif category == "log1p":
|
|
774
|
+
return math.log1p(expr)
|
|
775
|
+
elif category == "sqrt":
|
|
776
|
+
return math.sqrt(expr)
|
|
777
|
+
elif category == "sign":
|
|
778
|
+
return math.sign(expr)
|
|
779
|
+
elif category == "clip":
|
|
780
|
+
return math.clip(expr, params.get("lower"), params.get("upper"))
|
|
781
|
+
elif category == "floor":
|
|
782
|
+
return math.floor(expr)
|
|
783
|
+
elif category == "ceil":
|
|
784
|
+
return math.ceil(expr)
|
|
785
|
+
elif category == "round":
|
|
786
|
+
return math.round(expr, params.get("decimals", 2))
|
|
787
|
+
elif category == "nan_to_null":
|
|
788
|
+
return math.nan_to_null(expr)
|
|
789
|
+
elif category == "fill_null":
|
|
790
|
+
return math.fill_null(expr, params.get("value", 0.0))
|
|
791
|
+
elif category == "fill_zero":
|
|
792
|
+
return math.fill_zero(expr)
|
|
793
|
+
elif category == "sin":
|
|
794
|
+
return math.sin(expr)
|
|
795
|
+
elif category == "cos":
|
|
796
|
+
return math.cos(expr)
|
|
797
|
+
elif category == "tan":
|
|
798
|
+
return math.tan(expr)
|
|
799
|
+
elif category == "arcsin":
|
|
800
|
+
return math.arcsin(expr)
|
|
801
|
+
elif category == "arccos":
|
|
802
|
+
return math.arccos(expr)
|
|
803
|
+
elif category == "arctan":
|
|
804
|
+
return math.arctan(expr)
|
|
805
|
+
|
|
806
|
+
return None
|
|
807
|
+
|
|
808
|
+
def _apply_composite_operator(
|
|
809
|
+
self,
|
|
810
|
+
category: str,
|
|
811
|
+
exprs: List[pl.Expr],
|
|
812
|
+
params: Dict[str, Any]
|
|
813
|
+
) -> pl.Expr:
|
|
814
|
+
"""应用组合算子"""
|
|
815
|
+
if category == "weighted_sum":
|
|
816
|
+
default_weights = [1.0 / len(exprs)] * len(exprs)
|
|
817
|
+
weights = params.get("weights", default_weights)
|
|
818
|
+
return composite.weighted_sum(exprs, weights)
|
|
819
|
+
elif category == "weighted_avg":
|
|
820
|
+
return composite.weighted_avg(exprs)
|
|
821
|
+
elif category == "max":
|
|
822
|
+
return composite.max(exprs)
|
|
823
|
+
elif category == "min":
|
|
824
|
+
return composite.min(exprs)
|
|
825
|
+
elif category == "blend":
|
|
826
|
+
alpha = params.get("alpha", 0.5)
|
|
827
|
+
return composite.blend(exprs[0], exprs[1], alpha)
|
|
828
|
+
elif category == "abs_max":
|
|
829
|
+
return composite.abs_max(exprs)
|
|
830
|
+
elif category == "combine":
|
|
831
|
+
return composite.combine(exprs, params.get("method", "sum"))
|
|
832
|
+
elif category == "select_top":
|
|
833
|
+
return composite.select_top(
|
|
834
|
+
exprs[0], params.get("n", 10), params.get("ascending", False)
|
|
835
|
+
)
|
|
836
|
+
elif category == "filter_positive":
|
|
837
|
+
return composite.filter_positive(exprs[0])
|
|
838
|
+
elif category == "filter_negative":
|
|
839
|
+
return composite.filter_negative(exprs[0])
|
|
840
|
+
elif category == "abs_filter":
|
|
841
|
+
return composite.abs_filter(exprs[0], params.get("threshold", 0.0))
|
|
842
|
+
elif category == "rank_sort":
|
|
843
|
+
return composite.rank_sort(exprs, params.get("weights"))
|
|
844
|
+
|
|
845
|
+
return None
|
|
846
|
+
|
|
847
|
+
def _resolve_universe(self, universe: str) -> Optional[List[str]]:
|
|
848
|
+
"""解析 Universe 配置,返回股票代码列表。
|
|
849
|
+
|
|
850
|
+
支持格式:
|
|
851
|
+
- "all" / "" → None (不过滤)
|
|
852
|
+
- "A_stock" → 预留 A 股全市场(当前不过滤,返回 None)
|
|
853
|
+
- "/path/to/codes.txt" → 从文件读取,每行一个代码
|
|
854
|
+
- "000001.SZ,600000.SH" → 逗号分隔的代码列表
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
股票代码列表,或 None 表示不过滤
|
|
858
|
+
"""
|
|
859
|
+
if not universe or universe.strip().lower() in ("all", "*"):
|
|
860
|
+
return None
|
|
861
|
+
|
|
862
|
+
# 预留: A 股全市场
|
|
863
|
+
if universe.strip().lower() in ("a_stock", "a-share", "cn_stock"):
|
|
864
|
+
return None
|
|
865
|
+
|
|
866
|
+
# 文件路径
|
|
867
|
+
from pathlib import Path
|
|
868
|
+
u = universe.strip()
|
|
869
|
+
if Path(u).is_file():
|
|
870
|
+
codes = []
|
|
871
|
+
with open(u, encoding="utf-8") as f:
|
|
872
|
+
for line in f:
|
|
873
|
+
line = line.strip()
|
|
874
|
+
if line and not line.startswith("#"):
|
|
875
|
+
codes.append(line)
|
|
876
|
+
return codes if codes else None
|
|
877
|
+
|
|
878
|
+
# 逗号分隔的代码列表
|
|
879
|
+
if "," in u:
|
|
880
|
+
codes = [c.strip() for c in u.split(",") if c.strip()]
|
|
881
|
+
return codes if codes else None
|
|
882
|
+
|
|
883
|
+
# 单个代码
|
|
884
|
+
return [u]
|
|
885
|
+
|
|
886
|
+
def _apply_talib_operator(
|
|
887
|
+
self,
|
|
888
|
+
category: str,
|
|
889
|
+
input_exprs: List[pl.Expr],
|
|
890
|
+
params: Dict[str, Any]
|
|
891
|
+
) -> pl.Expr:
|
|
892
|
+
"""应用 TA-Lib 技术分析算子
|
|
893
|
+
|
|
894
|
+
直接从 factor_functions 注册表查找 talib_* 算子。
|
|
895
|
+
YAML 配置示例:
|
|
896
|
+
type: talib
|
|
897
|
+
category: talib_rsi
|
|
898
|
+
inputs: [close]
|
|
899
|
+
params: {timeperiod: 14}
|
|
900
|
+
"""
|
|
901
|
+
from QuantNodes.operators.proxy import get_operator
|
|
902
|
+
op_func = get_operator(category)
|
|
903
|
+
if op_func is not None:
|
|
904
|
+
return op_func(*input_exprs, **params)
|
|
905
|
+
return None
|
|
906
|
+
|
|
907
|
+
def _execute_plan(
|
|
908
|
+
self,
|
|
909
|
+
data: pl.LazyFrame,
|
|
910
|
+
result: ExecutionResult
|
|
911
|
+
) -> None:
|
|
912
|
+
"""执行计算计划
|
|
913
|
+
|
|
914
|
+
保留原始列 (date, code, close等) 并添加计算的因子列。
|
|
915
|
+
使用 with_columns 逐层添加,确保中间表达式可被下游引用。
|
|
916
|
+
"""
|
|
917
|
+
if not self._expressions:
|
|
918
|
+
result.data = data
|
|
919
|
+
return
|
|
920
|
+
|
|
921
|
+
# 用 with_columns 逐个添加计算列,解决表达式间依赖
|
|
922
|
+
computed = data
|
|
923
|
+
for name, expr in self._expressions.items():
|
|
924
|
+
# 检查是否来自 List[Expr] 的拆分结果(双下划线格式:name__0, name__1)
|
|
925
|
+
if "__" in name:
|
|
926
|
+
base_name, idx = name.rsplit("__", 1)
|
|
927
|
+
if idx.isdigit():
|
|
928
|
+
computed = computed.with_columns(expr.alias(base_name))
|
|
929
|
+
continue
|
|
930
|
+
|
|
931
|
+
computed = computed.with_columns(expr.alias(name))
|
|
932
|
+
|
|
933
|
+
result.data = computed
|
|
934
|
+
|
|
935
|
+
def get_expressions(self) -> Dict[str, pl.Expr]:
|
|
936
|
+
"""获取生成的表达式"""
|
|
937
|
+
return self._expressions
|
|
938
|
+
|
|
939
|
+
def compile(
|
|
940
|
+
self,
|
|
941
|
+
config: StrategyConfig,
|
|
942
|
+
data: pl.LazyFrame
|
|
943
|
+
) -> pl.LazyFrame:
|
|
944
|
+
"""编译配置为 LazyFrame
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
config: 策略配置
|
|
948
|
+
data: 数据 LazyFrame
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
计算后的 LazyFrame
|
|
952
|
+
"""
|
|
953
|
+
result = self.run(config, data)
|
|
954
|
+
|
|
955
|
+
if result.is_success and hasattr(result, "data"):
|
|
956
|
+
return result.data
|
|
957
|
+
|
|
958
|
+
return data
|