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,67 @@
1
+ """复权因子与价格视图契约。"""
2
+
3
+ from datetime import date, datetime
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from quant_core.app.data.schema.market_schema import BarResponse
10
+
11
+
12
+ class AdjustmentType(str, Enum):
13
+ NONE = "none"
14
+ PRE = "pre"
15
+ POST = "post"
16
+
17
+
18
+ class AdjustmentAnchorMode(str, Enum):
19
+ FIXED_DATE = "fixed_date"
20
+ LATEST = "latest"
21
+ DECISION_TIME = "decision_time"
22
+
23
+
24
+ class AdjustmentFactorResponse(BaseModel):
25
+ model_config = ConfigDict(from_attributes=True)
26
+
27
+ instrument_id: str
28
+ trade_date: date
29
+ factor: Decimal = Field(gt=0)
30
+ dataset_revision: str
31
+ source_provider: str
32
+ source_updated_at: datetime | None = None
33
+ recorded_at: datetime
34
+
35
+
36
+ class AdjustedBarResponse(BarResponse):
37
+ """BarResponse 的合成视图;仅放宽 volume 为可含小数的 Decimal。"""
38
+
39
+ volume: Decimal
40
+
41
+
42
+ class BarViewRequest(BaseModel):
43
+ adjustment_type: AdjustmentType = AdjustmentType.NONE
44
+ anchor_mode: AdjustmentAnchorMode | None = None
45
+ anchor_date: date | None = None
46
+ decision_time: datetime | None = None
47
+ known_at: datetime | None = None
48
+
49
+ @model_validator(mode="after")
50
+ def validate_anchor(self) -> "BarViewRequest":
51
+ if self.adjustment_type in {AdjustmentType.NONE, AdjustmentType.POST}:
52
+ return self
53
+ if self.anchor_mode is None:
54
+ raise ValueError("复权查询必须指定 anchor_mode")
55
+ if self.anchor_mode is AdjustmentAnchorMode.FIXED_DATE and self.anchor_date is None:
56
+ raise ValueError("fixed_date 必须指定 anchor_date")
57
+ if self.anchor_mode is AdjustmentAnchorMode.DECISION_TIME and self.decision_time is None:
58
+ raise ValueError("decision_time 模式必须指定 decision_time")
59
+ return self
60
+
61
+
62
+ class AdjustedBarSeries(BaseModel):
63
+ instrument_id: str
64
+ adjustment_type: AdjustmentType
65
+ anchor_date: date | None
66
+ factor_revision: str | None
67
+ bars: list[AdjustedBarResponse | BarResponse]
@@ -0,0 +1,46 @@
1
+ """公司行动 Canonical Fact 与运行时阶段事件。"""
2
+
3
+ from datetime import date
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+
10
+ class CorporateActionPhase(str, Enum):
11
+ RECORD = "record"
12
+ EX_DATE = "ex_date"
13
+ LISTING_DATE = "listing_date"
14
+ PAY_DATE = "pay_date"
15
+
16
+
17
+ class CorporateActionResponse(BaseModel):
18
+ model_config = ConfigDict(from_attributes=True)
19
+
20
+ action_id: str
21
+ instrument_id: str
22
+ record_date: date
23
+ ex_date: date
24
+ pay_date: date | None = None
25
+ listing_date: date | None = None
26
+ cash_per_share: Decimal = Field(Decimal("0"), ge=0)
27
+ stock_ratio: Decimal = Field(Decimal("0"), ge=0)
28
+ dataset_revision: str
29
+ source_provider: str
30
+
31
+ @model_validator(mode="after")
32
+ def validate_dates_and_value(self) -> "CorporateActionResponse":
33
+ if self.record_date > self.ex_date:
34
+ raise ValueError("公司行动日期必须满足 record_date <= ex_date")
35
+ if self.cash_per_share == 0 and self.stock_ratio == 0:
36
+ raise ValueError("公司行动必须至少包含现金或送转比例")
37
+ if self.cash_per_share > 0 and (self.pay_date is None or self.pay_date < self.ex_date):
38
+ raise ValueError("现金分红必须有不早于 ex_date 的 pay_date")
39
+ if self.stock_ratio > 0 and (self.listing_date is None or self.listing_date < self.ex_date):
40
+ raise ValueError("送转股必须有不早于 ex_date 的 listing_date")
41
+ return self
42
+
43
+
44
+ class CorporateActionEventPayload(BaseModel):
45
+ action: CorporateActionResponse
46
+ phase: CorporateActionPhase
@@ -0,0 +1,506 @@
1
+ """
2
+ Market Runtime Schema - MarketEvent 契约(SSOT 见 SPEC「MarketEvent 契约」)
3
+
4
+ 定义:MarketEvent 是具有市场时间语义、会改变市场参与者可观察信息集的
5
+ 市场事实事件。分 Structured(Bar/Quote/Tick/Session/CorporateAction/Snapshot)
6
+ 与 Unstructured(News/Announcement/Policy)两大类——非结构化的是 payload
7
+ 内容,不是事件协议本身。
8
+
9
+ 核心抽象:Live Stream 与 Historical Replay 产出同一个 MarketEvent,
10
+ Strategy 环境无关(实盘/回测只换 EventSource,不换事件语义)。
11
+
12
+ Envelope 语义(固定):
13
+ - event_id:事件唯一标识(uuid)
14
+ - event_time:市场世界中事件发生/生效的时间(Strategy 只允许认这个);
15
+ - received_at:PowerQuant 实际收到/生成事件的系统时间(回放时与
16
+ event_time 完全不同,如 replay:event_time=2025-05-20 10:31 / received_at=今天);
17
+ - sequence:流内单调递增序号;
18
+ - source:来源标识(live/replay),只允许用于 observability/debug/logging/trace,
19
+ Strategy 行为禁止依赖它。
20
+ """
21
+
22
+ from collections.abc import AsyncIterator
23
+ from datetime import datetime
24
+ from decimal import Decimal
25
+ from typing import TYPE_CHECKING, Any, Literal, cast
26
+ from uuid import uuid4
27
+
28
+ from pydantic import BaseModel, Field, field_validator, model_validator
29
+
30
+ from quant_core.app.data.constants import (
31
+ INSTRUMENT_ID_SEPARATOR,
32
+ BarFrequency,
33
+ MarketEventType,
34
+ MarketStreamMode,
35
+ )
36
+ from quant_core.app.data.schema.corporate_action_schema import CorporateActionEventPayload
37
+ from quant_core.app.data.schema.market_schema import BarResponse
38
+ from quant_core.app.data.schema.trading_state_schema import (
39
+ DailyStateSnapshotEventPayload,
40
+ PriceLimitEventPayload,
41
+ TradingState,
42
+ TradingStateEventPayload,
43
+ )
44
+
45
+ if TYPE_CHECKING:
46
+ from quant_core.app.data.schema.exchange_state_schema import (
47
+ ExchangeStateEventPayload,
48
+ ExchangeStateSnapshotEventPayload,
49
+ )
50
+
51
+
52
+ class MarketEvent(BaseModel):
53
+ """市场事件统一信封(Envelope)"""
54
+
55
+ event_id: str = Field(default_factory=lambda: str(uuid4()), description="事件唯一标识")
56
+ event_type: MarketEventType = Field(..., description="事件类型")
57
+ event_time: datetime = Field(..., description="市场事件时间(Strategy 只认这个)")
58
+ received_at: datetime = Field(..., description="系统接收/生成时间")
59
+ sequence: int = Field(..., ge=0, description="流内单调递增序号")
60
+ source: str = Field(..., description="来源标识(live/replay),仅用于 observability")
61
+ payload: dict[str, Any] = Field(..., description="事件载荷(如BarResponse dict)")
62
+
63
+
64
+ class BarEvent(MarketEvent):
65
+ """Bar 事件"""
66
+
67
+ event_type: Literal[MarketEventType.BAR] = MarketEventType.BAR
68
+ payload: BarResponse # type: ignore[assignment]
69
+
70
+
71
+ class CorporateActionEvent(MarketEvent):
72
+ """公司行动阶段事件;Portfolio 按 RECORD/EX_DATE/PAY_DATE 处理。"""
73
+
74
+ event_type: Literal[MarketEventType.CORPORATE_ACTION] = MarketEventType.CORPORATE_ACTION
75
+ payload: CorporateActionEventPayload # type: ignore[assignment]
76
+
77
+
78
+ class MarketStreamRequest(BaseModel):
79
+ """市场事件流请求(Live 与 Replay 共用)"""
80
+
81
+ mode: MarketStreamMode = Field(MarketStreamMode.REPLAY, description="流模式")
82
+ instrument_ids: list[str] = Field(default_factory=list, description="标的列表;空=全部")
83
+ frequency: BarFrequency = Field(BarFrequency.M1, description="频率")
84
+ coarse_open_decisions: bool = False
85
+ # replay 专用
86
+ start: datetime | None = Field(None, description="回放开始时间(含)")
87
+ end: datetime | None = Field(None, description="回放结束时间(含)")
88
+ speed: float = Field(0.0, ge=0.0, description="回放加速倍数;0=尽快(不等待事件间隔)")
89
+
90
+
91
+ class TimerEventPayload(BaseModel):
92
+ """Timer 事件载荷"""
93
+
94
+ period_seconds: int = Field(..., description="定时间隔(秒)")
95
+
96
+
97
+ TradingSessionStage = Literal[
98
+ MarketEventType.SESSION_PRE_OPEN,
99
+ MarketEventType.SESSION_OPEN,
100
+ MarketEventType.SESSION_BREAK_START,
101
+ MarketEventType.SESSION_BREAK_END,
102
+ MarketEventType.SESSION_CLOSE,
103
+ ]
104
+
105
+
106
+ class TradingSessionEventPayload(BaseModel):
107
+ """Trading Session 事件载荷
108
+
109
+ Session 阶段(通用,市场各自映射具体时刻;A股见 constants SESSION_*_TIME):
110
+ PRE_OPEN → OPEN → BREAK_START → BREAK_END → CLOSE。
111
+ TradingCalendar 是 Session Runtime 的权威基础,Strategy 禁止用 wall-clock
112
+ 或「周一到周五」自行推导时段。
113
+ """
114
+
115
+ exchange: str = Field(..., description="交易所")
116
+ session: TradingSessionStage = Field(..., description="时段事件类型")
117
+
118
+
119
+ class TradingSessionEvent(MarketEvent):
120
+ """Trading Session 事件:开盘/休市等时段边界,由 Runtime 基于 TradingCalendar 生成"""
121
+
122
+ payload: TradingSessionEventPayload # type: ignore[assignment]
123
+
124
+
125
+ class TradingStateEvent(MarketEvent):
126
+ """交易状态事实事件(通用市场事实;契约见 trading_state_schema)"""
127
+
128
+ event_type: Literal[MarketEventType.TRADING_STATE] = MarketEventType.TRADING_STATE
129
+ payload: TradingStateEventPayload # type: ignore[assignment]
130
+
131
+
132
+ class PriceLimitEvent(MarketEvent):
133
+ """价格限制事实事件(可选市场事实;契约见 trading_state_schema)"""
134
+
135
+ event_type: Literal[MarketEventType.PRICE_LIMIT] = MarketEventType.PRICE_LIMIT
136
+ payload: PriceLimitEventPayload # type: ignore[assignment]
137
+
138
+
139
+ class DailyStateSnapshotEvent(MarketEvent):
140
+ """日级市场状态完整快照事件(Q1-A 定案;契约见 trading_state_schema)
141
+
142
+ 表达「某标的某交易日的完整状态基线」:允许显式 UNKNOWN 与 price_limit=None
143
+ (显式清除)。既有 TradingStateEvent / PriceLimitEvent 保持增量事实语义,
144
+ 不承担日界清除职责;同片内增量事实比快照更具体、后生效覆盖。
145
+ """
146
+
147
+ event_type: Literal[MarketEventType.DAILY_STATE_SNAPSHOT] = MarketEventType.DAILY_STATE_SNAPSHOT
148
+ payload: DailyStateSnapshotEventPayload # type: ignore[assignment]
149
+
150
+
151
+ def trading_state_to_event(
152
+ instrument_id: str,
153
+ state: TradingState,
154
+ event_time: datetime,
155
+ sequence: int,
156
+ source: str,
157
+ received_at: datetime,
158
+ ) -> TradingStateEvent:
159
+ """把交易状态事实包装成 TradingStateEvent 的工厂(Runtime/Mock 统一入口)"""
160
+ return TradingStateEvent(
161
+ event_type=MarketEventType.TRADING_STATE,
162
+ event_time=event_time,
163
+ received_at=received_at,
164
+ sequence=sequence,
165
+ source=source,
166
+ payload=TradingStateEventPayload(instrument_id=instrument_id, state=state),
167
+ )
168
+
169
+
170
+ def daily_state_snapshot_to_event(
171
+ instrument_id: str,
172
+ state: TradingState,
173
+ event_time: datetime,
174
+ limit_up: Decimal | None,
175
+ limit_dn: Decimal | None,
176
+ sequence: int,
177
+ source: str,
178
+ received_at: datetime,
179
+ ) -> DailyStateSnapshotEvent:
180
+ """把日级状态快照包装成 DailyStateSnapshotEvent 的工厂(Replay 统一入口)"""
181
+ return DailyStateSnapshotEvent(
182
+ event_type=MarketEventType.DAILY_STATE_SNAPSHOT,
183
+ event_time=event_time,
184
+ received_at=received_at,
185
+ sequence=sequence,
186
+ source=source,
187
+ payload=DailyStateSnapshotEventPayload(
188
+ instrument_id=instrument_id,
189
+ trading_state=state,
190
+ limit_up=limit_up,
191
+ limit_dn=limit_dn,
192
+ ),
193
+ )
194
+
195
+
196
+ def price_limit_to_event(
197
+ instrument_id: str,
198
+ limit_up: Decimal,
199
+ limit_down: Decimal,
200
+ event_time: datetime,
201
+ sequence: int,
202
+ source: str,
203
+ received_at: datetime,
204
+ buy_side_locked: bool = False,
205
+ sell_side_locked: bool = False,
206
+ ) -> PriceLimitEvent:
207
+ """把价格限制事实包装成 PriceLimitEvent 的工厂(Runtime/Mock 统一入口)"""
208
+ return PriceLimitEvent(
209
+ event_type=MarketEventType.PRICE_LIMIT,
210
+ event_time=event_time,
211
+ received_at=received_at,
212
+ sequence=sequence,
213
+ source=source,
214
+ payload=PriceLimitEventPayload(
215
+ instrument_id=instrument_id,
216
+ limit_up=limit_up,
217
+ limit_down=limit_down,
218
+ buy_side_locked=buy_side_locked,
219
+ sell_side_locked=sell_side_locked,
220
+ ),
221
+ )
222
+
223
+
224
+ def trading_session_to_event(
225
+ exchange: str,
226
+ session: TradingSessionStage,
227
+ event_time: datetime,
228
+ sequence: int,
229
+ source: str,
230
+ received_at: datetime,
231
+ ) -> TradingSessionEvent:
232
+ """把时段边界包装成 TradingSessionEvent 的工厂(Runtime 统一入口)"""
233
+ return TradingSessionEvent(
234
+ event_type=session,
235
+ event_time=event_time,
236
+ received_at=received_at,
237
+ sequence=sequence,
238
+ source=source,
239
+ payload=TradingSessionEventPayload(exchange=exchange, session=session),
240
+ )
241
+
242
+
243
+ # ========== MarketTimeSlice 调度契约 ==========
244
+
245
+ # 同一 event_time 内的业务边界优先级:
246
+ # OPEN/RESUME 类边界先于市场事实生效;BREAK/CLOSE 类边界后于市场事实生效。
247
+ PRE_SESSION_STAGES = frozenset(
248
+ {
249
+ MarketEventType.SESSION_PRE_OPEN,
250
+ MarketEventType.SESSION_OPEN,
251
+ MarketEventType.SESSION_BREAK_END,
252
+ }
253
+ )
254
+ POST_SESSION_STAGES = frozenset(
255
+ {MarketEventType.SESSION_BREAK_START, MarketEventType.SESSION_CLOSE}
256
+ )
257
+ # 优先级:0=前置边界 < 1=市场事实(Bar/TradingState/PriceLimit,未来 Quote/News 同层) < 2=后置边界
258
+ _PRIORITY_PRE = 0
259
+ _PRIORITY_FACT = 1
260
+ _PRIORITY_POST = 2
261
+
262
+
263
+ def market_event_order_key(event: MarketEvent) -> tuple[datetime, int, str, str]:
264
+ """时间片内稳定排序键:(event_time, 边界优先级, 标识 tie-break, 事件类型)
265
+
266
+ tie-break 只服务于确定性与可测试性(同时间事件的技术顺序),
267
+ 不得被解释为交易、选股或资金优先级;不使用随机 event_id 排序。
268
+ event_type 使用 Canonical enum value(不依赖 str(Enum) 表示)作为
269
+ 末位 tie-break,保证同时间、同标的的多类事实(如 Bar 与
270
+ TradingState)在输入顺序打乱后规范化顺序仍一致。
271
+ """
272
+ if event.event_type in PRE_SESSION_STAGES:
273
+ priority = _PRIORITY_PRE
274
+ elif event.event_type in POST_SESSION_STAGES:
275
+ priority = _PRIORITY_POST
276
+ elif event.event_type in (
277
+ MarketEventType.EXCHANGE_STATE_EVENT,
278
+ MarketEventType.EXCHANGE_STATE_SNAPSHOT,
279
+ ):
280
+ # 延迟 import:exchange_state_schema 反向依赖 MarketEvent(T4R)
281
+ from quant_core.app.data.schema.exchange_state_schema import (
282
+ ExchangePhase,
283
+ ExchangeStateEventPayload,
284
+ )
285
+
286
+ if not isinstance(event.payload, ExchangeStateEventPayload):
287
+ return (event.event_time, _PRIORITY_FACT, _exchange_tie(event), event.event_type.value)
288
+ # 交易所状态推进继承其来源 Session 边界的优先级(T4R):
289
+ # 进入交易类(PRE_OPEN/OPEN)在前置边界层,退出交易类在后置边界层
290
+ priority = (
291
+ _PRIORITY_PRE
292
+ if event.payload.phase in (ExchangePhase.PRE_OPEN, ExchangePhase.OPEN)
293
+ else _PRIORITY_POST
294
+ )
295
+ else:
296
+ # ExchangeStateSnapshot 是状态事实(基线),属市场事实层
297
+ priority = _PRIORITY_FACT
298
+ payload = event.payload
299
+ tie = getattr(payload, "instrument_id", None) or getattr(payload, "exchange", None) or ""
300
+ return (event.event_time, priority, str(tie), event.event_type.value)
301
+
302
+
303
+ def _exchange_tie(event: MarketEvent) -> str:
304
+ return str(getattr(event.payload, "exchange", "") or "")
305
+
306
+
307
+ class CoarseBarExecutionContext(BaseModel):
308
+ """粗粒度(日线及以上)时间片的有界模拟执行资格(T4R R3 finding 1,方案 A)
309
+
310
+ 语义:日线 Bar(时点 15:00)代表该交易日「发生过交易时段」,但日线数据
311
+ 不能证明 15:00 时刻交易所处于 OPEN(盘中临停即反例)。因此:
312
+
313
+ - ExchangeState(ExchangePhase/Resolver 投影)**只表达真实交易所运行
314
+ 状态事实**:日线片 15:00 的真实投影是 CLOSED/未初始化,回放绝不发布
315
+ 伪造的 OPEN Snapshot;
316
+ - 日线 Bar 片可携带 coarse-bar 资格;显式启用的日线开盘 Session 片
317
+ 可携带 coarse-open 资格;仅有日级事实的片零资格;
318
+ - scope 为 eligible_exchanges:片内 Bar 标的或 Session Open 的交易所集合,
319
+ 该交易所全部标的可提交/撮合(跨标的选股策略友好);标的级把关仍由
320
+ OrderExecution 的 TradabilityPolicy 兜底;
321
+ - decision_time = slice.event_time(决策时点 provenance);
322
+ - 本上下文是执行资格,不是交易所事实:不写入 MarketEvent、不映射
323
+ ExchangePhase、不进入 trace 事实、不跨片累积。
324
+ """
325
+
326
+ reason: Literal["coarse_bar_decision_point", "coarse_open_decision_point"] = (
327
+ "coarse_bar_decision_point"
328
+ )
329
+ decision_time: datetime = Field(..., description="决策时点(= slice.event_time)")
330
+ eligible_exchanges: tuple[str, ...] = Field(
331
+ ..., description="本片决策事件的交易所(确定性升序去重)"
332
+ )
333
+
334
+ @field_validator("eligible_exchanges")
335
+ @classmethod
336
+ def _validate_scope(cls, scope: tuple[str, ...]) -> tuple[str, ...]:
337
+ if not scope:
338
+ raise ValueError("eligible_exchanges 不允许为空:无范围即无资格")
339
+ if list(scope) != sorted(set(scope)):
340
+ raise ValueError("eligible_exchanges 必须确定性升序且去重")
341
+ return scope
342
+
343
+
344
+ class MarketTimeSlice(BaseModel):
345
+ """同一精确 Canonical event_time 的全部市场事件(时间片调度契约)
346
+
347
+ 由完全相同的 event_time 自然形成:不按分钟/秒截断,不提供时间桶配置;
348
+ 不同 event_time 的事件绝不合并进同一切片。
349
+
350
+ Canonical 不变量(构造边界自维护,不依赖生产者自律):
351
+ - events 非空;
352
+ - event_time 必须 timezone-aware;
353
+ - 片内每个事件的 event_time 与 slice.event_time 完全相同;
354
+ - events 在构造时按 market_event_order_key 稳定排序——边界事件
355
+ (OPEN/RESUME 前、BREAK/CLOSE 后)与市场事实的相对顺序表达
356
+ 「边界在何时生效」,其余仅为确定性技术顺序,不表达交易优先级。
357
+
358
+ Strategy Runtime 以时间片为调度边界:片内事实全部就绪后才运行策略。
359
+ """
360
+
361
+ event_time: datetime = Field(..., description="时间片的精确市场时间(timezone-aware)")
362
+ events: list[MarketEvent] = Field(..., description="片内事件(已稳定排序)")
363
+ # 粗粒度(日线及以上)时间片的模拟执行资格:日线收盘 Bar 片,以及
364
+ # 显式启用的日线开盘 Session 片可成为策略决策/模拟撮合点;资格由回放
365
+ # 构造显式授予;与 ExchangeState(真实交易所运行状态事实)完全解耦,
366
+ # 不得用于伪造 phase,也不参与 DAY 日内到期判定。
367
+ execution_context: "CoarseBarExecutionContext | None" = Field(
368
+ default=None, description="粗粒度片的模拟执行资格;日内片恒为 None"
369
+ )
370
+
371
+ @field_validator("events")
372
+ @classmethod
373
+ def _stably_sort_events(cls, events: list[MarketEvent]) -> list[MarketEvent]:
374
+ return sorted(events, key=market_event_order_key)
375
+
376
+ @model_validator(mode="after")
377
+ def _check_slice_invariants(self) -> "MarketTimeSlice":
378
+ if not self.events:
379
+ raise ValueError("MarketTimeSlice.events 不允许为空:时间片由至少一个事件形成")
380
+ if self.event_time.tzinfo is None:
381
+ raise ValueError(
382
+ "MarketTimeSlice.event_time 必须 timezone-aware(Canonical 市场时间带时区)"
383
+ )
384
+ # T4R R4 finding 1:Context 与片内事件的关系不变量在公开模型边界
385
+ # 自维护(不依赖 producer 自律,capability 无法在模型层伪造):
386
+ # - decision_time 必须等于 slice.event_time;
387
+ # - 携带 Context 的片必须含 Bar 或 Session Open 决策事件;
388
+ # - eligible_exchanges 必须精确等于片内决策事件所属交易所的
389
+ # 确定性升序去重集合(缺失/多余/错误均拒绝)。
390
+ context = self.execution_context
391
+ if context is not None:
392
+ if context.decision_time != self.event_time:
393
+ raise ValueError(
394
+ f"execution_context.decision_time {context.decision_time} 必须等于 "
395
+ f"slice.event_time {self.event_time}(决策时点 provenance)"
396
+ )
397
+ bar_exchanges: list[str] = []
398
+ for event in self.events:
399
+ if event.event_type is not MarketEventType.BAR:
400
+ continue
401
+ parts = str(cast(BarResponse, event.payload).instrument_id).split(
402
+ INSTRUMENT_ID_SEPARATOR
403
+ )
404
+ if len(parts) != 3 or not parts[1]:
405
+ raise ValueError(
406
+ f"片内 Bar 的 instrument_id 非法(无法解析 exchange):"
407
+ f"{cast(BarResponse, event.payload).instrument_id!r}"
408
+ )
409
+ if parts[1] not in bar_exchanges:
410
+ bar_exchanges.append(parts[1])
411
+ open_exchanges = {
412
+ cast(TradingSessionEventPayload, event.payload).exchange
413
+ for event in self.events
414
+ if event.event_type is MarketEventType.SESSION_OPEN
415
+ }
416
+ if not bar_exchanges and not open_exchanges:
417
+ raise ValueError("无 Bar 或 Session Open 的时间片禁止携带 execution_context")
418
+ expected = tuple(sorted(set(bar_exchanges) | open_exchanges))
419
+ expected_reason = (
420
+ "coarse_bar_decision_point" if bar_exchanges else "coarse_open_decision_point"
421
+ )
422
+ if context.reason != expected_reason:
423
+ raise ValueError("execution_context.reason 与片内决策事件不匹配")
424
+ if context.eligible_exchanges != expected:
425
+ raise ValueError(
426
+ f"execution_context.eligible_exchanges {context.eligible_exchanges} "
427
+ f"必须精确等于片内决策事件交易所集合 {expected}"
428
+ )
429
+ # 同片同标的同类型事实唯一:互相冲突的事实会让最终状态依赖输入顺序,
430
+ # 不能用「最后一条覆盖」或随机 event_id 决胜。
431
+ # 同一标的同片同时拥有一条 TradingState + 一条 PriceLimit + 一条 Bar 是合法的。
432
+ # 交易所状态事件(Snapshot/Event)按 exchange 键唯一(T4R)。
433
+ fact_keys: set[tuple[str, str]] = set()
434
+ for event in self.events:
435
+ if event.event_time != self.event_time:
436
+ raise ValueError(
437
+ f"片内事件 event_time {event.event_time} 与 slice.event_time "
438
+ f"{self.event_time} 不一致(event_type={event.event_type}):"
439
+ "不同时间的事件不得合并进同一切片"
440
+ )
441
+ instrument_id = getattr(event.payload, "instrument_id", None)
442
+ if (
443
+ event.event_type
444
+ in (
445
+ MarketEventType.TRADING_STATE,
446
+ MarketEventType.PRICE_LIMIT,
447
+ MarketEventType.DAILY_STATE_SNAPSHOT,
448
+ )
449
+ and instrument_id is not None
450
+ ):
451
+ key = (event.event_type.value, str(instrument_id))
452
+ if key in fact_keys:
453
+ raise ValueError(
454
+ "同一时间片内同一标的的同类型事实必须唯一:重复 "
455
+ f"(event_type={key[0]}, instrument_id={key[1]}) 会"
456
+ "使结果依赖输入顺序,属于矛盾输入"
457
+ )
458
+ fact_keys.add(key)
459
+ elif event.event_type in (
460
+ MarketEventType.EXCHANGE_STATE_SNAPSHOT,
461
+ MarketEventType.EXCHANGE_STATE_EVENT,
462
+ ):
463
+ key = (
464
+ event.event_type.value,
465
+ str(
466
+ cast(
467
+ "ExchangeStateEventPayload | ExchangeStateSnapshotEventPayload",
468
+ event.payload,
469
+ ).exchange
470
+ ),
471
+ )
472
+ if key in fact_keys:
473
+ raise ValueError(
474
+ "同一时间片内同一交易所的同类型状态事件必须唯一:重复 "
475
+ f"(event_type={key[0]}, exchange={key[1]}) 属矛盾输入"
476
+ )
477
+ fact_keys.add(key)
478
+ return self
479
+
480
+
481
+ class SnapshotFact(BaseModel):
482
+ """快照里的单条事实(供 SSE/下游展示)"""
483
+
484
+ instrument_id: str
485
+ last: Decimal
486
+ pct_change: Decimal | None = None
487
+
488
+
489
+ def bar_to_bar_event(
490
+ bar: BarResponse, sequence: int, source: str, received_at: datetime
491
+ ) -> BarEvent:
492
+ """把 Canonical Bar 包装成 BarEvent 的工厂(Runtime 统一入口)"""
493
+ return BarEvent(
494
+ event_type=MarketEventType.BAR,
495
+ event_time=bar.timestamp,
496
+ received_at=received_at,
497
+ sequence=sequence,
498
+ source=source,
499
+ payload=bar,
500
+ )
501
+
502
+
503
+ async def events_to_aiter(events: list[MarketEvent]) -> AsyncIterator[MarketEvent]:
504
+ """把事件列表包装成异步迭代器(测试与小数据集复用)"""
505
+ for event in events:
506
+ yield event
@@ -0,0 +1,37 @@
1
+ """交易所阶段及其交易能力的只读投影。"""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+
6
+ from quant_core.app.data.schema.exchange_state_schema import ExchangePhase
7
+
8
+
9
+ def phase_capabilities(phase: ExchangePhase | None) -> tuple[bool, bool, bool]:
10
+ """投影报单、撮合、撤单能力;未知状态安全关闭。"""
11
+ if phase is ExchangePhase.OPEN:
12
+ return (True, True, True)
13
+ if phase is ExchangePhase.PRE_OPEN:
14
+ return (True, False, True)
15
+ if phase in (ExchangePhase.BREAK, ExchangePhase.CLOSED):
16
+ return (False, False, True)
17
+ return (False, False, False)
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ExchangeOperatingState:
22
+ exchange: str
23
+ phase: ExchangePhase | None
24
+ as_of: datetime | None = None
25
+ source_event_id: str | None = None
26
+
27
+ @property
28
+ def order_entry_allowed(self) -> bool:
29
+ return phase_capabilities(self.phase)[0]
30
+
31
+ @property
32
+ def matching_allowed(self) -> bool:
33
+ return phase_capabilities(self.phase)[1]
34
+
35
+ @property
36
+ def cancellation_allowed(self) -> bool:
37
+ return phase_capabilities(self.phase)[2]