powerquant-cli 0.2.6__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.
- powerquant_cli-0.2.6.dist-info/METADATA +62 -0
- powerquant_cli-0.2.6.dist-info/RECORD +355 -0
- powerquant_cli-0.2.6.dist-info/WHEEL +4 -0
- powerquant_cli-0.2.6.dist-info/entry_points.txt +2 -0
- powerquant_cli-0.2.6.dist-info/licenses/LICENSE +36 -0
- powerquant_cli-0.2.6.dist-info/licenses/licenses/LICENSE.vnpy +21 -0
- pqdata/__init__.py +6 -0
- pqdata/client.py +389 -0
- pqdata/download.py +60 -0
- pqdata/exceptions.py +14 -0
- pqdata/schema/__init__.py +1 -0
- pqdata/schema/adjustment_schema.py +67 -0
- pqdata/schema/bar_content_schema.py +24 -0
- pqdata/schema/bar_schema.py +56 -0
- pqdata/schema/corporate_action_schema.py +46 -0
- pqdata/schema/history_schema.py +61 -0
- pqdata/schema/pit_daily_window.py +16 -0
- pqdata/schema/query_schema.py +53 -0
- pqdata/schema/quote_schema.py +73 -0
- pqdata/schema/reference_schema.py +48 -0
- pqdata/schema/reliability_schema.py +152 -0
- quant_cli/__init__.py +0 -0
- quant_cli/api.py +39 -0
- quant_cli/app/__init__.py +0 -0
- quant_cli/app/backtest/__init__.py +0 -0
- quant_cli/app/backtest/cli/__init__.py +0 -0
- quant_cli/app/backtest/cli/local_backtest_cli.py +72 -0
- quant_cli/app/backtest/local_backtest_dependency.py +10 -0
- quant_cli/app/backtest/local_backtest_schema.py +49 -0
- quant_cli/app/backtest/repository/__init__.py +0 -0
- quant_cli/app/backtest/repository/backtest_local_repository.py +67 -0
- quant_cli/app/backtest/service/__init__.py +0 -0
- quant_cli/app/backtest/service/local_backtest_service.py +271 -0
- quant_cli/app/data/__init__.py +0 -0
- quant_cli/app/data/cli/__init__.py +0 -0
- quant_cli/app/data/cli/data_cli.py +78 -0
- quant_cli/app/data/data_dependency.py +35 -0
- quant_cli/app/data/repository/__init__.py +0 -0
- quant_cli/app/data/repository/parquet_repository.py +124 -0
- quant_cli/app/data/schema/__init__.py +0 -0
- quant_cli/app/data/schema/dataset_audit_schema.py +37 -0
- quant_cli/app/data/schema/dataset_request_schema.py +28 -0
- quant_cli/app/data/schema/parquet_file_schema.py +17 -0
- quant_cli/app/data/service/__init__.py +0 -0
- quant_cli/app/data/service/bar_cache_service.py +287 -0
- quant_cli/app/data/service/dataset_audit_service.py +137 -0
- quant_cli/app/data/service/dataset_load_service.py +115 -0
- quant_cli/app/data/service/dataset_prepare_service.py +187 -0
- quant_cli/app/factor/__init__.py +0 -0
- quant_cli/app/factor/cli/__init__.py +0 -0
- quant_cli/app/factor/cli/factor_definition_cli.py +81 -0
- quant_cli/app/factor/cli/local_factor_cli.py +102 -0
- quant_cli/app/factor/cli/local_selection_cli.py +58 -0
- quant_cli/app/factor/interface/__init__.py +0 -0
- quant_cli/app/factor/interface/local_selection_store.py +12 -0
- quant_cli/app/factor/local_factor_dependency.py +38 -0
- quant_cli/app/factor/repository/__init__.py +0 -0
- quant_cli/app/factor/repository/factor_local_repository.py +154 -0
- quant_cli/app/factor/repository/selection_local_repository.py +67 -0
- quant_cli/app/factor/schema/local_selection_schema.py +56 -0
- quant_cli/app/factor/service/__init__.py +0 -0
- quant_cli/app/factor/service/factor_data_prepare_service.py +181 -0
- quant_cli/app/factor/service/local_factor_query_service.py +146 -0
- quant_cli/app/factor/service/local_factor_service.py +120 -0
- quant_cli/app/factor/service/local_selection_service.py +139 -0
- quant_cli/app/job/__init__.py +0 -0
- quant_cli/app/job/cli/__init__.py +0 -0
- quant_cli/app/job/cli/local_job_cli.py +69 -0
- quant_cli/app/job/schema/__init__.py +0 -0
- quant_cli/app/job/schema/local_job_schema.py +11 -0
- quant_cli/app/job/service/__init__.py +0 -0
- quant_cli/app/job/service/local_job_service.py +60 -0
- quant_cli/app/quant_cli.py +49 -0
- quant_cli/app/research/__init__.py +0 -0
- quant_cli/app/research/cli/__init__.py +0 -0
- quant_cli/app/research/cli/local_research_cli.py +93 -0
- quant_cli/app/research/local_research_dependency.py +10 -0
- quant_cli/app/research/repository/__init__.py +0 -0
- quant_cli/app/research/repository/research_local_repository.py +67 -0
- quant_cli/app/research/schema/__init__.py +0 -0
- quant_cli/app/research/schema/local_research_schema.py +26 -0
- quant_cli/app/research/service/__init__.py +0 -0
- quant_cli/app/research/service/local_research_execution_service.py +105 -0
- quant_cli/app/research/service/local_research_service.py +101 -0
- quant_cli/app/strategy/__init__.py +0 -0
- quant_cli/app/strategy/cli/__init__.py +0 -0
- quant_cli/app/strategy/cli/strategy_cli.py +104 -0
- quant_cli/common/__init__.py +0 -0
- quant_cli/common/constants.py +7 -0
- quant_cli/common/database/__init__.py +0 -0
- quant_cli/common/database/duckdb_connection.py +30 -0
- quant_cli/common/http/__init__.py +0 -0
- quant_cli/common/http/definition_client.py +54 -0
- quant_cli/common/utils/__init__.py +0 -0
- quant_cli/common/utils/cli_utils.py +37 -0
- quant_cli/common/utils/platform_utils.py +11 -0
- quant_cli/common/utils/runtime_environment.py +12 -0
- quant_cli/examples/cached_factor_screen.py +165 -0
- quant_cli/examples/factors/ma_close_10.py +10 -0
- quant_cli/examples/factors/max_drawdown_20d.py +18 -0
- quant_cli/examples/factors/momentum_20d.py +13 -0
- quant_cli/examples/factors/multi_intraday_screen.py +16 -0
- quant_cli/examples/factors/risk_adjusted_momentum_20d.py +18 -0
- quant_cli/examples/factors/volume_ratio_5d.py +14 -0
- quant_cli/examples/factors/volume_weighted_close_pressure.py +14 -0
- quant_cli/examples/full_market_screen.py +189 -0
- quant_cli/examples/local_quant_validation.py +196 -0
- quant_cli/examples/published_definition_validation.py +219 -0
- quant_cli/examples/research/factor_pit_coverage.py +87 -0
- quant_cli/examples/research/ipo_first_day.py +348 -0
- quant_cli/examples/selection/README.md +59 -0
- quant_cli/examples/selection/backtest.json +18 -0
- quant_cli/examples/selection/buy_hold.py +7 -0
- quant_cli/examples/selection/compute.json +15 -0
- quant_cli/examples/selection/prepare.json +13 -0
- quant_cli/examples/selection/select.json +44 -0
- quant_cli/examples/strategies/bar_roundtrip.py +12 -0
- quant_cli/examples/strategies/buy_and_hold.py +11 -0
- quant_cli/examples/strategies/contrarian_timing.py +14 -0
- quant_cli/examples/strategies/factor_gate_vwap.py +22 -0
- quant_cli/examples/strategies/ipo_next_day_vwap.py +31 -0
- quant_cli/examples/strategies/momentum_timing.py +14 -0
- quant_cli/examples/strategies/monthly_equal_weight.py +19 -0
- quant_cli/examples/strategies/session_roundtrip.py +16 -0
- quant_cli/examples/strategies/trend_drawdown_guard.py +22 -0
- quant_cli/examples/strategies/weekly_risk_adjusted_rotation.py +61 -0
- quant_cli/examples/unified_snapshot_validation.py +260 -0
- quant_core/__init__.py +0 -0
- quant_core/app/SPEC.md +18 -0
- quant_core/app/__init__.py +0 -0
- quant_core/app/backtest/SPEC.md +65 -0
- quant_core/app/backtest/__init__.py +1 -0
- quant_core/app/backtest/backtest_input_schema.py +20 -0
- quant_core/app/backtest/backtest_run.py +393 -0
- quant_core/app/backtest/data_guard.py +77 -0
- quant_core/app/backtest/historical_clock.py +26 -0
- quant_core/app/backtest/legacy_result_schema.py +34 -0
- quant_core/app/backtest/metrics.py +194 -0
- quant_core/app/backtest/performance_report.py +61 -0
- quant_core/app/backtest/report_schema.py +64 -0
- quant_core/app/backtest/schema.py +477 -0
- quant_core/app/backtest/service/__init__.py +0 -0
- quant_core/app/backtest/service/backtest_execution_service.py +114 -0
- quant_core/app/data/__init__.py +0 -0
- quant_core/app/data/constants.py +284 -0
- quant_core/app/data/domain/__init__.py +0 -0
- quant_core/app/data/domain/trading_quantity_value.py +57 -0
- quant_core/app/data/exceptions.py +95 -0
- quant_core/app/data/interface/__init__.py +0 -0
- quant_core/app/data/interface/adjustment_factor_query.py +21 -0
- quant_core/app/data/interface/market_data_query.py +71 -0
- quant_core/app/data/interface/market_data_verification.py +20 -0
- quant_core/app/data/interface/market_event_source.py +11 -0
- quant_core/app/data/interface/market_revision_query.py +8 -0
- quant_core/app/data/interface/market_runtime_repository.py +44 -0
- quant_core/app/data/interface/market_time_slice_source.py +11 -0
- quant_core/app/data/repository/__init__.py +0 -0
- quant_core/app/data/schema/__init__.py +1 -0
- quant_core/app/data/schema/adjustment_schema.py +67 -0
- quant_core/app/data/schema/corporate_action_schema.py +46 -0
- quant_core/app/data/schema/event_schema.py +506 -0
- quant_core/app/data/schema/exchange_operating_state.py +37 -0
- quant_core/app/data/schema/exchange_state_schema.py +106 -0
- quant_core/app/data/schema/history_schema.py +48 -0
- quant_core/app/data/schema/instrument_identity.py +69 -0
- quant_core/app/data/schema/market_schema.py +128 -0
- quant_core/app/data/schema/pit_daily_window.py +16 -0
- quant_core/app/data/schema/quote_schema.py +73 -0
- quant_core/app/data/schema/reliability_schema.py +152 -0
- quant_core/app/data/schema/snapshot_schema.py +82 -0
- quant_core/app/data/schema/trading_state_schema.py +211 -0
- quant_core/app/data/service/__init__.py +0 -0
- quant_core/app/data/service/calendar_coverage_service.py +63 -0
- quant_core/app/data/service/prepared_data_verification_service.py +40 -0
- quant_core/app/data/service/prepared_market_service.py +189 -0
- quant_core/app/data/service/price_adjustment_service.py +235 -0
- quant_core/app/factor/LICENSE.vnpy +21 -0
- quant_core/app/factor/__init__.py +13 -0
- quant_core/app/factor/builtin/__init__.py +0 -0
- quant_core/app/factor/builtin/alpha101_recipe.py +1095 -0
- quant_core/app/factor/builtin/alpha158_recipe.py +1284 -0
- quant_core/app/factor/builtin/factor_recipe_catalog.py +100 -0
- quant_core/app/factor/computation/__init__.py +0 -0
- quant_core/app/factor/computation/factor_batch_evaluator.py +141 -0
- quant_core/app/factor/computation/factor_expression.py +106 -0
- quant_core/app/factor/computation/factor_source_loader.py +24 -0
- quant_core/app/factor/computation/operators/__init__.py +0 -0
- quant_core/app/factor/computation/operators/arithmetic_operator.py +182 -0
- quant_core/app/factor/computation/operators/cross_section_operator.py +66 -0
- quant_core/app/factor/computation/operators/time_series_operator.py +342 -0
- quant_core/app/factor/computation/panel_evaluator.py +59 -0
- quant_core/app/factor/computation/panel_expression.py +193 -0
- quant_core/app/factor/computation/python_factor_executor.py +87 -0
- quant_core/app/factor/computation/universe_filter.py +25 -0
- quant_core/app/factor/constants.py +114 -0
- quant_core/app/factor/exceptions.py +35 -0
- quant_core/app/factor/factor_dependency.py +65 -0
- quant_core/app/factor/interface/__init__.py +0 -0
- quant_core/app/factor/interface/factor_compute.py +12 -0
- quant_core/app/factor/interface/factor_execution.py +15 -0
- quant_core/app/factor/interface/factor_local_data.py +42 -0
- quant_core/app/factor/interface/factor_panel_compute.py +18 -0
- quant_core/app/factor/interface/factor_panel_input.py +12 -0
- quant_core/app/factor/interface/factor_query_store.py +39 -0
- quant_core/app/factor/interface/public/__init__.py +0 -0
- quant_core/app/factor/interface/public/factor_diagnostics.py +18 -0
- quant_core/app/factor/interface/public/factor_exposure_query.py +12 -0
- quant_core/app/factor/interface/public/factor_local.py +15 -0
- quant_core/app/factor/interface/public/factor_query.py +57 -0
- quant_core/app/factor/interface/public/factor_selection.py +20 -0
- quant_core/app/factor/model/__init__.py +0 -0
- quant_core/app/factor/repository/__init__.py +0 -0
- quant_core/app/factor/schema/__init__.py +1 -0
- quant_core/app/factor/schema/diagnostic_types.py +42 -0
- quant_core/app/factor/schema/factor_artifact_schema.py +36 -0
- quant_core/app/factor/schema/factor_computation_schema.py +102 -0
- quant_core/app/factor/schema/factor_compute_request.py +24 -0
- quant_core/app/factor/schema/factor_contract.py +257 -0
- quant_core/app/factor/schema/factor_input_schema.py +32 -0
- quant_core/app/factor/schema/factor_panel.py +65 -0
- quant_core/app/factor/schema/factor_publication_schema.py +41 -0
- quant_core/app/factor/schema/factor_query_result_schema.py +12 -0
- quant_core/app/factor/schema/factor_result_schema.py +6 -0
- quant_core/app/factor/schema/local_factor_schema.py +42 -0
- quant_core/app/factor/schema/panel_policy.py +13 -0
- quant_core/app/factor/schema/selection_report.py +116 -0
- quant_core/app/factor/schema/selection_types.py +135 -0
- quant_core/app/factor/service/__init__.py +1 -0
- quant_core/app/factor/service/factor_compute_service.py +140 -0
- quant_core/app/factor/service/factor_diagnostic_runner.py +134 -0
- quant_core/app/factor/service/factor_diagnostics_service.py +140 -0
- quant_core/app/factor/service/factor_exposure_service.py +24 -0
- quant_core/app/factor/service/factor_input_service.py +258 -0
- quant_core/app/factor/service/factor_local_execution_service.py +51 -0
- quant_core/app/factor/service/factor_panel_service.py +86 -0
- quant_core/app/factor/service/factor_runtime_service.py +306 -0
- quant_core/app/factor/service/factor_selection_runner.py +88 -0
- quant_core/app/factor/service/published_factor_execution_service.py +160 -0
- quant_core/app/factor/service/selection_service.py +234 -0
- quant_core/app/factor/visibility.py +22 -0
- quant_core/app/opportunity/SPEC.md +45 -0
- quant_core/app/opportunity/__init__.py +1 -0
- quant_core/app/order_execution/SPEC.md +64 -0
- quant_core/app/order_execution/__init__.py +1 -0
- quant_core/app/order_execution/bar_matcher.py +55 -0
- quant_core/app/order_execution/constants.py +142 -0
- quant_core/app/order_execution/domain/__init__.py +45 -0
- quant_core/app/order_execution/domain/execution_batch.py +108 -0
- quant_core/app/order_execution/domain/order_entity.py +197 -0
- quant_core/app/order_execution/domain/order_event.py +87 -0
- quant_core/app/order_execution/domain/order_policy.py +29 -0
- quant_core/app/order_execution/domain/order_value.py +142 -0
- quant_core/app/order_execution/fee_model.py +60 -0
- quant_core/app/order_execution/interface/__init__.py +0 -0
- quant_core/app/order_execution/interface/order_account.py +10 -0
- quant_core/app/order_execution/interface/order_trading_rules.py +10 -0
- quant_core/app/order_execution/interface/public/__init__.py +0 -0
- quant_core/app/order_execution/interface/public/execution_account.py +53 -0
- quant_core/app/order_execution/interface/public/order_events.py +19 -0
- quant_core/app/order_execution/interface/public/order_execution.py +107 -0
- quant_core/app/order_execution/interface/public/order_runtime.py +30 -0
- quant_core/app/order_execution/interface/public/order_submission.py +17 -0
- quant_core/app/order_execution/order_event_bus.py +28 -0
- quant_core/app/order_execution/order_execution_dependency.py +48 -0
- quant_core/app/order_execution/order_submission_service.py +301 -0
- quant_core/app/order_execution/schema/__init__.py +0 -0
- quant_core/app/order_execution/schema/order_request_schema.py +88 -0
- quant_core/app/order_execution/simulation_broker.py +883 -0
- quant_core/app/order_execution/slippage_model.py +40 -0
- quant_core/app/order_execution/tradability_policy.py +174 -0
- quant_core/app/order_execution/validators.py +40 -0
- quant_core/app/portfolio/SPEC.md +69 -0
- quant_core/app/portfolio/__init__.py +0 -0
- quant_core/app/portfolio/domain/__init__.py +32 -0
- quant_core/app/portfolio/domain/entitlement_entity.py +45 -0
- quant_core/app/portfolio/domain/portfolio_entity.py +511 -0
- quant_core/app/portfolio/domain/portfolio_event.py +59 -0
- quant_core/app/portfolio/domain/portfolio_value.py +21 -0
- quant_core/app/portfolio/domain/position_entity.py +165 -0
- quant_core/app/portfolio/domain/reservation_entity.py +69 -0
- quant_core/app/portfolio/interface/__init__.py +0 -0
- quant_core/app/portfolio/interface/public/__init__.py +0 -0
- quant_core/app/portfolio/interface/public/portfolio.py +77 -0
- quant_core/app/portfolio/interface/public/simulation_account.py +16 -0
- quant_core/app/portfolio/ledger_schema.py +130 -0
- quant_core/app/portfolio/order_account_listener.py +36 -0
- quant_core/app/portfolio/portfolio_dependency.py +21 -0
- quant_core/app/portfolio/portfolio_schema.py +204 -0
- quant_core/app/portfolio/simulated_portfolio.py +427 -0
- quant_core/app/research/SPEC.md +57 -0
- quant_core/app/research/__init__.py +0 -0
- quant_core/app/research/adapter/__init__.py +0 -0
- quant_core/app/research/adapter/market_label_query.py +84 -0
- quant_core/app/research/interface/__init__.py +0 -0
- quant_core/app/research/interface/research_dataset.py +47 -0
- quant_core/app/research/interface/research_evaluation.py +19 -0
- quant_core/app/research/schema/__init__.py +0 -0
- quant_core/app/research/schema/comparison_schema.py +12 -0
- quant_core/app/research/schema/dataset_schema.py +112 -0
- quant_core/app/research/schema/research_schema.py +120 -0
- quant_core/app/research/service/__init__.py +0 -0
- quant_core/app/research/service/research_dataset_service.py +80 -0
- quant_core/app/research/service/research_evaluation_service.py +56 -0
- quant_core/app/research/service/research_service.py +171 -0
- quant_core/app/strategy/SPEC.md +215 -0
- quant_core/app/strategy/__init__.py +1 -0
- quant_core/app/strategy/constants.py +87 -0
- quant_core/app/strategy/exceptions.py +32 -0
- quant_core/app/strategy/interface/__init__.py +0 -0
- quant_core/app/strategy/interface/public/__init__.py +0 -0
- quant_core/app/strategy/interface/public/portfolio_construction.py +19 -0
- quant_core/app/strategy/interface/public/strategy.py +30 -0
- quant_core/app/strategy/interface/public/strategy_market_query.py +53 -0
- quant_core/app/strategy/interface/public/strategy_portfolio_reader.py +33 -0
- quant_core/app/strategy/interface/strategy_factor_reader.py +19 -0
- quant_core/app/strategy/interface/strategy_order_reader.py +14 -0
- quant_core/app/strategy/schema/__init__.py +0 -0
- quant_core/app/strategy/schema/portfolio_schema.py +28 -0
- quant_core/app/strategy/schema/portfolio_target.py +70 -0
- quant_core/app/strategy/schema/python_strategy_schema.py +94 -0
- quant_core/app/strategy/schema/screener_schema.py +51 -0
- quant_core/app/strategy/schema/signal_schema.py +33 -0
- quant_core/app/strategy/schema/strategy_publication_schema.py +32 -0
- quant_core/app/strategy/schema/strategy_schema.py +96 -0
- quant_core/app/strategy/service/__init__.py +0 -0
- quant_core/app/strategy/service/factor_top_n_strategy.py +103 -0
- quant_core/app/strategy/service/portfolio_construction_service.py +142 -0
- quant_core/app/strategy/service/python_strategy_service.py +65 -0
- quant_core/app/strategy/service/screener_service.py +77 -0
- quant_core/app/strategy/service/store_factor_reader.py +120 -0
- quant_core/app/strategy/service/strategy_market_view.py +120 -0
- quant_core/app/strategy/service/strategy_runtime_service.py +319 -0
- quant_core/app/strategy/service/strategy_scheduler.py +50 -0
- quant_core/app/strategy/strategies/__init__.py +7 -0
- quant_core/app/strategy/strategies/momentum_volume_breakout.py +157 -0
- quant_core/app/strategy/strategy_context.py +316 -0
- quant_core/app/strategy/strategy_dependency.py +101 -0
- quant_core/app/trading_runtime/SPEC.md +70 -0
- quant_core/app/trading_runtime/__init__.py +1 -0
- quant_core/app/trading_runtime/domain/__init__.py +0 -0
- quant_core/app/trading_runtime/replay/__init__.py +0 -0
- quant_core/app/trading_runtime/replay/daily_state_expand.py +49 -0
- quant_core/app/trading_runtime/replay/exchange_state_resolver.py +90 -0
- quant_core/app/trading_runtime/replay/market_runtime_service.py +573 -0
- quant_core/app/trading_runtime/replay/market_state_resolver.py +150 -0
- quant_core/app/trading_runtime/runtime_ports.py +64 -0
- quant_core/app/trading_runtime/runtime_schema.py +129 -0
- quant_core/app/trading_runtime/runtime_session.py +361 -0
- quant_core/app/trading_runtime/strategy_portfolio_adapter.py +58 -0
- quant_core/app/trading_runtime/trading_rule_book.py +37 -0
- quant_core/app/trading_runtime/trading_rule_query.py +12 -0
- quant_core/common/__init__.py +0 -0
- quant_core/common/exceptions/__init__.py +0 -0
- quant_core/common/exceptions/base.py +89 -0
- quant_core/common/local_run.py +45 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""离线选股用例:资格与快照校验在应用层,评分只调用核心。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, time
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
from quant_core.app.factor.factor_dependency import build_factor_selection
|
|
7
|
+
from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
|
|
8
|
+
from quant_core.app.factor.schema.selection_report import build_selection_report
|
|
9
|
+
from quant_core.app.factor.schema.selection_types import (
|
|
10
|
+
FactorWeight,
|
|
11
|
+
NeutralizationMethod,
|
|
12
|
+
SelectionDefinition,
|
|
13
|
+
)
|
|
14
|
+
from quant_core.common.local_run import LocalRunRecord, RunStatus, content_digest
|
|
15
|
+
|
|
16
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
17
|
+
from quant_cli.app.factor.interface.local_selection_store import LocalSelectionStore
|
|
18
|
+
from quant_cli.app.factor.schema.local_selection_schema import LocalSelectionRequest
|
|
19
|
+
from quant_cli.app.factor.service.local_factor_query_service import build_local_factor_query
|
|
20
|
+
from quant_cli.app.factor.service.local_factor_service import load_factor_result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def compute_local_selection(
|
|
24
|
+
request: LocalSelectionRequest,
|
|
25
|
+
factor_store: LocalFactorStore,
|
|
26
|
+
) -> LocalRunRecord:
|
|
27
|
+
"""计算不创建连接;使用单一运行和单一完整行情快照。"""
|
|
28
|
+
record = LocalRunRecord(request=request.model_dump(mode="json"))
|
|
29
|
+
try:
|
|
30
|
+
market, _ = load_dataset(request.manifest_path, request.max_rows)
|
|
31
|
+
digest = content_digest(request.manifest_path.read_text(encoding="utf-8"))
|
|
32
|
+
factor_run, values = load_factor_result(factor_store, request.factor_run_uuid)
|
|
33
|
+
if factor_run.provenance.get("manifest_digest") != digest:
|
|
34
|
+
raise ValueError("因子与选股输入快照不同")
|
|
35
|
+
if factor_run.provenance.get("market_revision") != market.revision:
|
|
36
|
+
raise ValueError("因子与选股行情修订不同")
|
|
37
|
+
if not set(request.instrument_ids).issubset(factor_run.request.get("instrument_ids", ())):
|
|
38
|
+
raise ValueError("选股股票池不属于因子运行")
|
|
39
|
+
if (
|
|
40
|
+
not market.start.astimezone(ZoneInfo("Asia/Shanghai")).date()
|
|
41
|
+
<= request.target_as_of
|
|
42
|
+
<= market.end.astimezone(ZoneInfo("Asia/Shanghai")).date()
|
|
43
|
+
):
|
|
44
|
+
raise ValueError("选股日期超出已准备快照")
|
|
45
|
+
query, aliases = await build_local_factor_query(
|
|
46
|
+
factor_run, values, market, market.end.date()
|
|
47
|
+
)
|
|
48
|
+
if any(f.name not in aliases for f in request.factors):
|
|
49
|
+
raise ValueError("因子运行不包含所需名称")
|
|
50
|
+
if any(f.transform.neutralization != NeutralizationMethod.NONE for f in request.factors):
|
|
51
|
+
raise ValueError("当前快照未准备行业/市值暴露,不能执行中性化")
|
|
52
|
+
definition = SelectionDefinition(
|
|
53
|
+
factors=tuple(
|
|
54
|
+
FactorWeight(aliases[f.name], f.weight, f.direction, f.transform)
|
|
55
|
+
for f in request.factors
|
|
56
|
+
),
|
|
57
|
+
top_n=request.top_n,
|
|
58
|
+
missing_policy=request.missing_policy,
|
|
59
|
+
)
|
|
60
|
+
universe = []
|
|
61
|
+
exclusions = []
|
|
62
|
+
for iid in sorted(request.instrument_ids):
|
|
63
|
+
info = await market.get_instrument(iid)
|
|
64
|
+
reason = None
|
|
65
|
+
if info.listed_date is None:
|
|
66
|
+
reason = "unknown_listing"
|
|
67
|
+
elif info.listed_date > request.target_as_of or (
|
|
68
|
+
info.delisted_date and info.delisted_date < request.target_as_of
|
|
69
|
+
):
|
|
70
|
+
reason = "not_listed"
|
|
71
|
+
facts = await market.list_daily_facts(iid, request.target_as_of, request.target_as_of)
|
|
72
|
+
if len(facts) != 1:
|
|
73
|
+
reason = reason or "unknown_status"
|
|
74
|
+
else:
|
|
75
|
+
fact = facts[0]
|
|
76
|
+
if request.exclude_st:
|
|
77
|
+
if fact.special_type is None:
|
|
78
|
+
reason = reason or "unknown_special_treatment"
|
|
79
|
+
elif fact.is_st:
|
|
80
|
+
reason = reason or "special_treatment"
|
|
81
|
+
if request.exclude_suspended:
|
|
82
|
+
if fact.suspension_status is None:
|
|
83
|
+
reason = reason or "unknown_suspension"
|
|
84
|
+
elif fact.suspension_status != "none":
|
|
85
|
+
reason = reason or "suspended"
|
|
86
|
+
if request.exclude_limit_up:
|
|
87
|
+
cn = ZoneInfo("Asia/Shanghai")
|
|
88
|
+
bars = await market.list_bars(
|
|
89
|
+
iid,
|
|
90
|
+
"1d",
|
|
91
|
+
max(market.start, datetime.combine(request.target_as_of, time.min, cn)),
|
|
92
|
+
min(market.end, datetime.combine(request.target_as_of, time.max, cn)),
|
|
93
|
+
limit=2,
|
|
94
|
+
)
|
|
95
|
+
if fact.limit_up is None or len(bars) != 1:
|
|
96
|
+
reason = reason or "unknown_price_limit"
|
|
97
|
+
elif bars[0].close >= fact.limit_up:
|
|
98
|
+
reason = reason or "at_limit_up"
|
|
99
|
+
if reason:
|
|
100
|
+
exclusions.append({"instrument_id": iid, "reason": reason})
|
|
101
|
+
else:
|
|
102
|
+
universe.append(iid)
|
|
103
|
+
snapshot = await query.get_snapshot(
|
|
104
|
+
[f.identity for f in definition.factors],
|
|
105
|
+
universe,
|
|
106
|
+
request.decision_time,
|
|
107
|
+
target_as_of=request.target_as_of,
|
|
108
|
+
dataset_revision=str(factor_run.uuid),
|
|
109
|
+
)
|
|
110
|
+
result = build_factor_selection().select(definition, universe, snapshot)
|
|
111
|
+
report = build_selection_report(definition, universe, snapshot, result).model_dump(
|
|
112
|
+
mode="json"
|
|
113
|
+
)
|
|
114
|
+
report["qualification_excluded"] = exclusions
|
|
115
|
+
report["requested_universe_size"] = len(request.instrument_ids)
|
|
116
|
+
record.provenance = {
|
|
117
|
+
"manifest_digest": digest,
|
|
118
|
+
"market_revision": market.revision,
|
|
119
|
+
"factor_run_uuid": str(factor_run.uuid),
|
|
120
|
+
"input_digest": factor_run.provenance["input_digest"],
|
|
121
|
+
"definition_digest": factor_run.provenance["definition_digest"],
|
|
122
|
+
"selection_request_digest": content_digest(request.model_dump_json()),
|
|
123
|
+
"facts_basis": "current_revision_history; historical knowledge revisions unavailable",
|
|
124
|
+
}
|
|
125
|
+
record.result = report
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
record.status, record.error = RunStatus.FAILED, str(exc)
|
|
128
|
+
return record
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def run_local_selection(
|
|
132
|
+
request: LocalSelectionRequest,
|
|
133
|
+
factor_store: LocalFactorStore,
|
|
134
|
+
selection_store: LocalSelectionStore,
|
|
135
|
+
) -> LocalRunRecord:
|
|
136
|
+
"""完成计算后短事务归档;保存失败向调用方传播。"""
|
|
137
|
+
record = await compute_local_selection(request, factor_store)
|
|
138
|
+
selection_store.save(record)
|
|
139
|
+
return record
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""独立 Worker 命令,不实现调度中心。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from quant_core.common.local_run import RunStatus
|
|
9
|
+
|
|
10
|
+
from quant_cli.app.job.schema.local_job_schema import LocalJobRequest
|
|
11
|
+
from quant_cli.app.job.service.local_job_service import execute_local_job
|
|
12
|
+
from quant_cli.common.constants import DEFAULT_DATABASE_PATH
|
|
13
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="独立 Worker 执行与产物输出", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("execute")
|
|
19
|
+
@cli_errors
|
|
20
|
+
def execute(
|
|
21
|
+
config: Annotated[Path, typer.Option(exists=True)], output_dir: Annotated[Path, typer.Option()]
|
|
22
|
+
) -> None:
|
|
23
|
+
record, path = asyncio.run(
|
|
24
|
+
execute_local_job(
|
|
25
|
+
LocalJobRequest.model_validate_json(config.read_text(encoding="utf-8")), output_dir
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
output({"run_uuid": str(record.uuid), "status": record.status.value, "artifact": str(path)})
|
|
29
|
+
if record.status == RunStatus.FAILED:
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("import")
|
|
34
|
+
@cli_errors
|
|
35
|
+
def import_artifact(
|
|
36
|
+
artifact: Annotated[Path, typer.Option(exists=True)],
|
|
37
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""单一归档进程导入 Worker 结果,拒绝损坏或未知格式。"""
|
|
40
|
+
import json
|
|
41
|
+
|
|
42
|
+
from quant_core.common.local_run import LocalRunRecord, content_digest
|
|
43
|
+
|
|
44
|
+
from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
|
|
45
|
+
from quant_cli.app.factor.local_factor_dependency import (
|
|
46
|
+
build_local_factor_store,
|
|
47
|
+
import_factor_artifact,
|
|
48
|
+
)
|
|
49
|
+
from quant_cli.app.research.local_research_dependency import build_local_research_store
|
|
50
|
+
|
|
51
|
+
text = artifact.read_text(encoding="utf-8")
|
|
52
|
+
envelope = json.loads(text)
|
|
53
|
+
if envelope.get("format_version") != 1:
|
|
54
|
+
raise ValueError("不支持的产物版本")
|
|
55
|
+
canonical = json.dumps(envelope["payload"], sort_keys=True, ensure_ascii=False, allow_nan=False)
|
|
56
|
+
if envelope.get("sha256") != content_digest(canonical):
|
|
57
|
+
raise ValueError("产物摘要不匹配")
|
|
58
|
+
kind = envelope.get("kind")
|
|
59
|
+
if kind == "factor":
|
|
60
|
+
record = import_factor_artifact(text, build_local_factor_store(database))
|
|
61
|
+
elif kind in ("research", "backtest"):
|
|
62
|
+
record = LocalRunRecord.model_validate(envelope["payload"]["record"])
|
|
63
|
+
if kind == "research":
|
|
64
|
+
build_local_research_store(database).save(record)
|
|
65
|
+
else:
|
|
66
|
+
build_local_backtest_store(database).save(record)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError("不支持的产物类型")
|
|
69
|
+
output(record)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""调度无关的 Worker 执行入口;只写独立产物,不写共享 DuckDB。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from quant_core.app.factor.schema.factor_artifact_schema import FactorArtifact, FactorArtifactValue
|
|
7
|
+
from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
|
|
8
|
+
from quant_core.common.local_run import LocalRunRecord, content_digest
|
|
9
|
+
|
|
10
|
+
from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
|
|
11
|
+
from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
|
|
12
|
+
from quant_cli.app.factor.local_factor_dependency import compute_local_factor
|
|
13
|
+
from quant_cli.app.job.schema.local_job_schema import LocalJobRequest
|
|
14
|
+
from quant_cli.app.research.schema.local_research_schema import LocalPythonResearchRequest
|
|
15
|
+
from quant_cli.app.research.service.local_research_execution_service import run_local_research
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def execute_local_job(job: LocalJobRequest, output_dir: Path) -> tuple[LocalRunRecord, Path]:
|
|
19
|
+
"""执行独立任务并原子发布产物;上层调度负责重试、领取与状态登记。"""
|
|
20
|
+
if job.kind == "factor":
|
|
21
|
+
record, values = await compute_local_factor(LocalFactorRequest.model_validate(job.request))
|
|
22
|
+
payload = FactorArtifact(
|
|
23
|
+
record=record,
|
|
24
|
+
values=tuple(
|
|
25
|
+
FactorArtifactValue(
|
|
26
|
+
name=name, as_of=timestamp, instrument_id=instrument, value=value
|
|
27
|
+
)
|
|
28
|
+
for name, cells in values.items()
|
|
29
|
+
for (timestamp, instrument), value in cells.items()
|
|
30
|
+
),
|
|
31
|
+
).model_dump(mode="json")
|
|
32
|
+
elif job.kind == "research":
|
|
33
|
+
research = LocalPythonResearchRequest.model_validate(job.request)
|
|
34
|
+
record = await run_local_research(
|
|
35
|
+
research.research,
|
|
36
|
+
research.manifest_path,
|
|
37
|
+
research.max_rows,
|
|
38
|
+
factor_artifact_path=research.factor_artifact_path,
|
|
39
|
+
)
|
|
40
|
+
payload = {"record": record.model_dump(mode="json")}
|
|
41
|
+
else:
|
|
42
|
+
request = LocalBacktestRequest.model_validate(job.request)
|
|
43
|
+
if request.factor_run_uuid is not None:
|
|
44
|
+
raise ValueError("离线 Worker 回测需先提供独立因子产物,不能读取共享研究库")
|
|
45
|
+
record = await run_local_backtest(request)
|
|
46
|
+
payload = {"record": record.model_dump(mode="json")}
|
|
47
|
+
text = json.dumps(payload, sort_keys=True, ensure_ascii=False, allow_nan=False)
|
|
48
|
+
envelope = {
|
|
49
|
+
"format_version": 1,
|
|
50
|
+
"kind": job.kind,
|
|
51
|
+
"sha256": content_digest(text),
|
|
52
|
+
"payload": payload,
|
|
53
|
+
}
|
|
54
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
target = output_dir / f"{record.uuid}.json"
|
|
56
|
+
temporary = target.with_suffix(".tmp")
|
|
57
|
+
with temporary.open("x", encoding="utf-8") as stream:
|
|
58
|
+
json.dump(envelope, stream, ensure_ascii=False, allow_nan=False)
|
|
59
|
+
temporary.replace(target)
|
|
60
|
+
return record, target
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Quant 统一命令注册;领域业务逻辑留在所属模块。"""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from quant_cli.app.backtest.cli.local_backtest_cli import app as backtest
|
|
11
|
+
from quant_cli.app.data.cli.data_cli import app as data
|
|
12
|
+
from quant_cli.app.factor.cli.local_factor_cli import app as factor
|
|
13
|
+
from quant_cli.app.job.cli.local_job_cli import app as job
|
|
14
|
+
from quant_cli.app.research.cli.local_research_cli import app as research
|
|
15
|
+
from quant_cli.app.strategy.cli.strategy_cli import app as strategy
|
|
16
|
+
from quant_cli.common.utils.platform_utils import configure_utf8_output
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Power Quant 本地研究与计算", no_args_is_help=True)
|
|
19
|
+
app.add_typer(data, name="data")
|
|
20
|
+
app.add_typer(job, name="job")
|
|
21
|
+
app.add_typer(factor, name="factor")
|
|
22
|
+
app.add_typer(research, name="research")
|
|
23
|
+
app.add_typer(backtest, name="backtest")
|
|
24
|
+
app.add_typer(strategy, name="strategy")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def examples(
|
|
29
|
+
directory: Annotated[Path, typer.Option("--directory")] = Path("powerquant-examples"),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""导出随包示例到新目录,保留现有文件避免覆盖用户配置。"""
|
|
32
|
+
if directory.exists():
|
|
33
|
+
raise typer.BadParameter("目标目录已存在,请指定新目录")
|
|
34
|
+
source = files("quant_cli").joinpath("examples")
|
|
35
|
+
if not source.is_dir():
|
|
36
|
+
# 开发源码入口与 wheel 都引用同一份 examples,不生成重复示例。
|
|
37
|
+
source = Path(__file__).resolve().parents[3] / "examples"
|
|
38
|
+
shutil.copytree(str(source), directory)
|
|
39
|
+
typer.echo(str(directory.resolve()))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> None:
|
|
43
|
+
"""安装后的 pq 命令入口。"""
|
|
44
|
+
configure_utf8_output()
|
|
45
|
+
app()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""本地研究检验与比较命令。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from quant_core.app.factor.factor_dependency import build_factor_diagnostics
|
|
9
|
+
|
|
10
|
+
from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
|
|
11
|
+
from quant_cli.app.research.local_research_dependency import build_local_research_store
|
|
12
|
+
from quant_cli.app.research.schema.local_research_schema import LocalEvaluationRequest
|
|
13
|
+
from quant_cli.app.research.service.local_research_service import (
|
|
14
|
+
compare_local_research,
|
|
15
|
+
evaluate_local_factor,
|
|
16
|
+
)
|
|
17
|
+
from quant_cli.common.constants import DEFAULT_DATABASE_PATH
|
|
18
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help="本地研究检验与比较", no_args_is_help=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("evaluate")
|
|
24
|
+
@cli_errors
|
|
25
|
+
def evaluate(
|
|
26
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
27
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
28
|
+
) -> None:
|
|
29
|
+
request = LocalEvaluationRequest.model_validate_json(config.read_text(encoding="utf-8"))
|
|
30
|
+
record = evaluate_local_factor(
|
|
31
|
+
request, build_local_factor_store(database), build_factor_diagnostics()
|
|
32
|
+
)
|
|
33
|
+
build_local_research_store(database).save(record)
|
|
34
|
+
output(record)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("compare")
|
|
38
|
+
@cli_errors
|
|
39
|
+
def compare(
|
|
40
|
+
run: Annotated[list[UUID], typer.Option()],
|
|
41
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
42
|
+
) -> None:
|
|
43
|
+
output(compare_local_research(build_local_research_store(database), tuple(run)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("get")
|
|
47
|
+
@cli_errors
|
|
48
|
+
def get(
|
|
49
|
+
run: Annotated[UUID, typer.Option()],
|
|
50
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
51
|
+
) -> None:
|
|
52
|
+
output(build_local_research_store(database).get(run))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("list")
|
|
56
|
+
@cli_errors
|
|
57
|
+
def list_runs(
|
|
58
|
+
limit: int = 20,
|
|
59
|
+
offset: int = 0,
|
|
60
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
61
|
+
) -> None:
|
|
62
|
+
output(
|
|
63
|
+
[
|
|
64
|
+
row.model_dump(mode="json")
|
|
65
|
+
for row in build_local_research_store(database).list_runs(limit, offset)
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("run")
|
|
71
|
+
@cli_errors
|
|
72
|
+
def run(
|
|
73
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
74
|
+
manifest: Annotated[Path, typer.Option(exists=True)],
|
|
75
|
+
factor_run: Annotated[UUID | None, typer.Option()] = None,
|
|
76
|
+
max_rows: int = 1000000,
|
|
77
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""执行可信研究 Python 请求,默认归档结果。"""
|
|
80
|
+
import asyncio
|
|
81
|
+
|
|
82
|
+
from quant_core.app.research.schema.research_schema import ResearchRunRequest
|
|
83
|
+
|
|
84
|
+
from quant_cli.app.research.service.local_research_execution_service import run_local_research
|
|
85
|
+
|
|
86
|
+
request = ResearchRunRequest.model_validate_json(config.read_text(encoding="utf-8"))
|
|
87
|
+
record = asyncio.run(
|
|
88
|
+
run_local_research(
|
|
89
|
+
request, manifest, max_rows, build_local_factor_store(database), factor_run
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
build_local_research_store(database).save(record)
|
|
93
|
+
output(record)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""本地研究组装。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from quant_cli.app.research.repository.research_local_repository import ResearchLocalRepository
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_local_research_store(path: Path) -> ResearchLocalRepository:
|
|
9
|
+
"""返回本地研究存取。"""
|
|
10
|
+
return ResearchLocalRepository(path)
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""research 本地运行存取;短事务由本地连接上下文管理。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from quant_core.common.local_run import LocalRunRecord
|
|
7
|
+
|
|
8
|
+
from quant_cli.common.database.duckdb_connection import local_transaction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResearchLocalRepository:
|
|
12
|
+
"""只维护本领域的运行记录,查询使用数据库分页。"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, path: Path) -> None:
|
|
15
|
+
self.path = path
|
|
16
|
+
|
|
17
|
+
def save(self, record: LocalRunRecord) -> None:
|
|
18
|
+
with local_transaction(self.path) as connection:
|
|
19
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS research_local_run (
|
|
20
|
+
id BIGINT PRIMARY KEY, uuid UUID UNIQUE NOT NULL,
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL, document JSON NOT NULL)""")
|
|
22
|
+
existing = connection.execute(
|
|
23
|
+
"SELECT document FROM research_local_run WHERE uuid = ?", [str(record.uuid)]
|
|
24
|
+
).fetchone()
|
|
25
|
+
if existing is not None:
|
|
26
|
+
if LocalRunRecord.model_validate_json(existing[0]) != record:
|
|
27
|
+
raise ValueError("同一运行 UUID 的记录不可覆盖")
|
|
28
|
+
return
|
|
29
|
+
connection.execute(
|
|
30
|
+
"""INSERT INTO research_local_run
|
|
31
|
+
SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM research_local_run""",
|
|
32
|
+
[str(record.uuid), record.created_at, record.model_dump_json()],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def get(self, run_uuid: UUID) -> LocalRunRecord:
|
|
36
|
+
if not self.path.is_file():
|
|
37
|
+
raise ValueError("运行记录不存在")
|
|
38
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
39
|
+
if not connection.execute(
|
|
40
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ?",
|
|
41
|
+
["research_local_run"],
|
|
42
|
+
).fetchall():
|
|
43
|
+
raise ValueError("运行记录不存在")
|
|
44
|
+
row = connection.execute(
|
|
45
|
+
"SELECT document FROM research_local_run WHERE uuid = ?", [str(run_uuid)]
|
|
46
|
+
).fetchone()
|
|
47
|
+
if row is None:
|
|
48
|
+
raise ValueError("运行记录不存在")
|
|
49
|
+
return LocalRunRecord.model_validate_json(row[0])
|
|
50
|
+
|
|
51
|
+
def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]:
|
|
52
|
+
if not 1 <= limit <= 1000 or offset < 0:
|
|
53
|
+
raise ValueError("分页参数非法")
|
|
54
|
+
if not self.path.is_file():
|
|
55
|
+
return []
|
|
56
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
57
|
+
if not connection.execute(
|
|
58
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ?",
|
|
59
|
+
["research_local_run"],
|
|
60
|
+
).fetchall():
|
|
61
|
+
return []
|
|
62
|
+
rows = connection.execute(
|
|
63
|
+
"""SELECT document FROM research_local_run
|
|
64
|
+
ORDER BY id DESC LIMIT ? OFFSET ?""",
|
|
65
|
+
[limit, offset],
|
|
66
|
+
).fetchall()
|
|
67
|
+
return [LocalRunRecord.model_validate_json(row[0]) for row in rows]
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""本地研究输入:引用已归档因子运行,标签独立传入。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from quant_core.app.research.schema.dataset_schema import ResearchLabel
|
|
8
|
+
from quant_core.app.research.schema.research_schema import ResearchRunRequest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LocalEvaluationRequest(BaseModel):
|
|
12
|
+
model_config = ConfigDict(extra="forbid")
|
|
13
|
+
factor_run_uuid: UUID
|
|
14
|
+
factor_name: str
|
|
15
|
+
labels: tuple[ResearchLabel, ...]
|
|
16
|
+
quantiles: int = Field(default=5, ge=2, le=100)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LocalPythonResearchRequest(BaseModel):
|
|
20
|
+
"""独立 Worker 的 Python 研究请求。"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(extra="forbid")
|
|
23
|
+
research: ResearchRunRequest
|
|
24
|
+
manifest_path: Path
|
|
25
|
+
max_rows: int = Field(default=1000000, ge=1)
|
|
26
|
+
factor_artifact_path: Path | None = None
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""本地 Python 研究运行,复用管理入口的研究执行函数。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from quant_core.app.factor.factor_dependency import build_factor_diagnostics, build_factor_selection
|
|
8
|
+
from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
|
|
9
|
+
from quant_core.app.research.adapter.market_label_query import MarketLabelQuery
|
|
10
|
+
from quant_core.app.research.schema.research_schema import ResearchRunRequest
|
|
11
|
+
from quant_core.app.research.service.research_dataset_service import ResearchDatasetService
|
|
12
|
+
from quant_core.app.research.service.research_evaluation_service import ResearchEvaluationService
|
|
13
|
+
from quant_core.app.research.service.research_service import execute_research
|
|
14
|
+
from quant_core.common.local_run import LocalRunRecord, RunStatus, content_digest
|
|
15
|
+
|
|
16
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
17
|
+
from quant_cli.app.factor.local_factor_dependency import (
|
|
18
|
+
build_local_factor_query,
|
|
19
|
+
decode_factor_artifact,
|
|
20
|
+
load_factor_result,
|
|
21
|
+
)
|
|
22
|
+
from quant_cli.common.utils.runtime_environment import runtime_versions
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SnapshotRevision:
|
|
26
|
+
"""已加载快照的修订查询,不访问外部系统。"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, revision: str, first: date, last: date) -> None:
|
|
29
|
+
self.revision, self.first, self.last = revision, first, last
|
|
30
|
+
|
|
31
|
+
async def snapshot(self, start: date, end: date) -> dict[str, str]:
|
|
32
|
+
if start < self.first or end > self.last:
|
|
33
|
+
raise ValueError("研究范围超出输入快照")
|
|
34
|
+
return {"market": self.revision}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def run_local_research(
|
|
38
|
+
request: ResearchRunRequest,
|
|
39
|
+
manifest: Path,
|
|
40
|
+
max_rows: int,
|
|
41
|
+
factor_store: LocalFactorStore | None = None,
|
|
42
|
+
factor_run_uuid: UUID | None = None,
|
|
43
|
+
factor_artifact_path: Path | None = None,
|
|
44
|
+
) -> LocalRunRecord:
|
|
45
|
+
"""显式研究源码、市场快照和可选因子运行,返回待归档记录。"""
|
|
46
|
+
manifest = manifest.expanduser().resolve()
|
|
47
|
+
record = LocalRunRecord(
|
|
48
|
+
request={
|
|
49
|
+
**request.model_dump(mode="json"),
|
|
50
|
+
"manifest_path": str(manifest),
|
|
51
|
+
"max_rows": max_rows,
|
|
52
|
+
"factor_artifact_path": str(factor_artifact_path.resolve())
|
|
53
|
+
if factor_artifact_path
|
|
54
|
+
else None,
|
|
55
|
+
"factor_run_uuid": str(factor_run_uuid) if factor_run_uuid else None,
|
|
56
|
+
},
|
|
57
|
+
provenance={"environment": runtime_versions()},
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
market, _ = load_dataset(manifest, max_rows)
|
|
61
|
+
record.provenance.update(
|
|
62
|
+
manifest_digest=content_digest(manifest.read_text(encoding="utf-8")),
|
|
63
|
+
market_revision=market.revision,
|
|
64
|
+
)
|
|
65
|
+
factors = None
|
|
66
|
+
if factor_run_uuid and factor_artifact_path:
|
|
67
|
+
raise ValueError("只允许一个因子结果来源")
|
|
68
|
+
if factor_run_uuid or factor_artifact_path:
|
|
69
|
+
if factor_artifact_path:
|
|
70
|
+
factor_record, values = decode_factor_artifact(
|
|
71
|
+
factor_artifact_path.read_text(encoding="utf-8")
|
|
72
|
+
)
|
|
73
|
+
if factor_record.status != RunStatus.COMPLETED:
|
|
74
|
+
raise ValueError("因子产物未成功")
|
|
75
|
+
else:
|
|
76
|
+
if factor_store is None or factor_run_uuid is None:
|
|
77
|
+
raise ValueError("因子运行需要显式读取能力")
|
|
78
|
+
factor_record, values = load_factor_result(factor_store, factor_run_uuid)
|
|
79
|
+
record.provenance["factor_run_uuid"] = str(factor_record.uuid)
|
|
80
|
+
if factor_record.provenance["market_revision"] != market.revision:
|
|
81
|
+
raise ValueError("研究与因子使用的行情修订不同")
|
|
82
|
+
factors, _ = await build_local_factor_query(
|
|
83
|
+
factor_record, values, market, market.end.date()
|
|
84
|
+
)
|
|
85
|
+
result = await execute_research(
|
|
86
|
+
request,
|
|
87
|
+
market,
|
|
88
|
+
SnapshotRevision(market.revision, market.start.date(), market.end.date()),
|
|
89
|
+
factors_query=factors,
|
|
90
|
+
dataset_factory=lambda factors: ResearchDatasetService(
|
|
91
|
+
factors, MarketLabelQuery(market)
|
|
92
|
+
),
|
|
93
|
+
evaluation=ResearchEvaluationService(
|
|
94
|
+
build_factor_selection(), build_factor_diagnostics()
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
record.result = result.model_dump(mode="json")
|
|
98
|
+
if result.error:
|
|
99
|
+
record.status, record.error = RunStatus.FAILED, result.error
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
import logging
|
|
102
|
+
|
|
103
|
+
logging.getLogger(__name__).exception("本地研究执行失败")
|
|
104
|
+
record.status, record.error = RunStatus.FAILED, str(exc)
|
|
105
|
+
return record
|