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.
Files changed (355) hide show
  1. powerquant_cli-0.2.6.dist-info/METADATA +62 -0
  2. powerquant_cli-0.2.6.dist-info/RECORD +355 -0
  3. powerquant_cli-0.2.6.dist-info/WHEEL +4 -0
  4. powerquant_cli-0.2.6.dist-info/entry_points.txt +2 -0
  5. powerquant_cli-0.2.6.dist-info/licenses/LICENSE +36 -0
  6. powerquant_cli-0.2.6.dist-info/licenses/licenses/LICENSE.vnpy +21 -0
  7. pqdata/__init__.py +6 -0
  8. pqdata/client.py +389 -0
  9. pqdata/download.py +60 -0
  10. pqdata/exceptions.py +14 -0
  11. pqdata/schema/__init__.py +1 -0
  12. pqdata/schema/adjustment_schema.py +67 -0
  13. pqdata/schema/bar_content_schema.py +24 -0
  14. pqdata/schema/bar_schema.py +56 -0
  15. pqdata/schema/corporate_action_schema.py +46 -0
  16. pqdata/schema/history_schema.py +61 -0
  17. pqdata/schema/pit_daily_window.py +16 -0
  18. pqdata/schema/query_schema.py +53 -0
  19. pqdata/schema/quote_schema.py +73 -0
  20. pqdata/schema/reference_schema.py +48 -0
  21. pqdata/schema/reliability_schema.py +152 -0
  22. quant_cli/__init__.py +0 -0
  23. quant_cli/api.py +39 -0
  24. quant_cli/app/__init__.py +0 -0
  25. quant_cli/app/backtest/__init__.py +0 -0
  26. quant_cli/app/backtest/cli/__init__.py +0 -0
  27. quant_cli/app/backtest/cli/local_backtest_cli.py +72 -0
  28. quant_cli/app/backtest/local_backtest_dependency.py +10 -0
  29. quant_cli/app/backtest/local_backtest_schema.py +49 -0
  30. quant_cli/app/backtest/repository/__init__.py +0 -0
  31. quant_cli/app/backtest/repository/backtest_local_repository.py +67 -0
  32. quant_cli/app/backtest/service/__init__.py +0 -0
  33. quant_cli/app/backtest/service/local_backtest_service.py +271 -0
  34. quant_cli/app/data/__init__.py +0 -0
  35. quant_cli/app/data/cli/__init__.py +0 -0
  36. quant_cli/app/data/cli/data_cli.py +78 -0
  37. quant_cli/app/data/data_dependency.py +35 -0
  38. quant_cli/app/data/repository/__init__.py +0 -0
  39. quant_cli/app/data/repository/parquet_repository.py +124 -0
  40. quant_cli/app/data/schema/__init__.py +0 -0
  41. quant_cli/app/data/schema/dataset_audit_schema.py +37 -0
  42. quant_cli/app/data/schema/dataset_request_schema.py +28 -0
  43. quant_cli/app/data/schema/parquet_file_schema.py +17 -0
  44. quant_cli/app/data/service/__init__.py +0 -0
  45. quant_cli/app/data/service/bar_cache_service.py +287 -0
  46. quant_cli/app/data/service/dataset_audit_service.py +137 -0
  47. quant_cli/app/data/service/dataset_load_service.py +115 -0
  48. quant_cli/app/data/service/dataset_prepare_service.py +187 -0
  49. quant_cli/app/factor/__init__.py +0 -0
  50. quant_cli/app/factor/cli/__init__.py +0 -0
  51. quant_cli/app/factor/cli/factor_definition_cli.py +81 -0
  52. quant_cli/app/factor/cli/local_factor_cli.py +102 -0
  53. quant_cli/app/factor/cli/local_selection_cli.py +58 -0
  54. quant_cli/app/factor/interface/__init__.py +0 -0
  55. quant_cli/app/factor/interface/local_selection_store.py +12 -0
  56. quant_cli/app/factor/local_factor_dependency.py +38 -0
  57. quant_cli/app/factor/repository/__init__.py +0 -0
  58. quant_cli/app/factor/repository/factor_local_repository.py +154 -0
  59. quant_cli/app/factor/repository/selection_local_repository.py +67 -0
  60. quant_cli/app/factor/schema/local_selection_schema.py +56 -0
  61. quant_cli/app/factor/service/__init__.py +0 -0
  62. quant_cli/app/factor/service/factor_data_prepare_service.py +181 -0
  63. quant_cli/app/factor/service/local_factor_query_service.py +146 -0
  64. quant_cli/app/factor/service/local_factor_service.py +120 -0
  65. quant_cli/app/factor/service/local_selection_service.py +139 -0
  66. quant_cli/app/job/__init__.py +0 -0
  67. quant_cli/app/job/cli/__init__.py +0 -0
  68. quant_cli/app/job/cli/local_job_cli.py +69 -0
  69. quant_cli/app/job/schema/__init__.py +0 -0
  70. quant_cli/app/job/schema/local_job_schema.py +11 -0
  71. quant_cli/app/job/service/__init__.py +0 -0
  72. quant_cli/app/job/service/local_job_service.py +60 -0
  73. quant_cli/app/quant_cli.py +49 -0
  74. quant_cli/app/research/__init__.py +0 -0
  75. quant_cli/app/research/cli/__init__.py +0 -0
  76. quant_cli/app/research/cli/local_research_cli.py +93 -0
  77. quant_cli/app/research/local_research_dependency.py +10 -0
  78. quant_cli/app/research/repository/__init__.py +0 -0
  79. quant_cli/app/research/repository/research_local_repository.py +67 -0
  80. quant_cli/app/research/schema/__init__.py +0 -0
  81. quant_cli/app/research/schema/local_research_schema.py +26 -0
  82. quant_cli/app/research/service/__init__.py +0 -0
  83. quant_cli/app/research/service/local_research_execution_service.py +105 -0
  84. quant_cli/app/research/service/local_research_service.py +101 -0
  85. quant_cli/app/strategy/__init__.py +0 -0
  86. quant_cli/app/strategy/cli/__init__.py +0 -0
  87. quant_cli/app/strategy/cli/strategy_cli.py +104 -0
  88. quant_cli/common/__init__.py +0 -0
  89. quant_cli/common/constants.py +7 -0
  90. quant_cli/common/database/__init__.py +0 -0
  91. quant_cli/common/database/duckdb_connection.py +30 -0
  92. quant_cli/common/http/__init__.py +0 -0
  93. quant_cli/common/http/definition_client.py +54 -0
  94. quant_cli/common/utils/__init__.py +0 -0
  95. quant_cli/common/utils/cli_utils.py +37 -0
  96. quant_cli/common/utils/platform_utils.py +11 -0
  97. quant_cli/common/utils/runtime_environment.py +12 -0
  98. quant_cli/examples/cached_factor_screen.py +165 -0
  99. quant_cli/examples/factors/ma_close_10.py +10 -0
  100. quant_cli/examples/factors/max_drawdown_20d.py +18 -0
  101. quant_cli/examples/factors/momentum_20d.py +13 -0
  102. quant_cli/examples/factors/multi_intraday_screen.py +16 -0
  103. quant_cli/examples/factors/risk_adjusted_momentum_20d.py +18 -0
  104. quant_cli/examples/factors/volume_ratio_5d.py +14 -0
  105. quant_cli/examples/factors/volume_weighted_close_pressure.py +14 -0
  106. quant_cli/examples/full_market_screen.py +189 -0
  107. quant_cli/examples/local_quant_validation.py +196 -0
  108. quant_cli/examples/published_definition_validation.py +219 -0
  109. quant_cli/examples/research/factor_pit_coverage.py +87 -0
  110. quant_cli/examples/research/ipo_first_day.py +348 -0
  111. quant_cli/examples/selection/README.md +59 -0
  112. quant_cli/examples/selection/backtest.json +18 -0
  113. quant_cli/examples/selection/buy_hold.py +7 -0
  114. quant_cli/examples/selection/compute.json +15 -0
  115. quant_cli/examples/selection/prepare.json +13 -0
  116. quant_cli/examples/selection/select.json +44 -0
  117. quant_cli/examples/strategies/bar_roundtrip.py +12 -0
  118. quant_cli/examples/strategies/buy_and_hold.py +11 -0
  119. quant_cli/examples/strategies/contrarian_timing.py +14 -0
  120. quant_cli/examples/strategies/factor_gate_vwap.py +22 -0
  121. quant_cli/examples/strategies/ipo_next_day_vwap.py +31 -0
  122. quant_cli/examples/strategies/momentum_timing.py +14 -0
  123. quant_cli/examples/strategies/monthly_equal_weight.py +19 -0
  124. quant_cli/examples/strategies/session_roundtrip.py +16 -0
  125. quant_cli/examples/strategies/trend_drawdown_guard.py +22 -0
  126. quant_cli/examples/strategies/weekly_risk_adjusted_rotation.py +61 -0
  127. quant_cli/examples/unified_snapshot_validation.py +260 -0
  128. quant_core/__init__.py +0 -0
  129. quant_core/app/SPEC.md +18 -0
  130. quant_core/app/__init__.py +0 -0
  131. quant_core/app/backtest/SPEC.md +65 -0
  132. quant_core/app/backtest/__init__.py +1 -0
  133. quant_core/app/backtest/backtest_input_schema.py +20 -0
  134. quant_core/app/backtest/backtest_run.py +393 -0
  135. quant_core/app/backtest/data_guard.py +77 -0
  136. quant_core/app/backtest/historical_clock.py +26 -0
  137. quant_core/app/backtest/legacy_result_schema.py +34 -0
  138. quant_core/app/backtest/metrics.py +194 -0
  139. quant_core/app/backtest/performance_report.py +61 -0
  140. quant_core/app/backtest/report_schema.py +64 -0
  141. quant_core/app/backtest/schema.py +477 -0
  142. quant_core/app/backtest/service/__init__.py +0 -0
  143. quant_core/app/backtest/service/backtest_execution_service.py +114 -0
  144. quant_core/app/data/__init__.py +0 -0
  145. quant_core/app/data/constants.py +284 -0
  146. quant_core/app/data/domain/__init__.py +0 -0
  147. quant_core/app/data/domain/trading_quantity_value.py +57 -0
  148. quant_core/app/data/exceptions.py +95 -0
  149. quant_core/app/data/interface/__init__.py +0 -0
  150. quant_core/app/data/interface/adjustment_factor_query.py +21 -0
  151. quant_core/app/data/interface/market_data_query.py +71 -0
  152. quant_core/app/data/interface/market_data_verification.py +20 -0
  153. quant_core/app/data/interface/market_event_source.py +11 -0
  154. quant_core/app/data/interface/market_revision_query.py +8 -0
  155. quant_core/app/data/interface/market_runtime_repository.py +44 -0
  156. quant_core/app/data/interface/market_time_slice_source.py +11 -0
  157. quant_core/app/data/repository/__init__.py +0 -0
  158. quant_core/app/data/schema/__init__.py +1 -0
  159. quant_core/app/data/schema/adjustment_schema.py +67 -0
  160. quant_core/app/data/schema/corporate_action_schema.py +46 -0
  161. quant_core/app/data/schema/event_schema.py +506 -0
  162. quant_core/app/data/schema/exchange_operating_state.py +37 -0
  163. quant_core/app/data/schema/exchange_state_schema.py +106 -0
  164. quant_core/app/data/schema/history_schema.py +48 -0
  165. quant_core/app/data/schema/instrument_identity.py +69 -0
  166. quant_core/app/data/schema/market_schema.py +128 -0
  167. quant_core/app/data/schema/pit_daily_window.py +16 -0
  168. quant_core/app/data/schema/quote_schema.py +73 -0
  169. quant_core/app/data/schema/reliability_schema.py +152 -0
  170. quant_core/app/data/schema/snapshot_schema.py +82 -0
  171. quant_core/app/data/schema/trading_state_schema.py +211 -0
  172. quant_core/app/data/service/__init__.py +0 -0
  173. quant_core/app/data/service/calendar_coverage_service.py +63 -0
  174. quant_core/app/data/service/prepared_data_verification_service.py +40 -0
  175. quant_core/app/data/service/prepared_market_service.py +189 -0
  176. quant_core/app/data/service/price_adjustment_service.py +235 -0
  177. quant_core/app/factor/LICENSE.vnpy +21 -0
  178. quant_core/app/factor/__init__.py +13 -0
  179. quant_core/app/factor/builtin/__init__.py +0 -0
  180. quant_core/app/factor/builtin/alpha101_recipe.py +1095 -0
  181. quant_core/app/factor/builtin/alpha158_recipe.py +1284 -0
  182. quant_core/app/factor/builtin/factor_recipe_catalog.py +100 -0
  183. quant_core/app/factor/computation/__init__.py +0 -0
  184. quant_core/app/factor/computation/factor_batch_evaluator.py +141 -0
  185. quant_core/app/factor/computation/factor_expression.py +106 -0
  186. quant_core/app/factor/computation/factor_source_loader.py +24 -0
  187. quant_core/app/factor/computation/operators/__init__.py +0 -0
  188. quant_core/app/factor/computation/operators/arithmetic_operator.py +182 -0
  189. quant_core/app/factor/computation/operators/cross_section_operator.py +66 -0
  190. quant_core/app/factor/computation/operators/time_series_operator.py +342 -0
  191. quant_core/app/factor/computation/panel_evaluator.py +59 -0
  192. quant_core/app/factor/computation/panel_expression.py +193 -0
  193. quant_core/app/factor/computation/python_factor_executor.py +87 -0
  194. quant_core/app/factor/computation/universe_filter.py +25 -0
  195. quant_core/app/factor/constants.py +114 -0
  196. quant_core/app/factor/exceptions.py +35 -0
  197. quant_core/app/factor/factor_dependency.py +65 -0
  198. quant_core/app/factor/interface/__init__.py +0 -0
  199. quant_core/app/factor/interface/factor_compute.py +12 -0
  200. quant_core/app/factor/interface/factor_execution.py +15 -0
  201. quant_core/app/factor/interface/factor_local_data.py +42 -0
  202. quant_core/app/factor/interface/factor_panel_compute.py +18 -0
  203. quant_core/app/factor/interface/factor_panel_input.py +12 -0
  204. quant_core/app/factor/interface/factor_query_store.py +39 -0
  205. quant_core/app/factor/interface/public/__init__.py +0 -0
  206. quant_core/app/factor/interface/public/factor_diagnostics.py +18 -0
  207. quant_core/app/factor/interface/public/factor_exposure_query.py +12 -0
  208. quant_core/app/factor/interface/public/factor_local.py +15 -0
  209. quant_core/app/factor/interface/public/factor_query.py +57 -0
  210. quant_core/app/factor/interface/public/factor_selection.py +20 -0
  211. quant_core/app/factor/model/__init__.py +0 -0
  212. quant_core/app/factor/repository/__init__.py +0 -0
  213. quant_core/app/factor/schema/__init__.py +1 -0
  214. quant_core/app/factor/schema/diagnostic_types.py +42 -0
  215. quant_core/app/factor/schema/factor_artifact_schema.py +36 -0
  216. quant_core/app/factor/schema/factor_computation_schema.py +102 -0
  217. quant_core/app/factor/schema/factor_compute_request.py +24 -0
  218. quant_core/app/factor/schema/factor_contract.py +257 -0
  219. quant_core/app/factor/schema/factor_input_schema.py +32 -0
  220. quant_core/app/factor/schema/factor_panel.py +65 -0
  221. quant_core/app/factor/schema/factor_publication_schema.py +41 -0
  222. quant_core/app/factor/schema/factor_query_result_schema.py +12 -0
  223. quant_core/app/factor/schema/factor_result_schema.py +6 -0
  224. quant_core/app/factor/schema/local_factor_schema.py +42 -0
  225. quant_core/app/factor/schema/panel_policy.py +13 -0
  226. quant_core/app/factor/schema/selection_report.py +116 -0
  227. quant_core/app/factor/schema/selection_types.py +135 -0
  228. quant_core/app/factor/service/__init__.py +1 -0
  229. quant_core/app/factor/service/factor_compute_service.py +140 -0
  230. quant_core/app/factor/service/factor_diagnostic_runner.py +134 -0
  231. quant_core/app/factor/service/factor_diagnostics_service.py +140 -0
  232. quant_core/app/factor/service/factor_exposure_service.py +24 -0
  233. quant_core/app/factor/service/factor_input_service.py +258 -0
  234. quant_core/app/factor/service/factor_local_execution_service.py +51 -0
  235. quant_core/app/factor/service/factor_panel_service.py +86 -0
  236. quant_core/app/factor/service/factor_runtime_service.py +306 -0
  237. quant_core/app/factor/service/factor_selection_runner.py +88 -0
  238. quant_core/app/factor/service/published_factor_execution_service.py +160 -0
  239. quant_core/app/factor/service/selection_service.py +234 -0
  240. quant_core/app/factor/visibility.py +22 -0
  241. quant_core/app/opportunity/SPEC.md +45 -0
  242. quant_core/app/opportunity/__init__.py +1 -0
  243. quant_core/app/order_execution/SPEC.md +64 -0
  244. quant_core/app/order_execution/__init__.py +1 -0
  245. quant_core/app/order_execution/bar_matcher.py +55 -0
  246. quant_core/app/order_execution/constants.py +142 -0
  247. quant_core/app/order_execution/domain/__init__.py +45 -0
  248. quant_core/app/order_execution/domain/execution_batch.py +108 -0
  249. quant_core/app/order_execution/domain/order_entity.py +197 -0
  250. quant_core/app/order_execution/domain/order_event.py +87 -0
  251. quant_core/app/order_execution/domain/order_policy.py +29 -0
  252. quant_core/app/order_execution/domain/order_value.py +142 -0
  253. quant_core/app/order_execution/fee_model.py +60 -0
  254. quant_core/app/order_execution/interface/__init__.py +0 -0
  255. quant_core/app/order_execution/interface/order_account.py +10 -0
  256. quant_core/app/order_execution/interface/order_trading_rules.py +10 -0
  257. quant_core/app/order_execution/interface/public/__init__.py +0 -0
  258. quant_core/app/order_execution/interface/public/execution_account.py +53 -0
  259. quant_core/app/order_execution/interface/public/order_events.py +19 -0
  260. quant_core/app/order_execution/interface/public/order_execution.py +107 -0
  261. quant_core/app/order_execution/interface/public/order_runtime.py +30 -0
  262. quant_core/app/order_execution/interface/public/order_submission.py +17 -0
  263. quant_core/app/order_execution/order_event_bus.py +28 -0
  264. quant_core/app/order_execution/order_execution_dependency.py +48 -0
  265. quant_core/app/order_execution/order_submission_service.py +301 -0
  266. quant_core/app/order_execution/schema/__init__.py +0 -0
  267. quant_core/app/order_execution/schema/order_request_schema.py +88 -0
  268. quant_core/app/order_execution/simulation_broker.py +883 -0
  269. quant_core/app/order_execution/slippage_model.py +40 -0
  270. quant_core/app/order_execution/tradability_policy.py +174 -0
  271. quant_core/app/order_execution/validators.py +40 -0
  272. quant_core/app/portfolio/SPEC.md +69 -0
  273. quant_core/app/portfolio/__init__.py +0 -0
  274. quant_core/app/portfolio/domain/__init__.py +32 -0
  275. quant_core/app/portfolio/domain/entitlement_entity.py +45 -0
  276. quant_core/app/portfolio/domain/portfolio_entity.py +511 -0
  277. quant_core/app/portfolio/domain/portfolio_event.py +59 -0
  278. quant_core/app/portfolio/domain/portfolio_value.py +21 -0
  279. quant_core/app/portfolio/domain/position_entity.py +165 -0
  280. quant_core/app/portfolio/domain/reservation_entity.py +69 -0
  281. quant_core/app/portfolio/interface/__init__.py +0 -0
  282. quant_core/app/portfolio/interface/public/__init__.py +0 -0
  283. quant_core/app/portfolio/interface/public/portfolio.py +77 -0
  284. quant_core/app/portfolio/interface/public/simulation_account.py +16 -0
  285. quant_core/app/portfolio/ledger_schema.py +130 -0
  286. quant_core/app/portfolio/order_account_listener.py +36 -0
  287. quant_core/app/portfolio/portfolio_dependency.py +21 -0
  288. quant_core/app/portfolio/portfolio_schema.py +204 -0
  289. quant_core/app/portfolio/simulated_portfolio.py +427 -0
  290. quant_core/app/research/SPEC.md +57 -0
  291. quant_core/app/research/__init__.py +0 -0
  292. quant_core/app/research/adapter/__init__.py +0 -0
  293. quant_core/app/research/adapter/market_label_query.py +84 -0
  294. quant_core/app/research/interface/__init__.py +0 -0
  295. quant_core/app/research/interface/research_dataset.py +47 -0
  296. quant_core/app/research/interface/research_evaluation.py +19 -0
  297. quant_core/app/research/schema/__init__.py +0 -0
  298. quant_core/app/research/schema/comparison_schema.py +12 -0
  299. quant_core/app/research/schema/dataset_schema.py +112 -0
  300. quant_core/app/research/schema/research_schema.py +120 -0
  301. quant_core/app/research/service/__init__.py +0 -0
  302. quant_core/app/research/service/research_dataset_service.py +80 -0
  303. quant_core/app/research/service/research_evaluation_service.py +56 -0
  304. quant_core/app/research/service/research_service.py +171 -0
  305. quant_core/app/strategy/SPEC.md +215 -0
  306. quant_core/app/strategy/__init__.py +1 -0
  307. quant_core/app/strategy/constants.py +87 -0
  308. quant_core/app/strategy/exceptions.py +32 -0
  309. quant_core/app/strategy/interface/__init__.py +0 -0
  310. quant_core/app/strategy/interface/public/__init__.py +0 -0
  311. quant_core/app/strategy/interface/public/portfolio_construction.py +19 -0
  312. quant_core/app/strategy/interface/public/strategy.py +30 -0
  313. quant_core/app/strategy/interface/public/strategy_market_query.py +53 -0
  314. quant_core/app/strategy/interface/public/strategy_portfolio_reader.py +33 -0
  315. quant_core/app/strategy/interface/strategy_factor_reader.py +19 -0
  316. quant_core/app/strategy/interface/strategy_order_reader.py +14 -0
  317. quant_core/app/strategy/schema/__init__.py +0 -0
  318. quant_core/app/strategy/schema/portfolio_schema.py +28 -0
  319. quant_core/app/strategy/schema/portfolio_target.py +70 -0
  320. quant_core/app/strategy/schema/python_strategy_schema.py +94 -0
  321. quant_core/app/strategy/schema/screener_schema.py +51 -0
  322. quant_core/app/strategy/schema/signal_schema.py +33 -0
  323. quant_core/app/strategy/schema/strategy_publication_schema.py +32 -0
  324. quant_core/app/strategy/schema/strategy_schema.py +96 -0
  325. quant_core/app/strategy/service/__init__.py +0 -0
  326. quant_core/app/strategy/service/factor_top_n_strategy.py +103 -0
  327. quant_core/app/strategy/service/portfolio_construction_service.py +142 -0
  328. quant_core/app/strategy/service/python_strategy_service.py +65 -0
  329. quant_core/app/strategy/service/screener_service.py +77 -0
  330. quant_core/app/strategy/service/store_factor_reader.py +120 -0
  331. quant_core/app/strategy/service/strategy_market_view.py +120 -0
  332. quant_core/app/strategy/service/strategy_runtime_service.py +319 -0
  333. quant_core/app/strategy/service/strategy_scheduler.py +50 -0
  334. quant_core/app/strategy/strategies/__init__.py +7 -0
  335. quant_core/app/strategy/strategies/momentum_volume_breakout.py +157 -0
  336. quant_core/app/strategy/strategy_context.py +316 -0
  337. quant_core/app/strategy/strategy_dependency.py +101 -0
  338. quant_core/app/trading_runtime/SPEC.md +70 -0
  339. quant_core/app/trading_runtime/__init__.py +1 -0
  340. quant_core/app/trading_runtime/domain/__init__.py +0 -0
  341. quant_core/app/trading_runtime/replay/__init__.py +0 -0
  342. quant_core/app/trading_runtime/replay/daily_state_expand.py +49 -0
  343. quant_core/app/trading_runtime/replay/exchange_state_resolver.py +90 -0
  344. quant_core/app/trading_runtime/replay/market_runtime_service.py +573 -0
  345. quant_core/app/trading_runtime/replay/market_state_resolver.py +150 -0
  346. quant_core/app/trading_runtime/runtime_ports.py +64 -0
  347. quant_core/app/trading_runtime/runtime_schema.py +129 -0
  348. quant_core/app/trading_runtime/runtime_session.py +361 -0
  349. quant_core/app/trading_runtime/strategy_portfolio_adapter.py +58 -0
  350. quant_core/app/trading_runtime/trading_rule_book.py +37 -0
  351. quant_core/app/trading_runtime/trading_rule_query.py +12 -0
  352. quant_core/common/__init__.py +0 -0
  353. quant_core/common/exceptions/__init__.py +0 -0
  354. quant_core/common/exceptions/base.py +89 -0
  355. 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)