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,108 @@
|
|
|
1
|
+
"""One market slice's validated execution result."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from quant_core.app.order_execution.constants import ExecutionAttemptOutcome
|
|
7
|
+
from quant_core.app.order_execution.domain.order_event import (
|
|
8
|
+
ExecutionAttempt,
|
|
9
|
+
Fill,
|
|
10
|
+
OrderTransition,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _check_tz_aware(value: datetime, field: str) -> datetime:
|
|
15
|
+
if value.tzinfo is None:
|
|
16
|
+
raise ValueError(f"{field} 必须 timezone-aware(Canonical 市场时间带时区)")
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class MarketSliceProcessingResult:
|
|
22
|
+
"""一个时间片的订单撮合批次结果(结构化,不混用通用 error 字符串)
|
|
23
|
+
|
|
24
|
+
process_market_slice 只产生撮合结果;不修改 Portfolio——
|
|
25
|
+
调用方必须显式把 Fill 传给 IPortfolioUpdate.apply_fill。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
slice_time: datetime
|
|
29
|
+
fills: tuple[Fill, ...] = ()
|
|
30
|
+
execution_attempts: tuple[ExecutionAttempt, ...] = ()
|
|
31
|
+
order_updates: tuple[OrderTransition, ...] = ()
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
_check_tz_aware(self.slice_time, "slice_time")
|
|
35
|
+
self._check_result_time_consistency()
|
|
36
|
+
self._check_batch_reference_consistency()
|
|
37
|
+
|
|
38
|
+
def _check_result_time_consistency(self) -> None:
|
|
39
|
+
"""「本时间片撮合结果」:内部事件时间必须精确等于 slice_time"""
|
|
40
|
+
for fill in self.fills:
|
|
41
|
+
if fill.fill_time != self.slice_time:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Fill.fill_time {fill.fill_time} 必须等于 slice_time {self.slice_time}"
|
|
44
|
+
)
|
|
45
|
+
for attempt in self.execution_attempts:
|
|
46
|
+
if attempt.event_time != self.slice_time:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"ExecutionAttempt.event_time {attempt.event_time} 必须等于 "
|
|
49
|
+
f"slice_time {self.slice_time}"
|
|
50
|
+
)
|
|
51
|
+
for update in self.order_updates:
|
|
52
|
+
if update.event_time != self.slice_time:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"OrderUpdate.event_time {update.event_time} 必须等于 "
|
|
55
|
+
f"slice_time {self.slice_time}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _check_batch_reference_consistency(self) -> None:
|
|
59
|
+
"""批次一致性:技术 ID 唯一,FILLED attempt 与 Fill 一一对应"""
|
|
60
|
+
fill_ids = [f.fill_id for f in self.fills]
|
|
61
|
+
if len(fill_ids) != len(set(fill_ids)):
|
|
62
|
+
raise ValueError("fills 内 fill_id 不允许重复")
|
|
63
|
+
attempt_ids = [a.attempt_id for a in self.execution_attempts]
|
|
64
|
+
if len(attempt_ids) != len(set(attempt_ids)):
|
|
65
|
+
raise ValueError("execution_attempts 内 attempt_id 不允许重复")
|
|
66
|
+
|
|
67
|
+
fills_by_id = {f.fill_id: f for f in self.fills}
|
|
68
|
+
filled_attempt_fill_ids: list[str] = []
|
|
69
|
+
for attempt in self.execution_attempts:
|
|
70
|
+
if attempt.outcome is ExecutionAttemptOutcome.FILLED:
|
|
71
|
+
if attempt.fill_id is None:
|
|
72
|
+
raise ValueError("FILLED attempt 必须携带 fill_id")
|
|
73
|
+
if attempt.fill_id not in fills_by_id:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"FILLED attempt 引用的 fill_id {attempt.fill_id} 不存在于本批 fills"
|
|
76
|
+
)
|
|
77
|
+
if fills_by_id[attempt.fill_id].order_id != attempt.order_id:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"attempt.order_id {attempt.order_id} 与被引用 Fill.order_id "
|
|
80
|
+
f"{fills_by_id[attempt.fill_id].order_id} 不一致"
|
|
81
|
+
)
|
|
82
|
+
filled_attempt_fill_ids.append(attempt.fill_id)
|
|
83
|
+
# DEFERRED attempt 不携带 fill_id(由 ExecutionAttempt 自身校验保证)
|
|
84
|
+
if len(filled_attempt_fill_ids) != len(set(filled_attempt_fill_ids)):
|
|
85
|
+
raise ValueError("每个 Fill 只允许被一个 FILLED attempt 引用")
|
|
86
|
+
# 双向一一对应:比较集合本身,不依赖列表长度巧合
|
|
87
|
+
orphan = set(fill_ids) - set(filled_attempt_fill_ids)
|
|
88
|
+
if orphan:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"每个 Fill 必须恰好被一个 FILLED attempt 引用,"
|
|
91
|
+
f"以下 fill_id 缺少解释来源:{sorted(orphan)}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
update_order_ids = [u.order_id for u in self.order_updates]
|
|
95
|
+
if any(not oid for oid in update_order_ids):
|
|
96
|
+
raise ValueError("order_updates 内 order_id 不允许为空")
|
|
97
|
+
latest_by_order: dict[str, OrderTransition] = {}
|
|
98
|
+
for update in self.order_updates:
|
|
99
|
+
previous = latest_by_order.get(update.order_id)
|
|
100
|
+
if previous is not None and (
|
|
101
|
+
previous.current_status is not update.previous_status
|
|
102
|
+
or previous.cumulative_filled_quantity != update.previous_cumulative_filled_quantity
|
|
103
|
+
):
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"同片订单迁移链不连续:{update.order_id} "
|
|
106
|
+
f"{previous.current_status} → {update.previous_status}"
|
|
107
|
+
)
|
|
108
|
+
latest_by_order[update.order_id] = update
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""订单聚合与状态迁移。
|
|
2
|
+
|
|
3
|
+
本模块只依赖 Python 标准库和领域枚举,不依赖 Pydantic、入口协议或持久化框架。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from quant_core.app.order_execution.constants import (
|
|
10
|
+
OrderStatus,
|
|
11
|
+
OrderUpdateReason,
|
|
12
|
+
)
|
|
13
|
+
from quant_core.app.order_execution.domain.order_event import OrderTransition
|
|
14
|
+
from quant_core.app.order_execution.domain.order_value import OrderIntent, OrderSnapshot
|
|
15
|
+
|
|
16
|
+
ORDER_TRANSITIONS = {
|
|
17
|
+
OrderStatus.PENDING: frozenset(
|
|
18
|
+
{
|
|
19
|
+
OrderStatus.PARTIALLY_FILLED,
|
|
20
|
+
OrderStatus.FILLED,
|
|
21
|
+
OrderStatus.CANCELLED,
|
|
22
|
+
OrderStatus.EXPIRED,
|
|
23
|
+
}
|
|
24
|
+
),
|
|
25
|
+
OrderStatus.PARTIALLY_FILLED: frozenset(
|
|
26
|
+
{
|
|
27
|
+
OrderStatus.PARTIALLY_FILLED,
|
|
28
|
+
OrderStatus.FILLED,
|
|
29
|
+
OrderStatus.CANCELLED,
|
|
30
|
+
OrderStatus.EXPIRED,
|
|
31
|
+
}
|
|
32
|
+
),
|
|
33
|
+
OrderStatus.FILLED: frozenset(),
|
|
34
|
+
OrderStatus.CANCELLED: frozenset(),
|
|
35
|
+
OrderStatus.EXPIRED: frozenset(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
TERMINAL_ORDER_STATUSES = frozenset(
|
|
39
|
+
{OrderStatus.FILLED, OrderStatus.CANCELLED, OrderStatus.EXPIRED}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_terminal_order_status(status: OrderStatus) -> bool:
|
|
44
|
+
return status in TERMINAL_ORDER_STATUSES
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_REASONS = {
|
|
48
|
+
OrderStatus.PARTIALLY_FILLED: frozenset({OrderUpdateReason.PARTIALLY_FILLED}),
|
|
49
|
+
OrderStatus.FILLED: frozenset({OrderUpdateReason.FILLED}),
|
|
50
|
+
OrderStatus.EXPIRED: frozenset(
|
|
51
|
+
{
|
|
52
|
+
OrderUpdateReason.EXPIRED_AT_SESSION_CLOSE,
|
|
53
|
+
OrderUpdateReason.EXPIRED_AT_COARSE_SESSION_END,
|
|
54
|
+
OrderUpdateReason.EXPIRED_AT_BACKTEST_END,
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
OrderStatus.CANCELLED: frozenset(
|
|
58
|
+
{
|
|
59
|
+
OrderUpdateReason.CANCELLED,
|
|
60
|
+
OrderUpdateReason.INSUFFICIENT_CASH_AT_EXECUTION,
|
|
61
|
+
OrderUpdateReason.INSUFFICIENT_AVAILABLE_POSITION_AT_EXECUTION,
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_order_transition(previous: OrderStatus, current: OrderStatus) -> None:
|
|
68
|
+
if previous is current and current is not OrderStatus.PARTIALLY_FILLED:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"订单状态无变化不允许产生迁移:{previous} → {current}(无变化的 OrderUpdate 属于伪造)"
|
|
71
|
+
)
|
|
72
|
+
if current not in ORDER_TRANSITIONS[previous]:
|
|
73
|
+
raise ValueError(f"非法订单状态迁移:{previous} → {current}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_transition(update: OrderTransition) -> None:
|
|
77
|
+
validate_order_transition(update.previous_status, update.current_status)
|
|
78
|
+
previous = update.previous_cumulative_filled_quantity
|
|
79
|
+
current = update.cumulative_filled_quantity
|
|
80
|
+
quantity = update.order_quantity
|
|
81
|
+
if update.event_time.tzinfo is None:
|
|
82
|
+
raise ValueError("event_time 必须 timezone-aware(Canonical 市场时间带时区)")
|
|
83
|
+
if update.previous_status is OrderStatus.PENDING and previous != 0:
|
|
84
|
+
raise ValueError(f"PENDING 前态累计成交必须为 0,实际 {previous}")
|
|
85
|
+
if update.previous_status is OrderStatus.PARTIALLY_FILLED and not 0 < previous < quantity:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"PARTIALLY_FILLED 前态累计成交必须满足 0 < 前态累计 < 订单数量,"
|
|
88
|
+
f"实际 {previous}/{quantity}"
|
|
89
|
+
)
|
|
90
|
+
if previous > current:
|
|
91
|
+
raise ValueError(f"累计成交数量不允许倒退:{previous} → {current}")
|
|
92
|
+
if current > quantity:
|
|
93
|
+
raise ValueError(f"累计成交 {current} 超过订单数量 {quantity}")
|
|
94
|
+
if update.current_status is OrderStatus.PARTIALLY_FILLED:
|
|
95
|
+
if update.previous_status is OrderStatus.PENDING and not 0 < current < quantity:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"PENDING → PARTIALLY_FILLED 要求 0 < 累计成交 < 订单数量,"
|
|
98
|
+
f"实际 {current}/{quantity}"
|
|
99
|
+
)
|
|
100
|
+
if update.previous_status is OrderStatus.PARTIALLY_FILLED and current == previous:
|
|
101
|
+
raise ValueError("PARTIALLY_FILLED 自环要求累计成交严格增加(数量不增长属伪造)")
|
|
102
|
+
if update.current_status is OrderStatus.FILLED and current != quantity:
|
|
103
|
+
raise ValueError(f"FILLED 要求累计成交等于订单数量 {quantity},实际 {current}")
|
|
104
|
+
if (
|
|
105
|
+
update.current_status in (OrderStatus.CANCELLED, OrderStatus.EXPIRED)
|
|
106
|
+
and current != previous
|
|
107
|
+
):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"{update.current_status} 终态迁移不得伪造新的成交量:{previous} → {current}"
|
|
110
|
+
)
|
|
111
|
+
if current == 0 and update.average_fill_price is not None:
|
|
112
|
+
raise ValueError("累计成交为 0 时 average_fill_price 必须为 None")
|
|
113
|
+
if current > 0 and update.average_fill_price is None:
|
|
114
|
+
raise ValueError("累计成交 > 0 时必须携带正的 Decimal average_fill_price")
|
|
115
|
+
if update.average_fill_price is not None and update.average_fill_price <= 0:
|
|
116
|
+
raise ValueError("average_fill_price 必须为正 Decimal")
|
|
117
|
+
if update.average_fill_price is not None and not update.average_fill_price.is_finite():
|
|
118
|
+
raise ValueError("average_fill_price 必须是有限 Decimal")
|
|
119
|
+
if update.reason_code not in _REASONS[update.current_status]:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"reason_code {update.reason_code} 与目标状态 {update.current_status} 不一致"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Order:
|
|
126
|
+
"""订单生命周期聚合;迁移验证成功后才修改状态。"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
order_id: str,
|
|
131
|
+
intent: OrderIntent,
|
|
132
|
+
created_event_time: datetime,
|
|
133
|
+
submission_sequence: int,
|
|
134
|
+
) -> None:
|
|
135
|
+
if not order_id or order_id.strip() != order_id or any(ch.isspace() for ch in order_id):
|
|
136
|
+
raise ValueError("order_id 不允许为空或包含空白字符")
|
|
137
|
+
if created_event_time.tzinfo is None:
|
|
138
|
+
raise ValueError("created_event_time 必须 timezone-aware(Canonical 市场时间带时区)")
|
|
139
|
+
if created_event_time < intent.creation_event_time:
|
|
140
|
+
raise ValueError("created_event_time 不得早于 intent.creation_event_time")
|
|
141
|
+
if submission_sequence < 0:
|
|
142
|
+
raise ValueError("submission_sequence 必须 >= 0")
|
|
143
|
+
self.order_id = order_id
|
|
144
|
+
self.intent = intent
|
|
145
|
+
self.status = OrderStatus.PENDING
|
|
146
|
+
self.created_event_time = created_event_time
|
|
147
|
+
self.submission_sequence = submission_sequence
|
|
148
|
+
self.filled_quantity = 0
|
|
149
|
+
self.average_fill_price: Decimal | None = None
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def remaining_quantity(self) -> int:
|
|
153
|
+
return self.intent.quantity - self.filled_quantity
|
|
154
|
+
|
|
155
|
+
def transition(
|
|
156
|
+
self,
|
|
157
|
+
current_status: OrderStatus,
|
|
158
|
+
event_time: datetime,
|
|
159
|
+
reason_code: OrderUpdateReason,
|
|
160
|
+
cumulative_filled_quantity: int | None = None,
|
|
161
|
+
average_fill_price: Decimal | None = None,
|
|
162
|
+
) -> OrderTransition:
|
|
163
|
+
if isinstance(average_fill_price, float):
|
|
164
|
+
raise ValueError(
|
|
165
|
+
"average_fill_price 必须使用 Decimal,禁止 float(避免二进制浮点误差)"
|
|
166
|
+
)
|
|
167
|
+
update = OrderTransition(
|
|
168
|
+
order_id=self.order_id,
|
|
169
|
+
order_quantity=self.intent.quantity,
|
|
170
|
+
previous_status=self.status,
|
|
171
|
+
current_status=current_status,
|
|
172
|
+
previous_cumulative_filled_quantity=self.filled_quantity,
|
|
173
|
+
cumulative_filled_quantity=self.filled_quantity
|
|
174
|
+
if cumulative_filled_quantity is None
|
|
175
|
+
else cumulative_filled_quantity,
|
|
176
|
+
average_fill_price=self.average_fill_price
|
|
177
|
+
if average_fill_price is None
|
|
178
|
+
else average_fill_price,
|
|
179
|
+
event_time=event_time,
|
|
180
|
+
reason_code=reason_code,
|
|
181
|
+
)
|
|
182
|
+
validate_transition(update)
|
|
183
|
+
self.status = current_status
|
|
184
|
+
self.filled_quantity = update.cumulative_filled_quantity
|
|
185
|
+
self.average_fill_price = update.average_fill_price
|
|
186
|
+
return update
|
|
187
|
+
|
|
188
|
+
def snapshot(self) -> OrderSnapshot:
|
|
189
|
+
return OrderSnapshot(
|
|
190
|
+
self.order_id,
|
|
191
|
+
self.intent,
|
|
192
|
+
self.status,
|
|
193
|
+
self.created_event_time,
|
|
194
|
+
self.submission_sequence,
|
|
195
|
+
self.filled_quantity,
|
|
196
|
+
self.average_fill_price,
|
|
197
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""订单执行领域已经发生的不可变事实。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from quant_core.app.order_execution.constants import (
|
|
9
|
+
ExecutionAttemptOutcome,
|
|
10
|
+
ExecutionDeferReason,
|
|
11
|
+
OrderSide,
|
|
12
|
+
OrderStatus,
|
|
13
|
+
OrderUpdateReason,
|
|
14
|
+
)
|
|
15
|
+
from quant_core.app.order_execution.domain.order_value import FeeBreakdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class OrderTransition:
|
|
20
|
+
order_id: str
|
|
21
|
+
order_quantity: int
|
|
22
|
+
previous_status: OrderStatus
|
|
23
|
+
current_status: OrderStatus
|
|
24
|
+
previous_cumulative_filled_quantity: int
|
|
25
|
+
cumulative_filled_quantity: int
|
|
26
|
+
event_time: datetime
|
|
27
|
+
reason_code: OrderUpdateReason
|
|
28
|
+
average_fill_price: Decimal | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class Fill:
|
|
33
|
+
"""Portfolio 因交易而变化的唯一不可变输入。"""
|
|
34
|
+
|
|
35
|
+
order_id: str
|
|
36
|
+
instrument_id: str
|
|
37
|
+
side: OrderSide
|
|
38
|
+
quantity: int
|
|
39
|
+
price: Decimal
|
|
40
|
+
fees: FeeBreakdown
|
|
41
|
+
fill_time: datetime
|
|
42
|
+
fill_id: str = field(default_factory=lambda: str(uuid4()))
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if not self.fill_id or not self.order_id:
|
|
46
|
+
raise ValueError("fill_id 和 order_id 不允许为空")
|
|
47
|
+
if not isinstance(self.side, OrderSide):
|
|
48
|
+
raise ValueError("side 必须是 OrderSide")
|
|
49
|
+
if self.quantity <= 0:
|
|
50
|
+
raise ValueError("成交数量必须 > 0")
|
|
51
|
+
if isinstance(self.price, float) or not self.price.is_finite() or self.price <= 0:
|
|
52
|
+
raise ValueError("成交价必须是正的有限 Decimal")
|
|
53
|
+
if self.fill_time.tzinfo is None:
|
|
54
|
+
raise ValueError("fill_time 必须 timezone-aware")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class ExecutionAttempt:
|
|
59
|
+
"""一次实际撮合检查的不可变事实。"""
|
|
60
|
+
|
|
61
|
+
order_id: str
|
|
62
|
+
event_time: datetime
|
|
63
|
+
outcome: ExecutionAttemptOutcome
|
|
64
|
+
defer_reason: ExecutionDeferReason | None = None
|
|
65
|
+
market_event_refs: tuple[str, ...] = ()
|
|
66
|
+
fill_id: str | None = None
|
|
67
|
+
attempt_id: str = field(default_factory=lambda: str(uuid4()))
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
if self.event_time.tzinfo is None:
|
|
71
|
+
raise ValueError("event_time 必须 timezone-aware")
|
|
72
|
+
normalized_refs = tuple(sorted(self.market_event_refs))
|
|
73
|
+
if len(normalized_refs) != len(set(normalized_refs)) or any(
|
|
74
|
+
not ref for ref in normalized_refs
|
|
75
|
+
):
|
|
76
|
+
raise ValueError("market_event_refs 不允许空值或重复")
|
|
77
|
+
object.__setattr__(self, "market_event_refs", normalized_refs)
|
|
78
|
+
if self.outcome is ExecutionAttemptOutcome.DEFERRED:
|
|
79
|
+
if self.defer_reason is None:
|
|
80
|
+
raise ValueError("DEFERRED attempt 必须携带 defer_reason")
|
|
81
|
+
if self.fill_id is not None:
|
|
82
|
+
raise ValueError("DEFERRED attempt 不得携带 fill_id")
|
|
83
|
+
else:
|
|
84
|
+
if not self.fill_id:
|
|
85
|
+
raise ValueError("FILLED attempt 必须携带 fill_id")
|
|
86
|
+
if self.defer_reason is not None:
|
|
87
|
+
raise ValueError("FILLED attempt 不得携带 defer_reason")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""订单执行领域的可替换 Policy 契约。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from quant_core.app.data.schema.trading_state_schema import InstrumentMarketState
|
|
7
|
+
from quant_core.app.order_execution.constants import OrderSide
|
|
8
|
+
from quant_core.app.order_execution.domain.order_value import (
|
|
9
|
+
FeeBreakdown,
|
|
10
|
+
OrderSnapshot,
|
|
11
|
+
TradabilityDecision,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class FeeModelProtocol(Protocol):
|
|
17
|
+
def compute(self, side: OrderSide, quantity: int, fill_price: Decimal) -> FeeBreakdown: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class SlippageModelProtocol(Protocol):
|
|
22
|
+
def apply(self, side: OrderSide, bar_open: Decimal) -> Decimal: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class TradabilityPolicyProtocol(Protocol):
|
|
27
|
+
def decide(
|
|
28
|
+
self, order: OrderSnapshot, market_state: InstrumentMarketState
|
|
29
|
+
) -> TradabilityDecision: ...
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""订单执行领域的不可变值对象。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from quant_core.app.order_execution.constants import (
|
|
9
|
+
ExecutionDeferReason,
|
|
10
|
+
ExecutionPolicy,
|
|
11
|
+
OrderAckStatus,
|
|
12
|
+
OrderRejectReason,
|
|
13
|
+
OrderSide,
|
|
14
|
+
OrderStatus,
|
|
15
|
+
OrderType,
|
|
16
|
+
TimeInForce,
|
|
17
|
+
)
|
|
18
|
+
from quant_core.app.order_execution.constants import TradabilityDecision as TradabilityDecisionKind
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class OrderIntent:
|
|
23
|
+
"""已经完成仓位计算和下单前约束检查的不可变下单意图。"""
|
|
24
|
+
|
|
25
|
+
strategy_id: str
|
|
26
|
+
instrument_id: str
|
|
27
|
+
side: OrderSide
|
|
28
|
+
quantity: int
|
|
29
|
+
creation_event_time: datetime
|
|
30
|
+
intent_id: str = field(default_factory=lambda: str(uuid4()))
|
|
31
|
+
order_type: OrderType = OrderType.MARKET
|
|
32
|
+
limit_price: Decimal | None = None
|
|
33
|
+
execution_policy: ExecutionPolicy = ExecutionPolicy.NEXT_BAR_OPEN
|
|
34
|
+
time_in_force: TimeInForce = TimeInForce.DAY
|
|
35
|
+
signal_refs: tuple[str, ...] = ()
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
_identifier(self.intent_id, "intent_id")
|
|
39
|
+
_identifier(self.strategy_id, "strategy_id")
|
|
40
|
+
if not isinstance(self.side, OrderSide):
|
|
41
|
+
raise ValueError("side 必须是 OrderSide")
|
|
42
|
+
parts = self.instrument_id.split(".")
|
|
43
|
+
if len(parts) != 3 or any(not part for part in parts):
|
|
44
|
+
raise ValueError("instrument_id 必须是 Canonical 标识(market.exchange.symbol)")
|
|
45
|
+
if self.quantity <= 0:
|
|
46
|
+
raise ValueError("quantity 必须 > 0")
|
|
47
|
+
if self.creation_event_time.tzinfo is None:
|
|
48
|
+
raise ValueError("creation_event_time 必须 timezone-aware")
|
|
49
|
+
if self.limit_price is not None and (
|
|
50
|
+
isinstance(self.limit_price, float)
|
|
51
|
+
or not self.limit_price.is_finite()
|
|
52
|
+
or self.limit_price <= 0
|
|
53
|
+
):
|
|
54
|
+
raise ValueError("limit_price 必须是正的有限 Decimal")
|
|
55
|
+
if self.order_type is OrderType.LIMIT and self.limit_price is None:
|
|
56
|
+
raise ValueError("LIMIT OrderIntent 必须提供 limit_price")
|
|
57
|
+
if self.order_type is OrderType.MARKET and self.limit_price is not None:
|
|
58
|
+
raise ValueError("MARKET OrderIntent 不允许提供 limit_price")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class OrderAck:
|
|
63
|
+
"""提交意图后的接受或拒绝事实。"""
|
|
64
|
+
|
|
65
|
+
intent_id: str
|
|
66
|
+
status: OrderAckStatus
|
|
67
|
+
event_time: datetime
|
|
68
|
+
order_id: str | None = None
|
|
69
|
+
rejection_reason: OrderRejectReason | None = None
|
|
70
|
+
ack_id: str = field(default_factory=lambda: str(uuid4()))
|
|
71
|
+
|
|
72
|
+
def __post_init__(self) -> None:
|
|
73
|
+
_identifier(self.ack_id, "ack_id")
|
|
74
|
+
_identifier(self.intent_id, "intent_id")
|
|
75
|
+
if self.event_time.tzinfo is None:
|
|
76
|
+
raise ValueError("event_time 必须 timezone-aware")
|
|
77
|
+
if self.status is OrderAckStatus.ACCEPTED:
|
|
78
|
+
if self.order_id is None:
|
|
79
|
+
raise ValueError("ACCEPTED 的 OrderAck 必须携带 order_id")
|
|
80
|
+
_identifier(self.order_id, "order_id")
|
|
81
|
+
if self.rejection_reason is not None:
|
|
82
|
+
raise ValueError("ACCEPTED 的 OrderAck 不允许携带 rejection_reason")
|
|
83
|
+
elif self.order_id is not None or self.rejection_reason is None:
|
|
84
|
+
raise ValueError("REJECTED 的 OrderAck 必须仅携带 rejection_reason")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class OrderSnapshot:
|
|
89
|
+
"""订单聚合的不可变只读投影。"""
|
|
90
|
+
|
|
91
|
+
order_id: str
|
|
92
|
+
intent: OrderIntent
|
|
93
|
+
status: OrderStatus
|
|
94
|
+
created_event_time: datetime
|
|
95
|
+
submission_sequence: int
|
|
96
|
+
filled_quantity: int
|
|
97
|
+
average_fill_price: Decimal | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _identifier(value: str, field_name: str) -> None:
|
|
101
|
+
if not value or value.strip() != value or any(char.isspace() for char in value):
|
|
102
|
+
raise ValueError(f"{field_name} 不允许为空或包含空白字符")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class FeeBreakdown:
|
|
107
|
+
"""可核对的不可变成交费用值对象。"""
|
|
108
|
+
|
|
109
|
+
commission: Decimal
|
|
110
|
+
stamp_tax: Decimal
|
|
111
|
+
transfer_fee: Decimal
|
|
112
|
+
other: Decimal
|
|
113
|
+
total: Decimal
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
values = (self.commission, self.stamp_tax, self.transfer_fee, self.other, self.total)
|
|
117
|
+
if any(isinstance(value, float) for value in values):
|
|
118
|
+
raise ValueError("费用分项必须使用 Decimal,禁止 float")
|
|
119
|
+
if any(not value.is_finite() or value < 0 for value in values):
|
|
120
|
+
raise ValueError("费用分项必须是非负有限 Decimal")
|
|
121
|
+
computed = self.commission + self.stamp_tax + self.transfer_fee + self.other
|
|
122
|
+
if self.total != computed:
|
|
123
|
+
raise ValueError(f"FeeBreakdown.total {self.total} 必须严格等于分项之和 {computed}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class TradabilityDecision:
|
|
128
|
+
"""可交易性 Policy 产生的纯领域决策。"""
|
|
129
|
+
|
|
130
|
+
decision: TradabilityDecisionKind
|
|
131
|
+
defer_reason: ExecutionDeferReason | None = None
|
|
132
|
+
evidence_event_refs: tuple[str, ...] = ()
|
|
133
|
+
|
|
134
|
+
def __post_init__(self) -> None:
|
|
135
|
+
refs = tuple(sorted(self.evidence_event_refs))
|
|
136
|
+
if len(refs) != len(set(refs)) or any(not ref for ref in refs):
|
|
137
|
+
raise ValueError("evidence_event_refs 不允许空值或重复")
|
|
138
|
+
object.__setattr__(self, "evidence_event_refs", refs)
|
|
139
|
+
if self.decision is TradabilityDecisionKind.DEFER and self.defer_reason is None:
|
|
140
|
+
raise ValueError("DEFER 决策必须携带 defer_reason")
|
|
141
|
+
if self.decision is TradabilityDecisionKind.ALLOW and self.defer_reason is not None:
|
|
142
|
+
raise ValueError("ALLOW 决策不允许携带 defer_reason")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Fee Model - 可注入费用模型(T5)
|
|
2
|
+
|
|
3
|
+
SSOT:T5 Task「Mock Fee / Slippage 参数(2026-09-04 用户确认)」。
|
|
4
|
+
|
|
5
|
+
- FeeModelProtocol 是 order_execution 消费方拥有的最小消费面;
|
|
6
|
+
SimulationBroker 只依赖 Protocol,不硬编码任何费率。
|
|
7
|
+
- StandardFeeModel:Mock 确定性默认配置(佣金万三、最低 5 元、卖出印花税万五、
|
|
8
|
+
过户费 0),仅用于 Mock 回测可复现,不代表所有券商和历史时期;
|
|
9
|
+
未来按回测日期/市场/账户替换模型即可。
|
|
10
|
+
- 全程 Decimal;构造边界拒绝 float 与非有限值(含字符串形式);
|
|
11
|
+
不 quantize(与 T3 Portfolio「不二次舍入」裁定一致)。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from typing import cast
|
|
16
|
+
|
|
17
|
+
from quant_core.app.order_execution.constants import OrderSide
|
|
18
|
+
from quant_core.app.order_execution.domain.order_value import FeeBreakdown
|
|
19
|
+
from quant_core.app.order_execution.validators import reject_float_amount
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StandardFeeModel:
|
|
23
|
+
"""Mock 费用模型:commission = max(notional × rate, minimum);印花税仅 SELL"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
commission_rate: Decimal,
|
|
28
|
+
minimum_commission: Decimal,
|
|
29
|
+
sell_stamp_tax_rate: Decimal,
|
|
30
|
+
transfer_fee_rate: Decimal,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.commission_rate = self._check_ratio(commission_rate, "commission_rate")
|
|
33
|
+
self.minimum_commission = self._check_ratio(minimum_commission, "minimum_commission")
|
|
34
|
+
self.sell_stamp_tax_rate = self._check_ratio(sell_stamp_tax_rate, "sell_stamp_tax_rate")
|
|
35
|
+
self.transfer_fee_rate = self._check_ratio(transfer_fee_rate, "transfer_fee_rate")
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _check_ratio(value: object, field: str) -> Decimal:
|
|
39
|
+
checked = reject_float_amount(value, field)
|
|
40
|
+
ratio = checked if isinstance(checked, Decimal) else Decimal(cast(int | str, checked))
|
|
41
|
+
if not ratio.is_finite():
|
|
42
|
+
raise ValueError(f"{field} 必须是有限 Decimal,禁止字符串形式的 NaN / Infinity / sNaN")
|
|
43
|
+
if ratio < 0:
|
|
44
|
+
raise ValueError(f"{field} 必须 >= 0,实际 {ratio}")
|
|
45
|
+
return ratio
|
|
46
|
+
|
|
47
|
+
def compute(self, side: OrderSide, quantity: int, fill_price: Decimal) -> FeeBreakdown:
|
|
48
|
+
reject_float_amount(fill_price, "fill_price")
|
|
49
|
+
notional = Decimal(quantity) * fill_price
|
|
50
|
+
commission = max(notional * self.commission_rate, self.minimum_commission)
|
|
51
|
+
stamp_tax = notional * self.sell_stamp_tax_rate if side is OrderSide.SELL else Decimal("0")
|
|
52
|
+
transfer_fee = notional * self.transfer_fee_rate
|
|
53
|
+
other = Decimal("0")
|
|
54
|
+
return FeeBreakdown(
|
|
55
|
+
commission=commission,
|
|
56
|
+
stamp_tax=stamp_tax,
|
|
57
|
+
transfer_fee=transfer_fee,
|
|
58
|
+
other=other,
|
|
59
|
+
total=commission + stamp_tax + transfer_fee + other,
|
|
60
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""下单服务消费的账户契约。"""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from quant_core.app.order_execution.interface.public.execution_account import IExecutionReservation
|
|
6
|
+
from quant_core.app.portfolio.portfolio_schema import PortfolioSnapshot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IOrderAccount(IExecutionReservation, Protocol):
|
|
10
|
+
def snapshot(self) -> PortfolioSnapshot: ...
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""下单服务消费的交易规则契约。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from quant_core.app.data.schema.market_schema import TradingRuleResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IOrderTradingRules(Protocol):
|
|
10
|
+
def resolve(self, instrument_id: str, at: date | datetime) -> TradingRuleResponse: ...
|
|
File without changes
|