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,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Market Runtime Schema - 市场交易状态事实契约
|
|
3
|
+
|
|
4
|
+
定义两类客观市场事实(SSOT 见 DECISIONS.md 2026-09-04「市场状态采用增量事实投影」):
|
|
5
|
+
|
|
6
|
+
- TradingStateEvent:通用交易状态事实(TRADING/SUSPENDED/HALTED/AUCTION/CLOSED),
|
|
7
|
+
任何市场均可提供;不依赖 A 股规则。
|
|
8
|
+
- PriceLimitEvent:可选事实。没有价格限制的市场可以完全不提供该事件;
|
|
9
|
+
提供时表达上下限价与买/卖流动性锁定状态。
|
|
10
|
+
|
|
11
|
+
语义边界:
|
|
12
|
+
- 两类事件只表达客观市场事实,不输出订单的 ALLOW / DEFER / REJECT——
|
|
13
|
+
该决策属于 Order Execution 的 TradabilityPolicy。
|
|
14
|
+
- 状态是增量事实:从 event_time 生效并持续到后续事实显式覆盖;
|
|
15
|
+
事件缺席不表示恢复交易(投影见 MarketStateResolver)。
|
|
16
|
+
- Bar 只表达 OHLCV,禁止从 Bar 推断停牌、涨停封板或跌停封板。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from decimal import Decimal
|
|
22
|
+
from enum import Enum
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
25
|
+
|
|
26
|
+
from quant_core.app.data.constants import CANONICAL_INSTRUMENT_ID_PATTERN
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_canonical_instrument_id(instrument_id: str) -> str:
|
|
30
|
+
"""Canonical Instrument ID 唯一校验入口(复用 constants 的唯一格式规则)
|
|
31
|
+
|
|
32
|
+
拒绝空白值与非 Canonical 格式;域内事实 payload 统一走本函数,
|
|
33
|
+
不各自复制正则。
|
|
34
|
+
"""
|
|
35
|
+
if not instrument_id.strip():
|
|
36
|
+
raise ValueError("instrument_id 不允许为空或纯空白")
|
|
37
|
+
if not CANONICAL_INSTRUMENT_ID_PATTERN.match(instrument_id):
|
|
38
|
+
raise ValueError(f"非法 Canonical instrument_id: {instrument_id!r}")
|
|
39
|
+
return instrument_id
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TradingState(str, Enum):
|
|
43
|
+
"""通用交易状态(市场无关)
|
|
44
|
+
|
|
45
|
+
UNKNOWN 不是可被事件显式声明的状态:它只表达「尚无任何状态事实」,
|
|
46
|
+
由 MarketStateResolver 投影产生;TradingStateEvent 声明 UNKNOWN 属于矛盾输入。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
TRADING = "trading"
|
|
50
|
+
SUSPENDED = "suspended"
|
|
51
|
+
HALTED = "halted"
|
|
52
|
+
AUCTION = "auction"
|
|
53
|
+
CLOSED = "closed"
|
|
54
|
+
UNKNOWN = "unknown"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TradingStateEventPayload(BaseModel):
|
|
58
|
+
"""TradingStateEvent 载荷:单标的一次客观交易状态事实"""
|
|
59
|
+
|
|
60
|
+
instrument_id: str = Field(..., description="Canonical 标识(CN.XSHE.000001)")
|
|
61
|
+
state: TradingState = Field(..., description="生效交易状态")
|
|
62
|
+
|
|
63
|
+
_validate_id = field_validator("instrument_id")(
|
|
64
|
+
classmethod(lambda cls, value: validate_canonical_instrument_id(value))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@model_validator(mode="after")
|
|
68
|
+
def _reject_contradictory_state(self) -> "TradingStateEventPayload":
|
|
69
|
+
if self.state is TradingState.UNKNOWN:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"TradingStateEvent 不允许声明 UNKNOWN:UNKNOWN 只表示尚无状态事实,"
|
|
72
|
+
"只能由 MarketStateResolver 投影产生,不是客观市场事实"
|
|
73
|
+
)
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PriceLimitEventPayload(BaseModel):
|
|
78
|
+
"""PriceLimitEvent 载荷:单标的一次价格限制事实(可选市场提供)"""
|
|
79
|
+
|
|
80
|
+
instrument_id: str = Field(..., description="Canonical 标识(CN.XSHE.000001)")
|
|
81
|
+
limit_up: Decimal = Field(..., gt=0, description="涨停价(> 0)")
|
|
82
|
+
limit_down: Decimal = Field(..., gt=0, description="跌停价(> 0)")
|
|
83
|
+
buy_side_locked: bool = Field(False, description="买方向流动性锁定(涨停封板)")
|
|
84
|
+
sell_side_locked: bool = Field(False, description="卖方向流动性锁定(跌停封板)")
|
|
85
|
+
|
|
86
|
+
_validate_id = field_validator("instrument_id")(
|
|
87
|
+
classmethod(lambda cls, value: validate_canonical_instrument_id(value))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@model_validator(mode="after")
|
|
91
|
+
def _check_price_bounds(self) -> "PriceLimitEventPayload":
|
|
92
|
+
if self.limit_up <= self.limit_down:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"价格限制边界非法:limit_up {self.limit_up} 必须严格大于 "
|
|
95
|
+
f"limit_down {self.limit_down}"
|
|
96
|
+
)
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
@model_validator(mode="after")
|
|
100
|
+
def _reject_contradictory_locks(self) -> "PriceLimitEventPayload":
|
|
101
|
+
if self.buy_side_locked and self.sell_side_locked:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"买/卖方向不允许同时锁定:双向无流动性等价于停牌,"
|
|
104
|
+
"属于 TradingStateEvent 的表达范围,不是价格限制事实"
|
|
105
|
+
)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DailyStateSnapshotEventPayload(BaseModel):
|
|
110
|
+
"""DailyStateSnapshotEvent 载荷:单标的单日完整状态快照
|
|
111
|
+
|
|
112
|
+
与增量事实事件的语义边界(DECISIONS 2026-09-06「日级回放状态采用完整快照事件」):
|
|
113
|
+
- trading_state 允许显式 UNKNOWN(表达「当日无官方事实」),这是快照独有的
|
|
114
|
+
表达能力;增量 TradingStateEvent 仍拒绝 UNKNOWN;
|
|
115
|
+
- limit_up/limit_dn 同时为 None = 显式清除当日价格限制(不依赖事件缺席);
|
|
116
|
+
同时出现时必须满足 limit_up > limit_dn > 0;单边出现属矛盾输入。
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
instrument_id: str = Field(..., description="Canonical 标识(CN.XSHE.000001)")
|
|
120
|
+
trading_state: TradingState = Field(..., description="当日完整交易状态(允许 UNKNOWN)")
|
|
121
|
+
limit_up: Decimal | None = Field(None, gt=0, description="当日涨停价;None=无价格限制")
|
|
122
|
+
limit_dn: Decimal | None = Field(None, gt=0, description="当日跌停价;None=无价格限制")
|
|
123
|
+
|
|
124
|
+
_validate_id = field_validator("instrument_id")(
|
|
125
|
+
classmethod(lambda cls, value: validate_canonical_instrument_id(value))
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@model_validator(mode="after")
|
|
129
|
+
def _check_limit_pair(self) -> "DailyStateSnapshotEventPayload":
|
|
130
|
+
if (self.limit_up is None) != (self.limit_dn is None):
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"快照价格限制必须成对出现:limit_up/limit_dn 只允许同时为 None "
|
|
133
|
+
"(显式无限制)或同时给出(官方边界),单边出现属矛盾输入"
|
|
134
|
+
)
|
|
135
|
+
if (
|
|
136
|
+
self.limit_up is not None
|
|
137
|
+
and self.limit_dn is not None
|
|
138
|
+
and self.limit_up <= self.limit_dn
|
|
139
|
+
):
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"快照价格限制边界非法:limit_up {self.limit_up} 必须严格大于 "
|
|
142
|
+
f"limit_dn {self.limit_dn}"
|
|
143
|
+
)
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _check_optional_event_id(value: str | None, field: str) -> str | None:
|
|
148
|
+
"""可空事实来源标识校验:None 合法(非事件来源/默认态);非 None 必须是非空、非纯空白标识
|
|
149
|
+
|
|
150
|
+
Market 域自持校验,不 import order_execution validator。
|
|
151
|
+
"""
|
|
152
|
+
if value is None:
|
|
153
|
+
return None
|
|
154
|
+
if (
|
|
155
|
+
not isinstance(value, str)
|
|
156
|
+
or not value
|
|
157
|
+
or value.strip() != value
|
|
158
|
+
or any(ch.isspace() for ch in value)
|
|
159
|
+
):
|
|
160
|
+
raise ValueError(f"{field} 必须是 None 或非空、不含空白字符的事件标识:{value!r}")
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class PriceLimitState:
|
|
166
|
+
"""某标的 point-in-time 价格限制投影(不可变只读)
|
|
167
|
+
|
|
168
|
+
source_event_id:最近一次生效的价格限制来源事件的 event_id(provenance);
|
|
169
|
+
来源事件允许是增量 PriceLimitEvent 或日级 DailyStateSnapshotEvent(Q1-A)。
|
|
170
|
+
None 表示投影并非来自事件事实(兼容手工构造),下游不得伪造引用。
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
limit_up: Decimal
|
|
174
|
+
limit_down: Decimal
|
|
175
|
+
buy_side_locked: bool
|
|
176
|
+
sell_side_locked: bool
|
|
177
|
+
as_of: datetime
|
|
178
|
+
source_event_id: str | None = None
|
|
179
|
+
|
|
180
|
+
def __post_init__(self) -> None:
|
|
181
|
+
_check_optional_event_id(self.source_event_id, "source_event_id")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True)
|
|
185
|
+
class InstrumentMarketState:
|
|
186
|
+
"""某标的 point-in-time 市场状态只读投影(InstrumentMarketState@T)
|
|
187
|
+
|
|
188
|
+
由 MarketStateResolver 从状态事实投影而来(状态来源事件允许是增量
|
|
189
|
+
TradingStateEvent 或日级 DailyStateSnapshotEvent,Q1-A;价格限制来源
|
|
190
|
+
事件允许是增量 PriceLimitEvent 或 DailyStateSnapshotEvent):
|
|
191
|
+
- trading_state 为最近一次生效状态来源事件声明的状态;从未有任何状态
|
|
192
|
+
事件时为 UNKNOWN(默认态);
|
|
193
|
+
- trading_state_as_of 为该状态事实的 event_time;从未有状态事实时为 None
|
|
194
|
+
(即使已有价格限制事实也是 None——价格限制不改变交易状态语义);
|
|
195
|
+
- price_limit 为最近一次生效价格限制事实声明的边界,其生效时间独立记录
|
|
196
|
+
在 PriceLimitState.as_of;从未有价格限制事实时为 None。
|
|
197
|
+
- trading_state_event_id 为最近一次生效状态来源事件的 event_id
|
|
198
|
+
(provenance):增量 TradingStateEvent 或 DailyStateSnapshotEvent
|
|
199
|
+
(含显式 UNKNOWN 的快照——快照是事实来源,拥有 provenance);
|
|
200
|
+
仅 UNKNOWN 默认态(从未收到任何状态事件)或非事件来源为 None,
|
|
201
|
+
不伪造来源标识。
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
instrument_id: str
|
|
205
|
+
trading_state_as_of: datetime | None
|
|
206
|
+
trading_state: TradingState
|
|
207
|
+
price_limit: PriceLimitState | None = None
|
|
208
|
+
trading_state_event_id: str | None = None
|
|
209
|
+
|
|
210
|
+
def __post_init__(self) -> None:
|
|
211
|
+
_check_optional_event_id(self.trading_state_event_id, "trading_state_event_id")
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""CalendarCoverage - 交易所交易日历覆盖判定(T4R AC-7 三入口统一)
|
|
2
|
+
|
|
3
|
+
「返回行非空」≠「日历已完整同步」:请求涉及的每个自然日都必须有一行日历事实,
|
|
4
|
+
缺行是数据缺口(结构化失败),与休市(is_open=False,显式声明)严格区分。
|
|
5
|
+
|
|
6
|
+
三入口共享同一判定:
|
|
7
|
+
- sync_bars(写入前基线校验)
|
|
8
|
+
- verify_bars(数据集健康检查)
|
|
9
|
+
- Replay(回放窗口 open days 校验)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import date
|
|
14
|
+
|
|
15
|
+
from quant_core.app.data.exceptions import MarketDataNotSyncedException
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class CalendarCoverage:
|
|
20
|
+
"""一个交易所在 [start, end] 内的日历覆盖事实"""
|
|
21
|
+
|
|
22
|
+
exchange: str
|
|
23
|
+
start: date
|
|
24
|
+
end: date
|
|
25
|
+
open_days: tuple[date, ...]
|
|
26
|
+
missing_days: tuple[date, ...]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def complete(self) -> bool:
|
|
30
|
+
"""每个自然日都有日历行(is_open 与否均可)"""
|
|
31
|
+
return not self.missing_days
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def open_day_set(self) -> frozenset[date]:
|
|
35
|
+
return frozenset(self.open_days)
|
|
36
|
+
|
|
37
|
+
def require_complete(self) -> "CalendarCoverage":
|
|
38
|
+
"""日历不完整 → 结构化失败(绝不静默当作空/休市区间)"""
|
|
39
|
+
if not self.complete:
|
|
40
|
+
raise MarketDataNotSyncedException(
|
|
41
|
+
f"交易日历未同步({self.exchange} {self.start}~{self.end}),"
|
|
42
|
+
f"缺少 {len(self.missing_days)} 个自然日的日历行"
|
|
43
|
+
f"(如 {self.missing_days[0]}),请先 sync calendar"
|
|
44
|
+
)
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_calendar_coverage(exchange: str, start: date, end: date, rows: list) -> CalendarCoverage:
|
|
49
|
+
"""由日历行构建覆盖判定;rows 需带 trade_date / is_open 字段"""
|
|
50
|
+
row_dates = {r.trade_date for r in rows}
|
|
51
|
+
missing = tuple(d for d in _daterange(start, end) if d not in row_dates)
|
|
52
|
+
open_days = tuple(sorted(r.trade_date for r in rows if r.is_open))
|
|
53
|
+
return CalendarCoverage(
|
|
54
|
+
exchange=exchange,
|
|
55
|
+
start=start,
|
|
56
|
+
end=end,
|
|
57
|
+
open_days=open_days,
|
|
58
|
+
missing_days=missing,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _daterange(start: date, end: date) -> list[date]:
|
|
63
|
+
return [date.fromordinal(o) for o in range(start.toordinal(), end.toordinal() + 1)]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""固定下载批次的验证证据;回放不再读取远端可变状态。"""
|
|
2
|
+
|
|
3
|
+
from quant_core.app.data.schema.reliability_schema import DatasetHealth, DatasetHealthResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PreparedDataVerification:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self.results: dict[tuple, DatasetHealthResult] = {}
|
|
9
|
+
|
|
10
|
+
def add(self, dataset: str, instrument_id, frequency, start, end, result) -> None:
|
|
11
|
+
self.results[(dataset, instrument_id, frequency, start, end)] = (
|
|
12
|
+
DatasetHealthResult.model_validate(result.model_dump())
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
async def verify_bars(self, instrument_id, frequency, start, end):
|
|
16
|
+
return self._covering("bar", instrument_id, frequency, start, end)
|
|
17
|
+
|
|
18
|
+
async def verify_adjustment_factors(self, instrument_id, frequency, start, end):
|
|
19
|
+
return self._covering("adjustment_factor", instrument_id, frequency, start, end)
|
|
20
|
+
|
|
21
|
+
async def verify_corporate_actions(self, instrument_id, start, end):
|
|
22
|
+
return self._covering("corporate_action", instrument_id, "1d", start, end)
|
|
23
|
+
|
|
24
|
+
def _covering(self, dataset, instrument_id, frequency, start, end) -> DatasetHealthResult:
|
|
25
|
+
"""完整大区间证明可用于子区间;保留原验证范围与计数,不伪造裁剪统计。"""
|
|
26
|
+
exact = self.results.get((dataset, instrument_id, frequency, start, end))
|
|
27
|
+
if exact is not None:
|
|
28
|
+
return exact
|
|
29
|
+
candidates = [
|
|
30
|
+
result
|
|
31
|
+
for (kind, instrument, freq, lower, upper), result in self.results.items()
|
|
32
|
+
if kind == dataset
|
|
33
|
+
and instrument == instrument_id
|
|
34
|
+
and freq == frequency
|
|
35
|
+
and lower <= start <= end <= upper
|
|
36
|
+
and result.health in (DatasetHealth.HEALTHY, DatasetHealth.STALE)
|
|
37
|
+
]
|
|
38
|
+
if not candidates:
|
|
39
|
+
raise ValueError("本地快照没有覆盖请求范围的完整验证证据,请重新准备数据")
|
|
40
|
+
return min(candidates, key=lambda result: result.requested_end - result.requested_start)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""回测运行内的只读市场数据集;时间片推进期间不访问网络。"""
|
|
2
|
+
|
|
3
|
+
from bisect import bisect_left, bisect_right
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
|
|
6
|
+
from quant_core.app.data.exceptions import MarketDataNotSyncedException
|
|
7
|
+
from quant_core.app.data.schema.adjustment_schema import BarViewRequest
|
|
8
|
+
from quant_core.app.data.schema.corporate_action_schema import CorporateActionResponse
|
|
9
|
+
from quant_core.app.data.schema.history_schema import HistoryFacts
|
|
10
|
+
from quant_core.app.data.schema.market_schema import (
|
|
11
|
+
BarResponse,
|
|
12
|
+
InstrumentResponse,
|
|
13
|
+
TradingCalendarDayResponse,
|
|
14
|
+
TradingRuleResponse,
|
|
15
|
+
)
|
|
16
|
+
from quant_core.app.data.schema.pit_daily_window import PITDailyWindow
|
|
17
|
+
from quant_core.app.data.schema.quote_schema import DailyBasicResponse
|
|
18
|
+
from quant_core.app.data.service.price_adjustment_service import PriceAdjustmentService
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PreparedMarketData:
|
|
22
|
+
"""用已准备的事实提供范围查询与复权计算,不含数据库或 SDK 连接。"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, start: datetime, end: datetime, revision: str) -> None:
|
|
25
|
+
self.start = start
|
|
26
|
+
self.end = end
|
|
27
|
+
self.revision = revision
|
|
28
|
+
self.facts: dict[str, HistoryFacts] = {}
|
|
29
|
+
self.bars: dict[tuple[str, str], list[BarResponse]] = {}
|
|
30
|
+
self.timestamps: dict[tuple[str, str], list[datetime]] = {}
|
|
31
|
+
self.instruments: list[InstrumentResponse] = []
|
|
32
|
+
|
|
33
|
+
def add_bars(self, instrument_id: str, frequency: str, bars: list[BarResponse]) -> None:
|
|
34
|
+
key = (instrument_id, frequency)
|
|
35
|
+
self.bars[key] = bars
|
|
36
|
+
self.timestamps[key] = [bar.timestamp for bar in bars]
|
|
37
|
+
|
|
38
|
+
async def get_instrument(self, instrument_id: str) -> InstrumentResponse:
|
|
39
|
+
for row in self.instruments:
|
|
40
|
+
if row.instrument_id == instrument_id:
|
|
41
|
+
return row
|
|
42
|
+
raise MarketDataNotSyncedException(f"数据集不包含标的 {instrument_id}")
|
|
43
|
+
|
|
44
|
+
async def search_instruments(
|
|
45
|
+
self, keyword=None, instrument_type=None, exchange=None, limit=100
|
|
46
|
+
):
|
|
47
|
+
return [
|
|
48
|
+
row
|
|
49
|
+
for row in self.instruments
|
|
50
|
+
if (keyword is None or keyword in row.name or keyword in row.symbol)
|
|
51
|
+
and (instrument_type is None or row.instrument_type == instrument_type)
|
|
52
|
+
and (exchange is None or row.exchange == exchange)
|
|
53
|
+
][:limit]
|
|
54
|
+
|
|
55
|
+
async def list_instruments_on(self, on_date: date, limit=10000):
|
|
56
|
+
rows = [
|
|
57
|
+
row
|
|
58
|
+
for row in self.instruments
|
|
59
|
+
if row.listed_date is not None
|
|
60
|
+
and row.listed_date <= on_date
|
|
61
|
+
and (row.delisted_date is None or row.delisted_date >= on_date)
|
|
62
|
+
]
|
|
63
|
+
if len(rows) > limit:
|
|
64
|
+
raise ValueError("股票池超出上限,拒绝截断")
|
|
65
|
+
return rows
|
|
66
|
+
|
|
67
|
+
async def list_trading_rules(self, instrument_ids: list[str], start: date, end: date):
|
|
68
|
+
return [
|
|
69
|
+
TradingRuleResponse.model_validate(rule)
|
|
70
|
+
for instrument_id in instrument_ids
|
|
71
|
+
for rule in self.facts[instrument_id].trading_rules
|
|
72
|
+
if rule.effective_from <= end
|
|
73
|
+
and (rule.effective_to is None or rule.effective_to >= start)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
async def list_bars(self, instrument_id, frequency, start=None, end=None, limit=1000):
|
|
77
|
+
if (start is not None and start < self.start) or (end is not None and end > self.end):
|
|
78
|
+
raise ValueError("行情请求超出已准备时间范围,请扩大数据准备窗口")
|
|
79
|
+
key = (instrument_id, frequency)
|
|
80
|
+
if key not in self.bars:
|
|
81
|
+
raise MarketDataNotSyncedException(f"数据集未准备 {key}")
|
|
82
|
+
timestamps = self.timestamps[key]
|
|
83
|
+
left = bisect_left(timestamps, start) if start else 0
|
|
84
|
+
right = bisect_right(timestamps, end) if end else len(timestamps)
|
|
85
|
+
return self.bars[key][max(left, right - limit) : right]
|
|
86
|
+
|
|
87
|
+
async def get_bars(self, instrument_id, frequency, start=None, end=None, limit=1000):
|
|
88
|
+
rows = await self.list_bars(instrument_id, frequency, start, end, limit)
|
|
89
|
+
if not rows:
|
|
90
|
+
raise MarketDataNotSyncedException(f"数据集中没有 {instrument_id} 的请求行情")
|
|
91
|
+
return rows
|
|
92
|
+
|
|
93
|
+
async def get_adjusted_bars(
|
|
94
|
+
self, instrument_id, frequency, view: BarViewRequest, start=None, end=None, limit=1000
|
|
95
|
+
):
|
|
96
|
+
rows = await self.get_bars(instrument_id, frequency, start, end, limit)
|
|
97
|
+
return await PriceAdjustmentService(self).adjust(rows, view)
|
|
98
|
+
|
|
99
|
+
async def get_pit_daily_windows(
|
|
100
|
+
self,
|
|
101
|
+
instrument_id: str,
|
|
102
|
+
days: list[date],
|
|
103
|
+
next_days: list[date],
|
|
104
|
+
lookback: int,
|
|
105
|
+
) -> tuple[PITDailyWindow, ...]:
|
|
106
|
+
"""让已有 Alpha 输入适配器复用本地原始行情与复权事实,不发起 HTTP。"""
|
|
107
|
+
if not days or len(days) != len(next_days) or lookback < 1:
|
|
108
|
+
raise ValueError("PIT 窗口请求非法")
|
|
109
|
+
if min(days) < self.start.date() or max(next_days) > self.end.date():
|
|
110
|
+
raise ValueError("PIT 窗口超出本地数据范围,请扩展准备范围")
|
|
111
|
+
if instrument_id not in self.facts or (instrument_id, "1d") not in self.bars:
|
|
112
|
+
raise MarketDataNotSyncedException("本地数据缺少行情或复权事实")
|
|
113
|
+
factors = await self.list_adjustment_factors(
|
|
114
|
+
instrument_id, self.start.date(), max(next_days)
|
|
115
|
+
)
|
|
116
|
+
return PriceAdjustmentService.pit_daily_windows(
|
|
117
|
+
self.bars[(instrument_id, "1d")], factors, days, next_days, lookback
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def list_calendar_days(self, exchange: str, start: date, end: date, only_open=False):
|
|
121
|
+
if start < self.start.date() or end > self.end.date():
|
|
122
|
+
raise ValueError("日历请求超出已准备时间范围")
|
|
123
|
+
facts = next(
|
|
124
|
+
(item for item in self.facts.values() if item.instrument.exchange == exchange), None
|
|
125
|
+
)
|
|
126
|
+
if facts is None:
|
|
127
|
+
raise MarketDataNotSyncedException(f"数据集未准备 {exchange} 日历")
|
|
128
|
+
return [
|
|
129
|
+
TradingCalendarDayResponse.model_validate(row)
|
|
130
|
+
for row in facts.calendar
|
|
131
|
+
if start <= row.trade_date <= end and (not only_open or row.is_open)
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
async def list_trading_days(self, exchange: str, start: date, end: date):
|
|
135
|
+
return await self.list_calendar_days(exchange, start, end, only_open=True)
|
|
136
|
+
|
|
137
|
+
async def is_trading_day(self, exchange: str, trade_date: date) -> bool:
|
|
138
|
+
rows = await self.list_calendar_days(exchange, trade_date, trade_date)
|
|
139
|
+
if not rows:
|
|
140
|
+
raise MarketDataNotSyncedException(f"日历缺少 {trade_date}")
|
|
141
|
+
return rows[0].is_open
|
|
142
|
+
|
|
143
|
+
async def list_daily_facts(self, instrument_id: str, start: date, end: date):
|
|
144
|
+
return [
|
|
145
|
+
row for row in self.facts[instrument_id].daily_facts if start <= row.trade_date <= end
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
async def list_corporate_actions(
|
|
149
|
+
self, instrument_ids: list[str], start: date, end: date, known_at=None
|
|
150
|
+
):
|
|
151
|
+
if known_at is not None:
|
|
152
|
+
raise ValueError("当前公司行动快照未包含历史知识时间修订,不能执行 known_at 查询")
|
|
153
|
+
return [
|
|
154
|
+
CorporateActionResponse.model_validate(row)
|
|
155
|
+
for instrument_id in instrument_ids
|
|
156
|
+
for row in self.facts[instrument_id].corporate_actions
|
|
157
|
+
if any(
|
|
158
|
+
day is not None and start <= day <= end
|
|
159
|
+
for day in (row.record_date, row.ex_date, row.pay_date, row.listing_date)
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
async def list_adjustment_factors(
|
|
164
|
+
self, instrument_id: str, start: date, end: date, known_at=None
|
|
165
|
+
):
|
|
166
|
+
latest = {}
|
|
167
|
+
for row in self.facts[instrument_id].adjustment_factors:
|
|
168
|
+
if start <= row.trade_date <= end and (known_at is None or row.recorded_at <= known_at):
|
|
169
|
+
old = latest.get(row.trade_date)
|
|
170
|
+
if old is None or row.recorded_at > old.recorded_at:
|
|
171
|
+
latest[row.trade_date] = row
|
|
172
|
+
return [latest[day] for day in sorted(latest)]
|
|
173
|
+
|
|
174
|
+
async def get_adjustment_factor_on_or_before(
|
|
175
|
+
self, instrument_id, on_or_before=None, known_at=None
|
|
176
|
+
):
|
|
177
|
+
rows = await self.list_adjustment_factors(
|
|
178
|
+
instrument_id,
|
|
179
|
+
self.start.date(),
|
|
180
|
+
on_or_before or self.end.date(),
|
|
181
|
+
known_at,
|
|
182
|
+
)
|
|
183
|
+
return rows[-1] if rows else None
|
|
184
|
+
|
|
185
|
+
async def list_daily_basic_at(
|
|
186
|
+
self, decision_time: datetime, limit: int = 10000
|
|
187
|
+
) -> list[DailyBasicResponse]:
|
|
188
|
+
"""当前快照不含每日估值指标;明确缺少输入,不能隐式联网补数。"""
|
|
189
|
+
raise MarketDataNotSyncedException("本地数据快照未准备每日估值指标")
|