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,883 @@
|
|
|
1
|
+
"""模拟 Broker:同步接单、冻结事件、订单生命周期与 Bar 撮合。
|
|
2
|
+
|
|
3
|
+
定价由 BarMatcher 完成;成交与终态事件交给账户监听器入账和释放冻结。
|
|
4
|
+
NEXT_BAR_OPEN 严格使用后续 Bar,CURRENT_BAR_CLOSE 可在回调内撮合;
|
|
5
|
+
同一 Bar 的成交量限制在多次下单之间共享。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from decimal import ROUND_FLOOR, Decimal
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from quant_core.app.data.constants import INSTRUMENT_ID_SEPARATOR
|
|
14
|
+
from quant_core.app.data.schema.event_schema import (
|
|
15
|
+
BarEvent,
|
|
16
|
+
CoarseBarExecutionContext,
|
|
17
|
+
MarketTimeSlice,
|
|
18
|
+
)
|
|
19
|
+
from quant_core.app.data.schema.exchange_operating_state import ExchangeOperatingState
|
|
20
|
+
from quant_core.app.data.schema.exchange_state_schema import ExchangePhase
|
|
21
|
+
from quant_core.app.data.schema.trading_state_schema import (
|
|
22
|
+
InstrumentMarketState,
|
|
23
|
+
TradingState,
|
|
24
|
+
)
|
|
25
|
+
from quant_core.app.order_execution.bar_matcher import BarMatcher
|
|
26
|
+
from quant_core.app.order_execution.constants import (
|
|
27
|
+
ExecutionAttemptOutcome,
|
|
28
|
+
ExecutionDeferReason,
|
|
29
|
+
ExecutionPolicy,
|
|
30
|
+
OrderAckStatus,
|
|
31
|
+
OrderRejectReason,
|
|
32
|
+
OrderSide,
|
|
33
|
+
OrderStatus,
|
|
34
|
+
OrderType,
|
|
35
|
+
OrderUpdateReason,
|
|
36
|
+
TimeInForce,
|
|
37
|
+
TradabilityDecision,
|
|
38
|
+
)
|
|
39
|
+
from quant_core.app.order_execution.domain.execution_batch import MarketSliceProcessingResult
|
|
40
|
+
from quant_core.app.order_execution.domain.order_entity import Order
|
|
41
|
+
from quant_core.app.order_execution.domain.order_event import (
|
|
42
|
+
ExecutionAttempt,
|
|
43
|
+
Fill,
|
|
44
|
+
OrderTransition,
|
|
45
|
+
)
|
|
46
|
+
from quant_core.app.order_execution.domain.order_policy import (
|
|
47
|
+
FeeModelProtocol,
|
|
48
|
+
SlippageModelProtocol,
|
|
49
|
+
TradabilityPolicyProtocol,
|
|
50
|
+
)
|
|
51
|
+
from quant_core.app.order_execution.domain.order_value import (
|
|
52
|
+
FeeBreakdown,
|
|
53
|
+
OrderAck,
|
|
54
|
+
OrderIntent,
|
|
55
|
+
OrderSnapshot,
|
|
56
|
+
)
|
|
57
|
+
from quant_core.app.order_execution.domain.order_value import (
|
|
58
|
+
TradabilityDecision as TradabilityDecisionRecord,
|
|
59
|
+
)
|
|
60
|
+
from quant_core.app.order_execution.interface.public.execution_account import (
|
|
61
|
+
IExecutionAccountView,
|
|
62
|
+
IExecutionReservation,
|
|
63
|
+
)
|
|
64
|
+
from quant_core.app.order_execution.interface.public.order_events import IOrderEvents
|
|
65
|
+
from quant_core.app.order_execution.validators import reject_float_amount
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def instrument_exchange(instrument_id: str) -> str:
|
|
69
|
+
"""Canonical instrument_id 的 Exchange 归属(权威格式 {market}.{exchange}.{symbol})
|
|
70
|
+
|
|
71
|
+
复用 market 域文档化分隔符契约;不猜测、不硬编码市场。
|
|
72
|
+
"""
|
|
73
|
+
parts = instrument_id.split(INSTRUMENT_ID_SEPARATOR)
|
|
74
|
+
if len(parts) != 3 or not parts[1]:
|
|
75
|
+
raise ValueError(f"无法从 Canonical instrument_id 解析 exchange:{instrument_id!r}")
|
|
76
|
+
return parts[1]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _attempt_event_refs(bar_event_id: str, decision: TradabilityDecisionRecord) -> tuple[str, ...]:
|
|
80
|
+
"""Attempt 引用 = eligible Bar + Policy 决策证据(合并、去重、字典序稳定)
|
|
81
|
+
|
|
82
|
+
Engine 只合并/透传,不解释状态事实、不根据原因码或 Policy 类型推断引用。
|
|
83
|
+
"""
|
|
84
|
+
return tuple(sorted({bar_event_id, *decision.evidence_event_refs}))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _processing_sort_key(order: Order) -> tuple[datetime, int, str]:
|
|
88
|
+
"""片内稳定业务排序键:creation_event_time → submission_sequence → instrument_id"""
|
|
89
|
+
return (order.intent.creation_event_time, order.submission_sequence, order.intent.instrument_id)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _extract_slice_bars(time_slice: MarketTimeSlice) -> dict[str, BarEvent]:
|
|
93
|
+
"""从切片提取每标的一根 Bar(矛盾输入即报错)
|
|
94
|
+
|
|
95
|
+
T4R:切片内的 Session 事件不再由本引擎解释——交易所运行状态完全来自
|
|
96
|
+
Runtime 注入的 ExchangeStateResolver 投影(唯一 SSOT)。
|
|
97
|
+
"""
|
|
98
|
+
bars: dict[str, BarEvent] = {}
|
|
99
|
+
for event in time_slice.events:
|
|
100
|
+
if isinstance(event, BarEvent):
|
|
101
|
+
bar = event.payload
|
|
102
|
+
if bar.instrument_id in bars:
|
|
103
|
+
raise ValueError(f"同一时间片出现多根 {bar.instrument_id} Bar,属矛盾输入")
|
|
104
|
+
bars[bar.instrument_id] = event
|
|
105
|
+
return bars
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SimulationBroker:
|
|
109
|
+
"""确定性模拟撮合引擎;结构化满足 IOrderExecution
|
|
110
|
+
|
|
111
|
+
构造注入:TradabilityPolicyProtocol + FeeModelProtocol + SlippageModelProtocol;
|
|
112
|
+
引擎不硬编码具体 Policy、费率、滑点、市场字符串或 Runtime mode。
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
tradability_policy: TradabilityPolicyProtocol,
|
|
118
|
+
fee_model: FeeModelProtocol,
|
|
119
|
+
slippage_model: SlippageModelProtocol,
|
|
120
|
+
max_volume_participation: Decimal = Decimal("0.10"),
|
|
121
|
+
market_price_buffer_ratio: Decimal = Decimal("0"),
|
|
122
|
+
reservation_enabled: bool = True,
|
|
123
|
+
events: IOrderEvents | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
self._events = events
|
|
126
|
+
self._policy = tradability_policy
|
|
127
|
+
self._fee_model = fee_model
|
|
128
|
+
self._matcher = BarMatcher(slippage_model)
|
|
129
|
+
checked_participation = reject_float_amount(
|
|
130
|
+
max_volume_participation, "max_volume_participation"
|
|
131
|
+
)
|
|
132
|
+
self._max_volume_participation = Decimal(str(checked_participation))
|
|
133
|
+
if not Decimal("0") < self._max_volume_participation <= Decimal("1"):
|
|
134
|
+
raise ValueError("max_volume_participation 必须位于 (0, 1]")
|
|
135
|
+
checked_buffer = reject_float_amount(market_price_buffer_ratio, "market_price_buffer_ratio")
|
|
136
|
+
self._market_price_buffer_ratio = Decimal(str(checked_buffer))
|
|
137
|
+
if self._market_price_buffer_ratio < 0:
|
|
138
|
+
raise ValueError("market_price_buffer_ratio 必须 >= 0")
|
|
139
|
+
self._reservation_enabled = reservation_enabled
|
|
140
|
+
self._orders: dict[str, Order] = {}
|
|
141
|
+
self._intent_ids: set[str] = set()
|
|
142
|
+
self._next_sequence = 0
|
|
143
|
+
self._last_slice_time: datetime | None = None
|
|
144
|
+
# T4R R3/R4:粗粒度(coarse)生命周期簿记——只记 coarse 决策点接受的
|
|
145
|
+
# 订单 ID。机会消耗(R4 finding 3)改为片内局部集合确定性派生:
|
|
146
|
+
# 机会只影响当前片,不保留任何跨片可泄漏状态。
|
|
147
|
+
self._coarse_order_ids: set[str] = set() # coarse 决策点提交的订单
|
|
148
|
+
self._charged_fees: dict[str, FeeBreakdown] = {}
|
|
149
|
+
self._bar_volume_used: dict[str, int] = {}
|
|
150
|
+
self._current_slice: MarketTimeSlice | None = None
|
|
151
|
+
self._current_market_states: Mapping[str, InstrumentMarketState] = {}
|
|
152
|
+
self._current_exchange_states: Mapping[str, ExchangeOperatingState] = {}
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
# IOrderExecution
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def submit(
|
|
159
|
+
self,
|
|
160
|
+
intents: tuple[OrderIntent, ...],
|
|
161
|
+
exchange_states: Mapping[str, ExchangeOperatingState],
|
|
162
|
+
execution_context: CoarseBarExecutionContext | None = None,
|
|
163
|
+
) -> tuple[OrderAck, ...]:
|
|
164
|
+
return self._submit(
|
|
165
|
+
intents,
|
|
166
|
+
exchange_states,
|
|
167
|
+
execution_context=execution_context,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def estimate_transaction_cost(self, side: OrderSide, quantity: int, price: Decimal) -> Decimal:
|
|
171
|
+
"""费用估算与成交共用注入模型。"""
|
|
172
|
+
return self._fee_model.compute(side, quantity, price).total
|
|
173
|
+
|
|
174
|
+
def estimate_order_cash(self, intent: OrderIntent, reference_price: Decimal) -> Decimal:
|
|
175
|
+
"""下单预算与冻结共用费用和价格口径。"""
|
|
176
|
+
if intent.side is OrderSide.SELL:
|
|
177
|
+
return Decimal(0)
|
|
178
|
+
price = (
|
|
179
|
+
intent.limit_price
|
|
180
|
+
if intent.limit_price is not None
|
|
181
|
+
else reference_price * (Decimal(1) + self._market_price_buffer_ratio)
|
|
182
|
+
)
|
|
183
|
+
return (
|
|
184
|
+
Decimal(intent.quantity) * price
|
|
185
|
+
+ self._fee_model.compute(intent.side, intent.quantity, price).total
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def submit_with_reservation(
|
|
189
|
+
self,
|
|
190
|
+
intents: tuple[OrderIntent, ...],
|
|
191
|
+
exchange_states: Mapping[str, ExchangeOperatingState],
|
|
192
|
+
account_view: IExecutionReservation,
|
|
193
|
+
reference_prices: Mapping[str, Decimal],
|
|
194
|
+
execution_context: CoarseBarExecutionContext | None = None,
|
|
195
|
+
) -> tuple[OrderAck, ...]:
|
|
196
|
+
if not self._reservation_enabled:
|
|
197
|
+
return self.submit(intents, exchange_states, execution_context)
|
|
198
|
+
return self._submit(
|
|
199
|
+
intents,
|
|
200
|
+
exchange_states,
|
|
201
|
+
account_view=account_view,
|
|
202
|
+
reference_prices=reference_prices,
|
|
203
|
+
execution_context=execution_context,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _submit(
|
|
207
|
+
self,
|
|
208
|
+
intents: tuple[OrderIntent, ...],
|
|
209
|
+
exchange_states: Mapping[str, ExchangeOperatingState],
|
|
210
|
+
account_view: IExecutionReservation | None = None,
|
|
211
|
+
reference_prices: Mapping[str, Decimal] | None = None,
|
|
212
|
+
execution_context: CoarseBarExecutionContext | None = None,
|
|
213
|
+
) -> tuple[OrderAck, ...]:
|
|
214
|
+
"""提交意图批次:保持输入顺序逐条处理(确定性),每条恰一个 Ack
|
|
215
|
+
|
|
216
|
+
execution_context(T4R R3 方案 A):粗粒度 decision point 的**有界**
|
|
217
|
+
执行资格——Intent 的交易所必须落在 context.eligible_exchanges 内;
|
|
218
|
+
范围外与投影不放行一致走 SESSION_CLOSED 拒绝。它不是交易所运行
|
|
219
|
+
状态事实,不与 ExchangePhase 混用。
|
|
220
|
+
"""
|
|
221
|
+
acks: list[OrderAck] = []
|
|
222
|
+
batch_seen: set[str] = set()
|
|
223
|
+
for intent in intents:
|
|
224
|
+
event_time = intent.creation_event_time
|
|
225
|
+
if intent.intent_id in self._intent_ids or intent.intent_id in batch_seen:
|
|
226
|
+
acks.append(
|
|
227
|
+
OrderAck(
|
|
228
|
+
intent_id=intent.intent_id,
|
|
229
|
+
status=OrderAckStatus.REJECTED,
|
|
230
|
+
rejection_reason=OrderRejectReason.DUPLICATE_INTENT,
|
|
231
|
+
event_time=event_time,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
continue
|
|
235
|
+
reason = self._static_reject_reason(intent)
|
|
236
|
+
if reason is not None:
|
|
237
|
+
acks.append(
|
|
238
|
+
OrderAck(
|
|
239
|
+
intent_id=intent.intent_id,
|
|
240
|
+
status=OrderAckStatus.REJECTED,
|
|
241
|
+
rejection_reason=reason,
|
|
242
|
+
event_time=event_time,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
continue
|
|
246
|
+
# 报单准入(T4R R4 finding 2:授权来源互斥,绝不混合):
|
|
247
|
+
# - execution_context 存在(coarse 决策片):只按 Context scope
|
|
248
|
+
# 判定——范围外即使注入 OPEN 投影也 REJECTED;
|
|
249
|
+
# - execution_context 为 None(日内片):只按 ExchangeState 投影
|
|
250
|
+
# 判定——order_entry_allowed 不允许(BREAK/CLOSED/未初始化
|
|
251
|
+
# 安全默认)→ SESSION_CLOSED 拒绝。
|
|
252
|
+
# 与其他 REJECTED 一致不创建 Order、不登记 intent_id
|
|
253
|
+
# (同一 Intent 开市恢复后可重新提交)
|
|
254
|
+
exchange_state = exchange_states.get(instrument_exchange(intent.instrument_id))
|
|
255
|
+
if execution_context is not None:
|
|
256
|
+
in_coarse_scope = (
|
|
257
|
+
instrument_exchange(intent.instrument_id)
|
|
258
|
+
in execution_context.eligible_exchanges
|
|
259
|
+
)
|
|
260
|
+
allowed = in_coarse_scope
|
|
261
|
+
else:
|
|
262
|
+
in_coarse_scope = False
|
|
263
|
+
allowed = exchange_state is not None and exchange_state.order_entry_allowed
|
|
264
|
+
if not allowed:
|
|
265
|
+
acks.append(
|
|
266
|
+
OrderAck(
|
|
267
|
+
intent_id=intent.intent_id,
|
|
268
|
+
status=OrderAckStatus.REJECTED,
|
|
269
|
+
rejection_reason=OrderRejectReason.SESSION_CLOSED,
|
|
270
|
+
event_time=event_time,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
continue
|
|
274
|
+
batch_seen.add(intent.intent_id)
|
|
275
|
+
order_id = f"order-{uuid4().hex}"
|
|
276
|
+
reference_price = (
|
|
277
|
+
reference_prices.get(intent.instrument_id) if reference_prices is not None else None
|
|
278
|
+
)
|
|
279
|
+
cash_amount = Decimal("0")
|
|
280
|
+
if account_view is not None and intent.side is OrderSide.BUY:
|
|
281
|
+
reservation_price = intent.limit_price or reference_price
|
|
282
|
+
if reservation_price is None:
|
|
283
|
+
acks.append(
|
|
284
|
+
OrderAck(
|
|
285
|
+
intent_id=intent.intent_id,
|
|
286
|
+
status=OrderAckStatus.REJECTED,
|
|
287
|
+
rejection_reason=OrderRejectReason.MISSING_REFERENCE_PRICE,
|
|
288
|
+
event_time=event_time,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
continue
|
|
292
|
+
cash_amount = self.estimate_order_cash(intent, reservation_price)
|
|
293
|
+
reserved = True
|
|
294
|
+
if self._events is not None:
|
|
295
|
+
reserved = self._events.publish_pending_new(order_id, intent, cash_amount)
|
|
296
|
+
elif account_view is not None:
|
|
297
|
+
reserved = account_view.reserve_order(
|
|
298
|
+
order_id,
|
|
299
|
+
intent.instrument_id,
|
|
300
|
+
intent.side,
|
|
301
|
+
intent.quantity,
|
|
302
|
+
cash_amount,
|
|
303
|
+
event_time,
|
|
304
|
+
)
|
|
305
|
+
if not reserved:
|
|
306
|
+
acks.append(
|
|
307
|
+
OrderAck(
|
|
308
|
+
intent_id=intent.intent_id,
|
|
309
|
+
status=OrderAckStatus.REJECTED,
|
|
310
|
+
rejection_reason=OrderRejectReason.RESERVATION_FAILED,
|
|
311
|
+
event_time=event_time,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
continue
|
|
315
|
+
self._intent_ids.add(intent.intent_id)
|
|
316
|
+
order = Order(
|
|
317
|
+
order_id=order_id,
|
|
318
|
+
intent=intent,
|
|
319
|
+
# 冻结契约下唯一确定的确定性问题时间:created >= intent.creation_event_time
|
|
320
|
+
created_event_time=intent.creation_event_time,
|
|
321
|
+
submission_sequence=self._next_sequence,
|
|
322
|
+
)
|
|
323
|
+
self._next_sequence += 1
|
|
324
|
+
self._orders[order_id] = order
|
|
325
|
+
if in_coarse_scope: # 仅真正通过 coarse scope 接受的订单为 coarse origin
|
|
326
|
+
self._coarse_order_ids.add(order_id)
|
|
327
|
+
acks.append(
|
|
328
|
+
OrderAck(
|
|
329
|
+
intent_id=intent.intent_id,
|
|
330
|
+
status=OrderAckStatus.ACCEPTED,
|
|
331
|
+
order_id=order_id,
|
|
332
|
+
event_time=event_time,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
return tuple(acks)
|
|
336
|
+
|
|
337
|
+
def process_market_slice(
|
|
338
|
+
self,
|
|
339
|
+
time_slice: MarketTimeSlice,
|
|
340
|
+
market_states: Mapping[str, InstrumentMarketState],
|
|
341
|
+
exchange_states: Mapping[str, ExchangeOperatingState],
|
|
342
|
+
account_view: IExecutionAccountView,
|
|
343
|
+
) -> MarketSliceProcessingResult:
|
|
344
|
+
"""消费一个时间片:撮合此前已生效订单(新单本片绝不参与撮合)
|
|
345
|
+
|
|
346
|
+
粗粒度片(time_slice.execution_context 非空,方案 A):matching
|
|
347
|
+
由 execution_context 显式放行;且不做 DAY 日内到期判定——日线片
|
|
348
|
+
无「日内收盘」语义,到期归属后续粗粒度到期语义(见报告剩余限制)。
|
|
349
|
+
"""
|
|
350
|
+
self._check_slice_time(time_slice.event_time)
|
|
351
|
+
self._bar_volume_used.clear()
|
|
352
|
+
self._current_slice = time_slice
|
|
353
|
+
self._current_market_states = market_states
|
|
354
|
+
self._current_exchange_states = exchange_states
|
|
355
|
+
return self._match(time_slice, market_states, exchange_states, account_view)
|
|
356
|
+
|
|
357
|
+
def match_current_slice(
|
|
358
|
+
self, account_view: IExecutionAccountView
|
|
359
|
+
) -> MarketSliceProcessingResult:
|
|
360
|
+
"""当前 Bar 模式在下单调用内撮合,累计成交量限制跨多次下单共享。"""
|
|
361
|
+
if self._current_slice is None:
|
|
362
|
+
raise RuntimeError("撮合前必须注入当前时间片")
|
|
363
|
+
return self._match(
|
|
364
|
+
self._current_slice,
|
|
365
|
+
self._current_market_states,
|
|
366
|
+
self._current_exchange_states,
|
|
367
|
+
account_view,
|
|
368
|
+
immediate=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _match(
|
|
372
|
+
self,
|
|
373
|
+
time_slice: MarketTimeSlice,
|
|
374
|
+
market_states: Mapping[str, InstrumentMarketState],
|
|
375
|
+
exchange_states: Mapping[str, ExchangeOperatingState],
|
|
376
|
+
account_view: IExecutionAccountView,
|
|
377
|
+
*,
|
|
378
|
+
immediate: bool = False,
|
|
379
|
+
) -> MarketSliceProcessingResult:
|
|
380
|
+
bars = _extract_slice_bars(time_slice)
|
|
381
|
+
# R3 finding 1:有界资格——scope 内才放行,绝不全局旁路
|
|
382
|
+
context = time_slice.execution_context
|
|
383
|
+
|
|
384
|
+
def _in_coarse_scope(instrument_id: str) -> bool:
|
|
385
|
+
return context is not None and (
|
|
386
|
+
instrument_exchange(instrument_id) in context.eligible_exchanges
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
pending = sorted(
|
|
390
|
+
(
|
|
391
|
+
o
|
|
392
|
+
for o in self._orders.values()
|
|
393
|
+
if o.status in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}
|
|
394
|
+
),
|
|
395
|
+
key=_processing_sort_key,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
fills: list[Fill] = []
|
|
399
|
+
attempts: list[ExecutionAttempt] = []
|
|
400
|
+
updates: list[OrderTransition] = []
|
|
401
|
+
# R4 finding 3:机会消耗为片内局部集合(成功末尾无需提交、失败即
|
|
402
|
+
# 随局部作用域丢弃);只允许 coarse origin 订单进入
|
|
403
|
+
opportunity_used_now: set[str] = set()
|
|
404
|
+
# 片内预算(执行期临时,不修改 Portfolio)
|
|
405
|
+
remaining_cash = account_view.available_cash()
|
|
406
|
+
remaining_available: dict[str, int] = {}
|
|
407
|
+
remaining_volume: dict[str, int] = {}
|
|
408
|
+
|
|
409
|
+
# 先完成全部可失败计算(定价/费用/预算/Attempt/Fill 构造),
|
|
410
|
+
# 收集迁移指令;任何失败发生在任何 Order 聚合变更之前。
|
|
411
|
+
transitions: list[
|
|
412
|
+
tuple[Order, OrderStatus, OrderUpdateReason, int | None, Decimal | None]
|
|
413
|
+
] = []
|
|
414
|
+
# 本片已被撮合/取消的订单(迁移在批末统一应用,聚合状态尚未变更)
|
|
415
|
+
matched_order_ids: set[str] = set()
|
|
416
|
+
staged_charged_fees: dict[str, FeeBreakdown] = {}
|
|
417
|
+
for order in pending:
|
|
418
|
+
intent = order.intent
|
|
419
|
+
bar_event = bars.get(intent.instrument_id)
|
|
420
|
+
if immediate and intent.execution_policy is not ExecutionPolicy.CURRENT_BAR_CLOSE:
|
|
421
|
+
continue
|
|
422
|
+
if (
|
|
423
|
+
bar_event is None
|
|
424
|
+
or bar_event.event_time < intent.creation_event_time
|
|
425
|
+
or (
|
|
426
|
+
bar_event.event_time == intent.creation_event_time
|
|
427
|
+
and intent.execution_policy is not ExecutionPolicy.CURRENT_BAR_CLOSE
|
|
428
|
+
)
|
|
429
|
+
):
|
|
430
|
+
continue # 无 Bar / 尚不 eligible:未发生实际检查,不产生 Attempt
|
|
431
|
+
# 撮合准入(R4 finding 2:授权来源互斥,同 submit):
|
|
432
|
+
# - coarse 片:只按 Context scope 判定;
|
|
433
|
+
# - 日内片:只按注入投影 matching_allowed(BREAK/CLOSED/未初始化
|
|
434
|
+
# 安全默认)判定 → DEFERRED,Order 保持 PENDING,恢复后重试。
|
|
435
|
+
exchange_state = exchange_states.get(instrument_exchange(intent.instrument_id))
|
|
436
|
+
if context is not None:
|
|
437
|
+
matching_allowed = _in_coarse_scope(intent.instrument_id)
|
|
438
|
+
else:
|
|
439
|
+
matching_allowed = exchange_state is not None and exchange_state.matching_allowed
|
|
440
|
+
if not matching_allowed:
|
|
441
|
+
attempts.append(
|
|
442
|
+
ExecutionAttempt(
|
|
443
|
+
order_id=order.order_id,
|
|
444
|
+
event_time=time_slice.event_time,
|
|
445
|
+
outcome=ExecutionAttemptOutcome.DEFERRED,
|
|
446
|
+
defer_reason=ExecutionDeferReason.SESSION_NOT_TRADABLE,
|
|
447
|
+
market_event_refs=(bar_event.event_id,),
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
continue
|
|
451
|
+
market_state = market_states.get(
|
|
452
|
+
intent.instrument_id,
|
|
453
|
+
InstrumentMarketState(
|
|
454
|
+
instrument_id=intent.instrument_id,
|
|
455
|
+
trading_state_as_of=None,
|
|
456
|
+
trading_state=TradingState.UNKNOWN,
|
|
457
|
+
price_limit=None,
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
# coarse 机会标记(R3 方案 A / R4 finding 3 局部化):有 eligible
|
|
461
|
+
# Bar 且通过 scope gate 的机会片已真实处理本 coarse origin 订单
|
|
462
|
+
# (无论后续 FILLED/DEFER/CANCEL)——机会即已消耗。只记片内局部
|
|
463
|
+
# 集合,且只允许 coarse origin(日内订单永不 coarse-expire)。
|
|
464
|
+
if (
|
|
465
|
+
context is not None
|
|
466
|
+
and intent.time_in_force is TimeInForce.DAY
|
|
467
|
+
and order.order_id in self._coarse_order_ids
|
|
468
|
+
):
|
|
469
|
+
opportunity_used_now.add(order.order_id)
|
|
470
|
+
decision = self._policy.decide(order.snapshot(), market_state)
|
|
471
|
+
if decision.decision is TradabilityDecision.DEFER:
|
|
472
|
+
attempts.append(
|
|
473
|
+
ExecutionAttempt(
|
|
474
|
+
order_id=order.order_id,
|
|
475
|
+
event_time=time_slice.event_time,
|
|
476
|
+
outcome=ExecutionAttemptOutcome.DEFERRED,
|
|
477
|
+
defer_reason=decision.defer_reason,
|
|
478
|
+
market_event_refs=_attempt_event_refs(bar_event.event_id, decision),
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
bar = bar_event.payload
|
|
484
|
+
fill_price = self._matcher.execution_price(order.snapshot(), bar)
|
|
485
|
+
if fill_price is None:
|
|
486
|
+
attempts.append(
|
|
487
|
+
ExecutionAttempt(
|
|
488
|
+
order_id=order.order_id,
|
|
489
|
+
event_time=time_slice.event_time,
|
|
490
|
+
outcome=ExecutionAttemptOutcome.DEFERRED,
|
|
491
|
+
defer_reason=(
|
|
492
|
+
ExecutionDeferReason.INVALID_DAILY_VWAP
|
|
493
|
+
if intent.execution_policy is ExecutionPolicy.DAILY_VWAP
|
|
494
|
+
else ExecutionDeferReason.LIMIT_NOT_TOUCHED
|
|
495
|
+
),
|
|
496
|
+
market_event_refs=_attempt_event_refs(bar_event.event_id, decision),
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
continue
|
|
500
|
+
if intent.instrument_id not in remaining_volume:
|
|
501
|
+
remaining_volume[intent.instrument_id] = max(
|
|
502
|
+
0,
|
|
503
|
+
int(
|
|
504
|
+
(Decimal(bar.volume) * self._max_volume_participation).to_integral_value(
|
|
505
|
+
rounding=ROUND_FLOOR
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
- self._bar_volume_used.get(intent.instrument_id, 0),
|
|
509
|
+
)
|
|
510
|
+
fill_quantity = min(order.remaining_quantity, remaining_volume[intent.instrument_id])
|
|
511
|
+
if fill_quantity == 0:
|
|
512
|
+
attempts.append(
|
|
513
|
+
ExecutionAttempt(
|
|
514
|
+
order_id=order.order_id,
|
|
515
|
+
event_time=time_slice.event_time,
|
|
516
|
+
outcome=ExecutionAttemptOutcome.DEFERRED,
|
|
517
|
+
defer_reason=ExecutionDeferReason.INSUFFICIENT_BAR_LIQUIDITY,
|
|
518
|
+
market_event_refs=_attempt_event_refs(bar_event.event_id, decision),
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
continue
|
|
522
|
+
if intent.side is OrderSide.SELL:
|
|
523
|
+
reserved_quantity = getattr(account_view, "reserved_quantity", lambda _order_id: 0)(
|
|
524
|
+
order.order_id
|
|
525
|
+
)
|
|
526
|
+
if reserved_quantity:
|
|
527
|
+
executable_quantity = reserved_quantity
|
|
528
|
+
else:
|
|
529
|
+
if intent.instrument_id not in remaining_available:
|
|
530
|
+
remaining_available[intent.instrument_id] = account_view.available_quantity(
|
|
531
|
+
intent.instrument_id
|
|
532
|
+
)
|
|
533
|
+
executable_quantity = remaining_available[intent.instrument_id]
|
|
534
|
+
if fill_quantity > executable_quantity:
|
|
535
|
+
transitions.append(
|
|
536
|
+
(
|
|
537
|
+
order,
|
|
538
|
+
OrderStatus.CANCELLED,
|
|
539
|
+
OrderUpdateReason.INSUFFICIENT_AVAILABLE_POSITION_AT_EXECUTION,
|
|
540
|
+
None,
|
|
541
|
+
None,
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
matched_order_ids.add(order.order_id)
|
|
545
|
+
continue
|
|
546
|
+
fees, cumulative_fees = self._incremental_fees(order, fill_quantity, fill_price)
|
|
547
|
+
notional = Decimal(fill_quantity) * fill_price
|
|
548
|
+
if intent.side is OrderSide.BUY:
|
|
549
|
+
required = notional + fees.total
|
|
550
|
+
reserved_cash = getattr(
|
|
551
|
+
account_view, "reserved_cash", lambda _order_id: Decimal("0")
|
|
552
|
+
)(order.order_id)
|
|
553
|
+
released_cash = (
|
|
554
|
+
reserved_cash * Decimal(fill_quantity) / Decimal(order.remaining_quantity)
|
|
555
|
+
)
|
|
556
|
+
spendable_cash = released_cash + remaining_cash
|
|
557
|
+
if required > spendable_cash:
|
|
558
|
+
transitions.append(
|
|
559
|
+
(
|
|
560
|
+
order,
|
|
561
|
+
OrderStatus.CANCELLED,
|
|
562
|
+
OrderUpdateReason.INSUFFICIENT_CASH_AT_EXECUTION,
|
|
563
|
+
None,
|
|
564
|
+
None,
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
matched_order_ids.add(order.order_id)
|
|
568
|
+
continue
|
|
569
|
+
remaining_cash += released_cash - required
|
|
570
|
+
else:
|
|
571
|
+
# 卖出净现金校验(先于 Fill / 状态迁移):notional - fees.total
|
|
572
|
+
# 不得使片内现金预算为负(费用可能超过现金 + 成交额)
|
|
573
|
+
if remaining_cash + notional - fees.total < 0:
|
|
574
|
+
transitions.append(
|
|
575
|
+
(
|
|
576
|
+
order,
|
|
577
|
+
OrderStatus.CANCELLED,
|
|
578
|
+
OrderUpdateReason.INSUFFICIENT_CASH_AT_EXECUTION,
|
|
579
|
+
None,
|
|
580
|
+
None,
|
|
581
|
+
)
|
|
582
|
+
)
|
|
583
|
+
matched_order_ids.add(order.order_id)
|
|
584
|
+
continue
|
|
585
|
+
if not reserved_quantity:
|
|
586
|
+
remaining_available[intent.instrument_id] -= fill_quantity
|
|
587
|
+
remaining_cash += notional - fees.total
|
|
588
|
+
|
|
589
|
+
fill = Fill(
|
|
590
|
+
order_id=order.order_id,
|
|
591
|
+
instrument_id=intent.instrument_id,
|
|
592
|
+
side=intent.side,
|
|
593
|
+
quantity=fill_quantity,
|
|
594
|
+
price=fill_price,
|
|
595
|
+
fees=fees,
|
|
596
|
+
fill_time=time_slice.event_time,
|
|
597
|
+
)
|
|
598
|
+
attempts.append(
|
|
599
|
+
ExecutionAttempt(
|
|
600
|
+
order_id=order.order_id,
|
|
601
|
+
event_time=time_slice.event_time,
|
|
602
|
+
outcome=ExecutionAttemptOutcome.FILLED,
|
|
603
|
+
fill_id=fill.fill_id,
|
|
604
|
+
market_event_refs=_attempt_event_refs(bar_event.event_id, decision),
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
fills.append(fill)
|
|
608
|
+
staged_charged_fees[order.order_id] = cumulative_fees
|
|
609
|
+
remaining_volume[intent.instrument_id] -= fill_quantity
|
|
610
|
+
self._bar_volume_used[intent.instrument_id] = (
|
|
611
|
+
self._bar_volume_used.get(intent.instrument_id, 0) + fill_quantity
|
|
612
|
+
)
|
|
613
|
+
cumulative_quantity = order.filled_quantity + fill_quantity
|
|
614
|
+
cumulative_notional = (order.average_fill_price or Decimal("0")) * Decimal(
|
|
615
|
+
order.filled_quantity
|
|
616
|
+
) + fill_price * Decimal(fill_quantity)
|
|
617
|
+
cumulative_average = cumulative_notional / Decimal(cumulative_quantity)
|
|
618
|
+
target_status = (
|
|
619
|
+
OrderStatus.FILLED
|
|
620
|
+
if cumulative_quantity == intent.quantity
|
|
621
|
+
else OrderStatus.PARTIALLY_FILLED
|
|
622
|
+
)
|
|
623
|
+
transitions.append(
|
|
624
|
+
(
|
|
625
|
+
order,
|
|
626
|
+
target_status,
|
|
627
|
+
(
|
|
628
|
+
OrderUpdateReason.FILLED
|
|
629
|
+
if target_status is OrderStatus.FILLED
|
|
630
|
+
else OrderUpdateReason.PARTIALLY_FILLED
|
|
631
|
+
),
|
|
632
|
+
cumulative_quantity,
|
|
633
|
+
cumulative_average,
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
matched_order_ids.add(order.order_id)
|
|
637
|
+
|
|
638
|
+
# DAY 到期:投影 phase==CLOSED 的 Exchange 上仍 PENDING 的订单
|
|
639
|
+
# (matching 之后判定;本片已 FILLED/CANCELLED 的订单不再过期,
|
|
640
|
+
# 保证每单至多一条 Update)。状态来源是注入投影,不是切片事件解析。
|
|
641
|
+
# 粗粒度片跳过:日线 15:00 投影 CLOSED 是真实事实,但日片没有
|
|
642
|
+
# 「日内收盘」语义,不据此到期(方案 A:事实 ≠ 资格)。
|
|
643
|
+
expired_exchanges = (
|
|
644
|
+
{
|
|
645
|
+
exchange
|
|
646
|
+
for exchange, state in exchange_states.items()
|
|
647
|
+
if state.phase is ExchangePhase.CLOSED
|
|
648
|
+
}
|
|
649
|
+
if context is None and not immediate
|
|
650
|
+
else set()
|
|
651
|
+
)
|
|
652
|
+
for exchange in sorted(expired_exchanges):
|
|
653
|
+
for order in pending:
|
|
654
|
+
if order.order_id in matched_order_ids:
|
|
655
|
+
continue
|
|
656
|
+
if order.status not in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}:
|
|
657
|
+
continue
|
|
658
|
+
if order.intent.time_in_force is not TimeInForce.DAY:
|
|
659
|
+
continue
|
|
660
|
+
if instrument_exchange(order.intent.instrument_id) == exchange:
|
|
661
|
+
transitions.append(
|
|
662
|
+
(
|
|
663
|
+
order,
|
|
664
|
+
OrderStatus.EXPIRED,
|
|
665
|
+
OrderUpdateReason.EXPIRED_AT_SESSION_CLOSE,
|
|
666
|
+
None,
|
|
667
|
+
None,
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# coarse DAY 片级到期(R3 方案 A / R4 finding 3):本片机会已消耗且
|
|
672
|
+
# 仍未成交的 coarse origin DAY 单 → 本机会片末结构化 EXPIRED
|
|
673
|
+
# (matching 先于 expiry;停牌日无 Bar 不消耗机会,恢复日先试撮合)。
|
|
674
|
+
# 不使用交易所投影——资格语义,非市场事实;日内 origin 订单即使
|
|
675
|
+
# 进入 coarse 片也绝不参与。机会集合为片内局部派生,无跨片状态。
|
|
676
|
+
if context is not None and not immediate:
|
|
677
|
+
for order in pending:
|
|
678
|
+
if order.order_id not in opportunity_used_now:
|
|
679
|
+
continue # 本片未获得机会(含日内 origin 订单)→ 不到期
|
|
680
|
+
if order.order_id in matched_order_ids:
|
|
681
|
+
continue
|
|
682
|
+
if order.status not in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}:
|
|
683
|
+
continue
|
|
684
|
+
if order.intent.time_in_force is not TimeInForce.DAY:
|
|
685
|
+
continue
|
|
686
|
+
transitions.append(
|
|
687
|
+
(
|
|
688
|
+
order,
|
|
689
|
+
OrderStatus.EXPIRED,
|
|
690
|
+
OrderUpdateReason.EXPIRED_AT_COARSE_SESSION_END,
|
|
691
|
+
None,
|
|
692
|
+
None,
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# 应用全部迁移(transition 先构造并校验 OrderUpdate,再变更聚合)
|
|
697
|
+
for order, status, reason, cumulative, avg_price in transitions:
|
|
698
|
+
updates.append(
|
|
699
|
+
order.transition(
|
|
700
|
+
status,
|
|
701
|
+
time_slice.event_time,
|
|
702
|
+
reason,
|
|
703
|
+
cumulative_filled_quantity=cumulative,
|
|
704
|
+
average_fill_price=avg_price,
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
if status is OrderStatus.PARTIALLY_FILLED:
|
|
708
|
+
should_expire = (
|
|
709
|
+
not immediate
|
|
710
|
+
and (
|
|
711
|
+
context is None
|
|
712
|
+
and instrument_exchange(order.intent.instrument_id) in expired_exchanges
|
|
713
|
+
)
|
|
714
|
+
or (
|
|
715
|
+
not immediate
|
|
716
|
+
and context is not None
|
|
717
|
+
and order.order_id in opportunity_used_now
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
if should_expire:
|
|
721
|
+
updates.append(
|
|
722
|
+
order.transition(
|
|
723
|
+
OrderStatus.EXPIRED,
|
|
724
|
+
time_slice.event_time,
|
|
725
|
+
(
|
|
726
|
+
OrderUpdateReason.EXPIRED_AT_SESSION_CLOSE
|
|
727
|
+
if context is None
|
|
728
|
+
else OrderUpdateReason.EXPIRED_AT_COARSE_SESSION_END
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
result = MarketSliceProcessingResult(
|
|
734
|
+
slice_time=time_slice.event_time,
|
|
735
|
+
fills=tuple(fills),
|
|
736
|
+
execution_attempts=tuple(attempts),
|
|
737
|
+
order_updates=tuple(updates),
|
|
738
|
+
)
|
|
739
|
+
# 片处理成功(T4R:无内部会话状态需要提交)
|
|
740
|
+
self._charged_fees.update(staged_charged_fees)
|
|
741
|
+
self._last_slice_time = time_slice.event_time
|
|
742
|
+
if self._events is not None:
|
|
743
|
+
for fill in result.fills:
|
|
744
|
+
self._events.publish_trade(fill)
|
|
745
|
+
for update in result.order_updates:
|
|
746
|
+
self._events.publish_order_update(update)
|
|
747
|
+
return result
|
|
748
|
+
|
|
749
|
+
def finish_slice(self, time_slice: MarketTimeSlice) -> tuple[OrderTransition, ...]:
|
|
750
|
+
"""当前 Bar 日线模式在策略回调结束后清理剩余 DAY 订单。"""
|
|
751
|
+
if time_slice.execution_context is None:
|
|
752
|
+
return ()
|
|
753
|
+
updates = []
|
|
754
|
+
for order in self._orders.values():
|
|
755
|
+
if order.intent.execution_policy is not ExecutionPolicy.CURRENT_BAR_CLOSE:
|
|
756
|
+
continue
|
|
757
|
+
if order.status not in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}:
|
|
758
|
+
continue
|
|
759
|
+
update = order.transition(
|
|
760
|
+
OrderStatus.EXPIRED,
|
|
761
|
+
time_slice.event_time,
|
|
762
|
+
OrderUpdateReason.EXPIRED_AT_COARSE_SESSION_END,
|
|
763
|
+
)
|
|
764
|
+
updates.append(update)
|
|
765
|
+
if self._events is not None:
|
|
766
|
+
self._events.publish_order_update(update)
|
|
767
|
+
return tuple(updates)
|
|
768
|
+
|
|
769
|
+
def expire_pending_day_orders(self, event_time: datetime) -> tuple[OrderTransition, ...]:
|
|
770
|
+
"""回测终止兜底(R3 方案 C):coarse 决策点提交、至数据结束仍 PENDING
|
|
771
|
+
的 DAY 单批量结构化 EXPIRED(一次性终态,幂等:无粗粒度订单时为空)。
|
|
772
|
+
|
|
773
|
+
日内(非 coarse)提交的订单不参与——既有「数据结束时仍有效订单
|
|
774
|
+
如实保留」语义不变。只迁移订单状态,不修改 Portfolio。
|
|
775
|
+
"""
|
|
776
|
+
updates: list[OrderTransition] = []
|
|
777
|
+
for order in sorted(self._orders.values(), key=lambda o: o.submission_sequence):
|
|
778
|
+
if order.order_id not in self._coarse_order_ids:
|
|
779
|
+
continue
|
|
780
|
+
if order.status not in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}:
|
|
781
|
+
continue
|
|
782
|
+
if order.intent.time_in_force is not TimeInForce.DAY:
|
|
783
|
+
continue
|
|
784
|
+
updates.append(
|
|
785
|
+
order.transition(
|
|
786
|
+
OrderStatus.EXPIRED,
|
|
787
|
+
event_time,
|
|
788
|
+
OrderUpdateReason.EXPIRED_AT_BACKTEST_END,
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
if self._events is not None:
|
|
792
|
+
for update in updates:
|
|
793
|
+
self._events.publish_order_update(update)
|
|
794
|
+
return tuple(updates)
|
|
795
|
+
|
|
796
|
+
def order_snapshots(self) -> tuple[OrderSnapshot, ...]:
|
|
797
|
+
"""全部订单的不可变只读投影(按 submission_sequence 稳定排序)"""
|
|
798
|
+
return tuple(
|
|
799
|
+
order.snapshot()
|
|
800
|
+
for order in sorted(self._orders.values(), key=lambda o: o.submission_sequence)
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
def get_order(self, order_id: str) -> OrderSnapshot | None:
|
|
804
|
+
order = self._orders.get(order_id)
|
|
805
|
+
return order.snapshot() if order is not None else None
|
|
806
|
+
|
|
807
|
+
def open_order_snapshots(self, instrument_id: str | None = None) -> tuple[OrderSnapshot, ...]:
|
|
808
|
+
return tuple(
|
|
809
|
+
order.snapshot()
|
|
810
|
+
for order in sorted(self._orders.values(), key=lambda o: o.submission_sequence)
|
|
811
|
+
if order.status in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}
|
|
812
|
+
and (instrument_id is None or order.intent.instrument_id == instrument_id)
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
def cancel_order(self, order_id: str, event_time: datetime) -> OrderTransition:
|
|
816
|
+
order = self._orders.get(order_id)
|
|
817
|
+
if order is None:
|
|
818
|
+
raise ValueError(f"订单 {order_id} 不存在")
|
|
819
|
+
if order.status not in {OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED}:
|
|
820
|
+
raise ValueError(f"订单 {order_id} 已处于终态 {order.status.value},不能撤销")
|
|
821
|
+
update = order.transition(
|
|
822
|
+
OrderStatus.CANCELLED,
|
|
823
|
+
event_time,
|
|
824
|
+
OrderUpdateReason.CANCELLED,
|
|
825
|
+
)
|
|
826
|
+
if self._events is not None:
|
|
827
|
+
self._events.publish_order_update(update)
|
|
828
|
+
return update
|
|
829
|
+
|
|
830
|
+
# ------------------------------------------------------------------
|
|
831
|
+
|
|
832
|
+
def _check_slice_time(self, event_time: datetime) -> None:
|
|
833
|
+
"""时间片必须严格前进:拒绝倒流与同片重复消费"""
|
|
834
|
+
if self._last_slice_time is not None and event_time <= self._last_slice_time:
|
|
835
|
+
raise ValueError(
|
|
836
|
+
f"时间片必须严格前进:{event_time} 不晚于已消费 {self._last_slice_time}"
|
|
837
|
+
f"(重复消费会造成重复 Fill)"
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
@staticmethod
|
|
841
|
+
def _static_reject_reason(intent: OrderIntent) -> OrderRejectReason | None:
|
|
842
|
+
"""submit 静态校验(Policy 不参与;结构化原因码一一对应)"""
|
|
843
|
+
if not isinstance(intent.order_type, OrderType):
|
|
844
|
+
return OrderRejectReason.UNSUPPORTED_ORDER_TYPE
|
|
845
|
+
if intent.execution_policy not in {
|
|
846
|
+
ExecutionPolicy.CURRENT_BAR_CLOSE,
|
|
847
|
+
ExecutionPolicy.NEXT_BAR_OPEN,
|
|
848
|
+
ExecutionPolicy.DAILY_VWAP,
|
|
849
|
+
}:
|
|
850
|
+
return OrderRejectReason.UNSUPPORTED_EXECUTION_POLICY
|
|
851
|
+
if intent.time_in_force is not TimeInForce.DAY:
|
|
852
|
+
return OrderRejectReason.UNSUPPORTED_TIME_IN_FORCE
|
|
853
|
+
return None
|
|
854
|
+
|
|
855
|
+
def _incremental_fees(
|
|
856
|
+
self, order: Order, fill_quantity: int, fill_price: Decimal
|
|
857
|
+
) -> tuple[FeeBreakdown, FeeBreakdown]:
|
|
858
|
+
cumulative_quantity = order.filled_quantity + fill_quantity
|
|
859
|
+
cumulative_notional = (order.average_fill_price or Decimal("0")) * Decimal(
|
|
860
|
+
order.filled_quantity
|
|
861
|
+
) + fill_price * Decimal(fill_quantity)
|
|
862
|
+
cumulative_average = cumulative_notional / Decimal(cumulative_quantity)
|
|
863
|
+
cumulative = self._fee_model.compute(
|
|
864
|
+
order.intent.side, cumulative_quantity, cumulative_average
|
|
865
|
+
)
|
|
866
|
+
charged = self._charged_fees.get(
|
|
867
|
+
order.order_id,
|
|
868
|
+
FeeBreakdown(
|
|
869
|
+
commission=Decimal("0"),
|
|
870
|
+
stamp_tax=Decimal("0"),
|
|
871
|
+
transfer_fee=Decimal("0"),
|
|
872
|
+
other=Decimal("0"),
|
|
873
|
+
total=Decimal("0"),
|
|
874
|
+
),
|
|
875
|
+
)
|
|
876
|
+
incremental = FeeBreakdown(
|
|
877
|
+
commission=cumulative.commission - charged.commission,
|
|
878
|
+
stamp_tax=cumulative.stamp_tax - charged.stamp_tax,
|
|
879
|
+
transfer_fee=cumulative.transfer_fee - charged.transfer_fee,
|
|
880
|
+
other=cumulative.other - charged.other,
|
|
881
|
+
total=cumulative.total - charged.total,
|
|
882
|
+
)
|
|
883
|
+
return incremental, cumulative
|