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,211 @@
|
|
|
1
|
+
"""TrajectoryPool — 演化轨迹池, 双层 Parquet + JSON 持久化。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from ..constants import PARQUET_COLUMNS as _PARQUET_COLUMNS
|
|
12
|
+
from .entry import TrajectoryEntry
|
|
13
|
+
from .lineage import children_of, descendants, lineage
|
|
14
|
+
from QuantNodes.core.path_utils import ensure_dir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_PARQUET_NAME = "trajectories.parquet"
|
|
18
|
+
_ENTRIES_SUBDIR = "entries"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TrajectoryPool:
|
|
22
|
+
"""演化轨迹池 — 持久化每轮实验的完整记录。
|
|
23
|
+
|
|
24
|
+
双层存储:
|
|
25
|
+
- Layer 1: {parquet_name}.parquet (元数据, append)
|
|
26
|
+
- Layer 2: entries/{entry_id}.json (完整记录, 独立子目录)
|
|
27
|
+
|
|
28
|
+
Phase H2 (2026-06-20):
|
|
29
|
+
- all()/_sorted_cache memoizes the timestamp-sorted list and
|
|
30
|
+
invalidates only on add() or reset(). Avoids repeated sorts when
|
|
31
|
+
many callers iterate the pool in tight loops (selector, loop,
|
|
32
|
+
cli/commands/factor.py, knowledge_base.py).
|
|
33
|
+
- _load() uses df.to_dict("records") instead of df.iterrows()
|
|
34
|
+
(5-10x faster bulk load on startup with large pools).
|
|
35
|
+
|
|
36
|
+
H1 (parquet O(n^2) append) deferred: ParquetWriter requires strict
|
|
37
|
+
schema uniformity per column, but the existing Parquet file holds
|
|
38
|
+
heterogeneous types (int/float/bool/str/list per column) and tests
|
|
39
|
+
assert type preservation. Re-implementing with native types would
|
|
40
|
+
require inferring the schema from existing data on every open.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
base_dir: 池根目录 (自动创建)
|
|
44
|
+
parquet_name: Parquet 文件名 (默认 "trajectories.parquet"),
|
|
45
|
+
允许不同实验共用 base_dir 各自存盘。
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
base_dir: Path | str,
|
|
51
|
+
parquet_name: str = _PARQUET_NAME,
|
|
52
|
+
):
|
|
53
|
+
self.base_dir = Path(base_dir)
|
|
54
|
+
ensure_dir(self.base_dir)
|
|
55
|
+
self._entries_dir = self.base_dir / _ENTRIES_SUBDIR
|
|
56
|
+
ensure_dir(self._entries_dir)
|
|
57
|
+
self._parquet_name = parquet_name
|
|
58
|
+
self._entries: dict[str, TrajectoryEntry] = {}
|
|
59
|
+
self._parquet_path = self.base_dir / self._parquet_name
|
|
60
|
+
self._lock = threading.Lock()
|
|
61
|
+
self._sorted_cache: list[TrajectoryEntry] | None = None
|
|
62
|
+
self._load()
|
|
63
|
+
|
|
64
|
+
def add(self, entry: TrajectoryEntry) -> None:
|
|
65
|
+
"""添加一条 entry, 自动持久化。"""
|
|
66
|
+
with self._lock:
|
|
67
|
+
self._entries[entry.entry_id] = entry
|
|
68
|
+
self._persist(entry)
|
|
69
|
+
self._sorted_cache = None # invalidate cache (H2)
|
|
70
|
+
|
|
71
|
+
def get(self, entry_id: str) -> TrajectoryEntry:
|
|
72
|
+
"""按 ID 获取 entry, 不存在抛 KeyError。"""
|
|
73
|
+
if entry_id not in self._entries:
|
|
74
|
+
raise KeyError(f"entry_id 不存在: {entry_id}")
|
|
75
|
+
return self._entries[entry_id]
|
|
76
|
+
|
|
77
|
+
def all(self) -> list[TrajectoryEntry]:
|
|
78
|
+
"""返回所有 entry 列表 (按时间排序)。
|
|
79
|
+
|
|
80
|
+
Cached: subsequent calls return the same list until add()/reset()
|
|
81
|
+
invalidates the cache.
|
|
82
|
+
"""
|
|
83
|
+
if self._sorted_cache is None:
|
|
84
|
+
self._sorted_cache = sorted(self._entries.values(), key=lambda e: e.timestamp)
|
|
85
|
+
return list(self._sorted_cache)
|
|
86
|
+
|
|
87
|
+
def __iter__(self) -> Iterator[TrajectoryEntry]:
|
|
88
|
+
return iter(self.all())
|
|
89
|
+
|
|
90
|
+
def __len__(self) -> int:
|
|
91
|
+
return len(self._entries)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def size(self) -> int:
|
|
95
|
+
return len(self._entries)
|
|
96
|
+
|
|
97
|
+
def reset(self) -> None:
|
|
98
|
+
"""清空内存 + 删除所有持久化文件 (entries/ 子目录 + Parquet)。"""
|
|
99
|
+
with self._lock:
|
|
100
|
+
self._entries.clear()
|
|
101
|
+
self._sorted_cache = None
|
|
102
|
+
if self._parquet_path.exists():
|
|
103
|
+
self._parquet_path.unlink()
|
|
104
|
+
if self._entries_dir.exists():
|
|
105
|
+
for p in self._entries_dir.glob("*.json"):
|
|
106
|
+
p.unlink()
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
# 过滤
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def by_round(self, round_idx: int) -> list[TrajectoryEntry]:
|
|
113
|
+
return [e for e in self.all() if e.round_idx == round_idx]
|
|
114
|
+
|
|
115
|
+
def by_operation(self, operation: str) -> list[TrajectoryEntry]:
|
|
116
|
+
return [e for e in self.all() if e.operation == operation]
|
|
117
|
+
|
|
118
|
+
def filter(self, decision: bool | None = None) -> list[TrajectoryEntry]:
|
|
119
|
+
"""按 feedback.decision 过滤 (None=不过滤)。"""
|
|
120
|
+
if decision is None:
|
|
121
|
+
return self.all()
|
|
122
|
+
return [
|
|
123
|
+
e for e in self.all()
|
|
124
|
+
if e.feedback is not None and e.feedback.decision == decision
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def best(self, top_n: int = 5, metric: str = "sharpe") -> list[TrajectoryEntry]:
|
|
128
|
+
"""按 metric 降序, 返回 Top-N。"""
|
|
129
|
+
return sorted(
|
|
130
|
+
self.all(),
|
|
131
|
+
key=lambda e: float(e.metrics.get(metric, 0) or 0),
|
|
132
|
+
reverse=True,
|
|
133
|
+
)[:top_n]
|
|
134
|
+
|
|
135
|
+
def random(self, n: int, seed: int | None = None) -> list[TrajectoryEntry]:
|
|
136
|
+
"""从池中随机抽 n 条。"""
|
|
137
|
+
import numpy as np
|
|
138
|
+
rng = np.random.default_rng(seed)
|
|
139
|
+
k = min(n, len(self._entries))
|
|
140
|
+
if k == 0:
|
|
141
|
+
return []
|
|
142
|
+
indices = rng.choice(len(self._entries), size=k, replace=False)
|
|
143
|
+
all_entries = self.all()
|
|
144
|
+
return [all_entries[int(i)] for i in indices]
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
# 谱系 (代理到 lineage.py)
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def children_of(self, parent_id: str) -> list[TrajectoryEntry]:
|
|
151
|
+
return children_of(self._entries, parent_id)
|
|
152
|
+
|
|
153
|
+
def lineage(self, entry_id: str) -> list[TrajectoryEntry]:
|
|
154
|
+
return lineage(self._entries, entry_id)
|
|
155
|
+
|
|
156
|
+
def descendants(self, entry_id: str, max_depth: int | None = None) -> list[TrajectoryEntry]:
|
|
157
|
+
return descendants(self._entries, entry_id, max_depth=max_depth)
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
# 持久化
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def _persist(self, entry: TrajectoryEntry) -> None:
|
|
164
|
+
"""持久化单条 entry (Parquet append + JSON 单文件)。
|
|
165
|
+
|
|
166
|
+
保留原有 read+concat+rewrite 路径 (O(n^2) 但 schema 兼容)。
|
|
167
|
+
H1 的 ParquetWriter 增量追加方案因 schema 类型一致性限制暂未启用,
|
|
168
|
+
详见模块 docstring。H2 的 _sorted_cache 与 _load 优化已生效。
|
|
169
|
+
"""
|
|
170
|
+
new_row = pd.DataFrame([entry.to_parquet_row()], columns=list(_PARQUET_COLUMNS))
|
|
171
|
+
if self._parquet_path.exists():
|
|
172
|
+
existing = pd.read_parquet(self._parquet_path)
|
|
173
|
+
for col in _PARQUET_COLUMNS:
|
|
174
|
+
if col not in existing.columns:
|
|
175
|
+
existing[col] = None
|
|
176
|
+
existing = existing[list(_PARQUET_COLUMNS)].astype(object)
|
|
177
|
+
new_row = new_row.astype(object)
|
|
178
|
+
combined = pd.concat([existing, new_row], ignore_index=True)
|
|
179
|
+
else:
|
|
180
|
+
combined = new_row
|
|
181
|
+
combined.to_parquet(self._parquet_path, index=False)
|
|
182
|
+
|
|
183
|
+
json_path = self._entries_dir / f"{entry.entry_id}.json"
|
|
184
|
+
with json_path.open("w", encoding="utf-8") as f:
|
|
185
|
+
json.dump(entry.to_json_dict(), f, ensure_ascii=False, indent=2)
|
|
186
|
+
|
|
187
|
+
def _load(self) -> None:
|
|
188
|
+
"""启动时从磁盘加载元数据 + JSON 详情。
|
|
189
|
+
|
|
190
|
+
H2: vectorized row->dict via to_dict('records') instead of iterrows.
|
|
191
|
+
"""
|
|
192
|
+
if not self._parquet_path.exists():
|
|
193
|
+
return
|
|
194
|
+
try:
|
|
195
|
+
df = pd.read_parquet(self._parquet_path)
|
|
196
|
+
except Exception:
|
|
197
|
+
return
|
|
198
|
+
records = df.to_dict("records")
|
|
199
|
+
for row in records:
|
|
200
|
+
entry_id = str(row.get("entry_id", ""))
|
|
201
|
+
if not entry_id:
|
|
202
|
+
continue
|
|
203
|
+
json_path = self._entries_dir / f"{entry_id}.json"
|
|
204
|
+
if not json_path.exists():
|
|
205
|
+
continue
|
|
206
|
+
try:
|
|
207
|
+
with json_path.open("r", encoding="utf-8") as f:
|
|
208
|
+
data = json.load(f)
|
|
209
|
+
self._entries[entry_id] = TrajectoryEntry.from_json_dict(data)
|
|
210
|
+
except Exception:
|
|
211
|
+
continue
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""ParentSelector — 5 种父辈选择策略。
|
|
2
|
+
|
|
3
|
+
借鉴 QuantaAlpha `configs/experiment.yaml:73 parent_selection_strategy`。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .entry import TrajectoryEntry
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .pool import TrajectoryPool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SelectionStrategy(str, Enum):
|
|
19
|
+
"""5 种选择策略。"""
|
|
20
|
+
BEST = "best"
|
|
21
|
+
RANDOM = "random"
|
|
22
|
+
WEIGHTED = "weighted"
|
|
23
|
+
WEIGHTED_INVERSE = "weighted_inverse"
|
|
24
|
+
TOP_PERCENT_PLUS_RANDOM = "top_percent_plus_random"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParentSelector:
|
|
28
|
+
"""父辈选择策略 — 从 TrajectoryPool 选 n 个 entry 作为下一轮 parent。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
strategy: 策略名 (BEST/RANDOM/WEIGHTED/WEIGHTED_INVERSE/TOP_PERCENT_PLUS_RANDOM)
|
|
32
|
+
metric: 用于 best / weighted 的指标 (默认 'sharpe')
|
|
33
|
+
top_percent_threshold: top_percent_plus_random 中 top 比例 (默认 0.3)
|
|
34
|
+
seed: 随机种子 (None=不固定)
|
|
35
|
+
temperature: M4 weighted 策略的 softmax 温度 (默认 1.0,
|
|
36
|
+
> 1.0 更均匀采样, < 1.0 更集中于高分)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
strategy: str = "best",
|
|
42
|
+
metric: str = "sharpe",
|
|
43
|
+
top_percent_threshold: float = 0.3,
|
|
44
|
+
seed: int | None = None,
|
|
45
|
+
temperature: float = 1.0,
|
|
46
|
+
):
|
|
47
|
+
valid = {s.value for s in SelectionStrategy}
|
|
48
|
+
if strategy not in valid:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"未知 strategy: {strategy!r}, 应为 {sorted(valid)}"
|
|
51
|
+
)
|
|
52
|
+
self.strategy = strategy
|
|
53
|
+
self.metric = metric
|
|
54
|
+
self.top_percent_threshold = top_percent_threshold
|
|
55
|
+
self.temperature = temperature
|
|
56
|
+
self._rng = np.random.default_rng(seed)
|
|
57
|
+
|
|
58
|
+
def select(
|
|
59
|
+
self,
|
|
60
|
+
pool: list[TrajectoryEntry] | "TrajectoryPool",
|
|
61
|
+
n: int = 1,
|
|
62
|
+
) -> list[TrajectoryEntry]:
|
|
63
|
+
"""从 pool 选 n 个 entry (只选 feedback.decision=True 的)。"""
|
|
64
|
+
from .pool import TrajectoryPool # 避免循环 import
|
|
65
|
+
|
|
66
|
+
if isinstance(pool, TrajectoryPool):
|
|
67
|
+
valid = [e for e in pool.all() if e.feedback and e.feedback.decision]
|
|
68
|
+
else:
|
|
69
|
+
valid = [e for e in pool if e.feedback and e.feedback.decision]
|
|
70
|
+
if not valid:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
if self.strategy == SelectionStrategy.BEST.value:
|
|
74
|
+
return self._best(valid, n)
|
|
75
|
+
if self.strategy == SelectionStrategy.RANDOM.value:
|
|
76
|
+
return self._random(valid, n)
|
|
77
|
+
if self.strategy == SelectionStrategy.WEIGHTED.value:
|
|
78
|
+
return self._weighted_sample(valid, n, inverse=False)
|
|
79
|
+
if self.strategy == SelectionStrategy.WEIGHTED_INVERSE.value:
|
|
80
|
+
return self._weighted_sample(valid, n, inverse=True)
|
|
81
|
+
if self.strategy == SelectionStrategy.TOP_PERCENT_PLUS_RANDOM.value:
|
|
82
|
+
return self._top_percent_plus_random(valid, n)
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# 5 个策略
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def _best(self, valid: list[TrajectoryEntry], n: int) -> list[TrajectoryEntry]:
|
|
90
|
+
sorted_entries = sorted(
|
|
91
|
+
valid,
|
|
92
|
+
key=lambda e: float(e.metrics.get(self.metric, 0) or 0),
|
|
93
|
+
reverse=True,
|
|
94
|
+
)
|
|
95
|
+
return sorted_entries[:n]
|
|
96
|
+
|
|
97
|
+
def _random(self, valid: list[TrajectoryEntry], n: int) -> list[TrajectoryEntry]:
|
|
98
|
+
k = min(n, len(valid))
|
|
99
|
+
indices = self._rng.choice(len(valid), size=k, replace=False)
|
|
100
|
+
return [valid[int(i)] for i in indices]
|
|
101
|
+
|
|
102
|
+
def _weighted_sample(
|
|
103
|
+
self,
|
|
104
|
+
valid: list[TrajectoryEntry],
|
|
105
|
+
n: int,
|
|
106
|
+
inverse: bool,
|
|
107
|
+
) -> list[TrajectoryEntry]:
|
|
108
|
+
scores = np.array(
|
|
109
|
+
[float(e.metrics.get(self.metric, 0) or 0) for e in valid],
|
|
110
|
+
dtype=float,
|
|
111
|
+
)
|
|
112
|
+
if inverse:
|
|
113
|
+
scores = -scores
|
|
114
|
+
scores = scores - scores.max()
|
|
115
|
+
# M4: temperature 调节 softmax 锐度
|
|
116
|
+
# T → 0: 趋近 argmax; T → ∞: 趋近均匀; T=1: 标准 softmax
|
|
117
|
+
weights = np.exp(scores / max(self.temperature, 1e-9))
|
|
118
|
+
total = weights.sum()
|
|
119
|
+
if total == 0 or not np.isfinite(total):
|
|
120
|
+
return self._random(valid, n)
|
|
121
|
+
weights = weights / total
|
|
122
|
+
k = min(n, len(valid))
|
|
123
|
+
indices = self._rng.choice(len(valid), size=k, replace=False, p=weights)
|
|
124
|
+
return [valid[int(i)] for i in indices]
|
|
125
|
+
|
|
126
|
+
def _top_percent_plus_random(
|
|
127
|
+
self,
|
|
128
|
+
valid: list[TrajectoryEntry],
|
|
129
|
+
n: int,
|
|
130
|
+
) -> list[TrajectoryEntry]:
|
|
131
|
+
top_n = max(1, int(len(valid) * self.top_percent_threshold))
|
|
132
|
+
top = self._best(valid, top_n)
|
|
133
|
+
if n <= top_n:
|
|
134
|
+
return top[:n]
|
|
135
|
+
rest = [e for e in valid if e.entry_id not in {t.entry_id for t in top}]
|
|
136
|
+
if not rest:
|
|
137
|
+
return top[:n]
|
|
138
|
+
k = min(n - top_n, len(rest))
|
|
139
|
+
extra_indices = self._rng.choice(len(rest), size=k, replace=False)
|
|
140
|
+
return top + [rest[int(i)] for i in extra_indices]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Visualization — 演化实验交互式 HTML 报告。
|
|
2
|
+
|
|
3
|
+
公开 API:
|
|
4
|
+
- generate_report(entries, metric) -> dict[Figure]
|
|
5
|
+
- generate_html(entries, metric, output_path) -> str / 写文件
|
|
6
|
+
- ReportBuilder / Section / Report (Phase 1.3 fluent API)
|
|
7
|
+
- build_lineage_layout(entries, metric) -> {nodes, edges}
|
|
8
|
+
- lineage_dag_figure / metric_distribution_figure / metric_per_round_figure
|
|
9
|
+
- gate_breakdown_figure / operation_breakdown_figure
|
|
10
|
+
"""
|
|
11
|
+
from .lineage_dag import build_lineage_layout, lineage_dag_figure
|
|
12
|
+
from .metric_distribution import (
|
|
13
|
+
metric_distribution_figure,
|
|
14
|
+
metric_per_round_figure,
|
|
15
|
+
)
|
|
16
|
+
from .gate_breakdown import gate_breakdown_figure, operation_breakdown_figure
|
|
17
|
+
from .report import generate_html, generate_report
|
|
18
|
+
from .builder import ReportBuilder, Section, Report
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"build_lineage_layout",
|
|
22
|
+
"lineage_dag_figure",
|
|
23
|
+
"metric_distribution_figure",
|
|
24
|
+
"metric_per_round_figure",
|
|
25
|
+
"gate_breakdown_figure",
|
|
26
|
+
"operation_breakdown_figure",
|
|
27
|
+
"generate_report",
|
|
28
|
+
"generate_html",
|
|
29
|
+
# Phase 1.3
|
|
30
|
+
"ReportBuilder",
|
|
31
|
+
"Section",
|
|
32
|
+
"Report",
|
|
33
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""ReportBuilder — 流式构造演化报告 (Phase 1.3, Builder pattern).
|
|
3
|
+
|
|
4
|
+
替代原来 generate_report() 内部硬编码的 dict 拼接逻辑。
|
|
5
|
+
提供 .with_title().with_overview().add_section(...).build() 流式 API,
|
|
6
|
+
并保证向后兼容 (旧 generate_report() 内部调用 ReportBuilder)。
|
|
7
|
+
|
|
8
|
+
设计要点:
|
|
9
|
+
- Section 封装一个 fig + 可选标题
|
|
10
|
+
- 报告结构: title + overview table + 多个 section, 顺序可定制
|
|
11
|
+
- 与 plotly 解耦, 任何有 .to_html() 方法的对象都可作 section
|
|
12
|
+
- build() 返回 Report dataclass (含 sections 列表 + overview dict),
|
|
13
|
+
渲染时由 ReportRenderer 转换为 HTML
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union
|
|
20
|
+
|
|
21
|
+
from QuantNodes.core.path_utils import ensure_parent
|
|
22
|
+
from QuantNodes.core.trajectory.entry import TrajectoryEntry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Data classes
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Section:
|
|
31
|
+
"""报告中的一个 section, 含可选标题 + 可渲染对象 (有 .to_html() 方法)。"""
|
|
32
|
+
title: str
|
|
33
|
+
payload: Any # plotly Figure 或其他有 to_html 的对象
|
|
34
|
+
|
|
35
|
+
def render(self, include_plotlyjs: bool = False, div_id: Optional[str] = None) -> str:
|
|
36
|
+
"""渲染为 HTML fragment, 不含 <h2> 标题。
|
|
37
|
+
|
|
38
|
+
div_id: 传给 payload.to_html() 的 div id; 传 None 时用默认
|
|
39
|
+
f"fig_{title.lower().replace(' ', '_')}" (与旧实现兼容)。
|
|
40
|
+
|
|
41
|
+
v3.0.0 graceful degradation: when ``payload`` is None (e.g. plotly
|
|
42
|
+
not installed), we emit a friendly install hint instead of
|
|
43
|
+
crashing. ``to_dict`` still records the None value so callers
|
|
44
|
+
can introspect the report structure.
|
|
45
|
+
"""
|
|
46
|
+
if self.payload is None:
|
|
47
|
+
return (
|
|
48
|
+
"<p><em>(plotly not installed — install with "
|
|
49
|
+
"<code>pip install plotly</code> to render this chart)</em></p>"
|
|
50
|
+
)
|
|
51
|
+
if hasattr(self.payload, "to_html"):
|
|
52
|
+
kwargs = {"full_html": False, "include_plotlyjs": include_plotlyjs}
|
|
53
|
+
if div_id is None:
|
|
54
|
+
div_id = f"fig_{self.title.lower().replace(' ', '_')}"
|
|
55
|
+
kwargs["div_id"] = div_id
|
|
56
|
+
return self.payload.to_html(**kwargs)
|
|
57
|
+
return f"<pre>{self.payload!r}</pre>"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Report:
|
|
62
|
+
"""构建完成的报告。"""
|
|
63
|
+
title: str
|
|
64
|
+
overview: Dict[str, Any] = field(default_factory=dict)
|
|
65
|
+
sections: List[Section] = field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
68
|
+
"""与旧 generate_report() 返回格式兼容: {overview, *section_keys}。"""
|
|
69
|
+
out: Dict[str, Any] = {"overview": self.overview}
|
|
70
|
+
for i, s in enumerate(self.sections):
|
|
71
|
+
key = s.title.lower().replace(" ", "_")
|
|
72
|
+
out[key] = s.payload
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ============================================================================
|
|
77
|
+
# ReportBuilder
|
|
78
|
+
# ============================================================================
|
|
79
|
+
|
|
80
|
+
class ReportBuilder:
|
|
81
|
+
"""流式构造 Report (Builder pattern)。
|
|
82
|
+
|
|
83
|
+
用法:
|
|
84
|
+
>>> from QuantNodes.core.visualization import ReportBuilder
|
|
85
|
+
>>> report = (
|
|
86
|
+
... ReportBuilder()
|
|
87
|
+
... .with_title("演化报告")
|
|
88
|
+
... .with_overview({"size": 10, "passed": 8})
|
|
89
|
+
... .add_section("Lineage DAG", lineage_fig)
|
|
90
|
+
... .add_section("Metrics", metric_fig)
|
|
91
|
+
... .build()
|
|
92
|
+
... )
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self) -> None:
|
|
96
|
+
self._title: str = "Report"
|
|
97
|
+
self._overview: Dict[str, Any] = {}
|
|
98
|
+
self._sections: List[Section] = []
|
|
99
|
+
self._preset_loaded = False
|
|
100
|
+
|
|
101
|
+
def with_title(self, title: str) -> "ReportBuilder":
|
|
102
|
+
"""设置报告标题。"""
|
|
103
|
+
self._title = title
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def with_overview(self, overview: Dict[str, Any]) -> "ReportBuilder":
|
|
107
|
+
"""设置概览 dict (会被原样放入 Report.overview)。"""
|
|
108
|
+
self._overview = dict(overview)
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def add_section(self, title: str, payload: Any) -> "ReportBuilder":
|
|
112
|
+
"""追加一个 section。payload 通常是 plotly Figure, 任何有 to_html() 的对象都接受。"""
|
|
113
|
+
self._sections.append(Section(title=title, payload=payload))
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def with_evolve_preset(
|
|
117
|
+
self,
|
|
118
|
+
entries: Sequence[TrajectoryEntry],
|
|
119
|
+
metric: str = "sharpe",
|
|
120
|
+
figure_factories: Optional[Dict[str, Callable]] = None,
|
|
121
|
+
) -> "ReportBuilder":
|
|
122
|
+
"""预置: 加载 4-5 个演化实验常用图 + 概览 (Phase 1.3 兼容 generate_report)。
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
entries: TrajectoryEntry 列表或 Mapping
|
|
126
|
+
metric: 用于可视化的指标名
|
|
127
|
+
figure_factories: 覆盖默认 5 个图 builder 的 dict
|
|
128
|
+
e.g. {"lineage_dag": my_lineage_factory}
|
|
129
|
+
"""
|
|
130
|
+
from .gate_breakdown import gate_breakdown_figure, operation_breakdown_figure
|
|
131
|
+
from .lineage_dag import lineage_dag_figure
|
|
132
|
+
from .metric_distribution import metric_distribution_figure, metric_per_round_figure
|
|
133
|
+
|
|
134
|
+
items = list(entries.values() if isinstance(entries, Mapping) else entries)
|
|
135
|
+
n = len(items)
|
|
136
|
+
n_passed = sum(1 for e in items if e.feedback and e.feedback.decision)
|
|
137
|
+
n_rejected = n - n_passed
|
|
138
|
+
rounds = sorted({e.round_idx for e in items}) if items else []
|
|
139
|
+
metrics_vals = [
|
|
140
|
+
float((e.metrics or {}).get(metric, 0) or 0)
|
|
141
|
+
for e in items
|
|
142
|
+
if (e.metrics or {}).get(metric) is not None
|
|
143
|
+
]
|
|
144
|
+
best_metric = max(metrics_vals) if metrics_vals else 0.0
|
|
145
|
+
|
|
146
|
+
self._overview = {
|
|
147
|
+
"size": n,
|
|
148
|
+
"rounds": len(rounds),
|
|
149
|
+
"passed": n_passed,
|
|
150
|
+
"passed_pct": (n_passed / n) if n > 0 else 0.0,
|
|
151
|
+
"rejected": n_rejected,
|
|
152
|
+
"best_metric": best_metric,
|
|
153
|
+
"metric": metric,
|
|
154
|
+
}
|
|
155
|
+
self.with_title(self._title or "QuantNodes 演化实验报告")
|
|
156
|
+
|
|
157
|
+
factories = figure_factories or {
|
|
158
|
+
"lineage_dag": lambda: lineage_dag_figure(
|
|
159
|
+
items, metric=metric, title=f"演化谱系 DAG (按 {metric})"
|
|
160
|
+
),
|
|
161
|
+
"metric_distribution": lambda: metric_distribution_figure(items, metric=metric),
|
|
162
|
+
"metric_per_round": lambda: metric_per_round_figure(items, metric=metric),
|
|
163
|
+
"gate_breakdown": lambda: gate_breakdown_figure(items),
|
|
164
|
+
"operation_breakdown": lambda: operation_breakdown_figure(items),
|
|
165
|
+
}
|
|
166
|
+
for title, factory in factories.items():
|
|
167
|
+
self._sections.append(Section(title=title, payload=factory()))
|
|
168
|
+
self._preset_loaded = True
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def build(self) -> Report:
|
|
172
|
+
"""构建最终 Report 对象。"""
|
|
173
|
+
return Report(title=self._title, overview=self._overview, sections=list(self._sections))
|
|
174
|
+
|
|
175
|
+
def build_to_html(
|
|
176
|
+
self,
|
|
177
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
178
|
+
plotly_cdn: str = '<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>',
|
|
179
|
+
) -> str:
|
|
180
|
+
"""直接构建并渲染为 HTML 字符串 (可选写入文件)。"""
|
|
181
|
+
report = self.build()
|
|
182
|
+
html = _render_html(report, plotly_cdn=plotly_cdn)
|
|
183
|
+
if output_path is not None:
|
|
184
|
+
output_path = Path(output_path)
|
|
185
|
+
ensure_parent(output_path)
|
|
186
|
+
output_path.write_text(html, encoding="utf-8")
|
|
187
|
+
return html
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ============================================================================
|
|
191
|
+
# HTML rendering
|
|
192
|
+
# ============================================================================
|
|
193
|
+
|
|
194
|
+
_OVERVIEW_TEMPLATE = """
|
|
195
|
+
<h2>概览</h2>
|
|
196
|
+
<table border="1" cellpadding="6" style="border-collapse:collapse;">
|
|
197
|
+
<tr><th>指标</th><th>值</th></tr>
|
|
198
|
+
<tr><td>总 entry 数</td><td>{size}</td></tr>
|
|
199
|
+
<tr><td>演化轮数</td><td>{rounds}</td></tr>
|
|
200
|
+
<tr><td>通过数</td><td>{passed} ({passed_pct:.1%})</td></tr>
|
|
201
|
+
<tr><td>拒绝数</td><td>{rejected}</td></tr>
|
|
202
|
+
<tr><td>Best {metric}</td><td>{best_metric:.4f}</td></tr>
|
|
203
|
+
</table>
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _render_html(report: Report, plotly_cdn: str) -> str:
|
|
208
|
+
overview_html = _OVERVIEW_TEMPLATE.format(**report.overview) if report.overview else ""
|
|
209
|
+
# 使用 title 派生 div_id (与旧 generate_html 行为兼容, div_id 格式 fig_<key>)
|
|
210
|
+
parts = [f"<h2>{s.title}</h2>\n{s.render(include_plotlyjs=False)}"
|
|
211
|
+
for s in report.sections]
|
|
212
|
+
return f"""<!DOCTYPE html>
|
|
213
|
+
<html lang="zh-CN">
|
|
214
|
+
<head>
|
|
215
|
+
<meta charset="utf-8">
|
|
216
|
+
<title>{report.title}</title>
|
|
217
|
+
<style>
|
|
218
|
+
body {{ font-family: -apple-system, sans-serif; margin: 20px; max-width: 1200px; }}
|
|
219
|
+
h1 {{ border-bottom: 2px solid #4C78A8; padding-bottom: 8px; }}
|
|
220
|
+
table {{ background: #fafafa; }}
|
|
221
|
+
th {{ background: #4C78A8; color: white; }}
|
|
222
|
+
</style>
|
|
223
|
+
{plotly_cdn}
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<h1>{report.title}</h1>
|
|
227
|
+
{overview_html}
|
|
228
|
+
{"".join(parts)}
|
|
229
|
+
<hr>
|
|
230
|
+
<p style="color: #888; font-size: 12px;">Generated by QuantNodes ReportBuilder</p>
|
|
231
|
+
</body>
|
|
232
|
+
</html>
|
|
233
|
+
"""
|