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,477 @@
|
|
|
1
|
+
"""Backtest Schema - BacktestConfig / 生命周期 / Equity Curve / Metrics / Result(T7)
|
|
2
|
+
|
|
3
|
+
语义 SSOT:`app/backtest/SPEC.md`、T7 Task(2026-09-05 用户确认的 MVP
|
|
4
|
+
指标口径)与 DECISIONS.md 2026-09-04。
|
|
5
|
+
|
|
6
|
+
边界:
|
|
7
|
+
- 全部领域结果深度不可变(frozen + tuple),集合顺序确定,重复读取幂等;
|
|
8
|
+
- 随机技术 ID(uuid)不参与业务确定性比较,不进入指标;
|
|
9
|
+
- 指标严格 Decimal,不把 float 引入领域结果,不静默 quantize;
|
|
10
|
+
- MVP 不把 Fill 冒充完整策略交易:`trade_count` / `win_rate` 显式为 None,
|
|
11
|
+
limitations 以稳定机器可查 code 披露(无 CompletedTrade 账本)。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator, model_validator
|
|
19
|
+
|
|
20
|
+
from quant_core.app.backtest.legacy_result_schema import RebalancePlanSnapshot
|
|
21
|
+
from quant_core.app.data.constants import BarFrequency
|
|
22
|
+
from quant_core.app.data.schema.adjustment_schema import AdjustmentAnchorMode, AdjustmentType
|
|
23
|
+
from quant_core.app.data.schema.trading_state_schema import validate_canonical_instrument_id
|
|
24
|
+
from quant_core.app.order_execution.constants import ExecutionPolicy
|
|
25
|
+
from quant_core.app.order_execution.domain.order_event import (
|
|
26
|
+
ExecutionAttempt,
|
|
27
|
+
Fill,
|
|
28
|
+
OrderTransition,
|
|
29
|
+
)
|
|
30
|
+
from quant_core.app.order_execution.domain.order_value import OrderSnapshot
|
|
31
|
+
from quant_core.app.portfolio.ledger_schema import (
|
|
32
|
+
CashLedgerEntry,
|
|
33
|
+
ClosedTrade,
|
|
34
|
+
OrderReservationLedgerEntry,
|
|
35
|
+
PositionLotSnapshot,
|
|
36
|
+
)
|
|
37
|
+
from quant_core.app.portfolio.portfolio_schema import PortfolioSnapshot
|
|
38
|
+
from quant_core.app.strategy.schema.portfolio_target import PortfolioDecision
|
|
39
|
+
from quant_core.app.trading_runtime.runtime_schema import TraceEntry
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BacktestStatus(str, Enum):
|
|
43
|
+
"""BacktestRun 生命周期状态机
|
|
44
|
+
|
|
45
|
+
CREATED → DATA_CHECK → RUNNING → SETTLING → COMPLETED;
|
|
46
|
+
任意非终态阶段异常 → FAILED(终态,不可恢复)。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
CREATED = "created"
|
|
50
|
+
DATA_CHECK = "data_check"
|
|
51
|
+
RUNNING = "running"
|
|
52
|
+
SETTLING = "settling"
|
|
53
|
+
COMPLETED = "completed"
|
|
54
|
+
FAILED = "failed"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BacktestDataRevision(BaseModel):
|
|
58
|
+
"""回测实际验证通过的数据版本;revision 变化即形成新的缓存命名空间。"""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(frozen=True)
|
|
61
|
+
|
|
62
|
+
dataset: str
|
|
63
|
+
instrument_id: str
|
|
64
|
+
frequency: str | None = None
|
|
65
|
+
revision: str
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def cache_namespace(self) -> str:
|
|
69
|
+
frequency = self.frequency or "event"
|
|
70
|
+
return f"{self.dataset}:{self.instrument_id}:{frequency}:{self.revision}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BarSourceMode(str, Enum):
|
|
74
|
+
NATIVE = "native"
|
|
75
|
+
AGGREGATED = "aggregated"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BacktestFrequencyPlan(BaseModel):
|
|
79
|
+
"""回测观察与撮合频率及其唯一数据来源。"""
|
|
80
|
+
|
|
81
|
+
model_config = ConfigDict(frozen=True)
|
|
82
|
+
|
|
83
|
+
observation_frequency: BarFrequency
|
|
84
|
+
execution_frequency: BarFrequency
|
|
85
|
+
observation_source: BarSourceMode
|
|
86
|
+
execution_source: BarSourceMode
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BacktestConfig(BaseModel):
|
|
90
|
+
"""一次历史回测的配置(不可变;进入 Result 作为配置快照)"""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(frozen=True)
|
|
93
|
+
|
|
94
|
+
strategy_id: str = Field(..., min_length=1, description="本次回测执行的策略标识")
|
|
95
|
+
instrument_ids: tuple[str, ...] = Field(
|
|
96
|
+
..., min_length=1, description="标的(Canonical,非空去重)"
|
|
97
|
+
)
|
|
98
|
+
frequency: BarFrequency = Field(..., description="Bar 频率")
|
|
99
|
+
execution_frequency: BarFrequency | None = Field(
|
|
100
|
+
default=None, description="撮合频率;为空时与观察频率一致"
|
|
101
|
+
)
|
|
102
|
+
start: datetime = Field(..., description="回测区间开始(含,timezone-aware)")
|
|
103
|
+
end: datetime = Field(..., description="回测区间结束(含,timezone-aware)")
|
|
104
|
+
initial_cash: Decimal = Field(..., gt=0, description="初始资金(Decimal,> 0)")
|
|
105
|
+
execution_policy: ExecutionPolicy = ExecutionPolicy.CURRENT_BAR_CLOSE
|
|
106
|
+
auto_switch_order_value: bool = False
|
|
107
|
+
strategy_adjustment_type: AdjustmentType = Field(
|
|
108
|
+
default=AdjustmentType.PRE, description="Strategy 观察价格口径"
|
|
109
|
+
)
|
|
110
|
+
strategy_anchor_mode: AdjustmentAnchorMode = Field(
|
|
111
|
+
default=AdjustmentAnchorMode.DECISION_TIME, description="Strategy 复权锚点策略"
|
|
112
|
+
)
|
|
113
|
+
trading_days_per_year: Decimal = Field(
|
|
114
|
+
default=Decimal("252"), gt=0, description="年化交易日数(指标参数,> 0)"
|
|
115
|
+
)
|
|
116
|
+
annual_risk_free_rate: Decimal = Field(
|
|
117
|
+
default=Decimal("0"), ge=0, description="年化无风险收益率(>= 0)"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@field_validator("instrument_ids")
|
|
121
|
+
@classmethod
|
|
122
|
+
def _validate_instrument_ids(cls, ids: tuple[str, ...]) -> tuple[str, ...]:
|
|
123
|
+
for instrument_id in ids:
|
|
124
|
+
validate_canonical_instrument_id(instrument_id)
|
|
125
|
+
if len(set(ids)) != len(ids):
|
|
126
|
+
raise ValueError(f"instrument_ids 不允许重复:{ids}")
|
|
127
|
+
return tuple(sorted(ids))
|
|
128
|
+
|
|
129
|
+
@field_validator("strategy_id")
|
|
130
|
+
@classmethod
|
|
131
|
+
def _validate_strategy_id(cls, value: str) -> str:
|
|
132
|
+
if value != value.strip():
|
|
133
|
+
raise ValueError("strategy_id 不允许首尾空白")
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
@field_validator("start", "end")
|
|
137
|
+
@classmethod
|
|
138
|
+
def _check_tz_aware(cls, value: datetime) -> datetime:
|
|
139
|
+
if value.tzinfo is None:
|
|
140
|
+
raise ValueError("BacktestConfig.start/end 必须 timezone-aware")
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
@field_validator(
|
|
144
|
+
"initial_cash", "trading_days_per_year", "annual_risk_free_rate", mode="before"
|
|
145
|
+
)
|
|
146
|
+
@classmethod
|
|
147
|
+
def _reject_float(cls, value: object) -> object:
|
|
148
|
+
if isinstance(value, float):
|
|
149
|
+
raise ValueError("BacktestConfig 金额/参数必须使用 Decimal,禁止 float")
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
@model_validator(mode="after")
|
|
153
|
+
def _check_range(self) -> "BacktestConfig":
|
|
154
|
+
if self.start > self.end:
|
|
155
|
+
raise ValueError(f"start {self.start} 必须不晚于 end {self.end}")
|
|
156
|
+
if (
|
|
157
|
+
self.strategy_adjustment_type is AdjustmentType.PRE
|
|
158
|
+
and self.strategy_anchor_mode is not AdjustmentAnchorMode.DECISION_TIME
|
|
159
|
+
):
|
|
160
|
+
raise ValueError("历史回测 PRE 只允许 DECISION_TIME 动态锚点")
|
|
161
|
+
execution_frequency = self.execution_frequency or self.frequency
|
|
162
|
+
if (
|
|
163
|
+
self.execution_policy is ExecutionPolicy.DAILY_VWAP
|
|
164
|
+
and execution_frequency is not BarFrequency.D1
|
|
165
|
+
):
|
|
166
|
+
raise ValueError("DAILY_VWAP 只支持日线撮合")
|
|
167
|
+
durations = {
|
|
168
|
+
BarFrequency.M1: 1,
|
|
169
|
+
BarFrequency.M5: 5,
|
|
170
|
+
BarFrequency.M15: 15,
|
|
171
|
+
BarFrequency.M30: 30,
|
|
172
|
+
BarFrequency.H1: 60,
|
|
173
|
+
BarFrequency.D1: 1440,
|
|
174
|
+
BarFrequency.W1: 10080,
|
|
175
|
+
}
|
|
176
|
+
if durations[execution_frequency] > durations[self.frequency]:
|
|
177
|
+
raise ValueError("撮合频率不得粗于策略观察频率")
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class EquityPoint(BaseModel):
|
|
182
|
+
"""权益曲线单点:(成功片 slice_time, portfolio_snapshot.total_value)"""
|
|
183
|
+
|
|
184
|
+
model_config = ConfigDict(frozen=True)
|
|
185
|
+
|
|
186
|
+
time: datetime = Field(..., description="片市场时间(timezone-aware)")
|
|
187
|
+
total_value: Decimal = Field(..., description="该片估值后的组合总值")
|
|
188
|
+
|
|
189
|
+
@field_validator("time")
|
|
190
|
+
@classmethod
|
|
191
|
+
def _check_tz(cls, value: datetime) -> datetime:
|
|
192
|
+
if value.tzinfo is None:
|
|
193
|
+
raise ValueError("EquityPoint.time 必须 timezone-aware")
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
@field_validator("total_value", mode="before")
|
|
197
|
+
@classmethod
|
|
198
|
+
def _reject_float_value(cls, value: object) -> object:
|
|
199
|
+
if isinstance(value, float):
|
|
200
|
+
raise ValueError("EquityPoint.total_value 必须使用 Decimal,禁止 float")
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class EquityCurve(BaseModel):
|
|
205
|
+
"""权益曲线(每个成功 StepResult 恰好一个点;时间严格递增)"""
|
|
206
|
+
|
|
207
|
+
model_config = ConfigDict(frozen=True)
|
|
208
|
+
|
|
209
|
+
points: tuple[EquityPoint, ...] = Field(default_factory=tuple)
|
|
210
|
+
|
|
211
|
+
@field_validator("points")
|
|
212
|
+
@classmethod
|
|
213
|
+
def _check_strictly_increasing(cls, points: tuple[EquityPoint, ...]) -> tuple[EquityPoint, ...]:
|
|
214
|
+
for i in range(1, len(points)):
|
|
215
|
+
if points[i].time <= points[i - 1].time:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"EquityCurve 时间必须严格递增:{points[i - 1].time} -> {points[i].time}"
|
|
218
|
+
)
|
|
219
|
+
return points
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def last_value(self) -> Decimal | None:
|
|
223
|
+
"""最终权益(空曲线为 None)"""
|
|
224
|
+
return self.points[-1].total_value if self.points else None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class BacktestMetrics(BaseModel):
|
|
228
|
+
"""回测基础指标(口径 SSOT:T7 Task Confirmed Decisions 2026-09-05)
|
|
229
|
+
|
|
230
|
+
- total_return = ending_equity / initial_cash - 1(空曲线为 None)
|
|
231
|
+
- annualized_return = (ending/initial) ** (tdpy/n_days) - 1;少于 2 个
|
|
232
|
+
观测交易日为 None
|
|
233
|
+
- max_drawdown = 相对历史峰值最大跌幅(非负;无回撤为 0)
|
|
234
|
+
- sharpe_ratio = mean(daily_returns - daily_rf) / sample_stddev(daily_returns)
|
|
235
|
+
* sqrt(tdpy);少于 2 个日收益或样本标准差为 0 为 None
|
|
236
|
+
- gross_turnover_rate = 成交绝对金额合计 / 平均日末组合净值;费用不计入成交金额
|
|
237
|
+
- fill_count 精确统计 Fill 数量;trade_count / win_rate 显式 None
|
|
238
|
+
(Literal[None] 使「未建立 CompletedTrade 账本」在类型层机器可查)
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
model_config = ConfigDict(frozen=True)
|
|
242
|
+
|
|
243
|
+
total_return: Decimal | None = Field(default=None, description="累计收益率;空数据为 None")
|
|
244
|
+
annualized_return: Decimal | None = Field(
|
|
245
|
+
default=None, description="年化收益率;观测不足为 None"
|
|
246
|
+
)
|
|
247
|
+
max_drawdown: Decimal = Field(default=Decimal("0"), ge=0, description="最大回撤(非负)")
|
|
248
|
+
sharpe_ratio: Decimal | None = Field(default=None, description="Sharpe 比率;不可计算为 None")
|
|
249
|
+
fill_count: int = Field(default=0, ge=0, description="Fill 成交笔数(不等于完整交易)")
|
|
250
|
+
trade_count: int | None = Field(default=None, ge=0, description="ClosedTrade 数量")
|
|
251
|
+
win_rate: Decimal | None = Field(default=None, ge=0, le=1, description="盈利 ClosedTrade 占比")
|
|
252
|
+
profit_loss_ratio: Decimal | None = Field(
|
|
253
|
+
default=None, ge=0, description="平均盈利/平均亏损绝对值"
|
|
254
|
+
)
|
|
255
|
+
average_holding_days: Decimal | None = Field(default=None, ge=0, description="平均持有自然日")
|
|
256
|
+
gross_turnover_rate: Decimal | None = Field(
|
|
257
|
+
default=None,
|
|
258
|
+
ge=0,
|
|
259
|
+
description="成交绝对金额合计 / 平均日末组合净值;空曲线或分母为零时为 None",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@field_validator(
|
|
263
|
+
"total_return",
|
|
264
|
+
"annualized_return",
|
|
265
|
+
"max_drawdown",
|
|
266
|
+
"sharpe_ratio",
|
|
267
|
+
"win_rate",
|
|
268
|
+
"profit_loss_ratio",
|
|
269
|
+
"average_holding_days",
|
|
270
|
+
"gross_turnover_rate",
|
|
271
|
+
mode="before",
|
|
272
|
+
)
|
|
273
|
+
@classmethod
|
|
274
|
+
def _reject_float_metric(cls, value: object) -> object:
|
|
275
|
+
if isinstance(value, float):
|
|
276
|
+
raise ValueError("指标必须使用 Decimal,禁止 float")
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class DecisionTrace(BaseModel):
|
|
281
|
+
"""决策轨迹(append-only 聚合:只追加成功片的 TraceEntry,深度不可变)
|
|
282
|
+
|
|
283
|
+
时间轨迹不得倒退(同一时间片多条合法);聚合自
|
|
284
|
+
RuntimeStepResult.trace_entries;Backtest 不复制 Runtime 单片业务顺序。
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
model_config = ConfigDict(frozen=True)
|
|
288
|
+
|
|
289
|
+
entries: tuple[TraceEntry, ...] = Field(default_factory=tuple)
|
|
290
|
+
|
|
291
|
+
@field_validator("entries")
|
|
292
|
+
@classmethod
|
|
293
|
+
def _check_non_decreasing(cls, entries: tuple[TraceEntry, ...]) -> tuple[TraceEntry, ...]:
|
|
294
|
+
for i in range(1, len(entries)):
|
|
295
|
+
if entries[i].event_time < entries[i - 1].event_time:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"DecisionTrace 时间不得倒退:{entries[i - 1].event_time} -> "
|
|
298
|
+
f"{entries[i].event_time}(同一时间片多条合法)"
|
|
299
|
+
)
|
|
300
|
+
return entries
|
|
301
|
+
|
|
302
|
+
def appended(self, entries: tuple[TraceEntry, ...]) -> "DecisionTrace":
|
|
303
|
+
"""返回追加了新条目的新实例(append-only:原实例不可变)"""
|
|
304
|
+
return DecisionTrace(entries=self.entries + tuple(entries))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ResultLimitation(str, Enum):
|
|
308
|
+
"""结果能力限制的稳定机器可查 code(stable API,不得随意改值)"""
|
|
309
|
+
|
|
310
|
+
TRADE_COUNT_UNAVAILABLE = "TRADE_COUNT_UNAVAILABLE:NO_COMPLETED_TRADE_LEDGER"
|
|
311
|
+
WIN_RATE_UNAVAILABLE = "WIN_RATE_UNAVAILABLE:NO_COMPLETED_TRADE_LEDGER"
|
|
312
|
+
PRE_TAX_DIVIDEND = "PRE_TAX_DIVIDEND:NO_HOLDING_PERIOD_TAX"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class BacktestResult(BaseModel):
|
|
316
|
+
"""一次回测的最终结果(深度不可变;重复读取幂等且全等)
|
|
317
|
+
|
|
318
|
+
全部事实来自已成功提交的 RuntimeStepResult 与 Order Execution 公开
|
|
319
|
+
投影;不读取 Runtime / Portfolio / Execution 私有状态。
|
|
320
|
+
|
|
321
|
+
构造边界封闭(model_validator 校验,矛盾输入无法构造):
|
|
322
|
+
- MVP 只表达实际可产出的 COMPLETED Result;FAILED 仅由
|
|
323
|
+
BacktestRun.status 表达;
|
|
324
|
+
- metrics.fill_count == len(fills);
|
|
325
|
+
- started/ended_event_time 与 EquityCurve 首尾一致;空 Curve 对应
|
|
326
|
+
started/ended/final snapshot 均为 None,非空 Curve 对应均存在;
|
|
327
|
+
- final snapshot 的 as_of / total_value 与最后 EquityPoint 一致;
|
|
328
|
+
- limitations 只允许 ResultLimitation 权威 code,拒绝重复,按 Enum
|
|
329
|
+
定义顺序规范化,且必需的 CompletedTrade limitation 不得遗漏。
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
model_config = ConfigDict(frozen=True)
|
|
333
|
+
|
|
334
|
+
status: BacktestStatus = Field(..., description="最终状态(MVP 仅 COMPLETED)")
|
|
335
|
+
config: BacktestConfig = Field(..., description="配置快照")
|
|
336
|
+
data_revisions: tuple[BacktestDataRevision, ...] = Field(
|
|
337
|
+
default_factory=tuple,
|
|
338
|
+
description="复权/公司行动版本钉住;版本变化天然失效旧缓存",
|
|
339
|
+
)
|
|
340
|
+
started_event_time: datetime | None = Field(default=None, description="首个成功片时间")
|
|
341
|
+
ended_event_time: datetime | None = Field(default=None, description="最后成功片时间")
|
|
342
|
+
final_portfolio_snapshot: PortfolioSnapshot | None = Field(
|
|
343
|
+
default=None, description="最后成功片的组合投影(数据结束时持仓如实保留)"
|
|
344
|
+
)
|
|
345
|
+
order_snapshots: tuple[OrderSnapshot, ...] = Field(
|
|
346
|
+
default_factory=tuple, description="最终订单状态(含数据结束时仍有效的订单)"
|
|
347
|
+
)
|
|
348
|
+
# T4R R4 finding 4:SETTLING 阶段的结构化生命周期迁移(coarse PENDING
|
|
349
|
+
# DAY 单 EXPIRED_AT_BACKTEST_END)独立披露——不伪造成市场事件或成交,
|
|
350
|
+
# event_time = ended_event_time,为空 tuple 表示无到期发生
|
|
351
|
+
settling_order_updates: tuple[OrderTransition, ...] = Field(
|
|
352
|
+
default_factory=tuple,
|
|
353
|
+
description="回测终止阶段的结构化订单生命周期迁移(可审计;无则为空)",
|
|
354
|
+
)
|
|
355
|
+
fills: tuple[Fill, ...] = Field(default_factory=tuple, description="全部 Fill(成功片)")
|
|
356
|
+
position_lots: tuple[PositionLotSnapshot, ...] = Field(
|
|
357
|
+
default_factory=tuple, description="最终持仓批次投影(含已耗尽批次)"
|
|
358
|
+
)
|
|
359
|
+
cash_ledger_entries: tuple[CashLedgerEntry, ...] = Field(
|
|
360
|
+
default_factory=tuple, description="现金与应收账户的 append-only 流水"
|
|
361
|
+
)
|
|
362
|
+
reservation_ledger_entries: tuple[OrderReservationLedgerEntry, ...] = Field(
|
|
363
|
+
default_factory=tuple, description="订单现金与证券冻结的 append-only 流水"
|
|
364
|
+
)
|
|
365
|
+
closed_trades: tuple[ClosedTrade, ...] = Field(
|
|
366
|
+
default_factory=tuple, description="FIFO 开平配对事实"
|
|
367
|
+
)
|
|
368
|
+
execution_attempts: tuple[ExecutionAttempt, ...] = Field(
|
|
369
|
+
default_factory=tuple, description="全部撮合检查(成功片)"
|
|
370
|
+
)
|
|
371
|
+
equity_curve: EquityCurve = Field(..., description="权益曲线(每成功片一点)")
|
|
372
|
+
metrics: BacktestMetrics = Field(..., description="基础指标")
|
|
373
|
+
decision_trace: DecisionTrace = Field(..., description="决策轨迹(append-only)")
|
|
374
|
+
rebalance_plans: tuple[RebalancePlanSnapshot, ...] = () # 历史结果读取,不驱动新运行
|
|
375
|
+
portfolio_decisions: tuple[PortfolioDecision, ...] = ()
|
|
376
|
+
|
|
377
|
+
limitations: tuple[ResultLimitation, ...] = Field(
|
|
378
|
+
default_factory=tuple, description="能力/数据限制(权威 ResultLimitation,稳定 code)"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
@computed_field
|
|
382
|
+
@property
|
|
383
|
+
def frequency_plan(self) -> BacktestFrequencyPlan:
|
|
384
|
+
execution = self.config.execution_frequency or self.config.frequency
|
|
385
|
+
|
|
386
|
+
def source(frequency: BarFrequency) -> BarSourceMode:
|
|
387
|
+
return (
|
|
388
|
+
BarSourceMode.AGGREGATED
|
|
389
|
+
if frequency in (BarFrequency.H1, BarFrequency.W1)
|
|
390
|
+
else BarSourceMode.NATIVE
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return BacktestFrequencyPlan(
|
|
394
|
+
observation_frequency=self.config.frequency,
|
|
395
|
+
execution_frequency=execution,
|
|
396
|
+
observation_source=source(self.config.frequency),
|
|
397
|
+
execution_source=source(execution),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@field_validator("status")
|
|
401
|
+
@classmethod
|
|
402
|
+
def _check_completed_only(cls, status: BacktestStatus) -> BacktestStatus:
|
|
403
|
+
if status is not BacktestStatus.COMPLETED:
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"BacktestResult 只表达 COMPLETED 结果(FAILED 由 BacktestRun.status 表达),"
|
|
406
|
+
f"实际 {status}"
|
|
407
|
+
)
|
|
408
|
+
return status
|
|
409
|
+
|
|
410
|
+
@field_validator("limitations", mode="before")
|
|
411
|
+
@classmethod
|
|
412
|
+
def _normalize_limitations(cls, value: object) -> object:
|
|
413
|
+
"""权威类型契约:接受 Enum 成员或其稳定字符串 value,统一保存为
|
|
414
|
+
ResultLimitation 成员;未知 code、重复(成员与字符串等价)拒绝;
|
|
415
|
+
按 Enum 定义顺序规范化。"""
|
|
416
|
+
if not isinstance(value, (tuple, list)):
|
|
417
|
+
return value
|
|
418
|
+
members: list[ResultLimitation] = []
|
|
419
|
+
for item in value:
|
|
420
|
+
if isinstance(item, ResultLimitation):
|
|
421
|
+
members.append(item)
|
|
422
|
+
else:
|
|
423
|
+
try:
|
|
424
|
+
members.append(ResultLimitation(item))
|
|
425
|
+
except ValueError:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f"limitations 只允许 ResultLimitation 权威 code,未知:{item!r}"
|
|
428
|
+
) from None
|
|
429
|
+
if len(set(members)) != len(members):
|
|
430
|
+
raise ValueError(f"limitations 不允许重复:{tuple(value)}")
|
|
431
|
+
order = {item: i for i, item in enumerate(ResultLimitation)}
|
|
432
|
+
return tuple(sorted(members, key=order.__getitem__))
|
|
433
|
+
|
|
434
|
+
@model_validator(mode="after")
|
|
435
|
+
def _check_consistency(self) -> "BacktestResult":
|
|
436
|
+
points = self.equity_curve.points
|
|
437
|
+
if not points:
|
|
438
|
+
empty_fields = (
|
|
439
|
+
self.started_event_time,
|
|
440
|
+
self.ended_event_time,
|
|
441
|
+
self.final_portfolio_snapshot,
|
|
442
|
+
)
|
|
443
|
+
if any(field is not None for field in empty_fields):
|
|
444
|
+
raise ValueError("空 EquityCurve 对应 started/ended/final snapshot 必须均为 None")
|
|
445
|
+
else:
|
|
446
|
+
first, last = points[0], points[-1]
|
|
447
|
+
if self.started_event_time != first.time or self.ended_event_time != last.time:
|
|
448
|
+
raise ValueError("started/ended_event_time 必须与 EquityCurve 首尾一致")
|
|
449
|
+
snapshot = self.final_portfolio_snapshot
|
|
450
|
+
if snapshot is None:
|
|
451
|
+
raise ValueError("非空 EquityCurve 对应 final_portfolio_snapshot 必须存在")
|
|
452
|
+
if snapshot.as_of != last.time or snapshot.total_value != last.total_value:
|
|
453
|
+
raise ValueError(
|
|
454
|
+
f"final snapshot(as_of={snapshot.as_of}, "
|
|
455
|
+
f"total_value={snapshot.total_value})必须与最后 EquityPoint"
|
|
456
|
+
f"({last.time}, {last.total_value})一致"
|
|
457
|
+
)
|
|
458
|
+
if self.metrics.fill_count != len(self.fills):
|
|
459
|
+
raise ValueError(
|
|
460
|
+
f"metrics.fill_count {self.metrics.fill_count} 必须等于 "
|
|
461
|
+
f"len(fills) {len(self.fills)}"
|
|
462
|
+
)
|
|
463
|
+
ledger_limitations = {
|
|
464
|
+
ResultLimitation.TRADE_COUNT_UNAVAILABLE,
|
|
465
|
+
ResultLimitation.WIN_RATE_UNAVAILABLE,
|
|
466
|
+
ResultLimitation.PRE_TAX_DIVIDEND,
|
|
467
|
+
}
|
|
468
|
+
if self.closed_trades and ledger_limitations & set(self.limitations):
|
|
469
|
+
raise ValueError("已有 ClosedTrade 账本时不得披露旧账本限制")
|
|
470
|
+
if not self.cash_ledger_entries:
|
|
471
|
+
missing = ledger_limitations - set(self.limitations)
|
|
472
|
+
if missing:
|
|
473
|
+
raise ValueError(
|
|
474
|
+
"必需的 CompletedTrade limitation 不得遗漏:"
|
|
475
|
+
f"{sorted(member.name for member in missing)}"
|
|
476
|
+
)
|
|
477
|
+
return self
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""唯一历史运行组装:本地与管理入口共享,计算不依赖管理数据库。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from quant_core.app.backtest.backtest_input_schema import BacktestMarketComponents
|
|
6
|
+
from quant_core.app.backtest.backtest_run import BacktestRun
|
|
7
|
+
from quant_core.app.backtest.historical_clock import HistoricalClock
|
|
8
|
+
from quant_core.app.backtest.schema import BacktestConfig, BacktestDataRevision, BacktestResult
|
|
9
|
+
from quant_core.app.factor.interface.public.factor_query import IFactorQuery
|
|
10
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
11
|
+
from quant_core.app.order_execution.order_execution_dependency import (
|
|
12
|
+
build_order_api,
|
|
13
|
+
build_simulated_execution,
|
|
14
|
+
)
|
|
15
|
+
from quant_core.app.portfolio.portfolio_dependency import (
|
|
16
|
+
build_order_account_listener,
|
|
17
|
+
build_simulation_account,
|
|
18
|
+
)
|
|
19
|
+
from quant_core.app.strategy.interface.public.strategy import IStrategy
|
|
20
|
+
from quant_core.app.strategy.strategy_dependency import build_strategy_runtime
|
|
21
|
+
from quant_core.app.trading_runtime.runtime_session import RuntimeSession
|
|
22
|
+
from quant_core.app.trading_runtime.strategy_portfolio_adapter import StrategyPortfolioReaderAdapter
|
|
23
|
+
from quant_core.app.trading_runtime.trading_rule_book import EffectiveTradingRuleBook
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def execute_backtest(
|
|
27
|
+
config: BacktestConfig,
|
|
28
|
+
strategy: IStrategy,
|
|
29
|
+
components: BacktestMarketComponents,
|
|
30
|
+
*,
|
|
31
|
+
slippage_bps: Decimal = Decimal(0),
|
|
32
|
+
factors: IFactorQuery | None = None,
|
|
33
|
+
factor_refs: tuple[FactorIdentity, ...] = (),
|
|
34
|
+
factor_aliases: dict[str, FactorIdentity] | None = None,
|
|
35
|
+
factor_revision: str | None = None,
|
|
36
|
+
) -> BacktestResult:
|
|
37
|
+
"""消费已准备行情和显式因子查询,返回交易结果,不联网或持久化。"""
|
|
38
|
+
if slippage_bps < 0 or not slippage_bps.is_finite():
|
|
39
|
+
raise ValueError("滑点必须为非负有限值")
|
|
40
|
+
if factor_refs and factors is None:
|
|
41
|
+
raise ValueError("声明因子依赖时必须提供显式查询")
|
|
42
|
+
market = components.query
|
|
43
|
+
listings = {
|
|
44
|
+
key: (await market.get_instrument(key)).listed_date for key in config.instrument_ids
|
|
45
|
+
}
|
|
46
|
+
rules = EffectiveTradingRuleBook(
|
|
47
|
+
await market.list_trading_rules(
|
|
48
|
+
list(config.instrument_ids), config.start.date(), config.end.date()
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
portfolio = build_simulation_account("backtest-application", config.initial_cash, config.start)
|
|
52
|
+
execution = build_simulated_execution(
|
|
53
|
+
slippage_bps, config.execution_policy, build_order_account_listener(portfolio)
|
|
54
|
+
)
|
|
55
|
+
order_api = build_order_api(
|
|
56
|
+
execution, portfolio, rules, config.execution_policy, config.auto_switch_order_value
|
|
57
|
+
)
|
|
58
|
+
runtime = build_strategy_runtime(
|
|
59
|
+
strategy=strategy,
|
|
60
|
+
market=market,
|
|
61
|
+
factors=factors,
|
|
62
|
+
factor_aliases=factor_aliases,
|
|
63
|
+
identities=factor_refs,
|
|
64
|
+
dataset_revision=factor_revision,
|
|
65
|
+
listing_dates={key: value for key, value in listings.items() if value is not None},
|
|
66
|
+
portfolio=StrategyPortfolioReaderAdapter(portfolio),
|
|
67
|
+
orders=execution,
|
|
68
|
+
order_api=order_api,
|
|
69
|
+
adjustment_type=config.strategy_adjustment_type,
|
|
70
|
+
frequency=config.frequency,
|
|
71
|
+
)
|
|
72
|
+
clock = HistoricalClock()
|
|
73
|
+
session = RuntimeSession(
|
|
74
|
+
clock=clock,
|
|
75
|
+
market_state_resolver=components.market_state_resolver,
|
|
76
|
+
exchange_state_resolver=components.exchange_state_resolver,
|
|
77
|
+
order_execution=execution,
|
|
78
|
+
portfolio=portfolio,
|
|
79
|
+
strategy_runtime=runtime,
|
|
80
|
+
order_api=order_api,
|
|
81
|
+
)
|
|
82
|
+
run = BacktestRun(
|
|
83
|
+
config=config,
|
|
84
|
+
runtime_session=session,
|
|
85
|
+
order_execution=execution,
|
|
86
|
+
slice_source=components.slice_source,
|
|
87
|
+
verification=components.verification,
|
|
88
|
+
clock=clock,
|
|
89
|
+
ledger=portfolio,
|
|
90
|
+
input_revisions=(
|
|
91
|
+
(
|
|
92
|
+
BacktestDataRevision(
|
|
93
|
+
dataset="market_snapshot",
|
|
94
|
+
instrument_id="*",
|
|
95
|
+
frequency=config.frequency.value,
|
|
96
|
+
revision=components.query.revision,
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
+ (
|
|
100
|
+
(
|
|
101
|
+
BacktestDataRevision(
|
|
102
|
+
dataset="factor_value",
|
|
103
|
+
instrument_id="*",
|
|
104
|
+
frequency=factor_refs[0].frequency.value,
|
|
105
|
+
revision=factor_revision,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
if factor_revision is not None
|
|
109
|
+
else ()
|
|
110
|
+
)
|
|
111
|
+
),
|
|
112
|
+
listing_dates={key: value for key, value in listings.items() if value is not None},
|
|
113
|
+
)
|
|
114
|
+
return await run.run()
|
|
File without changes
|