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,511 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Helper functions and constants used by QuantNodes CLI commands."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import functools
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from QuantNodes.research.wiki import init_factor_wiki
|
|
16
|
+
from QuantNodes.core.path_utils import ensure_dir
|
|
17
|
+
|
|
18
|
+
PROG_NAME = "quantnodes"
|
|
19
|
+
|
|
20
|
+
# Re-export from QuantNodes.constants (single source of truth)
|
|
21
|
+
from QuantNodes.constants import ( # noqa: E402, F401
|
|
22
|
+
DEFAULT_API_PORT,
|
|
23
|
+
DEFAULT_FRONTEND_PORT,
|
|
24
|
+
DEFAULT_HOST,
|
|
25
|
+
DEFAULT_GATEWAY_PORT,
|
|
26
|
+
)
|
|
27
|
+
PIDFILE_NAME = ".quantnodes.pid"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_initialized() -> bool:
|
|
31
|
+
"""Check if current directory is already initialized."""
|
|
32
|
+
return Path(".env").exists() or Path("conn.ini").exists() or Path("wiki/index.md").exists()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_project_root() -> Path:
|
|
36
|
+
"""Get the QuantNodes project root directory.
|
|
37
|
+
|
|
38
|
+
Walks up from this file to find the repo root (containing pyproject.toml).
|
|
39
|
+
Works from any working directory.
|
|
40
|
+
"""
|
|
41
|
+
current = Path(__file__).resolve().parent
|
|
42
|
+
while current != current.parent:
|
|
43
|
+
if (current / "pyproject.toml").exists():
|
|
44
|
+
return current
|
|
45
|
+
current = current.parent
|
|
46
|
+
return Path(__file__).resolve().parent.parent
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_directory_structure():
|
|
50
|
+
"""Create necessary directories.
|
|
51
|
+
|
|
52
|
+
Two output roots are used by different subsystems:
|
|
53
|
+
- outputs/ backtest engine results (equity / signals / trades parquets)
|
|
54
|
+
- output/ factor_test pipeline artefacts (per-node parquets + final report)
|
|
55
|
+
"""
|
|
56
|
+
dirs = [
|
|
57
|
+
"data",
|
|
58
|
+
".agent/memory",
|
|
59
|
+
".agent/dream",
|
|
60
|
+
"output",
|
|
61
|
+
"outputs",
|
|
62
|
+
"logs",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
for d in dirs:
|
|
66
|
+
ensure_dir(Path(d))
|
|
67
|
+
print(f" ✓ 创建目录: {d}/")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def init_llmwikify_wiki(force: bool = False) -> bool:
|
|
71
|
+
"""Initialize QuantNodes wiki structure."""
|
|
72
|
+
print("\n 初始化 QuantNodes Wiki...")
|
|
73
|
+
|
|
74
|
+
wiki_exists = Path("wiki/index.md").exists()
|
|
75
|
+
|
|
76
|
+
if wiki_exists and not force:
|
|
77
|
+
print(" ⏭️ Wiki 已存在,跳过初始化")
|
|
78
|
+
try:
|
|
79
|
+
init_factor_wiki("wiki", force=False)
|
|
80
|
+
print(" ✓ Wiki 配置已更新")
|
|
81
|
+
return True
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f" ⚠ 配置更新失败: {e}")
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
if wiki_exists and force:
|
|
87
|
+
print(" 🔄 强制重新初始化 Wiki")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
[sys.executable, "-m", "llmwikify", "init"],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True
|
|
94
|
+
)
|
|
95
|
+
if result.returncode != 0:
|
|
96
|
+
print(f" ⚠ llmwikify 初始化失败: {result.stderr}")
|
|
97
|
+
return False
|
|
98
|
+
print(" ✓ llmwikify 基础结构创建完成")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f" ⚠ llmwikify 初始化失败: {e}")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
init_factor_wiki("wiki", force=True)
|
|
105
|
+
print(" ✓ QuantNodes 专用配置写入完成")
|
|
106
|
+
return True
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f" ⚠ 配置写入失败: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_input_with_default(prompt: str, default: str, required: bool = False) -> str:
|
|
113
|
+
"""Get user input with a default value."""
|
|
114
|
+
while True:
|
|
115
|
+
try:
|
|
116
|
+
response = input(f"{prompt} [{default}]: ").strip()
|
|
117
|
+
if not response:
|
|
118
|
+
return default
|
|
119
|
+
return response
|
|
120
|
+
except EOFError:
|
|
121
|
+
if required:
|
|
122
|
+
continue
|
|
123
|
+
return default
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# Phase I1: argparse builders for repeated argument groups (2026-06-20)
|
|
128
|
+
# ============================================================================
|
|
129
|
+
# Each builder attaches a common set of CLI args to the given parser.
|
|
130
|
+
# Used by _build_parser() to dedup 6+ repeated patterns across factor-* cmds:
|
|
131
|
+
# --pool-dir (6 sites), --top (2), --metric (2), --title (2),
|
|
132
|
+
# --ancestor-depth/--descendant-depth (2),
|
|
133
|
+
# --min-ipo-days/--min-group-size/--groups (2).
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def add_pool_dir_arg(parser: argparse.ArgumentParser) -> None:
|
|
137
|
+
"""Attach --pool-dir <path> (required)."""
|
|
138
|
+
parser.add_argument("--pool-dir", required=True, help="Pool 目录路径")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def add_top_arg(parser: argparse.ArgumentParser, default: int = 5) -> None:
|
|
142
|
+
"""Attach --top <N>."""
|
|
143
|
+
parser.add_argument("--top", type=int, default=default, help=f"Top-N (默认 {default})")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def add_metric_arg(parser: argparse.ArgumentParser, default: str = "sharpe") -> None:
|
|
147
|
+
"""Attach --metric <name>."""
|
|
148
|
+
parser.add_argument("--metric", default=default, help=f"排序指标 (默认 {default})")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add_title_arg(parser: argparse.ArgumentParser) -> None:
|
|
152
|
+
"""Attach --title <text>."""
|
|
153
|
+
parser.add_argument("--title", default=None, help="报告标题")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def add_output_arg(parser: argparse.ArgumentParser, default: str | None = None) -> None:
|
|
157
|
+
"""Attach --output <path>."""
|
|
158
|
+
parser.add_argument("--output", default=default, help="HTML 输出路径")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def add_lineage_depth_args(parser: argparse.ArgumentParser) -> None:
|
|
162
|
+
"""Attach --ancestor-depth / --descendant-depth (default 2 each)."""
|
|
163
|
+
parser.add_argument("--ancestor-depth", type=int, default=2, help="祖先深度 (默认 2)")
|
|
164
|
+
parser.add_argument("--descendant-depth", type=int, default=2, help="后裔深度 (默认 2)")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def add_cli_overrides(parser: argparse.ArgumentParser) -> None:
|
|
168
|
+
"""Attach --min-ipo-days / --min-group-size / --groups (CLI overrides)."""
|
|
169
|
+
parser.add_argument("--min-ipo-days", type=int, default=None,
|
|
170
|
+
help="剔除上市不足 N 日新股 (覆盖 config 默认值 360)")
|
|
171
|
+
parser.add_argument("--min-group-size", type=int, default=None,
|
|
172
|
+
help="计算 IC 最少样本数 (覆盖 config 默认值 5)")
|
|
173
|
+
parser.add_argument("--groups", type=int, default=None,
|
|
174
|
+
help="分组分析分组数 (覆盖 config 默认值 5)")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ============================================================================
|
|
178
|
+
# Phase I2: cli_safe_run decorator (2026-06-20)
|
|
179
|
+
# ============================================================================
|
|
180
|
+
|
|
181
|
+
def cli_safe_run(func):
|
|
182
|
+
"""Decorator: catch Exception, print 错误 + return 1.
|
|
183
|
+
|
|
184
|
+
Replaces the manual ``try: ... except Exception as e: print(f"错误: {e}"); return 1``
|
|
185
|
+
wrapper that was repeated 7+ times in factor.py and evolve.py.
|
|
186
|
+
"""
|
|
187
|
+
@functools.wraps(func)
|
|
188
|
+
def wrapper(*args, **kwargs):
|
|
189
|
+
try:
|
|
190
|
+
return func(*args, **kwargs)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"错误: {e}")
|
|
193
|
+
return 1
|
|
194
|
+
return wrapper
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ============================================================================
|
|
198
|
+
# Phase I3: confirm_section helper (2026-06-20)
|
|
199
|
+
# ============================================================================
|
|
200
|
+
|
|
201
|
+
def confirm_section(
|
|
202
|
+
name: str,
|
|
203
|
+
fields: Sequence[tuple[str, str]],
|
|
204
|
+
default: bool = False,
|
|
205
|
+
) -> dict | None:
|
|
206
|
+
"""Prompt "是否配置 {name}" → on Yes, ask each (label, default) tuple.
|
|
207
|
+
|
|
208
|
+
Used by init.py for ClickHouse / MySQL config blocks. Each block
|
|
209
|
+
was 7 lines of boilerplate (print header + 5 get_input_with_default calls);
|
|
210
|
+
confirm_section collapses to a single call with a field list.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
name: section name (e.g. "ClickHouse").
|
|
214
|
+
fields: sequence of (label, default) pairs to prompt for.
|
|
215
|
+
default: default for the yes/no confirm.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Dict of {label: value} on confirmation, or None if declined.
|
|
219
|
+
"""
|
|
220
|
+
if not get_yes_no(f"是否配置 {name}", default=default):
|
|
221
|
+
return None
|
|
222
|
+
print(f"\n {name} 配置:")
|
|
223
|
+
return {label: get_input_with_default(f" {label}", default) for label, default in fields}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_yes_no(prompt: str, default: bool = True) -> bool:
|
|
227
|
+
"""Get yes/no input from user."""
|
|
228
|
+
default_str = "Y/n" if default else "y/N"
|
|
229
|
+
while True:
|
|
230
|
+
try:
|
|
231
|
+
response = input(f"{prompt} ({default_str}): ").strip().lower()
|
|
232
|
+
if not response:
|
|
233
|
+
return default
|
|
234
|
+
if response in ("y", "yes"):
|
|
235
|
+
return True
|
|
236
|
+
if response in ("n", "no"):
|
|
237
|
+
return False
|
|
238
|
+
print(" 请输入 y 或 n")
|
|
239
|
+
except EOFError:
|
|
240
|
+
return default
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_model_choice() -> str:
|
|
244
|
+
"""Get model choice from user."""
|
|
245
|
+
models = {
|
|
246
|
+
"1": "gpt-4",
|
|
247
|
+
"2": "gpt-3.5-turbo",
|
|
248
|
+
"3": "gpt-4-turbo",
|
|
249
|
+
"4": "custom",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
print(" 选择模型:")
|
|
253
|
+
print(" 1) gpt-4 (默认)")
|
|
254
|
+
print(" 2) gpt-3.5-turbo")
|
|
255
|
+
print(" 3) gpt-4-turbo")
|
|
256
|
+
print(" 4) 自定义")
|
|
257
|
+
|
|
258
|
+
while True:
|
|
259
|
+
try:
|
|
260
|
+
choice = input(" 选择 [1]: ").strip()
|
|
261
|
+
if not choice:
|
|
262
|
+
return "gpt-4"
|
|
263
|
+
if choice in models:
|
|
264
|
+
return models[choice]
|
|
265
|
+
print(" 无效选择,请输入 1-4")
|
|
266
|
+
except EOFError:
|
|
267
|
+
return "gpt-4"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def write_env_file(api_key: str, base_url: str, model: str, duckdb_path: str,
|
|
271
|
+
clickhouse_config: dict, mysql_config: dict) -> None:
|
|
272
|
+
"""Write .env configuration file."""
|
|
273
|
+
env_content = f"""# LLM 配置
|
|
274
|
+
QUANTNODES__LLM__API_KEY={api_key}
|
|
275
|
+
QUANTNODES__LLM__BASE_URL={base_url}
|
|
276
|
+
|
|
277
|
+
# 模型配置
|
|
278
|
+
QUANTNODES__LLM__MODEL={model}
|
|
279
|
+
|
|
280
|
+
# DuckDB (默认本地文件)
|
|
281
|
+
QUANTNODES__DUCKDB__PATH={duckdb_path}
|
|
282
|
+
|
|
283
|
+
# 可选数据源
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
if clickhouse_config:
|
|
287
|
+
env_content += f"""QUANTNODES__CLICKHOUSE__HOST={clickhouse_config['host']}
|
|
288
|
+
QUANTNODES__CLICKHOUSE__PORT={clickhouse_config['port']}
|
|
289
|
+
QUANTNODES__CLICKHOUSE__USER={clickhouse_config['user']}
|
|
290
|
+
QUANTNODES__CLICKHOUSE__PASSWORD={clickhouse_config['passwd']}
|
|
291
|
+
QUANTNODES__CLICKHOUSE__DATABASE={clickhouse_config['db']}
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
if mysql_config:
|
|
296
|
+
env_content += f"""QUANTNODES__MYSQL__HOST={mysql_config['host']}
|
|
297
|
+
QUANTNODES__MYSQL__PORT={mysql_config['port']}
|
|
298
|
+
QUANTNODES__MYSQL__USER={mysql_config['user']}
|
|
299
|
+
QUANTNODES__MYSQL__PASSWORD={mysql_config['passwd']}
|
|
300
|
+
QUANTNODES__MYSQL__DATABASE={mysql_config['db']}
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
env_content += """# 缓存配置 (H18: 与 IFindFetcher.DEFAULT_CACHE_TTL_S=604800 对齐)
|
|
305
|
+
QUANTNODES__CACHE_ENABLED=true
|
|
306
|
+
QUANTNODES__CACHE_TTL=604800
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
with open(".env", "w", encoding="utf-8") as f:
|
|
310
|
+
f.write(env_content)
|
|
311
|
+
print(" ✓ 创建文件: .env")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def write_conn_ini(duckdb_path: str, clickhouse_config: dict, mysql_config: dict) -> None:
|
|
315
|
+
"""Write conn.ini configuration file."""
|
|
316
|
+
conn_content = f"""[DuckDB]
|
|
317
|
+
path = {duckdb_path}
|
|
318
|
+
read_only = False
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
if clickhouse_config:
|
|
323
|
+
conn_content += f"""[ClickHouse]
|
|
324
|
+
host = {clickhouse_config['host']}
|
|
325
|
+
port = {clickhouse_config['port']}
|
|
326
|
+
user = {clickhouse_config['user']}
|
|
327
|
+
passwd = {clickhouse_config['passwd']}
|
|
328
|
+
db = {clickhouse_config['db']}
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
if mysql_config:
|
|
333
|
+
conn_content += f"""[MySQL]
|
|
334
|
+
host = {mysql_config['host']}
|
|
335
|
+
port = {mysql_config['port']}
|
|
336
|
+
user = {mysql_config['user']}
|
|
337
|
+
passwd = {mysql_config['passwd']}
|
|
338
|
+
db = {mysql_config['db']}
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
with open("conn.ini", "w", encoding="utf-8") as f:
|
|
342
|
+
f.write(conn_content)
|
|
343
|
+
print(" ✓ 创建文件: conn.ini")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def install_talib() -> bool:
|
|
347
|
+
"""Install TA-Lib (optional)."""
|
|
348
|
+
print("\n 安装 TA-Lib...")
|
|
349
|
+
try:
|
|
350
|
+
result = subprocess.run(
|
|
351
|
+
["pip", "install", "TA-Lib>=0.6.0"],
|
|
352
|
+
capture_output=True,
|
|
353
|
+
text=True
|
|
354
|
+
)
|
|
355
|
+
if result.returncode == 0:
|
|
356
|
+
print(" ✓ TA-Lib 安装完成")
|
|
357
|
+
return True
|
|
358
|
+
else:
|
|
359
|
+
print(" ⚠ TA-Lib 安装失败 (可能需要系统库),跳过继续...")
|
|
360
|
+
return False
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f" ⚠ TA-Lib 安装失败: {e},跳过继续...")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ============================================================================
|
|
367
|
+
# v3.0.0 Stage 7 — serve/stop/status lifecycle helpers
|
|
368
|
+
# ============================================================================
|
|
369
|
+
#
|
|
370
|
+
# These helpers back the new ``quantnodes serve`` / ``stop`` / ``status`` /
|
|
371
|
+
# ``agent`` subcommands. Design goals:
|
|
372
|
+
# - Reuse from both CLI (foreground or daemon) and FastAPI startup
|
|
373
|
+
# (api/main.py uses ``load_env_file`` to populate os.environ before
|
|
374
|
+
# reading QUANTNODES__LLM__*).
|
|
375
|
+
# - Avoid new dependencies: use only stdlib + httpx (already required).
|
|
376
|
+
# - Keep pidfile simple: one file at project root, single int PID.
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def load_env_file(env_path: Optional[Path] = None) -> bool:
|
|
380
|
+
"""Load ``.env`` into ``os.environ`` (no override).
|
|
381
|
+
|
|
382
|
+
Returns True if the file existed and was loaded, False otherwise.
|
|
383
|
+
Used by both ``quantnodes serve`` (CLI) and ``api/main.py`` (FastAPI
|
|
384
|
+
lifespan startup) so a single source of truth populates
|
|
385
|
+
``QUANTNODES__LLM__*`` / ``NANOBOT_GATEWAY_*`` / ``FEISHU_*`` for
|
|
386
|
+
``os.environ.get(...)`` consumers (config_mapper.py, nanobot_runtime.py).
|
|
387
|
+
|
|
388
|
+
Idempotent: safe to call multiple times. ``override=False`` so an
|
|
389
|
+
explicit shell env var always wins (e.g. ``NANOBOT_GATEWAY_PORT=18090
|
|
390
|
+
quantnodes serve``).
|
|
391
|
+
"""
|
|
392
|
+
env_path = Path(env_path) if env_path else Path.cwd() / ".env"
|
|
393
|
+
if not env_path.is_file():
|
|
394
|
+
return False
|
|
395
|
+
try:
|
|
396
|
+
from dotenv import load_dotenv
|
|
397
|
+
load_dotenv(env_path, override=False)
|
|
398
|
+
return True
|
|
399
|
+
except ImportError:
|
|
400
|
+
# python-dotenv is optional. Without it the user must source .env
|
|
401
|
+
# manually (or use shell exports).
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def write_pidfile(pid: int, path: Optional[Path] = None) -> Path:
|
|
406
|
+
"""Write ``pid`` to ``.quantnodes.pid`` (project root by default)."""
|
|
407
|
+
path = Path(path) if path else Path.cwd() / PIDFILE_NAME
|
|
408
|
+
path.write_text(str(pid), encoding="utf-8")
|
|
409
|
+
return path
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def read_pidfile(path: Optional[Path] = None) -> Optional[int]:
|
|
413
|
+
"""Read PID from ``.quantnodes.pid``. Returns None if missing/invalid."""
|
|
414
|
+
path = Path(path) if path else Path.cwd() / PIDFILE_NAME
|
|
415
|
+
if not path.is_file():
|
|
416
|
+
return None
|
|
417
|
+
try:
|
|
418
|
+
return int(path.read_text(encoding="utf-8").strip())
|
|
419
|
+
except (ValueError, OSError):
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def remove_pidfile(path: Optional[Path] = None) -> None:
|
|
424
|
+
"""Remove ``.quantnodes.pid`` if it exists (best-effort)."""
|
|
425
|
+
path = Path(path) if path else Path.cwd() / PIDFILE_NAME
|
|
426
|
+
if path.is_file():
|
|
427
|
+
try:
|
|
428
|
+
path.unlink()
|
|
429
|
+
except OSError:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def is_pid_alive(pid: int) -> bool:
|
|
434
|
+
"""Return True if a process with ``pid`` is running.
|
|
435
|
+
|
|
436
|
+
Uses ``kill(pid, 0)`` which doesn't actually send a signal but raises
|
|
437
|
+
``ProcessLookupError`` if the process is gone. Works on Linux/macOS.
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
os.kill(pid, 0)
|
|
441
|
+
return True
|
|
442
|
+
except (ProcessLookupError, PermissionError):
|
|
443
|
+
return False
|
|
444
|
+
except OSError:
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def is_port_free(port: int, host: str = "127.0.0.1") -> bool:
|
|
449
|
+
"""Return True if ``port`` on ``host`` is currently bindable.
|
|
450
|
+
|
|
451
|
+
Tries to bind a TCP socket to the address. ``OSError`` (EADDRINUSE)
|
|
452
|
+
means the port is taken; anything else means it's free. We don't
|
|
453
|
+
actually listen (SO_REUSEADDR + immediate close).
|
|
454
|
+
"""
|
|
455
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
456
|
+
try:
|
|
457
|
+
sock.bind((host, port))
|
|
458
|
+
return True
|
|
459
|
+
except OSError:
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def wait_for_health(api_url: str, timeout_s: int = 30,
|
|
464
|
+
poll_interval_s: float = 1.0) -> bool:
|
|
465
|
+
"""Poll ``GET {api_url}/api/agent/status`` until ``state == "running"``.
|
|
466
|
+
|
|
467
|
+
Returns True on success, False on timeout. Used by ``quantnodes serve``
|
|
468
|
+
to block until the FastAPI lifespan has wired the nanobot runtime.
|
|
469
|
+
"""
|
|
470
|
+
import httpx # local import: avoid hard dep at module-import time
|
|
471
|
+
deadline = time.time() + timeout_s
|
|
472
|
+
last_state = "unreachable"
|
|
473
|
+
while time.time() < deadline:
|
|
474
|
+
try:
|
|
475
|
+
r = httpx.get(f"{api_url}/api/agent/status", timeout=2.0)
|
|
476
|
+
if r.status_code == 200:
|
|
477
|
+
data = r.json()
|
|
478
|
+
last_state = data.get("state", "unknown")
|
|
479
|
+
if last_state == "running":
|
|
480
|
+
return True
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
time.sleep(poll_interval_s)
|
|
484
|
+
print(f" ⚠ health check timeout after {timeout_s}s (last state={last_state})")
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def is_nanobot_installed() -> bool:
|
|
489
|
+
"""Return True if ``nanobot-ai`` is importable.
|
|
490
|
+
|
|
491
|
+
Used by ``quantnodes init`` and ``serve --check-env`` to give a
|
|
492
|
+
friendly non-blocking warning when the optional ``[agent]`` extra
|
|
493
|
+
is not installed. Quant tool library (Wiki/Factor/Backtest/Strategy)
|
|
494
|
+
works without it; only Agent Chat / WebUI / MCP / Feishu need it.
|
|
495
|
+
"""
|
|
496
|
+
try:
|
|
497
|
+
from nanobot.agent.tools.base import Tool # noqa: F401
|
|
498
|
+
return True
|
|
499
|
+
except ImportError:
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def print_nanobot_install_hint() -> None:
|
|
504
|
+
"""Print a friendly hint for installing the optional ``[agent]`` extra."""
|
|
505
|
+
if is_nanobot_installed():
|
|
506
|
+
return
|
|
507
|
+
print()
|
|
508
|
+
print("ℹ 未检测到 nanobot-ai(量化 agent 可选依赖)")
|
|
509
|
+
print(" 安装后可启用 Agent Chat / WebUI / MCP / 飞书:")
|
|
510
|
+
print(" pip install 'quantnodes[agent]' # 或 'quantnodes[all]'")
|
|
511
|
+
print(" 未安装时量化工具库(Wiki / Factor / Backtest / Strategy)完全可用")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""CLI Command pattern base (Phase 3.1, 2026-06-22).
|
|
3
|
+
|
|
4
|
+
替代原 cli/__init__.py:159-192 的 34 行 if/elif ladder 派发到 cmd_*
|
|
5
|
+
函数。新增 subcommand 只需:
|
|
6
|
+
1. 写一个 Command 子类 (name + add_arguments + run)
|
|
7
|
+
2. 在 commands/__init__.py 注册到 REGISTRY
|
|
8
|
+
3. cli/__init__.py:build_parser 自动加 parser, main() 自动 dispatch
|
|
9
|
+
|
|
10
|
+
向后兼容: 旧的 cmd_* 函数仍 export, ``from QuantNodes.cli import cmd_init`` 等
|
|
11
|
+
调用方式不变。
|
|
12
|
+
|
|
13
|
+
设计要点:
|
|
14
|
+
- Command ABC: name (str) + description (str) + add_arguments(subparsers) + run(args)
|
|
15
|
+
- CommandRegistry: register / get / all 三个方法, 支持同名注册报错
|
|
16
|
+
- 不修改 ``argparse.Namespace`` 结构: 仍走 args.command / args.* 字段
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
import argparse
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Command(ABC):
|
|
28
|
+
"""CLI 子命令抽象基类 (Command pattern).
|
|
29
|
+
|
|
30
|
+
子类需实现:
|
|
31
|
+
- name: 子命令名 (e.g. "init", "factor-info")
|
|
32
|
+
- description: 简短描述 (用于 help)
|
|
33
|
+
- add_arguments(subparsers): 注册该子命令的 argparse 参数
|
|
34
|
+
- run(args) -> int: 执行子命令, 返回 exit code (0 = 成功)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str = ""
|
|
38
|
+
description: str = ""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def add_arguments(self, subparsers: "argparse._SubParsersAction") -> None:
|
|
42
|
+
"""注册 argparse 子命令参数.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
subparsers: parser.add_subparsers() 返回的 action,
|
|
46
|
+
调用 subparsers.add_parser(self.name, ...) 添加.
|
|
47
|
+
"""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def run(self, args: "argparse.Namespace") -> int:
|
|
52
|
+
"""执行子命令. 返回 exit code (0 = 成功, 非 0 = 错误)."""
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return f"<{self.__class__.__name__} name={self.name!r}>"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CommandRegistry:
|
|
60
|
+
"""CLI 子命令注册表 (Phase 3.1).
|
|
61
|
+
|
|
62
|
+
提供 register / get / all 三个方法. 重复注册同名 command 抛 ValueError,
|
|
63
|
+
保证 1 个 name 对应 1 个 handler.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self) -> None:
|
|
67
|
+
self._cmds: dict[str, Command] = {}
|
|
68
|
+
|
|
69
|
+
def register(self, cmd: Command) -> Command:
|
|
70
|
+
"""注册一个 Command. 重复同名抛 ValueError.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
传入的 cmd (便于 ``@REGISTRY.register class ...`` 链式调用)
|
|
74
|
+
"""
|
|
75
|
+
if not cmd.name:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"{cmd!r} has empty .name, refusing to register"
|
|
78
|
+
)
|
|
79
|
+
if cmd.name in self._cmds:
|
|
80
|
+
existing = self._cmds[cmd.name]
|
|
81
|
+
if existing is cmd:
|
|
82
|
+
return cmd # idempotent
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Command '{cmd.name}' already registered to {existing!r}, "
|
|
85
|
+
f"refusing to overwrite with {cmd!r}"
|
|
86
|
+
)
|
|
87
|
+
self._cmds[cmd.name] = cmd
|
|
88
|
+
return cmd
|
|
89
|
+
|
|
90
|
+
def get(self, name: str) -> Optional[Command]:
|
|
91
|
+
"""按 name 查询. 未找到返回 None."""
|
|
92
|
+
return self._cmds.get(name)
|
|
93
|
+
|
|
94
|
+
def all(self) -> List[Command]:
|
|
95
|
+
"""返回所有注册 command 的 list (顺序与注册顺序一致)."""
|
|
96
|
+
return list(self._cmds.values())
|
|
97
|
+
|
|
98
|
+
def names(self) -> List[str]:
|
|
99
|
+
"""返回所有注册 name (按注册顺序)."""
|
|
100
|
+
return list(self._cmds.keys())
|
|
101
|
+
|
|
102
|
+
def clear(self) -> None:
|
|
103
|
+
"""清空注册表 (仅供测试用)."""
|
|
104
|
+
self._cmds.clear()
|
|
105
|
+
|
|
106
|
+
def __len__(self) -> int:
|
|
107
|
+
return len(self._cmds)
|
|
108
|
+
|
|
109
|
+
def __contains__(self, name: str) -> bool:
|
|
110
|
+
return name in self._cmds
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Command modules for QuantNodes CLI.
|
|
3
|
+
|
|
4
|
+
Each module exposes the command handler(s) it owns. Public re-exports are
|
|
5
|
+
performed by QuantNodes.cli.__init__ so the entry point
|
|
6
|
+
``from QuantNodes.cli import main`` keeps working unchanged.
|
|
7
|
+
|
|
8
|
+
Phase 3.1 (2026-06-22): 新增 COMMAND_REGISTRY (Command pattern).
|
|
9
|
+
各 command module 末尾追加 *Command 子类, 此文件在 import 时统一注册到
|
|
10
|
+
REGISTRY. cli/__init__.py:build_parser / main 改为用 registry 派发.
|
|
11
|
+
"""
|
|
12
|
+
from QuantNodes.cli.command import CommandRegistry
|
|
13
|
+
|
|
14
|
+
# 模块级 registry (单例)
|
|
15
|
+
COMMAND_REGISTRY = CommandRegistry()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _register_all() -> None:
|
|
19
|
+
"""注册所有 command. 顺序决定 argparse help 中子命令的显示顺序."""
|
|
20
|
+
# 顺序: 初始化 / 服务生命周期 / 启动 / 对话 / agent / 演化 / factor-* / version / help
|
|
21
|
+
from QuantNodes.cli.commands.init import InitCommand
|
|
22
|
+
# v3.0.0 Stage 7: 服务生命周期 (serve / stop / status / logs)
|
|
23
|
+
from QuantNodes.cli.commands.serve import (
|
|
24
|
+
ServeCommand, StopCommand, StatusCommand, LogsCommand,
|
|
25
|
+
)
|
|
26
|
+
from QuantNodes.cli.commands.run import RunCommand
|
|
27
|
+
from QuantNodes.cli.commands.chat import ChatCommand
|
|
28
|
+
# v3.0.0 Stage 7: HTTP 客户端 (agent status / chat / restart)
|
|
29
|
+
from QuantNodes.cli.commands.agent import AgentCommand
|
|
30
|
+
from QuantNodes.cli.commands.evolve import EvolveCommand
|
|
31
|
+
from QuantNodes.cli.commands.alpha import AlphaMctsCommand, AlphaGptCommand, AlphaPipelineCommand
|
|
32
|
+
from QuantNodes.cli.commands.factor import (
|
|
33
|
+
FactorInfoCommand,
|
|
34
|
+
FactorBestCommand,
|
|
35
|
+
FactorVisualCommand,
|
|
36
|
+
FactorDashboardCommand,
|
|
37
|
+
FactorDataFetchCommand,
|
|
38
|
+
FactorRagEvalCommand,
|
|
39
|
+
FactorRagShowCommand,
|
|
40
|
+
)
|
|
41
|
+
from QuantNodes.cli.commands.version import VersionCommand, HelpCommand
|
|
42
|
+
|
|
43
|
+
for cmd in [
|
|
44
|
+
InitCommand(),
|
|
45
|
+
ServeCommand(),
|
|
46
|
+
StopCommand(),
|
|
47
|
+
StatusCommand(),
|
|
48
|
+
LogsCommand(),
|
|
49
|
+
RunCommand(),
|
|
50
|
+
ChatCommand(),
|
|
51
|
+
AgentCommand(),
|
|
52
|
+
EvolveCommand(),
|
|
53
|
+
AlphaMctsCommand(),
|
|
54
|
+
AlphaGptCommand(),
|
|
55
|
+
AlphaPipelineCommand(),
|
|
56
|
+
FactorInfoCommand(),
|
|
57
|
+
FactorBestCommand(),
|
|
58
|
+
FactorVisualCommand(),
|
|
59
|
+
FactorRagShowCommand(),
|
|
60
|
+
FactorRagEvalCommand(),
|
|
61
|
+
FactorDataFetchCommand(),
|
|
62
|
+
FactorDashboardCommand(),
|
|
63
|
+
VersionCommand(),
|
|
64
|
+
HelpCommand(),
|
|
65
|
+
]:
|
|
66
|
+
COMMAND_REGISTRY.register(cmd)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_register_all()
|