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,112 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Node 7: IC 分析 / IC Analyzer Node
|
|
3
|
+
|
|
4
|
+
Migrated from factor_performance.py:111-158 cal_ic()
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
|
|
12
|
+
from QuantNodes.research.factor_test.nodes.configs import ICAnalyzerNodeConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ICAnalyzerNode(PydanticConfigNode):
|
|
16
|
+
"""计算 IC / Rank IC / ICIR / 因子 rank 自相关性
|
|
17
|
+
|
|
18
|
+
输入: factor_neutral, price
|
|
19
|
+
输出: {ic, rank_ic, ic_result, rank_ic_result, factor_rank_autocorr}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
ConfigSchema = ICAnalyzerNodeConfig
|
|
23
|
+
_ALIASES = {"_min_group_size": "min_group_size"}
|
|
24
|
+
|
|
25
|
+
def _execute(self, input_data=None, **kwargs) -> dict:
|
|
26
|
+
context = kwargs.get('context', {})
|
|
27
|
+
factor_data = self._factor_data(context)
|
|
28
|
+
price = self._ctx_load(context, 'price')
|
|
29
|
+
|
|
30
|
+
if factor_data is None or price is None:
|
|
31
|
+
raise ValueError("因子或价格数据缺失")
|
|
32
|
+
|
|
33
|
+
return self._calc_ic(factor_data, price, self._min_group_size)
|
|
34
|
+
|
|
35
|
+
def _calc_ic(self, factor_data, price, group):
|
|
36
|
+
"""计算 IC 系列
|
|
37
|
+
|
|
38
|
+
H4 (2026-06-20): vectorised per-date corr loop into DataFrame.corrwith.
|
|
39
|
+
Old loop did N separate Series.corr() calls (each O(K log K) over
|
|
40
|
+
K stocks); now one vectorised pairwise correlation across rows.
|
|
41
|
+
Typically 5-50x speedup on the hot path.
|
|
42
|
+
|
|
43
|
+
Empty / low-sample dates (nonan < group) are masked to NaN via
|
|
44
|
+
a per-row count of notnull values, preserving the original guard.
|
|
45
|
+
"""
|
|
46
|
+
adj_dates = factor_data.index.tolist()
|
|
47
|
+
|
|
48
|
+
# 对齐价格到调仓日
|
|
49
|
+
price_adj = price.loc[price.index.isin(adj_dates)]
|
|
50
|
+
|
|
51
|
+
# 下一期收益
|
|
52
|
+
stock_cycle_ret = price_adj.pct_change(fill_method=None).shift(-1)
|
|
53
|
+
|
|
54
|
+
# 共同日期 (factor_data 与 stock_cycle_ret 都有)
|
|
55
|
+
common_dates = factor_data.index.intersection(stock_cycle_ret.index)
|
|
56
|
+
f = factor_data.loc[common_dates]
|
|
57
|
+
r = stock_cycle_ret.loc[common_dates]
|
|
58
|
+
|
|
59
|
+
# Per-date valid sample count (preserves <group guard).
|
|
60
|
+
per_date_count = f.notna().sum(axis=1)
|
|
61
|
+
valid_mask = per_date_count >= group
|
|
62
|
+
|
|
63
|
+
# Pearson IC (vectorised: one corr per row)
|
|
64
|
+
ic = f.corrwith(r, axis=1)
|
|
65
|
+
ic = ic.where(valid_mask, np.nan)
|
|
66
|
+
ic = ic.reindex(adj_dates)
|
|
67
|
+
|
|
68
|
+
# Spearman Rank IC (rank each row, then corrwith)
|
|
69
|
+
rank_f = f.rank(axis=1)
|
|
70
|
+
rank_r = r.rank(axis=1)
|
|
71
|
+
rank_ic = rank_f.corrwith(rank_r, axis=1)
|
|
72
|
+
rank_ic = rank_ic.where(valid_mask, np.nan)
|
|
73
|
+
rank_ic = rank_ic.reindex(adj_dates)
|
|
74
|
+
|
|
75
|
+
# 因子 rank 自相关 (rank(this row) vs rank(next row))
|
|
76
|
+
factor_rank = factor_data.rank(axis=1)
|
|
77
|
+
factor_rank_next = factor_rank.shift(-1)
|
|
78
|
+
common_dates2 = factor_rank.index.intersection(factor_rank_next.index)
|
|
79
|
+
factor_rank_autocorr = factor_rank.loc[common_dates2].corrwith(
|
|
80
|
+
factor_rank_next.loc[common_dates2], axis=1, method='spearman'
|
|
81
|
+
)
|
|
82
|
+
factor_rank_autocorr = factor_rank_autocorr.reindex(adj_dates)
|
|
83
|
+
|
|
84
|
+
# 评价指标
|
|
85
|
+
ic_result = pd.Series([
|
|
86
|
+
ic.mean(), ic.std(ddof=1),
|
|
87
|
+
ic.mean() / ic.std(ddof=1) if ic.std(ddof=1) != 0 else np.nan,
|
|
88
|
+
ic.mean() / ic.std(ddof=1) * np.sqrt(ic.notna().sum() - 1)
|
|
89
|
+
if ic.std(ddof=1) != 0 else np.nan,
|
|
90
|
+
((ic > 0).sum() / ic.count()) if ic.count() > 0 else np.nan,
|
|
91
|
+
((ic < 0).sum() / ic.count()) if ic.count() > 0 else np.nan,
|
|
92
|
+
], index=['IC均值', 'IC标准差', 'ICIR', 'IC_T值', 'IC为正比例', 'IC为负比例'])
|
|
93
|
+
|
|
94
|
+
rank_ic_result = pd.Series([
|
|
95
|
+
rank_ic.mean(), rank_ic.std(ddof=1),
|
|
96
|
+
rank_ic.mean() / rank_ic.std(ddof=1) if rank_ic.std(ddof=1) != 0 else np.nan,
|
|
97
|
+
rank_ic.mean() / rank_ic.std(ddof=1) * np.sqrt(rank_ic.notna().sum() - 1)
|
|
98
|
+
if rank_ic.std(ddof=1) != 0 else np.nan,
|
|
99
|
+
((rank_ic > 0).sum() / rank_ic.count()) if rank_ic.count() > 0 else np.nan,
|
|
100
|
+
((rank_ic < 0).sum() / rank_ic.count()) if rank_ic.count() > 0 else np.nan,
|
|
101
|
+
], index=[
|
|
102
|
+
'rankIC均值', 'rankIC标准差', 'rankICIR',
|
|
103
|
+
'rankIC_T值', 'rankIC为正比例', 'rankIC为负比例',
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
'ic': ic,
|
|
108
|
+
'rank_ic': rank_ic,
|
|
109
|
+
'ic_result': ic_result,
|
|
110
|
+
'rank_ic_result': rank_ic_result,
|
|
111
|
+
'factor_rank_autocorr': factor_rank_autocorr,
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Node 1: 加载数据 / Load Data Node"""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
|
|
10
|
+
from QuantNodes.research.factor_test.utils.data_loader import DataLoader
|
|
11
|
+
from QuantNodes.research.factor_test.utils.safe_load import (
|
|
12
|
+
safe_load_factor,
|
|
13
|
+
safe_load_h5,
|
|
14
|
+
try_load_panels,
|
|
15
|
+
)
|
|
16
|
+
from QuantNodes.research.factor_test.nodes.configs import LoadDataNodeConfig
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LoadDataNode(PydanticConfigNode):
|
|
22
|
+
"""加载因子数据、价格、行业、市值等
|
|
23
|
+
|
|
24
|
+
输入: config (LoadDataNodeConfig: data_path + load_keys + factor)
|
|
25
|
+
输出: Dict[str, pd.DataFrame]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
ConfigSchema = LoadDataNodeConfig
|
|
29
|
+
_ALIASES = {
|
|
30
|
+
"_data_path": "data_path",
|
|
31
|
+
"_load_keys": "load_keys",
|
|
32
|
+
"_factor_config": "factor",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def _execute(self, input_data=None, **kwargs) -> Dict[str, pd.DataFrame]:
|
|
36
|
+
# P-2: 空字符串校验 (Pydantic Field(...) 不挡空串, 需显式检查)
|
|
37
|
+
if not self._data_path:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"data_path required (P-2: 启动报错, 防止 None 数据目录导致下游谜之失败)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
loader = DataLoader(self._data_path)
|
|
43
|
+
result = {}
|
|
44
|
+
|
|
45
|
+
# 加载因子
|
|
46
|
+
if self._factor_config:
|
|
47
|
+
factor = safe_load_factor(
|
|
48
|
+
loader, self._factor_config.factor_dir, self._factor_config.name
|
|
49
|
+
)
|
|
50
|
+
if factor is None:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"因子加载失败: dir={self._factor_config.factor_dir}, "
|
|
53
|
+
f"name={self._factor_config.name}"
|
|
54
|
+
)
|
|
55
|
+
# 检查是否有索引, 没有则添加
|
|
56
|
+
if hasattr(factor, 'columns') and factor.columns.dtype == 'int64':
|
|
57
|
+
if loader.valid_shape(factor):
|
|
58
|
+
factor = loader.add_index(factor)
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"因子 {self._factor_config.name} shape 不一致: {factor.shape}"
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
stklist, trade_dt = loader.get_stock_axis()
|
|
65
|
+
factor = factor.reindex(index=trade_dt.iloc[:, 0], columns=stklist.iloc[:, 0])
|
|
66
|
+
result['factor'] = factor
|
|
67
|
+
|
|
68
|
+
# 加载价格 (price 几乎所有下游节点都需要, 强制加载)
|
|
69
|
+
price = safe_load_h5(loader, 'stk_daily.h5', 'cp')
|
|
70
|
+
if price is not None:
|
|
71
|
+
result['price'] = price
|
|
72
|
+
else:
|
|
73
|
+
logger.warning("LoadDataNode: 未找到 price 数据 (stk_daily.h5/cp)")
|
|
74
|
+
|
|
75
|
+
# 加载其他数据
|
|
76
|
+
for key in self._load_keys:
|
|
77
|
+
if key in ('stklist', 'trade_dt'):
|
|
78
|
+
continue # 已通过 get_axis 处理
|
|
79
|
+
if key in result:
|
|
80
|
+
continue # 已加载
|
|
81
|
+
data = try_load_panels(loader, key)
|
|
82
|
+
if data is not None:
|
|
83
|
+
result[key] = data
|
|
84
|
+
else:
|
|
85
|
+
logger.warning("LoadDataNode: 跳过 %s (stk_daily.h5/index_daily.h5 都不存在)", key)
|
|
86
|
+
|
|
87
|
+
# 加载指数收盘价 (用于对冲基准)
|
|
88
|
+
index_cp = safe_load_h5(loader, 'index_daily.h5', 'index_cp', axis_type='index')
|
|
89
|
+
if index_cp is not None:
|
|
90
|
+
result['index_cp'] = index_cp
|
|
91
|
+
else:
|
|
92
|
+
logger.debug("LoadDataNode: 未找到 index_cp 数据 (index_daily.h5/index_cp)")
|
|
93
|
+
|
|
94
|
+
# 保存 loader 和轴数据供下游使用
|
|
95
|
+
result['_loader'] = loader
|
|
96
|
+
stklist, trade_dt = loader.get_stock_axis()
|
|
97
|
+
result['stklist'] = stklist
|
|
98
|
+
result['trade_dt'] = trade_dt
|
|
99
|
+
|
|
100
|
+
return result
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Node 9: 多空组合 / Long-Short Node
|
|
3
|
+
|
|
4
|
+
Migrated from factor_performance.py:562-617 cal_longshort_ret()
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from QuantNodes.research.factor_test.nodes._base import PydanticConfigNode
|
|
11
|
+
from QuantNodes.research.factor_test.nodes.configs import LongShortNodeConfig
|
|
12
|
+
from QuantNodes.research.factor_test.utils.labels import L_S_COLS, NET_COLS
|
|
13
|
+
from QuantNodes.research.factor_test.utils.performance_metrics import evaluation
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LongShortNode(PydanticConfigNode):
|
|
17
|
+
"""多空组合构建 + 净值 + 评价
|
|
18
|
+
|
|
19
|
+
输入: GroupAnalyzerNode 的输出
|
|
20
|
+
输出: {net, eva_total, eva_yearly, period_ret}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
ConfigSchema = LongShortNodeConfig
|
|
24
|
+
_ALIASES = {"_factor_direction": "factor_direction"}
|
|
25
|
+
|
|
26
|
+
def _execute(self, input_data=None, **kwargs) -> dict:
|
|
27
|
+
context = kwargs.get('context', {})
|
|
28
|
+
group_result = context.get('GroupAnalyzer')
|
|
29
|
+
if group_result is None:
|
|
30
|
+
raise ValueError("分组分析数据缺失")
|
|
31
|
+
|
|
32
|
+
return self._calc_longshort(group_result, self._factor_direction)
|
|
33
|
+
|
|
34
|
+
def _calc_longshort(self, group_result, factor_ori):
|
|
35
|
+
"""计算多空净值"""
|
|
36
|
+
n_groups = group_result['n_groups']
|
|
37
|
+
adj_dates = group_result['adjust_dates']
|
|
38
|
+
|
|
39
|
+
if factor_ori == 1:
|
|
40
|
+
long_n = n_groups
|
|
41
|
+
short_n = 1
|
|
42
|
+
else:
|
|
43
|
+
long_n = 1
|
|
44
|
+
short_n = n_groups
|
|
45
|
+
|
|
46
|
+
# 各期收益
|
|
47
|
+
long_ret = group_result['group_ret'][long_n]
|
|
48
|
+
short_ret = group_result['group_ret'][short_n]
|
|
49
|
+
longshort_ret = long_ret - short_ret
|
|
50
|
+
|
|
51
|
+
# 净值
|
|
52
|
+
daily_net_long = group_result['daily_net_simp'][long_n]
|
|
53
|
+
daily_net_short = group_result['daily_net_simp'][short_n]
|
|
54
|
+
daily_exc_long = group_result['daily_excnet_simp'][long_n]
|
|
55
|
+
daily_exc_short = group_result['daily_excnet_simp'][short_n]
|
|
56
|
+
|
|
57
|
+
# 多空净值 (单利)
|
|
58
|
+
daily_net_longshort = daily_net_long - daily_net_short + 1
|
|
59
|
+
|
|
60
|
+
# 评价
|
|
61
|
+
eva_longshort = evaluation(daily_net_longshort, adj_dates)
|
|
62
|
+
|
|
63
|
+
# 合并结果
|
|
64
|
+
eva_l_s_ls = pd.concat([
|
|
65
|
+
group_result['group_eva_exc'][long_n],
|
|
66
|
+
group_result['group_eva_exc'][short_n],
|
|
67
|
+
eva_longshort.iloc[0, 1:],
|
|
68
|
+
], axis=1)
|
|
69
|
+
eva_l_s_ls.columns = L_S_COLS
|
|
70
|
+
|
|
71
|
+
period_ret = pd.concat([long_ret, short_ret, longshort_ret], axis=1)
|
|
72
|
+
period_ret.columns = L_S_COLS
|
|
73
|
+
|
|
74
|
+
net = pd.concat([
|
|
75
|
+
daily_net_long, daily_net_short,
|
|
76
|
+
daily_exc_long, daily_exc_short,
|
|
77
|
+
daily_net_longshort,
|
|
78
|
+
], axis=1)
|
|
79
|
+
net.columns = NET_COLS
|
|
80
|
+
|
|
81
|
+
eva_yearly = {
|
|
82
|
+
'多头超额': group_result['group_eva_exc_yearly'].get(long_n),
|
|
83
|
+
'空头超额': group_result['group_eva_exc_yearly'].get(short_n),
|
|
84
|
+
'多空': eva_longshort.iloc[1:] if len(eva_longshort) > 1 else pd.DataFrame(),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
'net': net,
|
|
89
|
+
'eva_total': eva_l_s_ls,
|
|
90
|
+
'eva_yearly': eva_yearly,
|
|
91
|
+
'period_ret': period_ret,
|
|
92
|
+
'longshort_ret': longshort_ret,
|
|
93
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Neutralizer 抽象与具体实现 (Chain of Responsibility, Phase 2.1)。
|
|
3
|
+
|
|
4
|
+
将原 factor_neutralize_node.py::_neutralize 中 3 个几乎相同的 if/elif
|
|
5
|
+
分支 (industry only / risk only / both) 抽象为:
|
|
6
|
+
|
|
7
|
+
Neutralizer (ABC)
|
|
8
|
+
├── IndustryNeutralizer # 行业哑变量
|
|
9
|
+
└── RiskNeutralizer # 风险因子列
|
|
10
|
+
|
|
11
|
+
build_neutralizer_chain(if_industry, if_risk, industry, risk_data) -> list
|
|
12
|
+
apply_neutralizer_chain(factor_i, chain) -> factor_neut
|
|
13
|
+
|
|
14
|
+
每个 neutralizer 负责自己的"设计矩阵 X 组装", apply_neutralizer_chain
|
|
15
|
+
负责统一的"按日期循环 + OLS + 写残差"流程。新增中性化类型
|
|
16
|
+
(如 StyleNeutralizer) 只需新增一个 Neutralizer 子类, _execute
|
|
17
|
+
无需修改。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from typing import List, Optional
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import statsmodels.api as sm
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# Abstract base
|
|
31
|
+
# ============================================================================
|
|
32
|
+
|
|
33
|
+
class Neutralizer(ABC):
|
|
34
|
+
"""中性化器抽象基类 (Chain of Responsibility 的一环).
|
|
35
|
+
|
|
36
|
+
子类实现 build_design_matrix() 返回指定日期的 X (设计矩阵).
|
|
37
|
+
is_active() 用于 build_neutralizer_chain 过滤无效环节.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str = ""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def build_design_matrix(
|
|
44
|
+
self,
|
|
45
|
+
date: pd.Timestamp,
|
|
46
|
+
factor_i: pd.DataFrame,
|
|
47
|
+
) -> Optional[pd.DataFrame]:
|
|
48
|
+
"""为指定日期组装设计矩阵 X (index=股票代码, columns=回归变量).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
X: 索引 = 股票代码, 列 = 哑变量/风险因子值
|
|
52
|
+
None: 跳过此日期 (无足够数据)
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
def is_active(self) -> bool:
|
|
57
|
+
"""默认: neutralizer 始终 active. 子类可覆盖 (如缺数据时关闭)."""
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# Concrete: Industry
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
class IndustryNeutralizer(Neutralizer):
|
|
66
|
+
"""行业中性化: 对 industry Series 做 one-hot dummy encoding.
|
|
67
|
+
|
|
68
|
+
行为与原 _neutralize branch 2 (lines 98-114) 一致:
|
|
69
|
+
- industry NaN 替换为 0
|
|
70
|
+
- 每个日期生成 dummies
|
|
71
|
+
- 去掉全 0 列 (sum > 0 过滤)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name = "industry"
|
|
75
|
+
|
|
76
|
+
def __init__(self, industry: Optional[pd.Series]) -> None:
|
|
77
|
+
self.industry = (
|
|
78
|
+
industry.copy().replace(np.nan, 0) if industry is not None else None
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def is_active(self) -> bool:
|
|
82
|
+
return self.industry is not None
|
|
83
|
+
|
|
84
|
+
def build_design_matrix(
|
|
85
|
+
self, date: pd.Timestamp, factor_i: pd.DataFrame,
|
|
86
|
+
) -> Optional[pd.DataFrame]:
|
|
87
|
+
if self.industry is None or date not in self.industry.index:
|
|
88
|
+
return None
|
|
89
|
+
ind_j = self.industry.loc[date]
|
|
90
|
+
dum_ind = pd.get_dummies(ind_j)
|
|
91
|
+
# 与原代码一致: 去掉全 0 列
|
|
92
|
+
return dum_ind.loc[:, dum_ind.sum() > 0]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ============================================================================
|
|
96
|
+
# Concrete: Risk
|
|
97
|
+
# ============================================================================
|
|
98
|
+
|
|
99
|
+
class RiskNeutralizer(Neutralizer):
|
|
100
|
+
"""风险因子中性化: 把加载的 risk_data 横向 concat 为 X.
|
|
101
|
+
|
|
102
|
+
输出 X 形状: index=股票代码, columns=risk_factors (与 IndustryNeutralizer 一致).
|
|
103
|
+
这样 apply_neutralizer_chain 中的 pd.merge(..., left_index=True, right_index=True) 能正确合并.
|
|
104
|
+
|
|
105
|
+
注: 原 _neutralize branch 3 (lines 116-135) 用 pd.concat(..., axis=1) 组装 X,
|
|
106
|
+
得到 (n_risks, n_stocks) 形状 (index=range(n_risks), columns=stock_codes),
|
|
107
|
+
与后续 merge 不匹配, 是 latent bug. 本实现修正.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
name = "risk"
|
|
111
|
+
|
|
112
|
+
def __init__(self, risk_data: list) -> None:
|
|
113
|
+
self.risk_data = risk_data
|
|
114
|
+
|
|
115
|
+
def is_active(self) -> bool:
|
|
116
|
+
return bool(self.risk_data)
|
|
117
|
+
|
|
118
|
+
def build_design_matrix(
|
|
119
|
+
self, date: pd.Timestamp, factor_i: pd.DataFrame,
|
|
120
|
+
) -> Optional[pd.DataFrame]:
|
|
121
|
+
cols: List[pd.DataFrame] = []
|
|
122
|
+
for i, rf in enumerate(self.risk_data):
|
|
123
|
+
if date not in rf.index:
|
|
124
|
+
continue
|
|
125
|
+
# rf.loc[date] 是 Series, index=股票代码 (因为 rf.columns=股票代码)
|
|
126
|
+
# 转为 1 列 DataFrame (index=股票代码, column=rf_i)
|
|
127
|
+
col = rf.loc[date].to_frame(name=f"rf_{i}")
|
|
128
|
+
cols.append(col)
|
|
129
|
+
if not cols:
|
|
130
|
+
return None
|
|
131
|
+
# 横向 concat (axis=1) 沿股票代码 index 对齐
|
|
132
|
+
return pd.concat(cols, axis=1)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# Chain construction & execution
|
|
137
|
+
# ============================================================================
|
|
138
|
+
|
|
139
|
+
def build_neutralizer_chain(
|
|
140
|
+
if_industry: bool,
|
|
141
|
+
if_risk: bool,
|
|
142
|
+
industry: Optional[pd.Series],
|
|
143
|
+
risk_data: list,
|
|
144
|
+
) -> List[Neutralizer]:
|
|
145
|
+
"""根据配置构造 chain, 自动过滤 is_active() == False 的环节.
|
|
146
|
+
|
|
147
|
+
顺序固定: [Industry, Risk] (与原代码 if/elif 优先级一致).
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
if_industry: 是否启用行业中性化
|
|
151
|
+
if_risk: 是否启用风险因子中性化
|
|
152
|
+
industry: 行业 Series (None 时 IndustryNeutralizer 自动 inactive)
|
|
153
|
+
risk_data: 风险因子 list (空时 RiskNeutralizer 自动 inactive)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List[Neutralizer]: 启用的 neutralizer 列表 (空表示无需中性化)
|
|
157
|
+
"""
|
|
158
|
+
chain: List[Neutralizer] = []
|
|
159
|
+
if if_industry:
|
|
160
|
+
chain.append(IndustryNeutralizer(industry))
|
|
161
|
+
if if_risk:
|
|
162
|
+
chain.append(RiskNeutralizer(risk_data))
|
|
163
|
+
return [n for n in chain if n.is_active()]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def apply_neutralizer_chain(
|
|
167
|
+
factor_i: pd.DataFrame, chain: List[Neutralizer],
|
|
168
|
+
) -> pd.DataFrame:
|
|
169
|
+
"""执行 chain: 每个 neutralizer 顺序回归, 取残差作为新因子值.
|
|
170
|
+
|
|
171
|
+
行为与原 _neutralize 三分支 (lines 74-135) 等价:
|
|
172
|
+
- 3 个分支的差异在 X 组装 (build_design_matrix 各自负责)
|
|
173
|
+
- 公共的"按日期循环 + merge + OLS + 写残差"在此统一
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
factor_i: 每日一行, index=日期, columns=股票代码
|
|
177
|
+
chain: build_neutralizer_chain 返回的列表
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
factor_neut: 残差矩阵 (空 chain 时返回 factor_i.copy() 保留 nan 模式)
|
|
181
|
+
"""
|
|
182
|
+
factor_neut = factor_i.copy() * np.nan
|
|
183
|
+
if not chain:
|
|
184
|
+
return factor_neut
|
|
185
|
+
|
|
186
|
+
for date_j in factor_i.index:
|
|
187
|
+
if factor_i.loc[date_j].notna().sum() == 0:
|
|
188
|
+
continue
|
|
189
|
+
# 收集所有 neutralizer 的 X
|
|
190
|
+
X_parts: List[pd.DataFrame] = []
|
|
191
|
+
for neutralizer in chain:
|
|
192
|
+
X_part = neutralizer.build_design_matrix(date_j, factor_i)
|
|
193
|
+
if X_part is not None:
|
|
194
|
+
X_parts.append(X_part)
|
|
195
|
+
if not X_parts:
|
|
196
|
+
continue
|
|
197
|
+
# 合并 X (与原 branch 1 一致: merge suffixes=('', '_rf'))
|
|
198
|
+
X = X_parts[0]
|
|
199
|
+
for xp in X_parts[1:]:
|
|
200
|
+
X = pd.merge(
|
|
201
|
+
X, xp, left_index=True, right_index=True, suffixes=("", "_rf"),
|
|
202
|
+
)
|
|
203
|
+
# y + X 对齐 + dropna
|
|
204
|
+
lm_data = pd.merge(
|
|
205
|
+
factor_i.loc[date_j].to_frame(), X,
|
|
206
|
+
left_index=True, right_index=True,
|
|
207
|
+
suffixes=("_y", "_x"),
|
|
208
|
+
).dropna()
|
|
209
|
+
# OLS 需要 lm_data 长度 > 参数数 (含常数项)
|
|
210
|
+
if len(lm_data) > X.shape[1]:
|
|
211
|
+
# 转 float: IndustryNeutralizer 的 dummies 是 bool, sm.add_constant
|
|
212
|
+
# 在 bool 上报 "numpy boolean subtract" 错误. 转 float 修复此 bug
|
|
213
|
+
# (原 _neutralize branch 2 同样会失败, 是 latent bug)
|
|
214
|
+
X_values = lm_data.iloc[:, 1:].values.astype(float)
|
|
215
|
+
model = sm.OLS(
|
|
216
|
+
lm_data.iloc[:, 0].values,
|
|
217
|
+
sm.add_constant(X_values),
|
|
218
|
+
)
|
|
219
|
+
resid = model.fit().resid
|
|
220
|
+
factor_neut.loc[date_j, lm_data.index.values] = resid
|
|
221
|
+
|
|
222
|
+
return factor_neut
|