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