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,689 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""IFinDDatabase - iFinD API 包装为 DataLoader 兼容接口
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for DataLoader, backed by iFinD API.
|
|
5
|
+
所有 panel 数据返回 (dates × stocks) DataFrame, index=日期(int), columns=股票代码(str).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import numpy as np
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import ClassVar, Callable
|
|
14
|
+
|
|
15
|
+
from .fetcher import IFindFetcher
|
|
16
|
+
from QuantNodes.core.path_utils import ensure_dir
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Route Decorator (B5 refactor, 2026-06-19) ──────────────────
|
|
22
|
+
|
|
23
|
+
def register_route(filename: str, key: str) -> Callable:
|
|
24
|
+
"""装饰器: 把方法注册到 IFinDDatabase._ROUTE_TABLE[(filename, key)] = method 名.
|
|
25
|
+
|
|
26
|
+
使用::
|
|
27
|
+
|
|
28
|
+
@register_route("stk_daily.h5", "cp")
|
|
29
|
+
def _get_prices(self) -> pd.DataFrame:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
# 一个方法可注册到多个 (filename, key):
|
|
33
|
+
@register_route("stk_daily.h5", "trade_dt")
|
|
34
|
+
@register_route("index_daily.h5", "trade_dt")
|
|
35
|
+
def _get_trade_dt_raw(self) -> pd.DataFrame:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
装饰器给方法打 ``_routes`` (列表) 标记; 实际表填充在类定义后由
|
|
39
|
+
``_collect_routes(IFinDDatabase)`` 一次性扫描完成, 保证
|
|
40
|
+
``load_h5`` 路由查找零开销.
|
|
41
|
+
"""
|
|
42
|
+
def deco(method: Callable) -> Callable:
|
|
43
|
+
existing = getattr(method, "_routes", None) # type: ignore[attr-defined]
|
|
44
|
+
if existing is None:
|
|
45
|
+
method._routes = [(filename, key)] # type: ignore[attr-defined]
|
|
46
|
+
else:
|
|
47
|
+
method._routes = existing + [(filename, key)] # type: ignore[attr-defined]
|
|
48
|
+
return method
|
|
49
|
+
return deco
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _collect_routes(cls: type) -> dict[tuple[str, str], str]:
|
|
53
|
+
"""扫描类及所有父类, 收集 ``_routes`` 标记的方法.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
``{(filename, key): method_name}`` 字典.
|
|
57
|
+
"""
|
|
58
|
+
routes: dict[tuple[str, str], str] = {}
|
|
59
|
+
for klass in reversed(cls.__mro__):
|
|
60
|
+
for name, val in klass.__dict__.items():
|
|
61
|
+
if callable(val) and hasattr(val, "_routes"):
|
|
62
|
+
for route_key in val._routes: # type: ignore[attr-defined]
|
|
63
|
+
routes[route_key] = name
|
|
64
|
+
return routes
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Week 12: H5 兼容辅助 ─────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def _df_to_hdf_safe(df: pd.DataFrame) -> pd.DataFrame:
|
|
70
|
+
"""转 nullable/Int64 等 HDF5 不支持的 dtype 为兼容 dtype。
|
|
71
|
+
|
|
72
|
+
- Int64 → int64 (含 NaN 时会丢失 NaN, 但 HDF5 不支持 nullable int)
|
|
73
|
+
- int64 → int64
|
|
74
|
+
- 其余保持
|
|
75
|
+
"""
|
|
76
|
+
df = df.copy()
|
|
77
|
+
for col in df.columns:
|
|
78
|
+
if df[col].dtype == 'Int64':
|
|
79
|
+
# 全部 NaN → float (否则 .astype(int) 报错)
|
|
80
|
+
if df[col].isna().all():
|
|
81
|
+
df[col] = df[col].astype(float)
|
|
82
|
+
else:
|
|
83
|
+
df[col] = df[col].fillna(0).astype(int)
|
|
84
|
+
if df.index.dtype == 'Int64':
|
|
85
|
+
if df.index.isna().all():
|
|
86
|
+
df.index = df.index.astype(float)
|
|
87
|
+
else:
|
|
88
|
+
df.index = df.index.fillna(0).astype(int)
|
|
89
|
+
return df
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── 行业代码映射 ──────────────────────────────────────────────
|
|
93
|
+
# H13: 默认 30 申万一级, 可通过 constructor 的 industry_map 参数覆盖
|
|
94
|
+
_DEFAULT_INDUSTRY_MAP = {
|
|
95
|
+
'农林牧渔': 1, '基础化工': 2, '钢铁': 3, '有色金属': 4, '电子': 5,
|
|
96
|
+
'汽车': 6, '家用电器': 7, '食品饮料': 8, '纺织服饰': 9, '轻工制造': 10,
|
|
97
|
+
'医药生物': 11, '公用事业': 12, '交通运输': 13, '房地产': 14, '商贸零售': 15,
|
|
98
|
+
'社会服务': 16, '银行': 17, '非银金融': 18, '综合': 19, '建筑材料': 20,
|
|
99
|
+
'建筑装饰': 21, '电力设备': 22, '国防军工': 23, '计算机': 24, '传媒': 25,
|
|
100
|
+
'通信': 26, '煤炭': 27, '石油石化': 28, '环保': 29, '美容护理': 30,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class IFinDDatabase:
|
|
105
|
+
"""iFinD API-backed DataLoader replacement.
|
|
106
|
+
|
|
107
|
+
Usage:
|
|
108
|
+
db = IFinDDatabase(date_beg='20260101', date_end='20260630')
|
|
109
|
+
# 与 DataLoader 完全兼容
|
|
110
|
+
stklist, trade_dt = db.get_stock_axis()
|
|
111
|
+
cp = db.load_h5('stk_daily.h5', 'cp')
|
|
112
|
+
cp_labeled = db.add_index(cp)
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, api_path: str = '', date_beg: str = '',
|
|
116
|
+
date_end: str = '', universe: str = '沪深300',
|
|
117
|
+
fetcher: IFindFetcher = None,
|
|
118
|
+
industry_map: dict | None = None,
|
|
119
|
+
risk_registry: list[str] | None = None,
|
|
120
|
+
batch_size: int | None = None):
|
|
121
|
+
"""
|
|
122
|
+
Args:
|
|
123
|
+
api_path: 兼容 DataLoader, 被忽略
|
|
124
|
+
date_beg: 查询起始日期 (YYYYMMDD, 空=1 年前)
|
|
125
|
+
date_end: 查询截止日期 (空=今天)
|
|
126
|
+
universe: 股票池 ('沪深300', '中证500', 'all')
|
|
127
|
+
fetcher: 注入的 fetcher (测试用 IFindFetcherStub)
|
|
128
|
+
industry_map: H13 行业代码映射 (None=30 申万一级)
|
|
129
|
+
risk_registry: P-4 风险因子注册表 (None=10 默认 Barra 风格因子)
|
|
130
|
+
batch_size: M10 分批查询大小 (None=默认 50, 推荐 50-100 避免限流)
|
|
131
|
+
"""
|
|
132
|
+
# H9: 不再硬编码 '20260101', 默认 1 年前 (跨年/跨月可滚动)
|
|
133
|
+
if not date_beg:
|
|
134
|
+
one_year_ago = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
|
135
|
+
self._date_beg = one_year_ago
|
|
136
|
+
else:
|
|
137
|
+
self._date_beg = date_beg
|
|
138
|
+
self._date_end = date_end or datetime.now().strftime('%Y%m%d')
|
|
139
|
+
self._universe = universe
|
|
140
|
+
self._fetcher = fetcher or IFindFetcher()
|
|
141
|
+
# H13: 行业代码映射可覆盖
|
|
142
|
+
self._industry_map = industry_map if industry_map is not None else _DEFAULT_INDUSTRY_MAP
|
|
143
|
+
# P-4: 风险因子注册表可外部注入, None=10 默认 Barra 风格因子
|
|
144
|
+
self._risk_registry = list(risk_registry) if risk_registry is not None else [
|
|
145
|
+
'/beta', '/momentum', '/size', '/volatility',
|
|
146
|
+
'/value', '/quality', '/growth', '/leverage',
|
|
147
|
+
'/liquidity', '/non_linear_size',
|
|
148
|
+
]
|
|
149
|
+
# M10: 分批查询大小 (默认 50, 可自定义)
|
|
150
|
+
self._batch_size = batch_size if batch_size is not None else 50
|
|
151
|
+
|
|
152
|
+
# 缓存
|
|
153
|
+
self._stklist = None
|
|
154
|
+
self._indexlist = None
|
|
155
|
+
self._trade_dt = None
|
|
156
|
+
self._stock_prices = None
|
|
157
|
+
self._index_prices = None
|
|
158
|
+
self._stock_info_cache = {} # key -> DataFrame
|
|
159
|
+
|
|
160
|
+
# ── 路由表 (B5: 由 @register_route 装饰器收集, 类定义后一次性填充) ──
|
|
161
|
+
#
|
|
162
|
+
# 原 _ROUTE_TABLE 字面量已迁移到各 _get_* 方法上的 @register_route 装饰器,
|
|
163
|
+
# 见类下方 _collect_routes(IFinDDatabase) 调用.
|
|
164
|
+
# 旧 _ROUTE_TABLE 访问完全兼容 (dict 接口不变).
|
|
165
|
+
_ROUTE_TABLE: ClassVar[dict[tuple[str, str], str]] = {}
|
|
166
|
+
|
|
167
|
+
# ── DataLoader 兼容接口 ─────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def load_h5(self, filename: str, key: str) -> pd.DataFrame:
|
|
170
|
+
"""路由到 iFinD 查询, 模拟 H5 加载"""
|
|
171
|
+
route_key = (filename, key)
|
|
172
|
+
if route_key in self._ROUTE_TABLE:
|
|
173
|
+
method_name = self._ROUTE_TABLE[route_key]
|
|
174
|
+
return getattr(self, method_name)()
|
|
175
|
+
raise KeyError(
|
|
176
|
+
f"IFinDDatabase: 未映射的 (filename='{filename}', key='{key}'). "
|
|
177
|
+
f"可用路由: {list(self._ROUTE_TABLE.keys())}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def load_csv(self, path: str) -> pd.DataFrame:
|
|
181
|
+
return pd.read_csv(path, index_col=0)
|
|
182
|
+
|
|
183
|
+
def load_npy(self, path: str) -> pd.DataFrame:
|
|
184
|
+
return pd.DataFrame(np.load(path, allow_pickle=True))
|
|
185
|
+
|
|
186
|
+
def load_parquet(self, path: str) -> pd.DataFrame:
|
|
187
|
+
return pd.read_parquet(path)
|
|
188
|
+
|
|
189
|
+
def load_custom(self, data_dir: tuple) -> pd.DataFrame:
|
|
190
|
+
raise NotImplementedError(
|
|
191
|
+
"IFinDDatabase 不支持自定义路径加载, "
|
|
192
|
+
"请使用 load_h5 或直接通过 fetcher.query()"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def load_factor(self, factor_dir: str, factor_name: str) -> pd.DataFrame:
|
|
196
|
+
"""因子加载: 从 iFinD 获取因子数据"""
|
|
197
|
+
return self._get_factor(factor_dir, factor_name)
|
|
198
|
+
|
|
199
|
+
def get_stock_axis(self) -> tuple:
|
|
200
|
+
"""返回 (stklist, trade_dt), 格式与 DataLoader 一致"""
|
|
201
|
+
return self._get_stock_axis_raw(), self._get_trade_dt_raw()
|
|
202
|
+
|
|
203
|
+
def get_index_axis(self) -> tuple:
|
|
204
|
+
return self._get_index_axis_raw(), self._get_trade_dt_raw()
|
|
205
|
+
|
|
206
|
+
def get_axis(self, axis_type: str = 'stock') -> tuple:
|
|
207
|
+
if axis_type == 'stock':
|
|
208
|
+
return self.get_stock_axis()
|
|
209
|
+
elif axis_type == 'index':
|
|
210
|
+
return self.get_index_axis()
|
|
211
|
+
else:
|
|
212
|
+
raise ValueError(f"不支持的 axis_type: {axis_type}")
|
|
213
|
+
|
|
214
|
+
def add_index(self, factor: pd.DataFrame, axis_type: str = 'stock') -> pd.DataFrame:
|
|
215
|
+
"""给因子添加标准索引。iFinD 数据通常已带标签, 仅做验证"""
|
|
216
|
+
factor = factor.copy()
|
|
217
|
+
assetlist, trade_dt = self.get_axis(axis_type)
|
|
218
|
+
|
|
219
|
+
# 如果已有正确维度但缺少标签, 则添加
|
|
220
|
+
expected_dates = trade_dt.iloc[:, 0].values
|
|
221
|
+
expected_assets = assetlist.iloc[:, 0].values
|
|
222
|
+
|
|
223
|
+
if factor.shape == (len(expected_dates), len(expected_assets)):
|
|
224
|
+
if not factor.index.equals(pd.Index(expected_dates)):
|
|
225
|
+
factor.index = expected_dates
|
|
226
|
+
if not factor.columns.equals(pd.Index(expected_assets)):
|
|
227
|
+
factor.columns = expected_assets
|
|
228
|
+
return factor
|
|
229
|
+
|
|
230
|
+
def valid_shape(self, factor: pd.DataFrame, axis_type: str = 'stock') -> bool:
|
|
231
|
+
assetlist, trade_dt = self.get_axis(axis_type)
|
|
232
|
+
return factor.shape == (len(trade_dt), len(assetlist))
|
|
233
|
+
|
|
234
|
+
def get_apikeys(self, filename: str) -> list:
|
|
235
|
+
"""风险因子注册表 (P-4: 改为读 self._risk_registry, 可外部注入)"""
|
|
236
|
+
return list(self._risk_registry)
|
|
237
|
+
|
|
238
|
+
# ── 内部数据获取方法 ─────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def _query_stock_info(self, query: str) -> pd.DataFrame:
|
|
241
|
+
"""封装股票信息查询"""
|
|
242
|
+
return self._fetcher.query('stock', 'get_stock_info', {'query': query})
|
|
243
|
+
|
|
244
|
+
def _query_index_data(self, query: str) -> pd.DataFrame:
|
|
245
|
+
"""封装指数数据查询"""
|
|
246
|
+
return self._fetcher.query('index', 'index_data', {'query': query})
|
|
247
|
+
|
|
248
|
+
def _get_stock_codes(self) -> list[str]:
|
|
249
|
+
"""获取股票池代码列表"""
|
|
250
|
+
if self._stklist is not None:
|
|
251
|
+
return list(self._stklist.iloc[:, 0])
|
|
252
|
+
|
|
253
|
+
if self._universe == 'all':
|
|
254
|
+
query = f'A股市场所有股票代码({self._date_beg[:4]}年)'
|
|
255
|
+
else:
|
|
256
|
+
query = f'{self._universe}成分股列表'
|
|
257
|
+
|
|
258
|
+
df = self._query_index_data(query)
|
|
259
|
+
if df.empty:
|
|
260
|
+
raise RuntimeError(f"无法获取股票池: {self._universe}")
|
|
261
|
+
|
|
262
|
+
# 找到代码列
|
|
263
|
+
code_col = None
|
|
264
|
+
for col in df.columns:
|
|
265
|
+
if df[col].astype(str).str.match(r'\d{6}\.(SH|SZ)').any():
|
|
266
|
+
code_col = col
|
|
267
|
+
break
|
|
268
|
+
if code_col is None:
|
|
269
|
+
code_col = df.columns[0]
|
|
270
|
+
|
|
271
|
+
codes = df[code_col].astype(str).tolist()
|
|
272
|
+
self._stklist = pd.DataFrame(codes)
|
|
273
|
+
return codes
|
|
274
|
+
|
|
275
|
+
def _get_trade_dates(self) -> list[int]:
|
|
276
|
+
"""获取交易日历"""
|
|
277
|
+
if self._trade_dt is not None:
|
|
278
|
+
return list(self._trade_dt.iloc[:, 0])
|
|
279
|
+
|
|
280
|
+
# 尝试从指数数据获取交易日
|
|
281
|
+
try:
|
|
282
|
+
beg_year = self._date_beg[:4]
|
|
283
|
+
beg_month = self._date_beg[4:6]
|
|
284
|
+
end_month = self._date_end[4:6]
|
|
285
|
+
query = (
|
|
286
|
+
f'沪深300、中证500{beg_year}年{beg_month}月至{end_month}月的收盘点数'
|
|
287
|
+
)
|
|
288
|
+
df = self._query_index_data(query)
|
|
289
|
+
if not df.empty:
|
|
290
|
+
date_col = None
|
|
291
|
+
for col in df.columns:
|
|
292
|
+
if df[col].astype(str).str.match(r'^\d{8}$').any():
|
|
293
|
+
date_col = col
|
|
294
|
+
break
|
|
295
|
+
if date_col:
|
|
296
|
+
dates = sorted(
|
|
297
|
+
pd.to_numeric(df[date_col], errors='coerce')
|
|
298
|
+
.dropna().astype(int).unique().tolist()
|
|
299
|
+
)
|
|
300
|
+
self._trade_dt = pd.DataFrame(dates)
|
|
301
|
+
return dates
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
# fallback: 从价格数据中提取日期
|
|
306
|
+
prices = self._get_prices()
|
|
307
|
+
dates = sorted(prices.index.tolist())
|
|
308
|
+
self._trade_dt = pd.DataFrame(dates)
|
|
309
|
+
return dates
|
|
310
|
+
|
|
311
|
+
@register_route("stk_daily.h5", "stklist")
|
|
312
|
+
def _get_stock_axis_raw(self) -> pd.DataFrame:
|
|
313
|
+
"""stklist DataFrame: (N_stocks, 1)"""
|
|
314
|
+
if self._stklist is None:
|
|
315
|
+
self._get_stock_codes()
|
|
316
|
+
return self._stklist
|
|
317
|
+
|
|
318
|
+
@register_route("index_daily.h5", "indexlist")
|
|
319
|
+
def _get_index_axis_raw(self) -> pd.DataFrame:
|
|
320
|
+
"""indexlist DataFrame: (N_indices, 1)"""
|
|
321
|
+
if self._indexlist is None:
|
|
322
|
+
query = '沪深300、中证500收盘点数'
|
|
323
|
+
df = self._query_index_data(query)
|
|
324
|
+
if df.empty:
|
|
325
|
+
self._indexlist = pd.DataFrame(['000300.SH', '000905.SH'])
|
|
326
|
+
else:
|
|
327
|
+
code_col = df.columns[0]
|
|
328
|
+
codes = df[code_col].unique().tolist()
|
|
329
|
+
self._indexlist = pd.DataFrame(codes)
|
|
330
|
+
return self._indexlist
|
|
331
|
+
|
|
332
|
+
@register_route("stk_daily.h5", "trade_dt")
|
|
333
|
+
@register_route("index_daily.h5", "trade_dt")
|
|
334
|
+
def _get_trade_dt_raw(self) -> pd.DataFrame:
|
|
335
|
+
"""trade_dt DataFrame: (M_dates, 1)"""
|
|
336
|
+
if self._trade_dt is None:
|
|
337
|
+
self._get_trade_dates()
|
|
338
|
+
return self._trade_dt
|
|
339
|
+
|
|
340
|
+
@register_route("stk_daily.h5", "cp")
|
|
341
|
+
def _get_prices(self) -> pd.DataFrame:
|
|
342
|
+
"""获取股票收盘价面板 (dates × stocks)"""
|
|
343
|
+
if self._stock_prices is not None:
|
|
344
|
+
return self._stock_prices
|
|
345
|
+
|
|
346
|
+
codes = self._get_stock_codes()
|
|
347
|
+
# 分批查询 (每批最多 self._batch_size 个代码, M10 参数化)
|
|
348
|
+
all_dfs = []
|
|
349
|
+
for i in range(0, len(codes), self._batch_size):
|
|
350
|
+
batch = codes[i:i + self._batch_size]
|
|
351
|
+
code_str = '、'.join(batch)
|
|
352
|
+
beg_year = self._date_beg[:4]
|
|
353
|
+
beg_month = self._date_beg[4:6]
|
|
354
|
+
end_month = self._date_end[4:6]
|
|
355
|
+
query = (
|
|
356
|
+
f'{code_str}{beg_year}年{beg_month}月至{end_month}月的日收盘价'
|
|
357
|
+
)
|
|
358
|
+
df = self._query_stock_info(query)
|
|
359
|
+
if not df.empty:
|
|
360
|
+
all_dfs.append(df)
|
|
361
|
+
|
|
362
|
+
if not all_dfs:
|
|
363
|
+
raise RuntimeError("无法获取股票价格数据")
|
|
364
|
+
|
|
365
|
+
combined = pd.concat(all_dfs, ignore_index=True)
|
|
366
|
+
self._stock_prices = self._pivot_prices(combined)
|
|
367
|
+
return self._stock_prices
|
|
368
|
+
|
|
369
|
+
def _pivot_prices(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
370
|
+
"""将长格式价格表转为宽格式 (dates × stocks)"""
|
|
371
|
+
# 找到日期列、代码列、价格列
|
|
372
|
+
date_col = None
|
|
373
|
+
code_col = None
|
|
374
|
+
price_col = None
|
|
375
|
+
|
|
376
|
+
for col in df.columns:
|
|
377
|
+
sample = df[col].astype(str)
|
|
378
|
+
if sample.str.match(r'^\d{8}$').any():
|
|
379
|
+
date_col = col
|
|
380
|
+
elif sample.str.match(r'\d{6}\.(SH|SZ)').any():
|
|
381
|
+
code_col = col
|
|
382
|
+
elif '收盘' in col or '价' in col:
|
|
383
|
+
price_col = col
|
|
384
|
+
|
|
385
|
+
if date_col is None or code_col is None or price_col is None:
|
|
386
|
+
# fallback: 用前3列
|
|
387
|
+
cols = df.columns.tolist()
|
|
388
|
+
code_col = code_col or cols[0]
|
|
389
|
+
date_col = date_col or cols[1] if len(cols) > 1 else cols[0]
|
|
390
|
+
price_col = price_col or cols[2] if len(cols) > 2 else cols[1]
|
|
391
|
+
|
|
392
|
+
pivot = df.pivot_table(
|
|
393
|
+
index=date_col, columns=code_col, values=price_col,
|
|
394
|
+
aggfunc='first'
|
|
395
|
+
)
|
|
396
|
+
pivot.index = pd.to_numeric(pivot.index, errors='coerce').astype('Int64')
|
|
397
|
+
pivot.columns = pivot.columns.astype(str)
|
|
398
|
+
return pivot.sort_index()
|
|
399
|
+
|
|
400
|
+
def _get_stock_info_panel(self, key: str, query_template: str) -> pd.DataFrame:
|
|
401
|
+
"""通用股票信息面板获取"""
|
|
402
|
+
if key in self._stock_info_cache:
|
|
403
|
+
return self._stock_info_cache[key]
|
|
404
|
+
|
|
405
|
+
codes = self._get_stock_codes()
|
|
406
|
+
# M10: 分批查询用 self._batch_size
|
|
407
|
+
all_dfs = []
|
|
408
|
+
for i in range(0, len(codes), self._batch_size):
|
|
409
|
+
batch = codes[i:i + self._batch_size]
|
|
410
|
+
code_str = '、'.join(batch)
|
|
411
|
+
query = query_template.format(codes=code_str, beg=self._date_beg, end=self._date_end)
|
|
412
|
+
df = self._query_stock_info(query)
|
|
413
|
+
if not df.empty:
|
|
414
|
+
all_dfs.append(df)
|
|
415
|
+
|
|
416
|
+
if not all_dfs:
|
|
417
|
+
# 返回空面板
|
|
418
|
+
dates = self._get_trade_dates()
|
|
419
|
+
return pd.DataFrame(0, index=dates, columns=codes)
|
|
420
|
+
|
|
421
|
+
combined = pd.concat(all_dfs, ignore_index=True)
|
|
422
|
+
panel = self._pivot_stock_info(combined, codes)
|
|
423
|
+
self._stock_info_cache[key] = panel
|
|
424
|
+
return panel
|
|
425
|
+
|
|
426
|
+
def _pivot_stock_info(self, df: pd.DataFrame, codes: list) -> pd.DataFrame:
|
|
427
|
+
"""将长格式转为宽格式 (dates × stocks)"""
|
|
428
|
+
date_col = code_col = value_col = None
|
|
429
|
+
for col in df.columns:
|
|
430
|
+
sample = df[col].astype(str)
|
|
431
|
+
if sample.str.match(r'^\d{8}$').any():
|
|
432
|
+
date_col = col
|
|
433
|
+
elif sample.str.match(r'\d{6}\.(SH|SZ)').any():
|
|
434
|
+
code_col = col
|
|
435
|
+
else:
|
|
436
|
+
if value_col is None:
|
|
437
|
+
value_col = col
|
|
438
|
+
|
|
439
|
+
if date_col and code_col and value_col:
|
|
440
|
+
pivot = df.pivot_table(
|
|
441
|
+
index=date_col, columns=code_col, values=value_col,
|
|
442
|
+
aggfunc='first'
|
|
443
|
+
)
|
|
444
|
+
pivot.index = pd.to_numeric(pivot.index, errors='coerce').astype('Int64')
|
|
445
|
+
# 确保所有股票都在列中
|
|
446
|
+
for c in codes:
|
|
447
|
+
if c not in pivot.columns:
|
|
448
|
+
pivot[c] = 0
|
|
449
|
+
return pivot[codes].sort_index()
|
|
450
|
+
|
|
451
|
+
# fallback
|
|
452
|
+
dates = self._get_trade_dates()
|
|
453
|
+
return pd.DataFrame(0, index=dates, columns=codes)
|
|
454
|
+
|
|
455
|
+
@register_route("stk_daily.h5", "id_citic1")
|
|
456
|
+
def _get_industry(self) -> pd.DataFrame:
|
|
457
|
+
"""行业分类面板 (dates × stocks), 值为申万一级代码 (1-30)"""
|
|
458
|
+
return self._get_stock_info_panel(
|
|
459
|
+
'id_citic1',
|
|
460
|
+
'{codes}的行业分类(申万一级)'
|
|
461
|
+
).map(lambda x: self._industry_map.get(str(x), 0) if pd.notna(x) else 0)
|
|
462
|
+
|
|
463
|
+
@register_route("stk_daily.h5", "mv_float")
|
|
464
|
+
def _get_market_value(self) -> pd.DataFrame:
|
|
465
|
+
"""流通市值面板 (dates × stocks)"""
|
|
466
|
+
return self._get_stock_info_panel(
|
|
467
|
+
'mv_float',
|
|
468
|
+
'{codes}的流通市值'
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
@register_route("stk_daily.h5", "st")
|
|
472
|
+
def _get_st_status(self) -> pd.DataFrame:
|
|
473
|
+
"""ST 状态面板 (dates × stocks), 值为 0/1"""
|
|
474
|
+
return self._get_stock_info_panel(
|
|
475
|
+
'st',
|
|
476
|
+
'{codes}是否被ST处理'
|
|
477
|
+
).map(lambda x: 1 if str(x).strip() in ('是', 'True', '1', 'ST') else 0)
|
|
478
|
+
|
|
479
|
+
@register_route("stk_daily.h5", "suspend")
|
|
480
|
+
def _get_suspension(self) -> pd.DataFrame:
|
|
481
|
+
"""停牌状态面板 (dates × stocks), 值为 0/1"""
|
|
482
|
+
return self._get_stock_info_panel(
|
|
483
|
+
'suspend',
|
|
484
|
+
'{codes}是否停牌'
|
|
485
|
+
).map(lambda x: 1 if str(x).strip() in ('是', 'True', '1', '停牌') else 0)
|
|
486
|
+
|
|
487
|
+
@register_route("stk_daily.h5", "ud_limit")
|
|
488
|
+
def _get_limit(self) -> pd.DataFrame:
|
|
489
|
+
"""涨跌停面板 (dates × stocks), 值为 0/1/-1"""
|
|
490
|
+
return self._get_stock_info_panel(
|
|
491
|
+
'ud_limit',
|
|
492
|
+
'{codes}是否涨跌停'
|
|
493
|
+
).map(lambda x: 1 if '涨停' in str(x) else (-1 if '跌停' in str(x) else 0))
|
|
494
|
+
|
|
495
|
+
@register_route("stk_daily.h5", "ipo_days")
|
|
496
|
+
def _get_ipo_days(self) -> pd.DataFrame:
|
|
497
|
+
"""上市天数面板 (dates × stocks)"""
|
|
498
|
+
panel = self._get_stock_info_panel(
|
|
499
|
+
'ipo_days',
|
|
500
|
+
'{codes}的上市日期'
|
|
501
|
+
)
|
|
502
|
+
# 将上市日期转为天数
|
|
503
|
+
dates = panel.index
|
|
504
|
+
for col in panel.columns:
|
|
505
|
+
ipo_date = panel[col].iloc[0] if not panel[col].empty else 0
|
|
506
|
+
try:
|
|
507
|
+
ipo_dt = pd.to_datetime(str(int(ipo_date)), format='%Y%m%d')
|
|
508
|
+
panel[col] = [(d - ipo_dt).days if pd.notna(d) else 9999
|
|
509
|
+
for d in pd.to_datetime(dates.astype(str), format='%Y%m%d')]
|
|
510
|
+
except Exception:
|
|
511
|
+
panel[col] = 500
|
|
512
|
+
return panel
|
|
513
|
+
|
|
514
|
+
@register_route("index_daily.h5", "index_cp")
|
|
515
|
+
def _get_index_cp(self) -> pd.DataFrame:
|
|
516
|
+
"""指数收盘价面板 (dates × indices)"""
|
|
517
|
+
if self._index_prices is not None:
|
|
518
|
+
return self._index_prices
|
|
519
|
+
|
|
520
|
+
indexlist = self._get_index_axis_raw()
|
|
521
|
+
indices = indexlist.iloc[:, 0].tolist()
|
|
522
|
+
code_str = '、'.join(indices)
|
|
523
|
+
beg_year = self._date_beg[:4]
|
|
524
|
+
beg_month = self._date_beg[4:6]
|
|
525
|
+
end_month = self._date_end[4:6]
|
|
526
|
+
query = (
|
|
527
|
+
f'{code_str}{beg_year}年{beg_month}月至{end_month}月的收盘点数'
|
|
528
|
+
)
|
|
529
|
+
df = self._query_index_data(query)
|
|
530
|
+
|
|
531
|
+
if df.empty:
|
|
532
|
+
dates = self._get_trade_dates()
|
|
533
|
+
self._index_prices = pd.DataFrame(0, index=dates, columns=indices)
|
|
534
|
+
else:
|
|
535
|
+
self._index_prices = self._pivot_prices(df)
|
|
536
|
+
return self._index_prices
|
|
537
|
+
|
|
538
|
+
@register_route("stk_daily.h5", "id_300")
|
|
539
|
+
def _get_hs300_member(self) -> pd.DataFrame:
|
|
540
|
+
"""沪深300成分股面板 (dates × stocks), 值为 0/1"""
|
|
541
|
+
return self._get_index_member_panel('000300.SH', '沪深300')
|
|
542
|
+
|
|
543
|
+
@register_route("stk_daily.h5", "id_500")
|
|
544
|
+
def _get_zz500_member(self) -> pd.DataFrame:
|
|
545
|
+
"""中证500成分股面板 (dates × stocks), 值为 0/1"""
|
|
546
|
+
return self._get_index_member_panel('000905.SH', '中证500')
|
|
547
|
+
|
|
548
|
+
def _get_index_member_panel(self, index_code: str, index_name: str) -> pd.DataFrame:
|
|
549
|
+
"""指数成分股面板"""
|
|
550
|
+
dates = self._get_trade_dates()
|
|
551
|
+
codes = self._get_stock_codes()
|
|
552
|
+
|
|
553
|
+
# 查询成分股
|
|
554
|
+
query = f'{index_name}成分股列表'
|
|
555
|
+
df = self._query_index_data(query)
|
|
556
|
+
|
|
557
|
+
member_codes = set()
|
|
558
|
+
if not df.empty:
|
|
559
|
+
for col in df.columns:
|
|
560
|
+
vals = df[col].astype(str).str.strip()
|
|
561
|
+
member_codes.update(vals[vals.str.match(r'\d{6}\.(SH|SZ)')].tolist())
|
|
562
|
+
|
|
563
|
+
# 构建面板
|
|
564
|
+
panel = pd.DataFrame(0, index=dates, columns=codes)
|
|
565
|
+
for c in codes:
|
|
566
|
+
if c in member_codes:
|
|
567
|
+
panel[c] = 1
|
|
568
|
+
return panel
|
|
569
|
+
|
|
570
|
+
def _get_factor(self, factor_dir: str, factor_name: str) -> pd.DataFrame:
|
|
571
|
+
"""通过 iFinD 获取因子数据"""
|
|
572
|
+
beg_year = self._date_beg[:4]
|
|
573
|
+
beg_month = self._date_beg[4:6]
|
|
574
|
+
end_month = self._date_end[4:6]
|
|
575
|
+
query = (
|
|
576
|
+
f'{factor_name}因子{beg_year}年{beg_month}月至{end_month}月'
|
|
577
|
+
)
|
|
578
|
+
df = self._query_stock_info(query)
|
|
579
|
+
if df.empty:
|
|
580
|
+
raise RuntimeError(f"无法获取因子: {factor_name}")
|
|
581
|
+
codes = self._get_stock_codes()
|
|
582
|
+
return self._pivot_stock_info(df, codes)
|
|
583
|
+
|
|
584
|
+
# ── Week 12: 真实数据拉取 + H5 持久化 ────────────────────────
|
|
585
|
+
|
|
586
|
+
def fetch_to_h5(
|
|
587
|
+
self,
|
|
588
|
+
output_dir: str | Path,
|
|
589
|
+
factor_names: list[str] | None = None,
|
|
590
|
+
keys: list[str] | None = None,
|
|
591
|
+
) -> dict:
|
|
592
|
+
"""从 iFinD 拉取数据, 写为 HDF5 格式 (兼容 LoadDataNode)。
|
|
593
|
+
|
|
594
|
+
输出文件结构:
|
|
595
|
+
{output_dir}/
|
|
596
|
+
├── stk_daily.h5 # 7 keys: cp, st, suspend, ud_limit,
|
|
597
|
+
│ # ipo_days, id_citic1, mv_float
|
|
598
|
+
├── index_daily.h5 # 1 key: index_cp
|
|
599
|
+
├── stklist.h5 # 股票代码列表
|
|
600
|
+
├── trade_dt.h5 # 交易日历
|
|
601
|
+
└── {factor_name}.h5 # 因子数据 (单 key='data')
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
output_dir: 输出目录 (不存在自动创建)
|
|
605
|
+
factor_names: 因子名列表 (空=不拉因子)
|
|
606
|
+
keys: stk_daily.h5 要拉的 key 列表 (默认全 7 个)
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
dict: {file: {key: shape}, ...} 拉取统计
|
|
610
|
+
"""
|
|
611
|
+
output_dir = Path(output_dir)
|
|
612
|
+
ensure_dir(output_dir)
|
|
613
|
+
all_keys = ['cp', 'st', 'suspend', 'ud_limit', 'ipo_days', 'id_citic1', 'mv_float']
|
|
614
|
+
if keys is None:
|
|
615
|
+
keys = all_keys
|
|
616
|
+
stats: dict = {}
|
|
617
|
+
|
|
618
|
+
# 1. 拉股票池 + 交易日历
|
|
619
|
+
logger.info("[1/4] 拉股票池 (%s)...", self._universe)
|
|
620
|
+
stklist = self._get_stock_axis_raw()
|
|
621
|
+
trade_dt = self._get_trade_dt_raw()
|
|
622
|
+
stklist.to_hdf(output_dir / 'stklist.h5', key='data', mode='w')
|
|
623
|
+
trade_dt.to_hdf(output_dir / 'trade_dt.h5', key='data', mode='w')
|
|
624
|
+
stats['stklist.h5'] = {'data': stklist.shape}
|
|
625
|
+
stats['trade_dt.h5'] = {'data': trade_dt.shape}
|
|
626
|
+
logger.info(
|
|
627
|
+
" ✓ stklist.h5 (shape=%s), trade_dt.h5 (shape=%s)",
|
|
628
|
+
stklist.shape, trade_dt.shape,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# 2. 拉 stk_daily 7 keys
|
|
632
|
+
logger.info("[2/4] 拉 stk_daily.h5 (keys=%s)...", keys)
|
|
633
|
+
stk_daily_stats: dict = {}
|
|
634
|
+
with pd.HDFStore(output_dir / 'stk_daily.h5', mode='w') as store:
|
|
635
|
+
for key in keys:
|
|
636
|
+
if key not in all_keys:
|
|
637
|
+
logger.warning(" ⚠ 跳过未知 key: %s", key)
|
|
638
|
+
continue
|
|
639
|
+
try:
|
|
640
|
+
df = self.load_h5('stk_daily.h5', key)
|
|
641
|
+
if df is None or df.empty:
|
|
642
|
+
logger.warning(" ⚠ %s: 空数据, 跳过", key)
|
|
643
|
+
continue
|
|
644
|
+
# 转换 Int64 (nullable) → int64, HDF5 不支持 nullable int
|
|
645
|
+
df = _df_to_hdf_safe(df)
|
|
646
|
+
store.put(key, df, format='table')
|
|
647
|
+
stk_daily_stats[key] = df.shape
|
|
648
|
+
logger.info(" ✓ %s: shape=%s", key, df.shape)
|
|
649
|
+
except Exception as e:
|
|
650
|
+
logger.error(" ✗ %s 失败: %s", key, e)
|
|
651
|
+
stats['stk_daily.h5'] = stk_daily_stats
|
|
652
|
+
|
|
653
|
+
# 3. 拉 index_daily (沪深300 + 中证500)
|
|
654
|
+
logger.info("[3/4] 拉 index_daily.h5 (index_cp)...")
|
|
655
|
+
try:
|
|
656
|
+
index_cp = self._get_index_cp()
|
|
657
|
+
with pd.HDFStore(output_dir / 'index_daily.h5', mode='w') as store:
|
|
658
|
+
store.put('index_cp', _df_to_hdf_safe(index_cp), format='table')
|
|
659
|
+
stats['index_daily.h5'] = {'index_cp': index_cp.shape}
|
|
660
|
+
logger.info(" ✓ index_cp: shape=%s", index_cp.shape)
|
|
661
|
+
except Exception as e:
|
|
662
|
+
logger.error(" ✗ index_cp 失败: %s", e)
|
|
663
|
+
stats['index_daily.h5'] = {'index_cp': None}
|
|
664
|
+
|
|
665
|
+
# 4. 拉因子 (可选)
|
|
666
|
+
if factor_names:
|
|
667
|
+
logger.info("[4/4] 拉因子 (%s)...", factor_names)
|
|
668
|
+
for fname in factor_names:
|
|
669
|
+
try:
|
|
670
|
+
factor = self._get_factor(fname, fname)
|
|
671
|
+
factor = _df_to_hdf_safe(factor)
|
|
672
|
+
factor.to_hdf(output_dir / f'{fname}.h5', key='data', mode='w')
|
|
673
|
+
stats[f'{fname}.h5'] = {'data': factor.shape}
|
|
674
|
+
logger.info(" ✓ %s: shape=%s", fname, factor.shape)
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.error(" ✗ %s 失败: %s", fname, e)
|
|
677
|
+
stats[f'{fname}.h5'] = {'data': None}
|
|
678
|
+
else:
|
|
679
|
+
logger.info("[4/4] 跳过因子拉取 (factor_names 为空)")
|
|
680
|
+
|
|
681
|
+
return stats
|
|
682
|
+
|
|
683
|
+
def get_universe_stocks(self) -> list[str]:
|
|
684
|
+
"""获取股票池代码列表 (公开 API, 缓存)。"""
|
|
685
|
+
return self._get_stock_codes()
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
# ── B5: 类定义后扫描所有 @register_route 标记, 一次性填充 _ROUTE_TABLE ──
|
|
689
|
+
IFinDDatabase._ROUTE_TABLE = _collect_routes(IFinDDatabase)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# coding: utf-8
|