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,393 @@
1
+ """Backtest Run - 历史回测环境驱动、生命周期与结果聚合(T7)
2
+
3
+ SSOT:`app/backtest/SPEC.md`、T7 Task/Plan、DECISIONS.md 2026-09-04。
4
+
5
+ Backtest 只负责:历史环境组装输入、Replay 驱动、生命周期与结果聚合。
6
+ 单时间片业务循环由 RuntimeSession 唯一拥有;Backtest 的循环仅为:
7
+
8
+ Data Guard → Historical MarketTimeSlice Source
9
+ → HistoricalClock.set(T) → await RuntimeSession.step(slice@T)
10
+ → 成功后聚合 StepResult → 下一个 Slice
11
+
12
+ 硬边界:
13
+ - 不复制 Market → Execution → Fill → Portfolio → Snapshot → Strategy →
14
+ Planning → submit 的 Runtime 单片内部顺序;
15
+ - 每个 MarketTimeSlice 恰好 step 一次;Clock 时间与 slice.event_time 相等;
16
+ - 只有完整成功返回的 RuntimeStepResult 才进入聚合;step 异常时该片不
17
+ 提交 Equity/Trace/Fill/Attempt,BacktestRun 进入 FAILED 并停止;
18
+ - SETTLING 只聚合、算指标、冻结 Result——不合成 SESSION_CLOSE/Bar、
19
+ 不生成 Fill、不强平、不撤销/过期订单、不修改 Portfolio 业务状态;
20
+ 数据结束时仍存在的订单/持仓如实保留并披露;
21
+ - 全程不读 wall clock,无 sleep,无随机业务排序。
22
+ """
23
+
24
+ from collections.abc import AsyncIterator, Mapping
25
+ from datetime import date, datetime, time
26
+ from typing import Protocol, runtime_checkable
27
+
28
+ from quant_core.app.backtest.data_guard import (
29
+ ensure_backtest_data_ready,
30
+ ensure_backtest_dependencies_ready,
31
+ )
32
+ from quant_core.app.backtest.historical_clock import HistoricalClock
33
+ from quant_core.app.backtest.metrics import compute_metrics
34
+ from quant_core.app.backtest.schema import (
35
+ BacktestConfig,
36
+ BacktestDataRevision,
37
+ BacktestMetrics,
38
+ BacktestResult,
39
+ BacktestStatus,
40
+ DecisionTrace,
41
+ EquityCurve,
42
+ EquityPoint,
43
+ ResultLimitation,
44
+ )
45
+ from quant_core.app.data.constants import BarFrequency, MarketStreamMode
46
+ from quant_core.app.data.interface.market_data_verification import IMarketDataVerification
47
+ from quant_core.app.data.interface.market_time_slice_source import IMarketTimeSliceSource
48
+ from quant_core.app.data.schema.adjustment_schema import AdjustmentType
49
+ from quant_core.app.data.schema.event_schema import MarketStreamRequest, MarketTimeSlice
50
+ from quant_core.app.order_execution.interface.public.order_execution import IOrderExecution
51
+ from quant_core.app.portfolio.interface.public.portfolio import IPortfolioLedger
52
+ from quant_core.app.strategy.schema.portfolio_target import PortfolioDecision
53
+ from quant_core.app.trading_runtime.runtime_schema import RuntimeStepResult, TraceEntry
54
+
55
+
56
+ @runtime_checkable
57
+ class RuntimeSessionProtocol(Protocol):
58
+ """RuntimeSession 的消费方最小 Port(Backtest 只调用 step)"""
59
+
60
+ async def step(self, time_slice: MarketTimeSlice) -> RuntimeStepResult:
61
+ """严格执行一个时间片(固定因果顺序;失败即异常)"""
62
+ ...
63
+
64
+
65
+ class BacktestRun:
66
+ """一次历史回测的运行实例(状态机;单线程/单事件循环使用)
67
+
68
+ 构造注入已组装的历史环境(具体 Simulated 实现由组装点装配):
69
+
70
+ - config:不可变 BacktestConfig;
71
+ - runtime_session:组装完成的 RuntimeSession;
72
+ - clock:组装点创建的唯一 HistoricalClock(同一实例必须已注入
73
+ RuntimeSession;BacktestRun 只负责在每个片 step 前推进它,不创建
74
+ 第二个 Clock,也不读 Session 私有状态);
75
+ - order_execution:仅用于终态读取公开 ``order_snapshots()``;
76
+ - slice_source:IMarketTimeSliceSource(历史 Replay 语义 Port);
77
+ - verification:IMarketDataVerification(先 verify 后运行)。
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ config: BacktestConfig,
83
+ runtime_session: RuntimeSessionProtocol,
84
+ order_execution: IOrderExecution,
85
+ slice_source: IMarketTimeSliceSource,
86
+ verification: IMarketDataVerification,
87
+ clock: HistoricalClock,
88
+ ledger: IPortfolioLedger | None = None,
89
+ input_revisions: tuple[BacktestDataRevision, ...] = (),
90
+ listing_dates: Mapping[str, date] | None = None,
91
+ ) -> None:
92
+ self._config = config
93
+ self._session = runtime_session
94
+ self._execution = order_execution
95
+ self._slice_source = slice_source
96
+ self._verification = verification
97
+ self._listing_dates = dict(listing_dates or {})
98
+ # Clock ownership 归组装点(T8):先创建唯一 HistoricalClock,再以
99
+ # 同一实例显式注入 RuntimeSession 与本 Run(遗漏注入无法构造)
100
+ self.clock = clock
101
+ self._ledger = ledger
102
+ self._status = BacktestStatus.CREATED
103
+ self._result: BacktestResult | None = None
104
+
105
+ # 成功片聚合累积器(仅完整成功的 StepResult 才写入)。
106
+ # Trace 采用运行内增量积累:逐批校验与历史尾部的衔接后 append,
107
+ # 最终在 _build_result 一次性冻结为不可变 DecisionTrace(避免每片
108
+ # 复制并全量重校验历史序列的 O(n²) 开销)
109
+ self._equity_points: list[EquityPoint] = []
110
+ self._portfolio_decisions: list[PortfolioDecision] = []
111
+ self._fills: list = []
112
+ self._attempts: list = []
113
+ self._trace_entries: list = []
114
+ self._trace_last_event_time: datetime | None = None
115
+ self._started_event_time: datetime | None = None
116
+ self._ended_event_time: datetime | None = None
117
+ self._final_snapshot = None
118
+ self._last_slice_time: datetime | None = None
119
+ self._data_revisions: list[BacktestDataRevision] = list(input_revisions)
120
+
121
+ # ------------------------------------------------------------------
122
+ # 公开状态
123
+ # ------------------------------------------------------------------
124
+
125
+ @property
126
+ def status(self) -> BacktestStatus:
127
+ return self._status
128
+
129
+ @property
130
+ def result(self) -> BacktestResult:
131
+ """最终结果(只有 COMPLETED 后可读;幂等且全等)"""
132
+ if self._status is not BacktestStatus.COMPLETED or self._result is None:
133
+ raise RuntimeError(
134
+ f"BacktestResult 仅在 COMPLETED 后可读,当前状态 {self._status.value}"
135
+ )
136
+ return self._result
137
+
138
+ # ------------------------------------------------------------------
139
+ # 运行
140
+ # ------------------------------------------------------------------
141
+
142
+ async def run(self) -> BacktestResult:
143
+ """执行回测直到完成;任一阶段异常 → FAILED 并原样传播"""
144
+ if self._status is not BacktestStatus.CREATED:
145
+ raise RuntimeError(
146
+ f"BacktestRun 只能从 CREATED 运行一次,当前状态 {self._status.value}"
147
+ )
148
+ try:
149
+ await self._run()
150
+ except BaseException:
151
+ self._status = BacktestStatus.FAILED
152
+ raise
153
+ return self._require_result()
154
+
155
+ async def _run(self) -> None:
156
+ # DATA_CHECK:先 verify 后运行(不可用时抛领域异常)
157
+ self._status = BacktestStatus.DATA_CHECK
158
+ execution_frequency = self._config.execution_frequency or self._config.frequency
159
+ for instrument_id in self._config.instrument_ids:
160
+ listed_date = self._listing_dates.get(instrument_id)
161
+ verification_start = self._config.start
162
+ if listed_date is not None and listed_date > verification_start.date():
163
+ verification_start = datetime.combine(
164
+ listed_date, time(9, 30), tzinfo=self._config.start.tzinfo
165
+ )
166
+ if verification_start > self._config.end:
167
+ continue
168
+ bar_result = await ensure_backtest_data_ready(
169
+ self._verification,
170
+ instrument_id,
171
+ execution_frequency.value,
172
+ verification_start,
173
+ self._config.end,
174
+ )
175
+ if bar_result.dataset_revision is not None:
176
+ self._data_revisions.append(
177
+ BacktestDataRevision(
178
+ dataset=bar_result.dataset,
179
+ instrument_id=instrument_id,
180
+ frequency=execution_frequency.value,
181
+ revision=bar_result.dataset_revision,
182
+ )
183
+ )
184
+ if execution_frequency is not self._config.frequency:
185
+ observation_result = await ensure_backtest_data_ready(
186
+ self._verification,
187
+ instrument_id,
188
+ self._config.frequency.value,
189
+ verification_start,
190
+ self._config.end,
191
+ )
192
+ if observation_result.dataset_revision is not None:
193
+ self._data_revisions.append(
194
+ BacktestDataRevision(
195
+ dataset=observation_result.dataset,
196
+ instrument_id=instrument_id,
197
+ frequency=self._config.frequency.value,
198
+ revision=observation_result.dataset_revision,
199
+ )
200
+ )
201
+ dependency_results = await ensure_backtest_dependencies_ready(
202
+ self._verification,
203
+ instrument_id,
204
+ self._config.frequency.value,
205
+ verification_start,
206
+ self._config.end,
207
+ require_adjustment_factors=(
208
+ self._config.strategy_adjustment_type is not AdjustmentType.NONE
209
+ ),
210
+ )
211
+ self._data_revisions.extend(
212
+ BacktestDataRevision(
213
+ dataset=result.dataset,
214
+ instrument_id=instrument_id,
215
+ frequency=(
216
+ self._config.frequency.value
217
+ if result.dataset != "corporate_action"
218
+ else None
219
+ ),
220
+ revision=result.dataset_revision,
221
+ )
222
+ for result in dependency_results
223
+ if result.dataset_revision is not None
224
+ )
225
+
226
+ # RUNNING:逐片驱动 RuntimeSession(Backtest 不复制单片业务顺序)
227
+ self._status = BacktestStatus.RUNNING
228
+ request = MarketStreamRequest(
229
+ mode=MarketStreamMode.REPLAY,
230
+ speed=0.0,
231
+ instrument_ids=list(self._config.instrument_ids),
232
+ frequency=execution_frequency,
233
+ # 所有日线回放都需要显式开盘边界来完成 T+1 结算;
234
+ # 撮合价格仍由 execution_policy 独立决定。
235
+ coarse_open_decisions=execution_frequency in {BarFrequency.D1, BarFrequency.W1},
236
+ start=self._config.start,
237
+ end=self._config.end,
238
+ )
239
+ slices: AsyncIterator[MarketTimeSlice] = self._slice_source.stream_slices(request)
240
+ async for time_slice in slices:
241
+ self._check_slice_monotonic(time_slice)
242
+ self.clock.set(time_slice.event_time)
243
+ step_result = await self._session.step(time_slice)
244
+ # 错片防线:Runtime 输出必须仍属于本次输入时间片(早片/晚片
245
+ # 均属 Runtime 矛盾输出),任何聚合写入前校验
246
+ if step_result.slice_time != time_slice.event_time:
247
+ raise ValueError(
248
+ f"RuntimeStepResult.slice_time {step_result.slice_time} 与输入时间片 "
249
+ f"{time_slice.event_time} 不一致(错片输出,拒绝聚合)"
250
+ )
251
+ self._aggregate(step_result)
252
+
253
+ # SETTLING:只聚合已发生结果、算指标、冻结 Result
254
+ # (不合成事件/成交、不强平、不改 Portfolio 状态)。
255
+ # R3 finding 2(方案 C 兜底,用户裁定):数据结束时仍 PENDING 的
256
+ # **coarse DAY 单**结构化 EXPIRED(一次终态迁移,经 Execution 公开
257
+ # 方法产生 OrderUpdate 并反映在 order_snapshots);日内订单维持
258
+ # 「如实保留」语义不变。
259
+ # R4 finding 4:产生的 OrderUpdate 必须进入最终可审计结果——
260
+ # 作为独立 settling 生命周期迁移披露(BacktestResult.
261
+ # settling_order_updates)并追加为 DecisionTrace 条目(时间 =
262
+ # ended_event_time,不倒退),不伪造成市场事件或成交。
263
+ # 原子性:settle 迁移在 Result 冻结前完成;任一失败 → Run FAILED
264
+ # 并终止(无跨域回滚与既有失败语义一致),不产出半提交 Result。
265
+ self._status = BacktestStatus.SETTLING
266
+ settling_updates: tuple = ()
267
+ if self._ended_event_time is not None:
268
+ settling_updates = self._execution.expire_pending_day_orders(self._ended_event_time)
269
+ self._result = self._build_result(settling_updates)
270
+ self._status = BacktestStatus.COMPLETED
271
+
272
+ # ------------------------------------------------------------------
273
+
274
+ def _check_slice_monotonic(self, time_slice: MarketTimeSlice) -> None:
275
+ """Replay 驱动的调度边界:时间片必须严格递增(倒流/重复即失败)"""
276
+ if self._last_slice_time is not None and time_slice.event_time <= self._last_slice_time:
277
+ raise ValueError(
278
+ f"Replay 时间片必须严格递增:{time_slice.event_time} 不晚于已消费 "
279
+ f"{self._last_slice_time}(倒流/重复时间片属 Source 矛盾输出)"
280
+ )
281
+
282
+ def _aggregate(self, step_result: RuntimeStepResult) -> None:
283
+ """聚合一个完整成功的 StepResult(失败片根本不会进入本方法)"""
284
+ t = step_result.slice_time
285
+ self._equity_points.append(
286
+ EquityPoint(time=t, total_value=step_result.portfolio_snapshot.total_value)
287
+ )
288
+ self._fills.extend(step_result.fills)
289
+ self._portfolio_decisions.extend(step_result.portfolio_decisions)
290
+ self._attempts.extend(step_result.execution_attempts)
291
+ self._extend_trace(step_result.trace_entries)
292
+ if self._started_event_time is None:
293
+ self._started_event_time = t
294
+ self._ended_event_time = t
295
+ self._final_snapshot = step_result.portfolio_snapshot
296
+ self._last_slice_time = t
297
+
298
+ def _extend_trace(self, entries: tuple) -> None:
299
+ """增量追加一批 TraceEntry:只校验新增批次及与历史尾部的衔接
300
+ (时间不得倒退;同一时间片多条合法)。完整序列校验仍由
301
+ DecisionTrace 构造边界在最终冻结时执行,双保险不削弱。"""
302
+ if not entries:
303
+ return
304
+ if (
305
+ self._trace_last_event_time is not None
306
+ and entries[0].event_time < self._trace_last_event_time
307
+ ):
308
+ raise ValueError(
309
+ f"DecisionTrace 时间不得倒退:新增批次首条 {entries[0].event_time} 早于"
310
+ f"已聚合尾部 {self._trace_last_event_time}(成功片时间严格递增,"
311
+ f"属 Runtime 矛盾输出)"
312
+ )
313
+ self._trace_entries.extend(entries)
314
+ self._trace_last_event_time = entries[-1].event_time
315
+
316
+ def _build_result(self, settling_updates: tuple = ()) -> BacktestResult:
317
+ curve = EquityCurve(points=tuple(self._equity_points))
318
+ position_lots = self._ledger.position_lots() if self._ledger is not None else ()
319
+ cash_entries = self._ledger.cash_ledger_entries() if self._ledger is not None else ()
320
+ reservation_entries = (
321
+ getattr(self._ledger, "reservation_ledger_entries", lambda: ())()
322
+ if self._ledger is not None
323
+ else ()
324
+ )
325
+ closed_trades = self._ledger.closed_trades() if self._ledger is not None else None
326
+ metrics: BacktestMetrics = compute_metrics(
327
+ curve,
328
+ initial_cash=self._config.initial_cash,
329
+ fill_count=len(self._fills),
330
+ trading_days_per_year=self._config.trading_days_per_year,
331
+ annual_risk_free_rate=self._config.annual_risk_free_rate,
332
+ closed_trades=closed_trades,
333
+ fills=tuple(self._fills),
334
+ )
335
+ # SETTLING 结构化迁移追加为 trace 条目(kind=order_update;时间 =
336
+ # ended_event_time == 最后成功片时间,满足 trace 不倒退)
337
+ settling_traces = tuple(
338
+ TraceEntry(
339
+ event_time=update.event_time,
340
+ kind="order_update",
341
+ detail=(
342
+ f"order_id={update.order_id} status={update.current_status.value} "
343
+ f"reason={update.reason_code.value} phase=settling"
344
+ ),
345
+ )
346
+ for update in settling_updates
347
+ )
348
+ return BacktestResult(
349
+ status=BacktestStatus.COMPLETED,
350
+ config=self._config,
351
+ data_revisions=tuple(
352
+ sorted(
353
+ self._data_revisions,
354
+ key=lambda item: (
355
+ item.dataset,
356
+ item.instrument_id,
357
+ item.frequency or "event",
358
+ item.revision,
359
+ ),
360
+ )
361
+ ),
362
+ started_event_time=self._started_event_time,
363
+ ended_event_time=self._ended_event_time,
364
+ final_portfolio_snapshot=self._final_snapshot,
365
+ # 终态订单来自 Order Execution 公开投影;数据结束时仍 PENDING 的
366
+ # 日内订单如实保留(SETTLING 只对 coarse DAY 单结构化到期)
367
+ order_snapshots=self._execution.order_snapshots(),
368
+ settling_order_updates=settling_updates,
369
+ fills=tuple(self._fills),
370
+ portfolio_decisions=tuple(self._portfolio_decisions),
371
+ position_lots=position_lots,
372
+ cash_ledger_entries=cash_entries,
373
+ reservation_ledger_entries=reservation_entries,
374
+ closed_trades=closed_trades or (),
375
+ execution_attempts=tuple(self._attempts),
376
+ equity_curve=curve,
377
+ metrics=metrics,
378
+ decision_trace=DecisionTrace(entries=tuple(self._trace_entries) + settling_traces),
379
+ limitations=(
380
+ ()
381
+ if self._ledger is not None
382
+ else (
383
+ ResultLimitation.TRADE_COUNT_UNAVAILABLE,
384
+ ResultLimitation.WIN_RATE_UNAVAILABLE,
385
+ ResultLimitation.PRE_TAX_DIVIDEND,
386
+ )
387
+ ),
388
+ )
389
+
390
+ def _require_result(self) -> BacktestResult:
391
+ if self._result is None or self._status is not BacktestStatus.COMPLETED:
392
+ raise RuntimeError("BacktestRun 未正常完成:无最终结果")
393
+ return self._result
@@ -0,0 +1,77 @@
1
+ """
2
+ Backtest Data Guard - 回测启动前的数据前置检查。
3
+
4
+ 边界:
5
+ - 不找 Provider、不同步数据、不修复数据;
6
+ - 只依赖 Market 跨域稳定 Port;
7
+ - STALE 不拒绝,因为历史完整区间通常不要求接近当前时间。
8
+ """
9
+
10
+ from datetime import datetime
11
+
12
+ from quant_core.app.data.exceptions import MarketDataIncompleteException, MarketDataInvalidException
13
+ from quant_core.app.data.interface.market_data_verification import IMarketDataVerification
14
+ from quant_core.app.data.schema.reliability_schema import DatasetHealth, DatasetHealthResult
15
+
16
+
17
+ async def ensure_backtest_data_ready(
18
+ verification: IMarketDataVerification,
19
+ instrument_id: str,
20
+ frequency: str,
21
+ start: datetime,
22
+ end: datetime,
23
+ ) -> DatasetHealthResult:
24
+ """校验 Historical Backtest 所需 Bar 数据,不可用时抛出领域异常。"""
25
+ result = await verification.verify_bars(instrument_id, frequency, start, end)
26
+ if result.health in (DatasetHealth.HEALTHY, DatasetHealth.STALE):
27
+ return result
28
+ reason = f"health={result.health.value}"
29
+ if result.missing_count:
30
+ samples = ", ".join(i.timestamp.isoformat() for i in result.issues if i.timestamp)
31
+ reason += f"; missing {result.missing_count} bars (samples: {samples})"
32
+ if result.invalid_count:
33
+ reason += f"; invalid {result.invalid_count} bars"
34
+ if result.health is DatasetHealth.INVALID:
35
+ raise MarketDataInvalidException(
36
+ dataset=result.dataset,
37
+ instrument_id=instrument_id,
38
+ start=start.isoformat(),
39
+ end=end.isoformat(),
40
+ reason=reason,
41
+ )
42
+ raise MarketDataIncompleteException(
43
+ dataset=result.dataset,
44
+ instrument_id=instrument_id,
45
+ start=start.isoformat(),
46
+ end=end.isoformat(),
47
+ reason=reason,
48
+ )
49
+
50
+
51
+ async def ensure_backtest_dependencies_ready(
52
+ verification: IMarketDataVerification,
53
+ instrument_id: str,
54
+ frequency: str,
55
+ start: datetime,
56
+ end: datetime,
57
+ require_adjustment_factors: bool,
58
+ ) -> tuple[DatasetHealthResult, ...]:
59
+ """校验复权与公司行动依赖;任何缺失都在消费首片前 fail closed。"""
60
+ results = []
61
+ if require_adjustment_factors:
62
+ results.append(
63
+ await verification.verify_adjustment_factors(instrument_id, frequency, start, end)
64
+ )
65
+ results.append(await verification.verify_corporate_actions(instrument_id, start, end))
66
+ for result in results:
67
+ if result.health in (DatasetHealth.HEALTHY, DatasetHealth.STALE):
68
+ continue
69
+ reason = f"health={result.health.value}; missing={result.missing_count}"
70
+ raise MarketDataIncompleteException(
71
+ dataset=result.dataset,
72
+ instrument_id=instrument_id,
73
+ start=start.isoformat(),
74
+ end=end.isoformat(),
75
+ reason=reason,
76
+ )
77
+ return tuple(results)
@@ -0,0 +1,26 @@
1
+ """Historical Clock - Backtest 拥有的场景时钟(T7)
2
+
3
+ ClockProtocol 的历史回放实现:当前市场时间由 BacktestRun 在调用
4
+ ``RuntimeSession.step(slice@T)`` 前显式设置为 T,绝不读 wall clock。
5
+ """
6
+
7
+ from datetime import datetime
8
+
9
+
10
+ class HistoricalClock:
11
+ """历史回放时钟(结构化满足 ClockProtocol)"""
12
+
13
+ def __init__(self) -> None:
14
+ self._now: datetime | None = None
15
+
16
+ def set(self, now: datetime) -> None:
17
+ """由 BacktestRun 在每个时间片 step 前调用(必须 timezone-aware)"""
18
+ if not isinstance(now, datetime) or now.tzinfo is None:
19
+ raise ValueError("HistoricalClock.set 必须 timezone-aware datetime")
20
+ self._now = now
21
+
22
+ def now(self) -> datetime:
23
+ """返回当前回放市场时间(尚未设置时失败,不伪造时间)"""
24
+ if self._now is None:
25
+ raise ValueError("HistoricalClock 尚未设置:回放开始前没有当前市场时间")
26
+ return self._now
@@ -0,0 +1,34 @@
1
+ """旧回测结果的只读调仓投影;仅用于历史 JSON 反序列化。"""
2
+
3
+ from decimal import Decimal
4
+ from enum import Enum
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from quant_core.app.strategy.schema.portfolio_target import PortfolioDecision
9
+
10
+
11
+ class RebalancePhase(str, Enum):
12
+ SELLING = "selling"
13
+ BUYING = "buying"
14
+ COMPLETED = "completed"
15
+ CONSTRAINED = "constrained"
16
+ SUPERSEDED = "superseded"
17
+
18
+
19
+ class RebalanceMode(str, Enum):
20
+ COMPLETE = "complete"
21
+ PARTIAL = "partial"
22
+
23
+
24
+ class RebalancePlanSnapshot(BaseModel):
25
+ model_config = ConfigDict(frozen=True)
26
+
27
+ plan_id: str
28
+ phase: RebalancePhase
29
+ target_weights: tuple[tuple[str, Decimal], ...]
30
+ remaining_cash: Decimal = Field(..., ge=0)
31
+ target_value_error: tuple[tuple[str, Decimal], ...] = ()
32
+ reasons: tuple[str, ...] = ()
33
+ mode: RebalanceMode = RebalanceMode.COMPLETE
34
+ decision: PortfolioDecision | None = None