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,573 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Market Runtime Service - 市场数据按市场时间流动(② Runtime 能力)
|
|
3
|
+
|
|
4
|
+
两种事件源,同一个 MarketEvent 契约:
|
|
5
|
+
|
|
6
|
+
Historical Replay: 本地库 → ReplayMarketEventSource → MarketEvent
|
|
7
|
+
Live Stream: Provider → LiveMarketEventSource → MarketEvent
|
|
8
|
+
|
|
9
|
+
Strategy / Backtest 只消费事件流,不感知自己是回测还是实盘。
|
|
10
|
+
|
|
11
|
+
边界:Runtime 只负责事件产生与时间推进;Order/Fill/Portfolio/指标归 backtest 域。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import heapq
|
|
16
|
+
from collections.abc import AsyncIterator, Iterator
|
|
17
|
+
from datetime import UTC, date, datetime, time
|
|
18
|
+
from zoneinfo import ZoneInfo
|
|
19
|
+
|
|
20
|
+
from quant_core.app.data.constants import (
|
|
21
|
+
SESSION_BREAK_END_TIME,
|
|
22
|
+
SESSION_BREAK_START_TIME,
|
|
23
|
+
SESSION_CLOSE_TIME,
|
|
24
|
+
SESSION_OPEN_TIME,
|
|
25
|
+
SESSION_PRE_OPEN_TIME,
|
|
26
|
+
BarFrequency,
|
|
27
|
+
Exchange,
|
|
28
|
+
MarketEventType,
|
|
29
|
+
MarketStreamMode,
|
|
30
|
+
)
|
|
31
|
+
from quant_core.app.data.exceptions import MarketDataNotSyncedException
|
|
32
|
+
from quant_core.app.data.interface.market_event_source import IMarketEventSource
|
|
33
|
+
from quant_core.app.data.interface.market_runtime_repository import IMarketRuntimeRepository
|
|
34
|
+
from quant_core.app.data.schema.corporate_action_schema import (
|
|
35
|
+
CorporateActionEventPayload,
|
|
36
|
+
CorporateActionPhase,
|
|
37
|
+
CorporateActionResponse,
|
|
38
|
+
)
|
|
39
|
+
from quant_core.app.data.schema.event_schema import (
|
|
40
|
+
PRE_SESSION_STAGES,
|
|
41
|
+
BarEvent,
|
|
42
|
+
CoarseBarExecutionContext,
|
|
43
|
+
CorporateActionEvent,
|
|
44
|
+
MarketEvent,
|
|
45
|
+
MarketStreamRequest,
|
|
46
|
+
MarketTimeSlice,
|
|
47
|
+
TradingSessionEvent,
|
|
48
|
+
TradingSessionStage,
|
|
49
|
+
bar_to_bar_event,
|
|
50
|
+
daily_state_snapshot_to_event,
|
|
51
|
+
market_event_order_key,
|
|
52
|
+
trading_session_to_event,
|
|
53
|
+
)
|
|
54
|
+
from quant_core.app.data.schema.exchange_state_schema import (
|
|
55
|
+
ExchangePhase,
|
|
56
|
+
exchange_state_snapshot_to_event,
|
|
57
|
+
exchange_state_to_event,
|
|
58
|
+
)
|
|
59
|
+
from quant_core.app.data.schema.market_schema import BarResponse
|
|
60
|
+
from quant_core.app.data.service.calendar_coverage_service import build_calendar_coverage
|
|
61
|
+
from quant_core.app.trading_runtime.replay.daily_state_expand import DailyFact, expand_daily_state
|
|
62
|
+
|
|
63
|
+
# Session 事件只在日内频率回放中产生;日线及以上以「日」为粒度,无日内时段语义
|
|
64
|
+
_INTRADAY_FREQUENCIES = {
|
|
65
|
+
BarFrequency.M1,
|
|
66
|
+
BarFrequency.M5,
|
|
67
|
+
BarFrequency.M15,
|
|
68
|
+
BarFrequency.M30,
|
|
69
|
+
BarFrequency.H1,
|
|
70
|
+
}
|
|
71
|
+
# Session 五阶段(通用阶段 → 各市场具体时刻;A股见 constants SESSION_*_TIME)
|
|
72
|
+
_SESSION_STAGES: tuple[tuple[TradingSessionStage, str], ...] = (
|
|
73
|
+
(MarketEventType.SESSION_PRE_OPEN, SESSION_PRE_OPEN_TIME),
|
|
74
|
+
(MarketEventType.SESSION_OPEN, SESSION_OPEN_TIME),
|
|
75
|
+
(MarketEventType.SESSION_BREAK_START, SESSION_BREAK_START_TIME),
|
|
76
|
+
(MarketEventType.SESSION_BREAK_END, SESSION_BREAK_END_TIME),
|
|
77
|
+
(MarketEventType.SESSION_CLOSE, SESSION_CLOSE_TIME),
|
|
78
|
+
)
|
|
79
|
+
_MARKET_TZ = ZoneInfo("Asia/Shanghai")
|
|
80
|
+
# Session 阶段 → 交易所运行 phase(T4R;增量事件与 Session 边界同点同源)
|
|
81
|
+
_STAGE_TO_PHASE: dict[TradingSessionStage, ExchangePhase] = {
|
|
82
|
+
MarketEventType.SESSION_PRE_OPEN: ExchangePhase.PRE_OPEN,
|
|
83
|
+
MarketEventType.SESSION_OPEN: ExchangePhase.OPEN,
|
|
84
|
+
MarketEventType.SESSION_BREAK_START: ExchangePhase.BREAK,
|
|
85
|
+
MarketEventType.SESSION_BREAK_END: ExchangePhase.OPEN,
|
|
86
|
+
MarketEventType.SESSION_CLOSE: ExchangePhase.CLOSED,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _phase_at_time(hhmm: str) -> ExchangePhase:
|
|
91
|
+
"""确定性 A 股日内时段规则(HH:MM 字典序即时序):bounded 起点 phase 基线
|
|
92
|
+
|
|
93
|
+
- <09:15 CLOSED(凌晨/早盘前:禁止以 PRE_OPEN capability 接受新订单);
|
|
94
|
+
- 09:15–09:30 PRE_OPEN(开盘集合竞价);09:30–11:30 OPEN;
|
|
95
|
+
- 11:30–13:00 BREAK(午休);13:00–15:00 OPEN;>=15:00 CLOSED。
|
|
96
|
+
"""
|
|
97
|
+
if hhmm < SESSION_PRE_OPEN_TIME:
|
|
98
|
+
return ExchangePhase.CLOSED
|
|
99
|
+
if hhmm < SESSION_OPEN_TIME:
|
|
100
|
+
return ExchangePhase.PRE_OPEN
|
|
101
|
+
if hhmm < SESSION_BREAK_START_TIME:
|
|
102
|
+
return ExchangePhase.OPEN
|
|
103
|
+
if hhmm < SESSION_BREAK_END_TIME:
|
|
104
|
+
return ExchangePhase.BREAK
|
|
105
|
+
if hhmm < SESSION_CLOSE_TIME:
|
|
106
|
+
return ExchangePhase.OPEN
|
|
107
|
+
return ExchangePhase.CLOSED
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _make_slice(slice_time: datetime, batch: list[MarketEvent], coarse: bool) -> MarketTimeSlice:
|
|
111
|
+
"""粗粒度开盘决策片和 Bar 撮合片均携带有界交易所资格。"""
|
|
112
|
+
context = None
|
|
113
|
+
if coarse:
|
|
114
|
+
bar_exchanges = sorted(
|
|
115
|
+
{
|
|
116
|
+
_exchange_of(e.payload.instrument_id)
|
|
117
|
+
for e in batch
|
|
118
|
+
if e.event_type is MarketEventType.BAR and isinstance(e, BarEvent)
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
e.payload.exchange
|
|
122
|
+
for e in batch
|
|
123
|
+
if e.event_type is MarketEventType.SESSION_OPEN
|
|
124
|
+
and isinstance(e, TradingSessionEvent)
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
if bar_exchanges:
|
|
128
|
+
context = CoarseBarExecutionContext(
|
|
129
|
+
decision_time=slice_time,
|
|
130
|
+
eligible_exchanges=tuple(bar_exchanges),
|
|
131
|
+
reason=(
|
|
132
|
+
"coarse_bar_decision_point"
|
|
133
|
+
if any(e.event_type is MarketEventType.BAR for e in batch)
|
|
134
|
+
else "coarse_open_decision_point"
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
return MarketTimeSlice(event_time=slice_time, events=batch, execution_context=context)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ReplayMarketEventSource:
|
|
141
|
+
"""历史重放事件源:多标的 Bar + 各交易所 Trading Session → 全局时间归并
|
|
142
|
+
|
|
143
|
+
归并语义(确定性,与 instrument_ids 输入顺序无关):
|
|
144
|
+
- Bar 按标的字典序作为独立归并流(heapq.merge,同 key 时先到者优先);
|
|
145
|
+
- 每个涉及的 Exchange 分别基于 TradingCalendar 生成 Session 事件,
|
|
146
|
+
按边界优先级分两路参与归并:OPEN/RESUME 类(PRE_OPEN/OPEN/BREAK_END)
|
|
147
|
+
在同 event_time 市场事实前,BREAK/CLOSE 类(BREAK_START/CLOSE)在后;
|
|
148
|
+
- event_time 全局单调不减,sequence 在整条流上严格递增(归并后统一编号);
|
|
149
|
+
- 同一精确 event_time 的事件可经 stream_slices() 形成 MarketTimeSlice,
|
|
150
|
+
与 stream() 复用同一份归并实现。
|
|
151
|
+
|
|
152
|
+
Strategy 只认 event_time,禁止用 received_at。
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, repository: IMarketRuntimeRepository):
|
|
156
|
+
self.repository = repository
|
|
157
|
+
|
|
158
|
+
async def stream(self, request: MarketStreamRequest) -> AsyncIterator[MarketEvent]:
|
|
159
|
+
"""原子事件流(全局时间归并,sequence 严格递增)"""
|
|
160
|
+
async for event in self._stream_merged(request):
|
|
161
|
+
yield event
|
|
162
|
+
|
|
163
|
+
async def stream_slices(self, request: MarketStreamRequest) -> AsyncIterator[MarketTimeSlice]:
|
|
164
|
+
"""时间片流:复用同一份全局归并,按精确 event_time 增量分组(不整体物化)
|
|
165
|
+
|
|
166
|
+
R2 finding 3(方案 A):粗粒度(日线及以上)片携带显式
|
|
167
|
+
CoarseBarExecutionContext(模拟决策/撮合资格);日内片恒为 None。
|
|
168
|
+
ExchangeState 事实与执行资格由此彻底分离。
|
|
169
|
+
"""
|
|
170
|
+
coarse = request.frequency not in _INTRADAY_FREQUENCIES
|
|
171
|
+
slice_time: datetime | None = None
|
|
172
|
+
batch: list[MarketEvent] = []
|
|
173
|
+
async for event in self._stream_merged(request):
|
|
174
|
+
if slice_time is not None and event.event_time != slice_time:
|
|
175
|
+
yield _make_slice(slice_time, batch, coarse)
|
|
176
|
+
batch = []
|
|
177
|
+
slice_time = event.event_time
|
|
178
|
+
batch.append(event)
|
|
179
|
+
if batch:
|
|
180
|
+
assert slice_time is not None
|
|
181
|
+
yield _make_slice(slice_time, batch, coarse)
|
|
182
|
+
|
|
183
|
+
async def _stream_merged(self, request: MarketStreamRequest) -> AsyncIterator[MarketEvent]:
|
|
184
|
+
if request.start is None or request.end is None:
|
|
185
|
+
raise ValueError("replay 模式必须指定 start/end")
|
|
186
|
+
received_at = datetime.now(UTC)
|
|
187
|
+
# 归并流顺序 = market_event_order_key 规范序(R1 契约):
|
|
188
|
+
# (event_time, 边界优先级, 标的/交易所 tie-break, 事件类型)。
|
|
189
|
+
# 生产侧在进入 MarketTimeSlice 之前就产出规范序,sequence 按该序编号
|
|
190
|
+
# 严格递增;切片内重排因此是幂等 no-op,stream 与 stream_slices
|
|
191
|
+
# 的逐事件业务顺序完全一致。
|
|
192
|
+
if request.frequency in _INTRADAY_FREQUENCIES:
|
|
193
|
+
# 每交易所查询一次完整日历基线(R1 契约):Session 与 Snapshot
|
|
194
|
+
# 共享同一份 open days,不完整日历结构化失败,绝不分别查询后不一致
|
|
195
|
+
calendars: dict[str, list[date]] = {
|
|
196
|
+
exchange: await self._open_days_with_full_coverage(
|
|
197
|
+
exchange,
|
|
198
|
+
request.start.astimezone(_MARKET_TZ).date(),
|
|
199
|
+
request.end.astimezone(_MARKET_TZ).date(),
|
|
200
|
+
)
|
|
201
|
+
for exchange in sorted(_exchanges_from_instrument_ids(request.instrument_ids))
|
|
202
|
+
}
|
|
203
|
+
pre_sessions, post_sessions = self._session_events(request, calendars)
|
|
204
|
+
ex_snapshots, ex_pre, ex_post = self._exchange_state_events(request, calendars)
|
|
205
|
+
pre_sessions = pre_sessions + ex_pre
|
|
206
|
+
post_sessions = post_sessions + ex_post
|
|
207
|
+
else:
|
|
208
|
+
# 日线及以上:无日内 Session 语义,但必须提供完整日历基线
|
|
209
|
+
# (日级事实快照消费同一份 open days,CalendarCoverage 统一判定)
|
|
210
|
+
calendars = {
|
|
211
|
+
exchange: await self._open_days_with_full_coverage(
|
|
212
|
+
exchange,
|
|
213
|
+
request.start.astimezone(_MARKET_TZ).date(),
|
|
214
|
+
request.end.astimezone(_MARKET_TZ).date(),
|
|
215
|
+
)
|
|
216
|
+
for exchange in sorted(_exchanges_from_instrument_ids(request.instrument_ids))
|
|
217
|
+
}
|
|
218
|
+
# 日级 Bar 只在收盘可见;开盘 Session 是真实日历边界,
|
|
219
|
+
# 给策略提供不含当日 Bar 的事前决策点,而不伪造交易所状态事实。
|
|
220
|
+
received_at = datetime.now(UTC)
|
|
221
|
+
pre_sessions = [
|
|
222
|
+
trading_session_to_event(
|
|
223
|
+
exchange,
|
|
224
|
+
MarketEventType.SESSION_OPEN,
|
|
225
|
+
datetime(day.year, day.month, day.day, 9, 30, tzinfo=_MARKET_TZ).astimezone(
|
|
226
|
+
UTC
|
|
227
|
+
),
|
|
228
|
+
0,
|
|
229
|
+
"replay",
|
|
230
|
+
received_at,
|
|
231
|
+
)
|
|
232
|
+
for exchange, days in sorted(calendars.items())
|
|
233
|
+
for day in days
|
|
234
|
+
if request.coarse_open_decisions
|
|
235
|
+
if request.start
|
|
236
|
+
<= datetime(day.year, day.month, day.day, 9, 30, tzinfo=_MARKET_TZ).astimezone(UTC)
|
|
237
|
+
<= request.end
|
|
238
|
+
]
|
|
239
|
+
post_sessions = []
|
|
240
|
+
ex_snapshots: list[MarketEvent] = [] # 日线及以上:无交易所事件(方案 A)
|
|
241
|
+
bar_streams: list[Iterator[MarketEvent]] = []
|
|
242
|
+
fact_events: list[MarketEvent] = []
|
|
243
|
+
for instrument_id in sorted(set(request.instrument_ids)):
|
|
244
|
+
snapshot_events: list[MarketEvent] = []
|
|
245
|
+
has_facts = False
|
|
246
|
+
snapshot_events, has_facts = await self._daily_snapshot_events(
|
|
247
|
+
instrument_id,
|
|
248
|
+
request,
|
|
249
|
+
received_at,
|
|
250
|
+
calendars[_exchange_of(instrument_id)],
|
|
251
|
+
)
|
|
252
|
+
bars = await self.repository.list_bars(
|
|
253
|
+
instrument_id, request.frequency.value, request.start, request.end, limit=1000000
|
|
254
|
+
)
|
|
255
|
+
if not bars and not has_facts:
|
|
256
|
+
# 无 Bar 且区间内无任何官方事实行:真正未同步,结构化失败
|
|
257
|
+
# (不伪装成功,也不用日历-derived UNKNOWN 快照冒充数据);
|
|
258
|
+
# 仅有官方停牌事实(纯停牌区间无 Bar)是合法回放
|
|
259
|
+
raise MarketDataNotSyncedException(
|
|
260
|
+
f"回放数据未同步({instrument_id} {request.frequency.value}),请先 sync bars"
|
|
261
|
+
)
|
|
262
|
+
if bars:
|
|
263
|
+
bar_streams.append(
|
|
264
|
+
iter(
|
|
265
|
+
bar_to_bar_event(BarResponse.model_validate(b), 0, "replay", received_at)
|
|
266
|
+
for b in bars
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
fact_events.extend(snapshot_events)
|
|
270
|
+
# R2 finding 3(方案 A):日线及以上片不再生成任何交易所事件——
|
|
271
|
+
# 15:00 的真实投影是 CLOSED,粗粒度撮合资格由时间片
|
|
272
|
+
# execution_context(CoarseBarExecutionContext)显式表达,
|
|
273
|
+
# 禁止伪造 Snapshot(OPEN) 混淆市场事实与模拟撮合资格。
|
|
274
|
+
fact_events.extend(ex_snapshots)
|
|
275
|
+
fact_events.extend(await self._corporate_action_events(request, received_at))
|
|
276
|
+
fact_events.sort(key=market_event_order_key)
|
|
277
|
+
pre_sessions.sort(key=market_event_order_key)
|
|
278
|
+
post_sessions.sort(key=market_event_order_key)
|
|
279
|
+
merged = heapq.merge(
|
|
280
|
+
iter(pre_sessions),
|
|
281
|
+
iter(fact_events),
|
|
282
|
+
*bar_streams,
|
|
283
|
+
iter(post_sessions),
|
|
284
|
+
key=market_event_order_key,
|
|
285
|
+
)
|
|
286
|
+
for sequence, event in enumerate(merged):
|
|
287
|
+
event.sequence = sequence
|
|
288
|
+
yield event
|
|
289
|
+
if request.speed > 0:
|
|
290
|
+
await asyncio.sleep(0) # 限速模式让出事件循环,按speed等待由调用方控制
|
|
291
|
+
|
|
292
|
+
async def _corporate_action_events(
|
|
293
|
+
self, request: MarketStreamRequest, received_at: datetime
|
|
294
|
+
) -> list[MarketEvent]:
|
|
295
|
+
"""把本地公司行动事实展开为三阶段事件。
|
|
296
|
+
|
|
297
|
+
record 在收盘后固化权益;ex/pay 在开盘前依次生效。刻意使用不同时间,
|
|
298
|
+
保证同日除权且派息时,应收先建立、现金后到账,不依赖输入行顺序。
|
|
299
|
+
"""
|
|
300
|
+
assert request.start is not None and request.end is not None
|
|
301
|
+
actions = await self.repository.list_corporate_actions(
|
|
302
|
+
sorted(set(request.instrument_ids)),
|
|
303
|
+
request.start.astimezone(_MARKET_TZ).date(),
|
|
304
|
+
request.end.astimezone(_MARKET_TZ).date(),
|
|
305
|
+
)
|
|
306
|
+
events: list[MarketEvent] = []
|
|
307
|
+
phase_times = (
|
|
308
|
+
(CorporateActionPhase.RECORD, time(15, 0)),
|
|
309
|
+
(CorporateActionPhase.EX_DATE, time(9, 20)),
|
|
310
|
+
(CorporateActionPhase.LISTING_DATE, time(9, 25)),
|
|
311
|
+
(CorporateActionPhase.PAY_DATE, time(9, 26)),
|
|
312
|
+
)
|
|
313
|
+
for row in actions:
|
|
314
|
+
action = CorporateActionResponse.model_validate(row)
|
|
315
|
+
dates = {
|
|
316
|
+
CorporateActionPhase.RECORD: action.record_date,
|
|
317
|
+
CorporateActionPhase.EX_DATE: action.ex_date,
|
|
318
|
+
CorporateActionPhase.PAY_DATE: action.pay_date,
|
|
319
|
+
CorporateActionPhase.LISTING_DATE: action.listing_date,
|
|
320
|
+
}
|
|
321
|
+
for phase, effective_time in phase_times:
|
|
322
|
+
effective_date = dates[phase]
|
|
323
|
+
if effective_date is None:
|
|
324
|
+
continue
|
|
325
|
+
event_time = datetime.combine(
|
|
326
|
+
effective_date, effective_time, tzinfo=_MARKET_TZ
|
|
327
|
+
).astimezone(UTC)
|
|
328
|
+
if request.start <= event_time <= request.end:
|
|
329
|
+
events.append(
|
|
330
|
+
CorporateActionEvent(
|
|
331
|
+
event_time=event_time,
|
|
332
|
+
received_at=received_at,
|
|
333
|
+
sequence=0,
|
|
334
|
+
source="replay",
|
|
335
|
+
payload=CorporateActionEventPayload(action=action, phase=phase),
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
return events
|
|
339
|
+
|
|
340
|
+
async def _open_days_with_full_coverage(
|
|
341
|
+
self, exchange: str, start_d: date, end_d: date
|
|
342
|
+
) -> list[date]:
|
|
343
|
+
"""完整日历基线(R1 契约):请求涉及的每个本地自然日都必须有日历行
|
|
344
|
+
|
|
345
|
+
「返回行非空」不代表日历已完整同步;缺任何一天 → 结构化失败
|
|
346
|
+
(无法区分「休市」与「日历缺失」,禁止静默当作非交易日跳过)。
|
|
347
|
+
返回区间内 open days(升序)。
|
|
348
|
+
"""
|
|
349
|
+
days = await self.repository.list_calendar_days(exchange, start_d, end_d, only_open=False)
|
|
350
|
+
coverage = build_calendar_coverage(exchange, start_d, end_d, days)
|
|
351
|
+
coverage.require_complete()
|
|
352
|
+
return list(coverage.open_days)
|
|
353
|
+
|
|
354
|
+
async def _daily_snapshot_events(
|
|
355
|
+
self,
|
|
356
|
+
instrument_id: str,
|
|
357
|
+
request: MarketStreamRequest,
|
|
358
|
+
received_at: datetime,
|
|
359
|
+
open_days: list[date],
|
|
360
|
+
) -> tuple[list[MarketEvent], bool]:
|
|
361
|
+
"""日级官方事实 → 每交易日一条完整 DailyStateSnapshotEvent(T1/T3 契约)
|
|
362
|
+
|
|
363
|
+
事件时间取当日 SESSION_OPEN(A股 09:30):与当日首根 Bar 同片,符合
|
|
364
|
+
MarketTimeSlice「同片同标的快照/Bar 各一条」契约。
|
|
365
|
+
- 覆盖区间内每个交易日(TradingCalendar open days):
|
|
366
|
+
有事实行 → 按官方事实展开;无事实行 → 显式 UNKNOWN 快照
|
|
367
|
+
(跨日重置基线,不依赖「事件缺席」;价格限制随快照显式清除);
|
|
368
|
+
- bounded replay 初始化(R1 契约):request.start 晚于当日开盘时,
|
|
369
|
+
快照以窗口起点 max(开盘, start) 作为生效时间注入——当日官方事实
|
|
370
|
+
必须先于首根被消费的 Bar 生效,不允许让 Resolver 从 UNKNOWN 起步;
|
|
371
|
+
不使用未来信息、不反查 Provider、不从 Bar 推断,且 event_time
|
|
372
|
+
不早于 request.start(无时间倒退)、保持请求闭区间;
|
|
373
|
+
- 交易日历由调用方传入完整基线(每交易所一次查询,Session 共享);
|
|
374
|
+
- Bar 流与 Runtime 因果顺序零修改:快照只作为附加事实流入切片。
|
|
375
|
+
"""
|
|
376
|
+
assert request.start is not None and request.end is not None
|
|
377
|
+
start_d = request.start.astimezone(_MARKET_TZ).date()
|
|
378
|
+
end_d = request.end.astimezone(_MARKET_TZ).date()
|
|
379
|
+
facts = {
|
|
380
|
+
f.trade_date: f
|
|
381
|
+
for f in await self.repository.list_daily_facts(instrument_id, start_d, end_d)
|
|
382
|
+
}
|
|
383
|
+
events: list[MarketEvent] = []
|
|
384
|
+
hour, minute = (int(x) for x in SESSION_OPEN_TIME.split(":"))
|
|
385
|
+
for day in open_days:
|
|
386
|
+
f = facts.get(day)
|
|
387
|
+
expanded = expand_daily_state(
|
|
388
|
+
None
|
|
389
|
+
if f is None
|
|
390
|
+
else DailyFact(
|
|
391
|
+
trade_date=f.trade_date,
|
|
392
|
+
suspension_status=f.suspension_status,
|
|
393
|
+
limit_up=f.limit_up,
|
|
394
|
+
limit_dn=f.limit_dn,
|
|
395
|
+
extra=getattr(f, "extra", None),
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
day_open = datetime(
|
|
399
|
+
day.year,
|
|
400
|
+
day.month,
|
|
401
|
+
day.day,
|
|
402
|
+
hour,
|
|
403
|
+
minute,
|
|
404
|
+
tzinfo=_MARKET_TZ,
|
|
405
|
+
).astimezone(UTC)
|
|
406
|
+
# bounded replay 初始化:窗口起点晚于开盘 → 快照推迟到窗口起点生效
|
|
407
|
+
event_time = max(day_open, request.start)
|
|
408
|
+
if not (request.start <= event_time <= request.end):
|
|
409
|
+
continue
|
|
410
|
+
events.append(
|
|
411
|
+
daily_state_snapshot_to_event(
|
|
412
|
+
instrument_id,
|
|
413
|
+
expanded.trading_state,
|
|
414
|
+
event_time,
|
|
415
|
+
expanded.limit_up if expanded.has_price_limit else None,
|
|
416
|
+
expanded.limit_dn if expanded.has_price_limit else None,
|
|
417
|
+
0,
|
|
418
|
+
"replay",
|
|
419
|
+
received_at,
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
return events, bool(facts)
|
|
423
|
+
|
|
424
|
+
def _session_events(
|
|
425
|
+
self, request: MarketStreamRequest, calendars: dict[str, list[date]]
|
|
426
|
+
) -> tuple[list[MarketEvent], list[MarketEvent]]:
|
|
427
|
+
"""基于共享日历基线为每个涉及 Exchange 生成 Session 事件
|
|
428
|
+
|
|
429
|
+
calendars 由 _stream_merged 以「每交易所一次完整覆盖查询」构建,
|
|
430
|
+
Session 与 Snapshot 消费同一份 open days(R1 契约)。
|
|
431
|
+
返回 (前置边界列表, 后置边界列表)。
|
|
432
|
+
"""
|
|
433
|
+
assert request.start is not None and request.end is not None
|
|
434
|
+
pre: list[MarketEvent] = []
|
|
435
|
+
post: list[MarketEvent] = []
|
|
436
|
+
received_at = datetime.now(UTC)
|
|
437
|
+
for exchange, open_days in sorted(calendars.items()):
|
|
438
|
+
for day in open_days:
|
|
439
|
+
for stage, hhmm in _SESSION_STAGES:
|
|
440
|
+
hour, minute = int(hhmm[:2]), int(hhmm[3:])
|
|
441
|
+
event_time = datetime(
|
|
442
|
+
day.year,
|
|
443
|
+
day.month,
|
|
444
|
+
day.day,
|
|
445
|
+
hour,
|
|
446
|
+
minute,
|
|
447
|
+
tzinfo=_MARKET_TZ,
|
|
448
|
+
).astimezone(UTC)
|
|
449
|
+
if request.start <= event_time <= request.end:
|
|
450
|
+
event = trading_session_to_event(
|
|
451
|
+
exchange, stage, event_time, 0, "replay", received_at
|
|
452
|
+
)
|
|
453
|
+
(pre if stage in PRE_SESSION_STAGES else post).append(event)
|
|
454
|
+
sort_key = lambda e: (e.event_time, e.payload.exchange) # noqa: E731
|
|
455
|
+
pre.sort(key=sort_key)
|
|
456
|
+
post.sort(key=sort_key)
|
|
457
|
+
return pre, post
|
|
458
|
+
|
|
459
|
+
def _exchange_state_events(
|
|
460
|
+
self, request: MarketStreamRequest, calendars: dict[str, list[date]]
|
|
461
|
+
) -> tuple[list[MarketEvent], list[MarketEvent], list[MarketEvent]]:
|
|
462
|
+
"""交易所运行状态事实(T4R):(Snapshot 基线, 前置边界增量, 后置边界增量)
|
|
463
|
+
|
|
464
|
+
- Snapshot 在 request.start 为每交易所建立 bounded replay 起点的完整
|
|
465
|
+
状态基线:开市日按确定性 A 股时段规则(_phase_at_time)推导,
|
|
466
|
+
非开市日 CLOSED。不伪造「request.start 发生状态转换」——窗口内
|
|
467
|
+
没有对应 Session 边界就没有增量事件;
|
|
468
|
+
- 增量 Event 与 Session 边界同点同源生成(_STAGE_TO_PHASE 映射),
|
|
469
|
+
排序优先级由 market_event_order_key 按目标 phase 继承边界层;
|
|
470
|
+
- 日历基线与 Session/DailySnapshot 共享(每交易所一次查询)。
|
|
471
|
+
"""
|
|
472
|
+
assert request.start is not None and request.end is not None
|
|
473
|
+
snapshots: list[MarketEvent] = []
|
|
474
|
+
pre: list[MarketEvent] = []
|
|
475
|
+
post: list[MarketEvent] = []
|
|
476
|
+
received_at = datetime.now(UTC)
|
|
477
|
+
start_local = request.start.astimezone(_MARKET_TZ)
|
|
478
|
+
open_day_sets = {exchange: set(days) for exchange, days in calendars.items()}
|
|
479
|
+
for exchange in sorted(calendars):
|
|
480
|
+
baseline_phase = (
|
|
481
|
+
ExchangePhase.CLOSED
|
|
482
|
+
if start_local.date() not in open_day_sets[exchange]
|
|
483
|
+
else _phase_at_time(start_local.strftime("%H:%M"))
|
|
484
|
+
)
|
|
485
|
+
snapshots.append(
|
|
486
|
+
exchange_state_snapshot_to_event(
|
|
487
|
+
exchange, baseline_phase, request.start, 0, "replay", received_at
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
for exchange, open_days in sorted(calendars.items()):
|
|
491
|
+
for day in open_days:
|
|
492
|
+
for stage, hhmm in _SESSION_STAGES:
|
|
493
|
+
hour, minute = int(hhmm[:2]), int(hhmm[3:])
|
|
494
|
+
event_time = datetime(
|
|
495
|
+
day.year, day.month, day.day, hour, minute, tzinfo=_MARKET_TZ
|
|
496
|
+
).astimezone(UTC)
|
|
497
|
+
if request.start <= event_time <= request.end:
|
|
498
|
+
event = exchange_state_to_event(
|
|
499
|
+
exchange,
|
|
500
|
+
_STAGE_TO_PHASE[stage],
|
|
501
|
+
event_time,
|
|
502
|
+
0,
|
|
503
|
+
"replay",
|
|
504
|
+
received_at,
|
|
505
|
+
)
|
|
506
|
+
(pre if stage in PRE_SESSION_STAGES else post).append(event)
|
|
507
|
+
return snapshots, pre, post
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _exchange_of(instrument_id: str) -> str:
|
|
511
|
+
"""单标的 Canonical ID(CN.XSHE.000001)→ 交易所段(非法 ID 显式失败)"""
|
|
512
|
+
parts = instrument_id.split(".")
|
|
513
|
+
if len(parts) != 3 or parts[1] not in Exchange.__members__:
|
|
514
|
+
raise ValueError(f"非法 Canonical instrument_id: {instrument_id}")
|
|
515
|
+
return parts[1]
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _exchanges_from_instrument_ids(instrument_ids: list[str]) -> set[str]:
|
|
519
|
+
"""从 Canonical instrument_id(CN.XSHE.000001)提取涉及的交易所;推断不出默认 XSHG"""
|
|
520
|
+
exchanges = {
|
|
521
|
+
parts[1]
|
|
522
|
+
for instrument_id in instrument_ids
|
|
523
|
+
if len(parts := instrument_id.split(".")) == 3 and parts[1] in Exchange.__members__
|
|
524
|
+
}
|
|
525
|
+
return exchanges or {Exchange.XSHG.value}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _daterange(start_d: date, end_d: date) -> list[date]:
|
|
529
|
+
"""闭区间内全部本地自然日(完整日历覆盖检查的基准)"""
|
|
530
|
+
return [date.fromordinal(o) for o in range(start_d.toordinal(), end_d.toordinal() + 1)]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class LiveMarketEventSource:
|
|
534
|
+
"""实时事件源:Provider 最新快照轮询包装为事件流(V1 轮询形态)"""
|
|
535
|
+
|
|
536
|
+
def __init__(self, provider):
|
|
537
|
+
self.provider = provider
|
|
538
|
+
|
|
539
|
+
async def stream(self, request: MarketStreamRequest) -> AsyncIterator[MarketEvent]:
|
|
540
|
+
sequence = 0
|
|
541
|
+
while True:
|
|
542
|
+
quotes = await self.provider.get_spot_quotes()
|
|
543
|
+
now = datetime.now(UTC)
|
|
544
|
+
for q in quotes:
|
|
545
|
+
if request.instrument_ids and q.instrument_id not in request.instrument_ids:
|
|
546
|
+
continue
|
|
547
|
+
# V1 快照以 bar 事件形态发布;tick 事件属未来 Level-2
|
|
548
|
+
event = MarketEvent(
|
|
549
|
+
event_type=MarketEventType.BAR,
|
|
550
|
+
event_time=q.timestamp,
|
|
551
|
+
received_at=now,
|
|
552
|
+
sequence=sequence,
|
|
553
|
+
source="live",
|
|
554
|
+
payload=q.model_dump(mode="json"),
|
|
555
|
+
)
|
|
556
|
+
yield event
|
|
557
|
+
sequence += 1
|
|
558
|
+
await asyncio.sleep(3.0 if request.speed <= 0 else 3.0 / request.speed)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class MarketRuntimeService:
|
|
562
|
+
"""市场运行时 Service:按模式把请求路由到对应事件源"""
|
|
563
|
+
|
|
564
|
+
def __init__(
|
|
565
|
+
self,
|
|
566
|
+
replay_source: IMarketEventSource,
|
|
567
|
+
live_source: IMarketEventSource,
|
|
568
|
+
):
|
|
569
|
+
self._sources = {MarketStreamMode.REPLAY: replay_source, MarketStreamMode.LIVE: live_source}
|
|
570
|
+
|
|
571
|
+
def stream(self, request: MarketStreamRequest) -> AsyncIterator[MarketEvent]:
|
|
572
|
+
"""获取市场事件流(Live 与 Replay 的统一入口)"""
|
|
573
|
+
return self._sources[request.mode].stream(request)
|