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,667 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Composite DAG operators — DAG 模板复合算子 (PR-QN-3a, 2026-06-21)
|
|
3
|
+
|
|
4
|
+
Level 1 抽象: 介于 primitive ops (L0) 和 业务语义 (L3) 之间.
|
|
5
|
+
由 @composite_operator 装饰器注册, 统一通过 get_operator() 查询,
|
|
6
|
+
并用 is_composite=True 标记位与 multi_section 等 L0 ops 区分.
|
|
7
|
+
|
|
8
|
+
设计要点:
|
|
9
|
+
- ParamSpec: 参数 schema (name / type / default / required / description)
|
|
10
|
+
- CompositeSpec: DAG 模板 + 参数 schema + 文档 + 例子
|
|
11
|
+
- _CompositeRegistry: 隔离注册表, 与 _CustomOperatorRegistry 平级
|
|
12
|
+
- composite_operator: 用户自定义入口
|
|
13
|
+
- load_composites_from_yaml: YAML 扩展入口 (ast 解析, 非裸 exec)
|
|
14
|
+
- get_composite_doc_for_llm: 给 LLM prompt 用的 markdown 文档
|
|
15
|
+
|
|
16
|
+
对齐规范: docs/22-算子系统设计与规范.md §十七
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
import functools
|
|
22
|
+
from collections.abc import Iterator
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from polars import Expr
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============== 参数 Schema ==============
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ParamSpec:
|
|
34
|
+
"""Composite 参数 schema.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: 参数名
|
|
38
|
+
type_hint: "expr" | "int" | "float" | "str" | "bool"
|
|
39
|
+
default: 默认值 (None 表示无默认)
|
|
40
|
+
required: 是否必填
|
|
41
|
+
description: 用于 LLM prompt 的描述
|
|
42
|
+
"""
|
|
43
|
+
name: str
|
|
44
|
+
type_hint: str = "expr"
|
|
45
|
+
default: Any = None
|
|
46
|
+
required: bool = False
|
|
47
|
+
description: str = ""
|
|
48
|
+
|
|
49
|
+
def validate(self, value: Any) -> None:
|
|
50
|
+
"""运行时类型校验.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: 必填参数为 None
|
|
54
|
+
TypeError: 类型不匹配
|
|
55
|
+
"""
|
|
56
|
+
if value is None and self.required:
|
|
57
|
+
raise ValueError(f"Composite param '{self.name}' is required")
|
|
58
|
+
type_check = {"int": int, "float": float, "str": str, "bool": bool}
|
|
59
|
+
if self.type_hint in type_check and not isinstance(value, type_check[self.type_hint]):
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"Composite param '{self.name}' must be {self.type_hint}, "
|
|
62
|
+
f"got {type(value).__name__}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ============== Composite Spec ==============
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class CompositeSpec:
|
|
70
|
+
"""DAG 模板复合算子.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
name: 唯一标识 (如 "industry_neutralize")
|
|
74
|
+
template: 接收 **params, 返回 polars.Expr 或 pd.Series 的函数
|
|
75
|
+
category: 复用 QuantNodes 的 5 类之一 (默认 multi_section)
|
|
76
|
+
engine: 引擎类型 ("polars" | "pandas"), 同名 op 分引擎注册
|
|
77
|
+
params: 参数 schema 字典
|
|
78
|
+
doc: 文档 (用于 LLM prompt)
|
|
79
|
+
examples: LLM few-shot 例子
|
|
80
|
+
"""
|
|
81
|
+
name: str
|
|
82
|
+
template: Callable[..., Any]
|
|
83
|
+
category: str = "multi_section"
|
|
84
|
+
engine: str = "polars"
|
|
85
|
+
params: Dict[str, ParamSpec] = field(default_factory=dict)
|
|
86
|
+
doc: str = ""
|
|
87
|
+
examples: List[dict] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
def instantiate(self, **kwargs: Any) -> "Expr":
|
|
90
|
+
"""参数化实例化: 校验 + 填默认 + 调用 template.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: 必填参数缺失
|
|
94
|
+
TypeError: 参数类型不匹配
|
|
95
|
+
"""
|
|
96
|
+
bound = dict(kwargs)
|
|
97
|
+
for pname, pspec in self.params.items():
|
|
98
|
+
if pname in bound:
|
|
99
|
+
pspec.validate(bound[pname])
|
|
100
|
+
elif pspec.required:
|
|
101
|
+
raise ValueError(f"Missing required param: {pname}")
|
|
102
|
+
elif pspec.default is not None:
|
|
103
|
+
bound[pname] = pspec.default
|
|
104
|
+
return self.template(**bound)
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> dict:
|
|
107
|
+
"""序列化为 dict (用于 LLM prompt / JSON 持久化)."""
|
|
108
|
+
return {
|
|
109
|
+
"name": self.name,
|
|
110
|
+
"category": self.category,
|
|
111
|
+
"doc": self.doc,
|
|
112
|
+
"params": {
|
|
113
|
+
pname: {
|
|
114
|
+
"type": pspec.type_hint,
|
|
115
|
+
"default": pspec.default,
|
|
116
|
+
"required": pspec.required,
|
|
117
|
+
"description": pspec.description,
|
|
118
|
+
}
|
|
119
|
+
for pname, pspec in self.params.items()
|
|
120
|
+
},
|
|
121
|
+
"examples": self.examples,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ============== 注册表 ==============
|
|
126
|
+
|
|
127
|
+
class _CompositeRegistry:
|
|
128
|
+
"""Composite op 注册表 (与 _CustomOperatorRegistry 隔离但接口对齐)."""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self._registry: Dict[str, CompositeSpec] = {}
|
|
132
|
+
|
|
133
|
+
def register(self, spec: CompositeSpec) -> None:
|
|
134
|
+
"""注册一个 composite.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: name 已注册 (重复)
|
|
138
|
+
|
|
139
|
+
Note:
|
|
140
|
+
Composite 隔离存放在 ``_COMPOSITE_REGISTRY`` 中, **不**注入到主
|
|
141
|
+
``_OPERATOR_REGISTRY``. 原因:
|
|
142
|
+
|
|
143
|
+
1. 主注册表存 L0 primitive, schema 严格要求 ``signature`` /
|
|
144
|
+
``parameters`` 等键 (见 ``test_all_operators_have_doc``).
|
|
145
|
+
2. 主注册表需 JSON 可序列化 (见 ``generate_documentation_json``),
|
|
146
|
+
CompositeSpec 不可序列化.
|
|
147
|
+
3. composite 应通过 ``is_composite_op`` / ``get_composite_spec`` /
|
|
148
|
+
``list_composite_ops`` 三套独立 API 访问, 与 L0 严格隔离.
|
|
149
|
+
"""
|
|
150
|
+
if spec.name in self._registry:
|
|
151
|
+
raise ValueError(f"Composite '{spec.name}' already registered")
|
|
152
|
+
self._registry[spec.name] = spec
|
|
153
|
+
|
|
154
|
+
def _build_param_specs(self, params_dict: Dict[str, dict]) -> Dict[str, ParamSpec]:
|
|
155
|
+
"""从用户传入的 dict 构造 ParamSpec (兼容 'type' 字段)."""
|
|
156
|
+
out: Dict[str, ParamSpec] = {}
|
|
157
|
+
for pname, pdict in params_dict.items():
|
|
158
|
+
# 兼容 'type' 字段 (与 type_hint 同义, 避免 Python 关键字冲突)
|
|
159
|
+
pdict = dict(pdict)
|
|
160
|
+
if "type" in pdict and "type_hint" not in pdict:
|
|
161
|
+
pdict["type_hint"] = pdict.pop("type")
|
|
162
|
+
out[pname] = ParamSpec(name=pname, **pdict)
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
def get(self, name: str) -> Optional[CompositeSpec]:
|
|
166
|
+
return self._registry.get(name)
|
|
167
|
+
|
|
168
|
+
def list(self, category: Optional[str] = None) -> List[str]:
|
|
169
|
+
if category:
|
|
170
|
+
return [n for n, s in self._registry.items() if s.category == category]
|
|
171
|
+
return list(self._registry.keys())
|
|
172
|
+
|
|
173
|
+
def all_specs(self) -> Iterator[CompositeSpec]:
|
|
174
|
+
return iter(self._registry.values())
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_COMPOSITE_REGISTRY = _CompositeRegistry()
|
|
178
|
+
_COMPOSITE_REGISTRY_POLARS = _COMPOSITE_REGISTRY # alias for clarity
|
|
179
|
+
_COMPOSITE_REGISTRY_PANDAS = _CompositeRegistry()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ============== 装饰器 ==============
|
|
183
|
+
|
|
184
|
+
def composite_operator(
|
|
185
|
+
name: str,
|
|
186
|
+
category: str = "multi_section",
|
|
187
|
+
params: Optional[Dict[str, dict]] = None,
|
|
188
|
+
doc: str = "",
|
|
189
|
+
examples: Optional[List[dict]] = None,
|
|
190
|
+
engine: str = "polars",
|
|
191
|
+
):
|
|
192
|
+
"""注册 DAG 模板复合算子.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
name: 算子唯一名
|
|
196
|
+
category: 5 类之一 (默认 multi_section, 与 L0 共存)
|
|
197
|
+
params: {pname: {type, default, required, description}}
|
|
198
|
+
doc: 文档 (LLM prompt 用)
|
|
199
|
+
examples: LLM few-shot 例子
|
|
200
|
+
engine: 引擎类型 ("polars" | "pandas"), 同名 op 分引擎注册
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
装饰器函数
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
@composite_operator(
|
|
207
|
+
name="industry_neutralize",
|
|
208
|
+
params={
|
|
209
|
+
"x": {"type": "expr", "required": True},
|
|
210
|
+
"industry_col": {"type": "str", "default": "citic_1"},
|
|
211
|
+
},
|
|
212
|
+
doc="行业中性化: x 减去行业内均值",
|
|
213
|
+
)
|
|
214
|
+
def industry_neutralize(x: Expr, industry_col: str = "citic_1") -> Expr:
|
|
215
|
+
return x - x.group_by(industry_col).mean()
|
|
216
|
+
"""
|
|
217
|
+
def decorator(func: Callable) -> Callable:
|
|
218
|
+
param_specs = _COMPOSITE_REGISTRY._build_param_specs(params or {})
|
|
219
|
+
spec = CompositeSpec(
|
|
220
|
+
name=name,
|
|
221
|
+
template=func,
|
|
222
|
+
category=category,
|
|
223
|
+
engine=engine,
|
|
224
|
+
params=param_specs,
|
|
225
|
+
doc=doc or (func.__doc__ or ""),
|
|
226
|
+
examples=examples or [],
|
|
227
|
+
)
|
|
228
|
+
if engine == "pandas":
|
|
229
|
+
_COMPOSITE_REGISTRY_PANDAS.register(spec)
|
|
230
|
+
else:
|
|
231
|
+
_COMPOSITE_REGISTRY.register(spec)
|
|
232
|
+
|
|
233
|
+
@functools.wraps(func)
|
|
234
|
+
def wrapper(*args: Any, **kwargs: Any) -> "Expr":
|
|
235
|
+
return spec.instantiate(**kwargs)
|
|
236
|
+
|
|
237
|
+
wrapper.__composite_spec__ = spec # type: ignore[attr-defined]
|
|
238
|
+
return wrapper
|
|
239
|
+
|
|
240
|
+
return decorator
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ============== 查询接口 ==============
|
|
244
|
+
|
|
245
|
+
def is_composite_op(name: str, engine: str = "any") -> bool:
|
|
246
|
+
"""判断 op 是否是 composite.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
name: 算子名
|
|
250
|
+
engine: "any" (default, union) | "polars" | "pandas"
|
|
251
|
+
|
|
252
|
+
PR-QN-3a: composite 完全隔离存放, 不污染主 ``_OPERATOR_REGISTRY``.
|
|
253
|
+
PR-QN-4: engine="any" 查双 registry (union), engine="polars"/"pandas" 分查.
|
|
254
|
+
"""
|
|
255
|
+
if engine == "polars":
|
|
256
|
+
return name in _COMPOSITE_REGISTRY_POLARS.list()
|
|
257
|
+
if engine == "pandas":
|
|
258
|
+
return name in _COMPOSITE_REGISTRY_PANDAS.list()
|
|
259
|
+
# engine == "any"
|
|
260
|
+
return name in _COMPOSITE_REGISTRY.list() or name in _COMPOSITE_REGISTRY_PANDAS.list()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_composite_spec(name: str, engine: str = "any") -> Optional[CompositeSpec]:
|
|
264
|
+
"""获取 composite spec (用于 LLM 编译时的 schema 查询).
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
name: 算子名
|
|
268
|
+
engine: "any" (default, first found) | "polars" | "pandas"
|
|
269
|
+
"""
|
|
270
|
+
if engine == "polars":
|
|
271
|
+
return _COMPOSITE_REGISTRY_POLARS.get(name)
|
|
272
|
+
if engine == "pandas":
|
|
273
|
+
return _COMPOSITE_REGISTRY_PANDAS.get(name)
|
|
274
|
+
# engine == "any": prefer polars, fallback pandas
|
|
275
|
+
return _COMPOSITE_REGISTRY.get(name) or _COMPOSITE_REGISTRY_PANDAS.get(name)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def list_composite_ops(category: Optional[str] = None, engine: str = "any") -> List[str]:
|
|
279
|
+
"""列出所有 composite ops (可选按 category + engine 过滤).
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
category: 按类别过滤
|
|
283
|
+
engine: "any" (default, union) | "polars" | "pandas"
|
|
284
|
+
"""
|
|
285
|
+
if engine == "polars":
|
|
286
|
+
return _COMPOSITE_REGISTRY_POLARS.list(category=category)
|
|
287
|
+
if engine == "pandas":
|
|
288
|
+
return _COMPOSITE_REGISTRY_PANDAS.list(category=category)
|
|
289
|
+
# engine == "any": union — sorted for deterministic order across runs
|
|
290
|
+
# (set ordering depends on PYTHONHASHSEED which is randomized by default).
|
|
291
|
+
# v2.9.1: stable ordering eliminates flake in tests that index list[0].
|
|
292
|
+
polars_ops = set(_COMPOSITE_REGISTRY_POLARS.list(category=category))
|
|
293
|
+
pandas_ops = set(_COMPOSITE_REGISTRY_PANDAS.list(category=category))
|
|
294
|
+
return sorted(polars_ops | pandas_ops)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_composite_doc_for_llm(engine: str = "any") -> str:
|
|
298
|
+
"""生成给 LLM prompt 的 composite 文档 (markdown 格式).
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
engine: "any" (default, all ops) | "polars" | "pandas"
|
|
302
|
+
|
|
303
|
+
Phase 1.5: 内部委托 LLMDocVisitor, 保持向后兼容的输出格式。
|
|
304
|
+
"""
|
|
305
|
+
visitor = LLMDocVisitor()
|
|
306
|
+
if engine == "polars":
|
|
307
|
+
specs = _COMPOSITE_REGISTRY_POLARS.all_specs()
|
|
308
|
+
elif engine == "pandas":
|
|
309
|
+
specs = _COMPOSITE_REGISTRY_PANDAS.all_specs()
|
|
310
|
+
else:
|
|
311
|
+
# engine == "any": polars first, then pandas (dedup by name)
|
|
312
|
+
seen: set = set()
|
|
313
|
+
specs_list = []
|
|
314
|
+
for spec in _COMPOSITE_REGISTRY_POLARS.all_specs():
|
|
315
|
+
if spec.name not in seen:
|
|
316
|
+
seen.add(spec.name)
|
|
317
|
+
specs_list.append(spec)
|
|
318
|
+
for spec in _COMPOSITE_REGISTRY_PANDAS.all_specs():
|
|
319
|
+
if spec.name not in seen:
|
|
320
|
+
seen.add(spec.name)
|
|
321
|
+
specs_list.append(spec)
|
|
322
|
+
specs = iter(specs_list)
|
|
323
|
+
for spec in specs:
|
|
324
|
+
visitor.visit_spec(spec)
|
|
325
|
+
return visitor.result
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ============== Visitor Pattern (Phase 1.5) ==============
|
|
329
|
+
|
|
330
|
+
class CompositeSpecVisitor:
|
|
331
|
+
"""CompositeSpec 的访问者基类 (Phase 1.5, Visitor pattern).
|
|
332
|
+
|
|
333
|
+
用途:
|
|
334
|
+
- 统一访问 _COMPOSITE_REGISTRY 中所有 CompositeSpec
|
|
335
|
+
- 不修改 CompositeSpec 即可扩展新的遍历/分析能力
|
|
336
|
+
- 具体子类: LLMDocVisitor / DependencyVisitor / ValidationVisitor
|
|
337
|
+
|
|
338
|
+
使用:
|
|
339
|
+
>>> visitor = LLMDocVisitor()
|
|
340
|
+
>>> for spec in _COMPOSITE_REGISTRY.all_specs():
|
|
341
|
+
... visitor.visit_spec(spec)
|
|
342
|
+
>>> print(visitor.result)
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def visit_spec(self, spec: CompositeSpec) -> None:
|
|
346
|
+
"""访问一个 CompositeSpec。子类重写此方法实现具体逻辑。"""
|
|
347
|
+
raise NotImplementedError
|
|
348
|
+
|
|
349
|
+
def visit_all(self) -> None:
|
|
350
|
+
"""便利方法: 遍历整个 _COMPOSITE_REGISTRY。"""
|
|
351
|
+
for spec in _COMPOSITE_REGISTRY.all_specs():
|
|
352
|
+
self.visit_spec(spec)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class LLMDocVisitor(CompositeSpecVisitor):
|
|
356
|
+
"""为 LLM prompt 生成 markdown 格式的 composite 文档。
|
|
357
|
+
|
|
358
|
+
输出格式与原 get_composite_doc_for_llm() 完全一致 (向后兼容)。
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self) -> None:
|
|
362
|
+
self.lines: List[str] = ["# Available Composite Operators"]
|
|
363
|
+
|
|
364
|
+
def visit_spec(self, spec: CompositeSpec) -> None:
|
|
365
|
+
self.lines.append(f"## {spec.name}")
|
|
366
|
+
self.lines.append(f" {spec.doc}")
|
|
367
|
+
for pname, pspec in spec.params.items():
|
|
368
|
+
if pspec.required:
|
|
369
|
+
tag = "(required)"
|
|
370
|
+
elif pspec.default is not None:
|
|
371
|
+
tag = f"(default: {pspec.default})"
|
|
372
|
+
else:
|
|
373
|
+
tag = "(optional)"
|
|
374
|
+
self.lines.append(
|
|
375
|
+
f" - {pname}: {pspec.type_hint} {tag} — {pspec.description}"
|
|
376
|
+
)
|
|
377
|
+
if spec.examples:
|
|
378
|
+
self.lines.append(f" Example: {spec.examples[0]}")
|
|
379
|
+
self.lines.append("")
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def result(self) -> str:
|
|
383
|
+
return "\n".join(self.lines)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class DependencyVisitor(CompositeSpecVisitor):
|
|
387
|
+
"""提取 composite 之间的依赖图 (基于 template 源码中的函数名引用)。
|
|
388
|
+
|
|
389
|
+
输出: dict[name, set[dependency_name]], 边从 spec 指向被引用的其他 composite。
|
|
390
|
+
目前实现是粗略的: 用 inspect.getsource() 提取 template 函数源码,
|
|
391
|
+
匹配其他 composite 的 name 字符串。
|
|
392
|
+
|
|
393
|
+
用途:
|
|
394
|
+
- DAG 可视化
|
|
395
|
+
- 检测循环依赖
|
|
396
|
+
- 增量重编译优化
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
def __init__(self) -> None:
|
|
400
|
+
self.graph: Dict[str, set] = {}
|
|
401
|
+
|
|
402
|
+
def visit_spec(self, spec: CompositeSpec) -> None:
|
|
403
|
+
deps: set = set()
|
|
404
|
+
try:
|
|
405
|
+
import inspect
|
|
406
|
+
source = inspect.getsource(spec.template)
|
|
407
|
+
for other_name in _COMPOSITE_REGISTRY.list():
|
|
408
|
+
if other_name == spec.name:
|
|
409
|
+
continue
|
|
410
|
+
if other_name in source:
|
|
411
|
+
deps.add(other_name)
|
|
412
|
+
except (OSError, TypeError):
|
|
413
|
+
pass
|
|
414
|
+
self.graph[spec.name] = deps
|
|
415
|
+
|
|
416
|
+
def detect_cycles(self) -> List[List[str]]:
|
|
417
|
+
"""返回所有循环依赖路径, 每条路径是 list of names."""
|
|
418
|
+
cycles: List[List[str]] = []
|
|
419
|
+
visited: set = set()
|
|
420
|
+
path: List[str] = []
|
|
421
|
+
|
|
422
|
+
def dfs(node: str) -> None:
|
|
423
|
+
if node in path:
|
|
424
|
+
cycle_start = path.index(node)
|
|
425
|
+
cycles.append(path[cycle_start:] + [node])
|
|
426
|
+
return
|
|
427
|
+
if node in visited:
|
|
428
|
+
return
|
|
429
|
+
path.append(node)
|
|
430
|
+
for nxt in self.graph.get(node, ()):
|
|
431
|
+
dfs(nxt)
|
|
432
|
+
path.pop()
|
|
433
|
+
visited.add(node)
|
|
434
|
+
|
|
435
|
+
for n in self.graph:
|
|
436
|
+
dfs(n)
|
|
437
|
+
return cycles
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class ValidationVisitor(CompositeSpecVisitor):
|
|
441
|
+
"""检查 CompositeSpec 的语义正确性。
|
|
442
|
+
|
|
443
|
+
校验:
|
|
444
|
+
- spec.name 唯一 (注册时已保证, 这里双重检查)
|
|
445
|
+
- 必填参数 (required=True) 没有 default (避免语义冲突)
|
|
446
|
+
- 文档非空 (LLM prompt 友好)
|
|
447
|
+
- examples 数量 > 0 (推荐)
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
def __init__(self) -> None:
|
|
451
|
+
self.errors: List[str] = []
|
|
452
|
+
self.warnings: List[str] = []
|
|
453
|
+
|
|
454
|
+
def visit_spec(self, spec: CompositeSpec) -> None:
|
|
455
|
+
# 必填参数不应该有 default
|
|
456
|
+
for pname, pspec in spec.params.items():
|
|
457
|
+
if pspec.required and pspec.default is not None:
|
|
458
|
+
self.errors.append(
|
|
459
|
+
f"Composite '{spec.name}' param '{pname}' is required but has "
|
|
460
|
+
f"default={pspec.default!r} (语义冲突)"
|
|
461
|
+
)
|
|
462
|
+
# 文档空 → warning
|
|
463
|
+
if not spec.doc.strip():
|
|
464
|
+
self.warnings.append(
|
|
465
|
+
f"Composite '{spec.name}' has empty doc (LLM prompt 会缺少说明)"
|
|
466
|
+
)
|
|
467
|
+
# 没有 examples → warning
|
|
468
|
+
if not spec.examples:
|
|
469
|
+
self.warnings.append(
|
|
470
|
+
f"Composite '{spec.name}' has no examples (LLM few-shot 会少)"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def has_errors(self) -> bool:
|
|
475
|
+
return bool(self.errors)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ============== 用户 YAML 扩展 ==============
|
|
479
|
+
|
|
480
|
+
def load_composites_from_yaml(yaml_path: str) -> int:
|
|
481
|
+
"""从 YAML 文件加载用户自定义 composite ops.
|
|
482
|
+
|
|
483
|
+
YAML 格式 (见 docs/22 §十七):
|
|
484
|
+
composites:
|
|
485
|
+
- name: my_op
|
|
486
|
+
category: multi_section
|
|
487
|
+
doc: "我的 op"
|
|
488
|
+
params:
|
|
489
|
+
x: {type: expr, required: true}
|
|
490
|
+
k: {type: float, default: 1.0}
|
|
491
|
+
template: "x + k"
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
加载的 composite 数量
|
|
495
|
+
"""
|
|
496
|
+
import yaml
|
|
497
|
+
from pathlib import Path
|
|
498
|
+
p = Path(yaml_path)
|
|
499
|
+
if not p.exists():
|
|
500
|
+
raise FileNotFoundError(f"YAML file not found: {yaml_path}")
|
|
501
|
+
with open(p, encoding="utf-8") as f:
|
|
502
|
+
data = yaml.safe_load(f)
|
|
503
|
+
if not data or "composites" not in data:
|
|
504
|
+
return 0
|
|
505
|
+
count = 0
|
|
506
|
+
for entry in data["composites"]:
|
|
507
|
+
template_str = entry.get("template")
|
|
508
|
+
if not template_str:
|
|
509
|
+
continue
|
|
510
|
+
engine = entry.get("engine", "polars") # 缺省 = "polars" (向后兼容)
|
|
511
|
+
template = _compile_template_string(template_str, engine=engine)
|
|
512
|
+
param_specs = _COMPOSITE_REGISTRY._build_param_specs(
|
|
513
|
+
entry.get("params", {})
|
|
514
|
+
)
|
|
515
|
+
spec = CompositeSpec(
|
|
516
|
+
name=entry["name"],
|
|
517
|
+
template=template,
|
|
518
|
+
category=entry.get("category", "multi_section"),
|
|
519
|
+
engine=engine,
|
|
520
|
+
params=param_specs,
|
|
521
|
+
doc=entry.get("doc", ""),
|
|
522
|
+
examples=entry.get("examples", []),
|
|
523
|
+
)
|
|
524
|
+
if engine == "pandas":
|
|
525
|
+
_COMPOSITE_REGISTRY_PANDAS.register(spec)
|
|
526
|
+
else:
|
|
527
|
+
_COMPOSITE_REGISTRY.register(spec)
|
|
528
|
+
count += 1
|
|
529
|
+
return count
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# 允许的 Expr / 函数名白名单 (YAML template 解析用, polars 引擎)
|
|
533
|
+
_ALLOWED_FUNC_NAMES: set = {
|
|
534
|
+
"col", "lit", "when", "otherwise", "then",
|
|
535
|
+
"abs", "log", "sqrt", "pow", "exp",
|
|
536
|
+
"rolling_mean", "rolling_std", "rolling_corr",
|
|
537
|
+
"rolling_sum", "rolling_min", "rolling_max", "rolling_median",
|
|
538
|
+
"ewm_mean", "ewm_std",
|
|
539
|
+
"shift", "diff", "pct_change", "rank",
|
|
540
|
+
"mean", "std", "sum", "min", "max", "median", "quantile",
|
|
541
|
+
"count", "first", "last",
|
|
542
|
+
"group_by", "over", "alias",
|
|
543
|
+
"clip", "fill_null", "fill_nan", "drop_nulls", "drop_nans",
|
|
544
|
+
"is_null", "is_nan", "is_not_null",
|
|
545
|
+
"round", "floor", "ceil",
|
|
546
|
+
"and_", "or_", "not_",
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# 禁止的 base name (防止 ``os.system`` / ``subprocess.run`` 等)
|
|
550
|
+
_DENIED_BASE_NAMES: set = {
|
|
551
|
+
"os", "sys", "subprocess", "socket", "urllib", "requests",
|
|
552
|
+
"shutil", "pathlib", "path", "open", "file", "io",
|
|
553
|
+
"importlib", "builtins", "eval", "exec", "compile",
|
|
554
|
+
"getattr", "setattr", "delattr", "globals", "locals",
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _compile_template_string(template_str: str, engine: str = "polars") -> Callable[..., Any]:
|
|
559
|
+
"""把字符串模板编译为 callable (AST 解析 + 白名单校验).
|
|
560
|
+
|
|
561
|
+
模板形式: 单个表达式, 引用 params dict 中的 key, 如 ``x + k`` 或
|
|
562
|
+
``x.group_by(industry_col).mean()``. 编译为 ``def _t(x, k, industry_col):
|
|
563
|
+
return <expr>`` 形式, 可被 ``template(**bound)`` 调用.
|
|
564
|
+
|
|
565
|
+
与 CodeSandbox 配合: 这里**只**解析 Expr 调用链, 不执行任意 Python.
|
|
566
|
+
PR-QN-3a 修复: 文档原版用裸 exec, 会被 CodeSandbox 拒绝, 改用 ast.parse
|
|
567
|
+
+ 节点类型白名单.
|
|
568
|
+
PR-QN-4: engine 参数选择正确的白名单 (polars vs pandas, 严格分流).
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
tree = ast.parse(template_str, mode="eval")
|
|
572
|
+
except SyntaxError as e:
|
|
573
|
+
raise ValueError(f"YAML template 语法错误: {e}") from e
|
|
574
|
+
|
|
575
|
+
# 提取表达式中所有自由变量作为函数参数
|
|
576
|
+
free_vars = _extract_free_vars(tree.body)
|
|
577
|
+
# PR-QN-4: 严格分流白名单
|
|
578
|
+
if engine == "pandas":
|
|
579
|
+
from ._engine import ALLOWED_FUNC_NAMES_PANDAS
|
|
580
|
+
allowed_funcs = ALLOWED_FUNC_NAMES_PANDAS
|
|
581
|
+
else:
|
|
582
|
+
allowed_funcs = _ALLOWED_FUNC_NAMES
|
|
583
|
+
# 白名单校验: 仅允许 Name / Call / Attribute / Constant / BinOp
|
|
584
|
+
_validate_ast_nodes(tree.body, allowed_funcs=allowed_funcs)
|
|
585
|
+
|
|
586
|
+
# 编译为函数
|
|
587
|
+
func_name = "_composite_template"
|
|
588
|
+
params_str = ", ".join(free_vars) if free_vars else ""
|
|
589
|
+
code = f"def {func_name}({params_str}):\n return {template_str}\n"
|
|
590
|
+
namespace: dict = {}
|
|
591
|
+
exec(code, namespace) # noqa: S102 — AST 已校验, 安全
|
|
592
|
+
return namespace[func_name]
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _extract_free_vars(node: ast.AST) -> List[str]:
|
|
596
|
+
"""从 AST 节点提取所有 Name 节点 (去重, 保序)."""
|
|
597
|
+
seen: set = set()
|
|
598
|
+
out: List[str] = []
|
|
599
|
+
for child in ast.walk(node):
|
|
600
|
+
if isinstance(child, ast.Name) and child.id not in seen:
|
|
601
|
+
seen.add(child.id)
|
|
602
|
+
out.append(child.id)
|
|
603
|
+
return out
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _validate_chain_base(attr: ast.Attribute, allowed_funcs: set) -> None:
|
|
607
|
+
"""校验链式调用的 base 节点 (PR-QN-4).
|
|
608
|
+
|
|
609
|
+
链式调用可以是:
|
|
610
|
+
- polars: x.method() — base is Name
|
|
611
|
+
- pandas: x.groupby('a').mean() — base is Call (groupby returns a GroupBy)
|
|
612
|
+
- deep chain: x.rolling(10).mean() — base is Call
|
|
613
|
+
|
|
614
|
+
校验逻辑: 每个 Attribute.attr 都必须在白名单中, 递归验证整个链.
|
|
615
|
+
"""
|
|
616
|
+
# 当前层方法名必须在白名单
|
|
617
|
+
if attr.attr not in allowed_funcs:
|
|
618
|
+
raise ValueError(
|
|
619
|
+
f"YAML template 调用了不允许的方法: {attr.attr!r}. "
|
|
620
|
+
f"允许: {sorted(allowed_funcs)[:10]}..."
|
|
621
|
+
)
|
|
622
|
+
# 递归验证 base
|
|
623
|
+
if isinstance(attr.value, ast.Name):
|
|
624
|
+
if attr.value.id in _DENIED_BASE_NAMES:
|
|
625
|
+
raise ValueError(
|
|
626
|
+
f"YAML template 调用了禁止的 base: "
|
|
627
|
+
f"{attr.value.id!r}.{attr.attr}"
|
|
628
|
+
)
|
|
629
|
+
elif isinstance(attr.value, ast.Call):
|
|
630
|
+
if isinstance(attr.value.func, ast.Attribute):
|
|
631
|
+
_validate_chain_base(attr.value.func, allowed_funcs)
|
|
632
|
+
elif isinstance(attr.value.func, ast.Name):
|
|
633
|
+
if attr.value.func.id not in allowed_funcs:
|
|
634
|
+
raise ValueError(
|
|
635
|
+
f"YAML template 调用了不允许的函数: {attr.value.func.id!r}. "
|
|
636
|
+
f"允许: {sorted(allowed_funcs)[:10]}..."
|
|
637
|
+
)
|
|
638
|
+
else:
|
|
639
|
+
raise ValueError(
|
|
640
|
+
f"YAML template 不支持的链式属性: {ast.dump(attr)[:80]}"
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _validate_ast_nodes(node: ast.AST, allowed_funcs: set) -> None:
|
|
645
|
+
"""递归校验 AST 节点, 仅允许白名单内的函数名.
|
|
646
|
+
|
|
647
|
+
Name 节点 = 自由变量 (作函数参数, 不在白名单中) 或白名单内的函数.
|
|
648
|
+
Call 节点的 func 必须是白名单内的.
|
|
649
|
+
Attribute 访问: 支持链式方法调用 (x.method() 或 x.method().method2()).
|
|
650
|
+
PR-QN-4: 支持 pandas 风格链式调用 (x.groupby('a').mean()).
|
|
651
|
+
"""
|
|
652
|
+
for child in ast.walk(node):
|
|
653
|
+
if isinstance(child, ast.Call):
|
|
654
|
+
if isinstance(child.func, ast.Name) and child.func.id not in allowed_funcs:
|
|
655
|
+
raise ValueError(
|
|
656
|
+
f"YAML template 调用了不允许的函数: {child.func.id!r}. "
|
|
657
|
+
f"允许: {sorted(allowed_funcs)[:10]}..."
|
|
658
|
+
)
|
|
659
|
+
elif isinstance(child.func, ast.Attribute):
|
|
660
|
+
# 链式方法调用: x.method() 或 x.method().method2()
|
|
661
|
+
# PR-QN-4: base 可以是 Name (polars) 或 Call (pandas chain)
|
|
662
|
+
_validate_chain_base(child.func, allowed_funcs)
|
|
663
|
+
elif isinstance(child, ast.Attribute):
|
|
664
|
+
# Attribute 访问: base 是 Name (变量) 或 Call (链式)
|
|
665
|
+
_validate_chain_base(child, allowed_funcs)
|
|
666
|
+
elif isinstance(child, (ast.Import, ast.ImportFrom, ast.Lambda, ast.FunctionDef)):
|
|
667
|
+
raise ValueError(f"YAML template 不允许: {type(child).__name__}")
|