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,91 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Pydantic 化节点公共基类 / Pydantic-config Node Base.
|
|
3
|
+
|
|
4
|
+
抽取 12 节点 __init__ 的 Union 校验样板 (~144 行重复) 到此处.
|
|
5
|
+
子类只需声明 ``ConfigSchema = XxxNodeConfig`` 即可:
|
|
6
|
+
|
|
7
|
+
>>> class FooNode(PydanticConfigNode):
|
|
8
|
+
... ConfigSchema = FooNodeConfig
|
|
9
|
+
... def _execute(self, input_data=None, **kw): ...
|
|
10
|
+
|
|
11
|
+
构造时:
|
|
12
|
+
- ``isinstance(config, ConfigSchema)`` → 直接使用
|
|
13
|
+
- ``dict / None`` → ``ConfigSchema.model_validate``
|
|
14
|
+
- 其他类型 → ``TypeError``
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Any, ClassVar, Optional, Union
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from QuantNodes.core.node import BaseNode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PydanticConfigNode(BaseNode):
|
|
25
|
+
"""统一处理 12 节点的 ``Union[dict, ConfigSchema, None]`` 配置入参.
|
|
26
|
+
|
|
27
|
+
子类必须设置 ``ConfigSchema``; 构造完成后, 实例上挂载:
|
|
28
|
+
|
|
29
|
+
- ``self.cfg`` : ``ConfigSchema`` 实例 (推荐访问入口)
|
|
30
|
+
- ``self.config``: ``dict`` (BaseNode 协议要求)
|
|
31
|
+
- ``self._xxx`` : 由 ``_ALIASES`` 声明的 cfg 字段别名 (向后兼容)
|
|
32
|
+
|
|
33
|
+
自动 alias 规则 (Phase G3):
|
|
34
|
+
声明 ``_ALIASES = {"_data_path": "data_path", ...}`` 后, 基类
|
|
35
|
+
``__init__`` 自动 copy cfg 字段到 self._xxx。list/tuple 字段会被浅拷贝
|
|
36
|
+
以避免下游误改 cfg。
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
ConfigSchema: ClassVar[type[BaseModel]]
|
|
40
|
+
_ALIASES: ClassVar[dict[str, str]] = {}
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
name: Optional[str] = None,
|
|
45
|
+
config: Union[dict, BaseModel, None] = None,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
) -> None:
|
|
48
|
+
Schema = self.ConfigSchema
|
|
49
|
+
if isinstance(config, Schema):
|
|
50
|
+
cfg = config
|
|
51
|
+
super().__init__(name, cfg.model_dump(), **kwargs)
|
|
52
|
+
elif isinstance(config, dict) or config is None:
|
|
53
|
+
cfg = Schema.model_validate(config or {})
|
|
54
|
+
super().__init__(name, config, **kwargs)
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError(
|
|
57
|
+
f"config must be dict/None/{Schema.__name__}, "
|
|
58
|
+
f"got {type(config).__name__}"
|
|
59
|
+
)
|
|
60
|
+
self.cfg: BaseModel = cfg
|
|
61
|
+
# Auto-apply aliases
|
|
62
|
+
for alias, field in self._ALIASES.items():
|
|
63
|
+
value = getattr(self.cfg, field)
|
|
64
|
+
if isinstance(value, list):
|
|
65
|
+
value = list(value) # shallow copy
|
|
66
|
+
elif isinstance(value, tuple):
|
|
67
|
+
value = list(value)
|
|
68
|
+
setattr(self, alias, value)
|
|
69
|
+
|
|
70
|
+
def _ctx(self, context: dict) -> dict:
|
|
71
|
+
"""Drill into context['LoadData'] dict (Phase G4)."""
|
|
72
|
+
return context.get("LoadData", {})
|
|
73
|
+
|
|
74
|
+
def _ctx_load(self, context: dict, key: str, default=None):
|
|
75
|
+
"""Get one key from context['LoadData'] (Phase G4).
|
|
76
|
+
|
|
77
|
+
Replaces 13 sites of: ``context.get('LoadData', {}).get(KEY)``.
|
|
78
|
+
"""
|
|
79
|
+
return self._ctx(context).get(key, default)
|
|
80
|
+
|
|
81
|
+
def _factor_data(self, context: dict):
|
|
82
|
+
"""Return FactorNeutralize output if set, else FactorPreprocess (Phase G4).
|
|
83
|
+
|
|
84
|
+
Replaces the 4-line pattern in 4 analysis nodes (ic_analyzer,
|
|
85
|
+
factor_score, group_analyzer, risk_correlation). Returns None
|
|
86
|
+
if neither is set, leaving None-handling to the caller.
|
|
87
|
+
"""
|
|
88
|
+
neutralized = context.get("FactorNeutralize")
|
|
89
|
+
if neutralized is not None:
|
|
90
|
+
return neutralized
|
|
91
|
+
return context.get("FactorPreprocess")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Node 4: 调仓日生成 / Adjust Date Node
|
|
3
|
+
|
|
4
|
+
Migrated from date_utils.py:134-191 get_adjust_date()
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
|
|
10
|
+
from QuantNodes.research.factor_test.nodes.configs import AdjustDateNodeConfig
|
|
11
|
+
from QuantNodes.research.factor_test.utils.date_utils import get_adjust_date
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AdjustDateNode(PydanticConfigNode):
|
|
15
|
+
"""根据起始日、截止日、调仓模式生成调仓日序列
|
|
16
|
+
|
|
17
|
+
输入: context["LoadData"] 的输出
|
|
18
|
+
输出: adj_dates (yyyymmdd int DataFrame)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
ConfigSchema = AdjustDateNodeConfig
|
|
22
|
+
_ALIASES = {
|
|
23
|
+
"_adj_date_beg": "adj_date_beg",
|
|
24
|
+
"_adj_date_end": "adj_date_end",
|
|
25
|
+
"_adj_mode": "adj_mode",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def _execute(self, input_data=None, **kwargs) -> pd.DataFrame:
|
|
29
|
+
# T0-3: H10 启动校验 (避免静默跑废日期)
|
|
30
|
+
if self._adj_date_beg is None or self._adj_date_end is None:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"AdjustDateNode 需要 adj_date_beg 和 adj_date_end 字段 "
|
|
33
|
+
f"(H10: 默认 None, 当前 beg={self._adj_date_beg}, end={self._adj_date_end})"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
context = kwargs.get('context', {})
|
|
37
|
+
load_data = context.get('LoadData') or input_data or {}
|
|
38
|
+
|
|
39
|
+
trade_dt = load_data['trade_dt']
|
|
40
|
+
|
|
41
|
+
adj_dates = get_adjust_date(
|
|
42
|
+
trade_dt,
|
|
43
|
+
self._adj_date_beg,
|
|
44
|
+
self._adj_date_end,
|
|
45
|
+
tuple(self._adj_mode)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return adj_dates
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""12 节点 Pydantic 配置模型 / Node Configuration Models
|
|
3
|
+
|
|
4
|
+
Phase 3.1 T0: 12 节点的 __init__ 改 Union 接受 (dict / *Config / None),
|
|
5
|
+
内部 model_validate() 校验, 保留 self._xxx 实例属性 (向后兼容 5 处测试).
|
|
6
|
+
|
|
7
|
+
拼写错 (extra="forbid") 立即 ValidationError, 跨进程 model_dump() → dict 链稳定.
|
|
8
|
+
|
|
9
|
+
Phase R3-A (2026-06-19): Schema 收敛
|
|
10
|
+
- 8 节点 Config 直接继承 config.py 子模型 (避免字段重复定义)
|
|
11
|
+
- 4 节点 (LoadData/AdjustDate/SamplePool/Report) 字段语义不同, 保留独立定义
|
|
12
|
+
- 单一真值源: 改默认值只改 config.py 一处即可同步到 nodes/configs.py
|
|
13
|
+
|
|
14
|
+
Phase 1.4: register_node_config 装饰器
|
|
15
|
+
- 替代手工维护的 NODE_CONFIG_SCHEMAS 路由表
|
|
16
|
+
- 用法: @register_node_config("GroupAnalyzer") class GroupAnalyzerNodeConfig(GroupSetting): ...
|
|
17
|
+
- 自动注册到 NODE_CONFIG_SCHEMAS, 避免新增节点时漏改路由表
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Optional
|
|
21
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
22
|
+
|
|
23
|
+
from QuantNodes.research.factor_test.config import (
|
|
24
|
+
FactorSetting,
|
|
25
|
+
GroupSetting,
|
|
26
|
+
ICSetting,
|
|
27
|
+
LongShortSetting,
|
|
28
|
+
PreprocessSetting,
|
|
29
|
+
RiskCorrelationSetting,
|
|
30
|
+
ScoreSetting,
|
|
31
|
+
TradableSetting,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_FORBID = ConfigDict(extra="forbid")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _NodeBase(BaseModel):
|
|
39
|
+
"""12 节点 Config 公共基类
|
|
40
|
+
- extra="forbid": 拼写错立即 ValidationError (新防线)
|
|
41
|
+
"""
|
|
42
|
+
model_config = _FORBID
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── 节点名 → Config Schema 路由表 (Phase 1.4: 装饰器自动注册) ─────
|
|
46
|
+
NODE_CONFIG_SCHEMAS: dict[str, type[BaseModel]] = {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def register_node_config(node_name: str):
|
|
50
|
+
"""装饰器: 将 Config Schema 注册到 NODE_CONFIG_SCHEMAS[node_name]。
|
|
51
|
+
|
|
52
|
+
Phase 1.4: 替代手工维护的路由表。新增节点 Config 时只需:
|
|
53
|
+
@register_node_config("MyNode")
|
|
54
|
+
class MyNodeConfig(BaseModel):
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
node_name: 节点在 pipeline 中的名称 (与 BaseNode 子类的注册名一致)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
装饰器函数, 接受 Config 类并原样返回 (同时写入 NODE_CONFIG_SCHEMAS)
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
TypeError: 装饰的不是 pydantic BaseModel 子类
|
|
65
|
+
ValueError: 重复注册同一 node_name (且类不同)
|
|
66
|
+
"""
|
|
67
|
+
def decorator(cls: type[BaseModel]) -> type[BaseModel]:
|
|
68
|
+
if not issubclass(cls, BaseModel):
|
|
69
|
+
raise TypeError(
|
|
70
|
+
f"@register_node_config({node_name!r}) requires a pydantic "
|
|
71
|
+
f"BaseModel subclass, got {cls!r}"
|
|
72
|
+
)
|
|
73
|
+
if node_name in NODE_CONFIG_SCHEMAS:
|
|
74
|
+
existing = NODE_CONFIG_SCHEMAS[node_name]
|
|
75
|
+
if existing is not cls:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"NODE_CONFIG_SCHEMAS[{node_name!r}] already registered "
|
|
78
|
+
f"to {existing.__name__}; refusing to overwrite with {cls.__name__}"
|
|
79
|
+
)
|
|
80
|
+
NODE_CONFIG_SCHEMAS[node_name] = cls
|
|
81
|
+
return cls
|
|
82
|
+
return decorator
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── 注册所有 12 节点 Config (Phase 1.4: 装饰器替代手工表) ─────────
|
|
86
|
+
@register_node_config("LoadData")
|
|
87
|
+
class LoadDataNodeConfig(_NodeBase):
|
|
88
|
+
"""Node 1: LoadDataNode 配置
|
|
89
|
+
|
|
90
|
+
P-2: data_path 改 Field(...) 必填, 启动时报 ValidationError (None/缺字段).
|
|
91
|
+
现有 test_data_loader_edges.py:309-316 已显式提供 load_keys 但缺 data_path,
|
|
92
|
+
需更新为提供 data_path (load_keys 测试不变).
|
|
93
|
+
"""
|
|
94
|
+
data_path: str = Field(..., description="数据根目录 (P-2 必填, 启动校验)")
|
|
95
|
+
load_keys: list = Field(
|
|
96
|
+
default_factory=lambda: [
|
|
97
|
+
"stklist", "trade_dt", "cp", "id_citic1", "mv_float",
|
|
98
|
+
"st", "suspend", "ud_limit", "ipo_days",
|
|
99
|
+
],
|
|
100
|
+
description="需要加载的数据 key 列表 (M7 默认含 tradability 必需键)",
|
|
101
|
+
)
|
|
102
|
+
factor: Optional[FactorSetting] = Field(
|
|
103
|
+
default=None, description="因子配置 (None=不加载因子)",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@register_node_config("SamplePoolFilter")
|
|
108
|
+
class SamplePoolNodeConfig(_NodeBase):
|
|
109
|
+
"""Node 2: SamplePoolFilterNode 配置
|
|
110
|
+
|
|
111
|
+
M9: index_mapping 可自定义,合并全局默认 INDEX_MAPPING。
|
|
112
|
+
M12: i18n_name_map 可自定义行业代码→名称映射,覆盖全局默认 INDUSTRY_MAPPING。
|
|
113
|
+
|
|
114
|
+
字段语义独立于 PreprocessSetting (sample_index/sample_industry 是节点专属),
|
|
115
|
+
保留独立定义.
|
|
116
|
+
"""
|
|
117
|
+
sample_index: str = Field(
|
|
118
|
+
default="all", description="样本池: all/HS300/ZZ500/ZZ800/custom",
|
|
119
|
+
)
|
|
120
|
+
sample_industry: str = Field(
|
|
121
|
+
default="all", description="行业筛选: all/中信行业名",
|
|
122
|
+
)
|
|
123
|
+
sample_index_customdir: Optional[tuple] = Field(
|
|
124
|
+
default=None, description="自定义样本池路径 (sample_index=custom 时必填)",
|
|
125
|
+
)
|
|
126
|
+
index_mapping: Optional[dict[str, tuple[str, str]]] = Field(
|
|
127
|
+
default=None, description="自定义指数映射 (覆盖全局默认 INDEX_MAPPING)",
|
|
128
|
+
)
|
|
129
|
+
i18n_name_map: Optional[dict[str, str]] = Field(
|
|
130
|
+
default=None, description="自定义行业代码→名称映射 (覆盖全局默认 INDUSTRY_MAPPING)",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@register_node_config("TradabilityFilter")
|
|
135
|
+
class TradabilityNodeConfig(_NodeBase):
|
|
136
|
+
"""Node 3: TradabilityFilterNode 配置"""
|
|
137
|
+
tradable: TradableSetting = Field(
|
|
138
|
+
default_factory=TradableSetting,
|
|
139
|
+
description="可交易性配置 (no_st/no_suspended/no_up_down_limit/min_ipo_days/trace)",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@register_node_config("AdjustDate")
|
|
144
|
+
class AdjustDateNodeConfig(_NodeBase):
|
|
145
|
+
"""Node 4: AdjustDateNode 配置
|
|
146
|
+
|
|
147
|
+
H10 兼容: adj_date_beg/end 默认 None → _execute 启动校验抛 ValueError,
|
|
148
|
+
避免静默跑废日期范围.
|
|
149
|
+
|
|
150
|
+
字段语义独立 (adj_date_beg/end 在 PreprocessSetting 中是预处理日期窗口,
|
|
151
|
+
在节点中是调仓日生成范围), 保留独立定义.
|
|
152
|
+
"""
|
|
153
|
+
adj_date_beg: Optional[int] = Field(
|
|
154
|
+
default=None, description="起始日期 yyyymmdd (None → 启动报错)",
|
|
155
|
+
)
|
|
156
|
+
adj_date_end: Optional[int] = Field(
|
|
157
|
+
default=None, description="截止日期 yyyymmdd (None → 启动报错)",
|
|
158
|
+
)
|
|
159
|
+
adj_mode: list = Field(
|
|
160
|
+
default_factory=lambda: ["M", "end"],
|
|
161
|
+
description="调仓模式: [mode, position], mode=M/W/Q/D, position=end/start",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@register_node_config("FactorPreprocess")
|
|
166
|
+
class PreprocessNodeConfig(BaseModel):
|
|
167
|
+
"""Node 5: FactorPreprocessNode 配置 (R3-A: 继承 config.py::PreprocessSetting 子集字段).
|
|
168
|
+
|
|
169
|
+
字段全部从 PreprocessSetting 继承 (missing/extreme/norm/mad_n/pct_low/pct_high/i18n_name_map),
|
|
170
|
+
通过 model_dump(include={...}) 切片传入。改 mad_n 默认值只需改 PreprocessSetting 一处.
|
|
171
|
+
"""
|
|
172
|
+
model_config = _FORBID
|
|
173
|
+
missing: str = PreprocessSetting.model_fields["missing"]
|
|
174
|
+
extreme: str = PreprocessSetting.model_fields["extreme"]
|
|
175
|
+
norm: str = PreprocessSetting.model_fields["norm"]
|
|
176
|
+
mad_n: float = PreprocessSetting.model_fields["mad_n"]
|
|
177
|
+
pct_low: float = PreprocessSetting.model_fields["pct_low"]
|
|
178
|
+
pct_high: float = PreprocessSetting.model_fields["pct_high"]
|
|
179
|
+
i18n_name_map: Optional[dict[str, str]] = PreprocessSetting.model_fields["i18n_name_map"]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@register_node_config("FactorNeutralize")
|
|
183
|
+
class NeutralizeNodeConfig(BaseModel):
|
|
184
|
+
"""Node 6: FactorNeutralizeNode 配置 (R3-A: 继承 PreprocessSetting 中性化字段)."""
|
|
185
|
+
model_config = _FORBID
|
|
186
|
+
industry_neutral: bool = PreprocessSetting.model_fields["industry_neutral"]
|
|
187
|
+
risk_neutral: bool = PreprocessSetting.model_fields["risk_neutral"]
|
|
188
|
+
risk_factors: list = PreprocessSetting.model_fields["risk_factors"]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@register_node_config("ICAnalyzer")
|
|
192
|
+
class ICAnalyzerNodeConfig(ICSetting):
|
|
193
|
+
"""Node 7: ICAnalyzerNode 配置 (R3-A: 继承 ICSetting)."""
|
|
194
|
+
model_config = _FORBID
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@register_node_config("GroupAnalyzer")
|
|
198
|
+
class GroupAnalyzerNodeConfig(GroupSetting):
|
|
199
|
+
"""Node 8: GroupAnalyzerNode 配置 (R3-A: 继承 GroupSetting)."""
|
|
200
|
+
model_config = _FORBID
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@register_node_config("LongShort")
|
|
204
|
+
class LongShortNodeConfig(LongShortSetting):
|
|
205
|
+
"""Node 9: LongShortNode 配置 (R3-A: 继承 LongShortSetting)."""
|
|
206
|
+
model_config = _FORBID
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@register_node_config("FactorScore")
|
|
210
|
+
class ScoreNodeConfig(ScoreSetting):
|
|
211
|
+
"""Node 10: FactorScoreNode 配置 (R3-A: 继承 ScoreSetting).
|
|
212
|
+
|
|
213
|
+
T0-2: 4 字段 (enabled/n_industries=29/n_size_groups=3/n_quantile_groups=5)
|
|
214
|
+
全部继承自 ScoreSetting.
|
|
215
|
+
"""
|
|
216
|
+
model_config = _FORBID
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@register_node_config("RiskCorrelation")
|
|
220
|
+
class RiskCorrelationNodeConfig(RiskCorrelationSetting):
|
|
221
|
+
"""Node 11: RiskCorrelationNode 配置 (R3-A: 继承 RiskCorrelationSetting)."""
|
|
222
|
+
model_config = _FORBID
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@register_node_config("FactorTestReport")
|
|
226
|
+
class ReportNodeConfig(_NodeBase):
|
|
227
|
+
"""Node 12: FactorTestReportNode 配置
|
|
228
|
+
|
|
229
|
+
P-1: dir 路径优先级 env QUANTNODES_OUTPUT_DIR > expanduser > default='./output/'.
|
|
230
|
+
|
|
231
|
+
字段语义独立 (OutputSetting.dir vs ReportNodeConfig.dir 同名但生命周期不同),
|
|
232
|
+
保留独立定义.
|
|
233
|
+
"""
|
|
234
|
+
dir: str = Field(
|
|
235
|
+
default="./output/", description="输出目录 (P-1: env > expanduser > default)",
|
|
236
|
+
)
|
|
237
|
+
format: list = Field(
|
|
238
|
+
default_factory=lambda: ["parquet", "json"],
|
|
239
|
+
description="输出格式",
|
|
240
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Node 6: 因子中性化 / Factor Neutralize Node
|
|
3
|
+
|
|
4
|
+
Migrated from factor_utils.py:534-625 neutralize()
|
|
5
|
+
|
|
6
|
+
Phase 2.1 (Chain of Responsibility):
|
|
7
|
+
- 原 70 行 _neutralize 三个 if/elif 分支 (industry only / risk only / both)
|
|
8
|
+
替换为 chain dispatch.
|
|
9
|
+
- 中性化逻辑 (设计矩阵 X 组装) 抽到 nodes/neutralizers.py:
|
|
10
|
+
Neutralizer (ABC) / IndustryNeutralizer / RiskNeutralizer
|
|
11
|
+
build_neutralizer_chain() / apply_neutralizer_chain()
|
|
12
|
+
- 新增中性化类型 (如 StyleNeutralizer) 只需新增一个 Neutralizer 子类,
|
|
13
|
+
_execute 无需修改.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
|
|
21
|
+
from QuantNodes.research.factor_test.nodes.configs import NeutralizeNodeConfig
|
|
22
|
+
from QuantNodes.research.factor_test.nodes.neutralizers import (
|
|
23
|
+
apply_neutralizer_chain,
|
|
24
|
+
build_neutralizer_chain,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FactorNeutralizeNode(PydanticConfigNode):
|
|
31
|
+
"""行业/风险因子中性化 (OLS 残差)
|
|
32
|
+
|
|
33
|
+
输入: factor_std, industry, risk_factors
|
|
34
|
+
输出: factor_neutral
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
ConfigSchema = NeutralizeNodeConfig
|
|
38
|
+
_ALIASES = {
|
|
39
|
+
"_if_industry": "industry_neutral",
|
|
40
|
+
"_if_risk": "risk_neutral",
|
|
41
|
+
"_risk_factor_specs": "risk_factors",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def _execute(self, input_data=None, **kwargs) -> pd.DataFrame:
|
|
45
|
+
context = kwargs.get('context', {})
|
|
46
|
+
factor_std = context.get('FactorPreprocess')
|
|
47
|
+
if factor_std is None:
|
|
48
|
+
raise ValueError("因子预处理数据缺失")
|
|
49
|
+
|
|
50
|
+
if not self._if_industry and not self._if_risk:
|
|
51
|
+
return factor_std
|
|
52
|
+
|
|
53
|
+
industry = self._ctx_load(context, 'id_citic1')
|
|
54
|
+
if industry is None and self._if_industry:
|
|
55
|
+
raise ValueError("行业数据缺失")
|
|
56
|
+
|
|
57
|
+
# 加载风险因子
|
|
58
|
+
risk_data = []
|
|
59
|
+
if self._if_risk and self._risk_factor_specs:
|
|
60
|
+
loader = self._ctx_load(context, '_loader')
|
|
61
|
+
for file_key, factor_key in self._risk_factor_specs:
|
|
62
|
+
try:
|
|
63
|
+
if file_key == 'risk_factor.h5':
|
|
64
|
+
rf = loader.load_h5(file_key, factor_key)
|
|
65
|
+
else:
|
|
66
|
+
rf = loader.load_custom((file_key, factor_key))
|
|
67
|
+
if loader.valid_shape(rf):
|
|
68
|
+
rf = loader.add_index(rf)
|
|
69
|
+
risk_data.append(rf)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.warning(f"加载风险因子 {factor_key} 失败: {e}")
|
|
72
|
+
|
|
73
|
+
return self._neutralize(factor_std, self._if_industry, industry,
|
|
74
|
+
self._if_risk, risk_data)
|
|
75
|
+
|
|
76
|
+
def _neutralize(self, factor_i, if_industry, industry, if_risk, risk_data):
|
|
77
|
+
"""中性化处理 (Phase 2.1: 委托给 chain).
|
|
78
|
+
|
|
79
|
+
Phase 2.1 行为完全等价于旧实现:
|
|
80
|
+
- chain 为空 → 返回 factor_i (全 nan, 与原 _neutralize 入口一致)
|
|
81
|
+
- 4 种 flag 组合的输出与原 branch 1/2/3 bitwise 一致
|
|
82
|
+
"""
|
|
83
|
+
chain = build_neutralizer_chain(if_industry, if_risk, industry, risk_data)
|
|
84
|
+
if not chain:
|
|
85
|
+
# 无 neutralizer 时返回原 factor (保留 nan 模式), 与旧 line 40 一致
|
|
86
|
+
return factor_i
|
|
87
|
+
return apply_neutralizer_chain(factor_i, chain)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Node 5: 因子预处理 / Factor Preprocess Node
|
|
3
|
+
|
|
4
|
+
Migrated from factor_utils.py:310-532 preprocess_onePeriod + preprocess_factor
|
|
5
|
+
Phase 1: 原样迁移保持行为一致性
|
|
6
|
+
Phase 2: 逐步替换为 QuantNodes section_ops 算子
|
|
7
|
+
Phase 3 (H5, 2026-06-20): vectorised _preprocess_one_period's per-date
|
|
8
|
+
Python loop into DataFrame-level operations (groupby/transform/clip/sub).
|
|
9
|
+
Expected 10-100x speedup on the preprocess step.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from scipy.stats import norm as scipy_norm
|
|
15
|
+
|
|
16
|
+
from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
|
|
17
|
+
from QuantNodes.research.factor_test.nodes.configs import PreprocessNodeConfig
|
|
18
|
+
from QuantNodes.research.factor_test.nodes.preprocess_strategies import (
|
|
19
|
+
build_preprocess_strategies,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FactorPreprocessNode(PydanticConfigNode):
|
|
24
|
+
"""因子预处理: 缺失值填充 + 去极值 + 标准化
|
|
25
|
+
|
|
26
|
+
输入: factor, tradable, adj_dates, industry
|
|
27
|
+
输出: factor_std (预处理后的因子, 仅调仓日)
|
|
28
|
+
|
|
29
|
+
Phase 2.2 (Strategy pattern): 3 类预处理 (missing fill / de-extreme /
|
|
30
|
+
normalise) 抽到 preprocess_strategies.py, _preprocess_vectorized 退化为
|
|
31
|
+
简单的 3 行 dispatch. 新增策略类型 (如 winsorize) 只需新增一个 Strategy
|
|
32
|
+
子类, 本文件无需修改.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
ConfigSchema = PreprocessNodeConfig
|
|
36
|
+
_ALIASES = {
|
|
37
|
+
"_missing": "missing",
|
|
38
|
+
"_extreme": "extreme",
|
|
39
|
+
"_norm": "norm",
|
|
40
|
+
"_mad_n": "mad_n",
|
|
41
|
+
"_pct_low": "pct_low",
|
|
42
|
+
"_pct_high": "pct_high",
|
|
43
|
+
"_i18n_name_map": "i18n_name_map",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def _execute(self, input_data=None, **kwargs) -> pd.DataFrame:
|
|
47
|
+
context = kwargs.get('context', {})
|
|
48
|
+
factor = self._ctx_load(context, 'factor')
|
|
49
|
+
tradable = context.get('TradabilityFilter')
|
|
50
|
+
adj_dates = context.get('AdjustDate')
|
|
51
|
+
|
|
52
|
+
if factor is None:
|
|
53
|
+
raise ValueError("因子数据缺失")
|
|
54
|
+
if tradable is None:
|
|
55
|
+
tradable = pd.DataFrame(np.ones_like(factor.values))
|
|
56
|
+
|
|
57
|
+
# 合并可交易性
|
|
58
|
+
tradable_factor = factor * tradable
|
|
59
|
+
|
|
60
|
+
# 提取调仓日的因子值
|
|
61
|
+
adj_date_values = (
|
|
62
|
+
adj_dates.iloc[:, 0].values
|
|
63
|
+
if isinstance(adj_dates, pd.DataFrame)
|
|
64
|
+
else adj_dates
|
|
65
|
+
)
|
|
66
|
+
tradable_factor_adj = tradable_factor.loc[tradable_factor.index.isin(adj_date_values)]
|
|
67
|
+
tradable_adj = tradable.loc[tradable.index.isin(adj_date_values)]
|
|
68
|
+
|
|
69
|
+
# 行业数据
|
|
70
|
+
industry = self._ctx_load(context, 'id_citic1')
|
|
71
|
+
industry_adj = (
|
|
72
|
+
industry.loc[industry.index.isin(adj_date_values)]
|
|
73
|
+
if industry is not None else None
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Vectorised preprocessing (H5, 2026-06-20).
|
|
77
|
+
result = self._preprocess_vectorized(
|
|
78
|
+
tradable_factor_adj, tradable_adj, industry_adj,
|
|
79
|
+
missing=self._missing, extreme=self._extreme, norm=self._norm,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 清理索引
|
|
83
|
+
if hasattr(result.index, 'get_level_values'):
|
|
84
|
+
result.index = result.index.get_level_values(0).values
|
|
85
|
+
if hasattr(result.columns, 'get_level_values'):
|
|
86
|
+
result.columns = result.columns.get_level_values(0).values
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# Vectorised preprocessing (H5, 2026-06-20, refactored Phase 2.2)
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
def _preprocess_vectorized(
|
|
94
|
+
self,
|
|
95
|
+
tradable_factor: pd.DataFrame,
|
|
96
|
+
tradable: pd.DataFrame,
|
|
97
|
+
industry: pd.DataFrame | None,
|
|
98
|
+
*,
|
|
99
|
+
missing: str,
|
|
100
|
+
extreme: str,
|
|
101
|
+
norm: str,
|
|
102
|
+
) -> pd.DataFrame:
|
|
103
|
+
"""Apply missing-fill + de-extreme + normalise across all dates.
|
|
104
|
+
|
|
105
|
+
Replaces the previous ``apply(_preprocess_one_period, axis=1)`` loop.
|
|
106
|
+
Each step is a vectorised DataFrame operation, dispatched via
|
|
107
|
+
strategy pattern (Phase 2.2).
|
|
108
|
+
|
|
109
|
+
1. Tradable mask (per-row): factor where tradable != 0 & notnan,
|
|
110
|
+
else NaN.
|
|
111
|
+
2. Missing fill ('ind_avg'): per (date, industry) group mean.
|
|
112
|
+
3. De-extreme ('median'): per-date median +/- n * MAD.
|
|
113
|
+
4. De-extreme ('pct_shrink'): per-date quantiles.
|
|
114
|
+
5. Norm ('zscore'): per-date (x - mean) / std.
|
|
115
|
+
6. Norm ('norm'): per-date rank -> scipy.stats.norm.ppf.
|
|
116
|
+
|
|
117
|
+
Behavioural parity: produces same NaN pattern and same numeric
|
|
118
|
+
result as the per-date loop, to within floating-point noise.
|
|
119
|
+
|
|
120
|
+
Phase 2.2: 3 个 strategy 通过 build_preprocess_strategies() 构造,
|
|
121
|
+
apply() 顺序串接. 原硬编码 if 链消除.
|
|
122
|
+
"""
|
|
123
|
+
result = tradable_factor.copy()
|
|
124
|
+
|
|
125
|
+
# 1. Tradable mask (vectorised)
|
|
126
|
+
if tradable is not None:
|
|
127
|
+
tradable_mask = tradable.reindex(index=result.index, columns=result.columns)
|
|
128
|
+
tradable_mask = tradable_mask.notna() & (tradable_mask != 0)
|
|
129
|
+
result = result.where(tradable_mask, np.nan)
|
|
130
|
+
|
|
131
|
+
# Phase 2.2: dispatch via 3 strategies
|
|
132
|
+
missing_s, extreme_s, norm_s = build_preprocess_strategies(missing, extreme, norm)
|
|
133
|
+
result = missing_s.apply(result, industry=industry)
|
|
134
|
+
result = extreme_s.apply(
|
|
135
|
+
result, mad_n=self._mad_n,
|
|
136
|
+
pct_low=self._pct_low, pct_high=self._pct_high,
|
|
137
|
+
)
|
|
138
|
+
result = norm_s.apply(result)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Legacy per-date loop (kept for reference / fallback)
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
def _preprocess_one_period(self, factor_i, tradable_adj, industry_adj, method):
|
|
146
|
+
"""对单日因子值进行预处理 (legacy per-date loop, kept for reference).
|
|
147
|
+
|
|
148
|
+
H5 (2026-06-20): _execute now uses _preprocess_vectorized instead.
|
|
149
|
+
This method is preserved for unit-testing the legacy semantics but
|
|
150
|
+
is no longer called from the pipeline.
|
|
151
|
+
"""
|
|
152
|
+
factor_i_new_all = factor_i.copy()
|
|
153
|
+
|
|
154
|
+
# 获取当日可交易和行业数据
|
|
155
|
+
date_key = factor_i.name[0] if hasattr(factor_i.name, '__len__') else factor_i.name
|
|
156
|
+
|
|
157
|
+
if tradable_adj is not None and date_key in tradable_adj.index:
|
|
158
|
+
tradable_i = tradable_adj.loc[date_key]
|
|
159
|
+
else:
|
|
160
|
+
return factor_i_new_all
|
|
161
|
+
|
|
162
|
+
if industry_adj is not None and date_key in industry_adj.index:
|
|
163
|
+
ind_i = industry_adj.loc[date_key]
|
|
164
|
+
else:
|
|
165
|
+
ind_i = pd.Series(np.nan, index=factor_i.index)
|
|
166
|
+
|
|
167
|
+
# 合并数据
|
|
168
|
+
df = pd.DataFrame({
|
|
169
|
+
'factor': factor_i,
|
|
170
|
+
'ind': ind_i,
|
|
171
|
+
'tradable': tradable_i,
|
|
172
|
+
}, index=factor_i.index)
|
|
173
|
+
|
|
174
|
+
# 剔除不可交易
|
|
175
|
+
df = df.loc[df['tradable'].notna() & (df['tradable'] != 0)]
|
|
176
|
+
if df.empty:
|
|
177
|
+
return factor_i_new_all
|
|
178
|
+
|
|
179
|
+
df['ind'] = df['ind'].replace(np.nan, 0)
|
|
180
|
+
df['factor_filled'] = df['factor'].copy()
|
|
181
|
+
|
|
182
|
+
# 1. 缺失值处理
|
|
183
|
+
if method['missing'] == 'ind_avg':
|
|
184
|
+
has_ind = df['ind'] > 0
|
|
185
|
+
if has_ind.any():
|
|
186
|
+
df.loc[has_ind, 'factor_filled'] = (
|
|
187
|
+
df.loc[has_ind].groupby('ind')['factor']
|
|
188
|
+
.transform(lambda x: x.fillna(x.mean()))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# 2. 去极值
|
|
192
|
+
if method['extreme'] == 'median':
|
|
193
|
+
n = self._mad_n # M5: 可调
|
|
194
|
+
d_m = df['factor_filled'].dropna().median()
|
|
195
|
+
d_mad = (df['factor_filled'] - d_m).abs().dropna().median()
|
|
196
|
+
df['factor_filled'] = df['factor_filled'].clip(d_m - n * d_mad, d_m + n * d_mad)
|
|
197
|
+
elif method['extreme'] == 'pct_shrink':
|
|
198
|
+
q1 = df['factor_filled'].quantile(self._pct_low) # M5: 可调
|
|
199
|
+
q2 = df['factor_filled'].quantile(self._pct_high) # M5: 可调
|
|
200
|
+
df['factor_filled'] = df['factor_filled'].clip(q1, q2)
|
|
201
|
+
|
|
202
|
+
# 3. 标准化
|
|
203
|
+
if method['norm'] == 'zscore':
|
|
204
|
+
f_mean = df['factor_filled'].dropna().mean()
|
|
205
|
+
f_std = df['factor_filled'].dropna().std(ddof=1)
|
|
206
|
+
if f_std > 0:
|
|
207
|
+
df['factor_filled'] = (df['factor_filled'] - f_mean) / f_std
|
|
208
|
+
elif method['norm'] == 'norm':
|
|
209
|
+
valid = df['factor_filled'].notna()
|
|
210
|
+
if valid.sum() > 1:
|
|
211
|
+
df.loc[valid, 'rank'] = df.loc[valid, 'factor_filled'].rank(pct=True)
|
|
212
|
+
# 处理 0 和 1 的边界
|
|
213
|
+
ranks = df['rank'].dropna()
|
|
214
|
+
df['rank'] = df['rank'].clip(
|
|
215
|
+
lower=ranks[ranks > 0].min() * 0.5 if (ranks > 0).any() else 0.01,
|
|
216
|
+
upper=(ranks[ranks < 1].max() + 1) * 0.5 if (ranks < 1).any() else 0.99
|
|
217
|
+
)
|
|
218
|
+
df['factor_filled'] = scipy_norm.ppf(df['rank'], 0, 1)
|
|
219
|
+
|
|
220
|
+
# 写回
|
|
221
|
+
factor_i_new_all.loc[df.index] = df['factor_filled'].values
|
|
222
|
+
return factor_i_new_all
|