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,361 @@
1
+ """共享市场事件循环;策略在回调内同步下单,Runtime 不计算或推进调仓计划。"""
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+
7
+ from quant_core.app.data.constants import MarketEventType
8
+ from quant_core.app.data.schema.event_schema import (
9
+ BarEvent,
10
+ CorporateActionEvent,
11
+ MarketTimeSlice,
12
+ TradingSessionEvent,
13
+ )
14
+ from quant_core.app.order_execution.constants import ExecutionAttemptOutcome
15
+ from quant_core.app.order_execution.domain.order_event import (
16
+ ExecutionAttempt,
17
+ Fill,
18
+ OrderTransition,
19
+ )
20
+ from quant_core.app.order_execution.domain.order_value import OrderAck, OrderIntent
21
+ from quant_core.app.order_execution.interface.public.order_execution import IOrderExecution
22
+ from quant_core.app.order_execution.interface.public.order_runtime import IOrderRuntime
23
+ from quant_core.app.order_execution.schema.order_request_schema import OrderRequest
24
+ from quant_core.app.trading_runtime.runtime_ports import (
25
+ ExchangeStateProjectionProtocol,
26
+ MarketStateProjectionProtocol,
27
+ RuntimePortfolioProtocol,
28
+ StrategyRuntimeProtocol,
29
+ )
30
+ from quant_core.app.trading_runtime.runtime_schema import (
31
+ ClockProtocol,
32
+ RuntimeStepResult,
33
+ TraceEntry,
34
+ )
35
+
36
+
37
+ class RuntimeSessionState(str, Enum):
38
+ """Session 生命周期状态机:READY → STEPPING → READY;任一异常 → FAILED"""
39
+
40
+ READY = "ready"
41
+ STEPPING = "stepping"
42
+ FAILED = "failed"
43
+
44
+
45
+ class RuntimeSession:
46
+ """单时间片编排器(Backtest / Paper / Live 共享同一因果顺序)
47
+
48
+ 构造注入全部协作 Port(具体实现只在组合点装配):
49
+
50
+ - clock:场景无关时钟(clock.now() 必须等于本片时间);
51
+ - market_state_resolver:Market 域增量投影 Port;
52
+ - order_execution:Order Execution Port;
53
+ - portfolio:组合聚合(RuntimePortfolioProtocol:组合更新 + 执行期
54
+ 只读账户视图,同一对象两个能力面);
55
+ - strategy_runtime:StrategyRuntimeProtocol(现有 StrategyRuntimeService
56
+ 零修改即满足);
57
+ - order_api:同步交易环境及审计读取能力。
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ clock: ClockProtocol,
63
+ market_state_resolver: MarketStateProjectionProtocol,
64
+ exchange_state_resolver: ExchangeStateProjectionProtocol,
65
+ order_execution: IOrderExecution,
66
+ portfolio: RuntimePortfolioProtocol,
67
+ strategy_runtime: StrategyRuntimeProtocol,
68
+ order_api: IOrderRuntime,
69
+ ) -> None:
70
+ self._clock = clock
71
+ self._resolver = market_state_resolver
72
+ self._exchange_resolver = exchange_state_resolver
73
+ self._execution = order_execution
74
+ self._portfolio = portfolio
75
+ self._strategy_runtime = strategy_runtime
76
+ self._order_api = order_api
77
+ self._state = RuntimeSessionState.READY
78
+ self._last_slice_time: datetime | None = None
79
+
80
+ # ------------------------------------------------------------------
81
+ # 公开状态
82
+ # ------------------------------------------------------------------
83
+
84
+ @property
85
+ def state(self) -> RuntimeSessionState:
86
+ return self._state
87
+
88
+ @property
89
+ def last_slice_time(self) -> datetime | None:
90
+ """上一个成功 step 的时间片时间(尚未成功 step 时为 None)"""
91
+ return self._last_slice_time
92
+
93
+ # ------------------------------------------------------------------
94
+ # 单时间片编排
95
+ # ------------------------------------------------------------------
96
+
97
+ async def step(self, time_slice: MarketTimeSlice) -> RuntimeStepResult:
98
+ """严格执行一个 MarketTimeSlice(固定因果顺序;任一异常即 FAILED)"""
99
+ if self._state is RuntimeSessionState.FAILED:
100
+ raise RuntimeError(
101
+ "RuntimeSession 已处于 FAILED 状态:写阶段可能已部分完成,"
102
+ "禁止在同一 Session 继续运行(由上层 BacktestRun 终止整个 run)"
103
+ )
104
+ if self._state is RuntimeSessionState.STEPPING:
105
+ raise RuntimeError("RuntimeSession.step 不允许重入")
106
+ self._state = RuntimeSessionState.STEPPING
107
+ try:
108
+ return await self._step(time_slice)
109
+ except BaseException:
110
+ # fail-fast:任一阶段失败后 Session 不可继续(无跨域事务回滚)
111
+ self._state = RuntimeSessionState.FAILED
112
+ raise
113
+
114
+ # ------------------------------------------------------------------
115
+
116
+ def _validate_time(self, slice_time: datetime) -> None:
117
+ """step 前时间边界:全部失败路径零下游调用"""
118
+ if slice_time.tzinfo is None:
119
+ raise ValueError("MarketTimeSlice.event_time 必须 timezone-aware")
120
+ now = self._clock.now()
121
+ if not isinstance(now, datetime) or now.tzinfo is None:
122
+ raise ValueError("Clock.now() 必须返回 timezone-aware datetime")
123
+ if now != slice_time:
124
+ raise ValueError(f"clock.now() {now} 必须等于 slice_time {slice_time}")
125
+ if self._last_slice_time is not None and slice_time <= self._last_slice_time:
126
+ raise ValueError(
127
+ f"时间片必须严格递增:{slice_time} 不晚于上一个成功片 "
128
+ f"{self._last_slice_time}(重复/倒流 slice 拒绝)"
129
+ )
130
+
131
+ def _has_session_open(self, time_slice: MarketTimeSlice) -> bool:
132
+ """新交易日结算的唯一触发边界:显式 TradingSessionEvent(SESSION_OPEN)"""
133
+ return any(
134
+ isinstance(event, TradingSessionEvent)
135
+ and event.payload.session is MarketEventType.SESSION_OPEN
136
+ for event in time_slice.events
137
+ )
138
+
139
+ def _settle_if_new_trading_day(self, time_slice: MarketTimeSlice) -> None:
140
+ """显式 SESSION_OPEN 才结算;账户级一次(同片多 Exchange OPEN 去重)"""
141
+ if self._has_session_open(time_slice):
142
+ self._portfolio.settle_trading_day(time_slice.event_time.date())
143
+
144
+ def _bar_close_prices(self, time_slice: MarketTimeSlice) -> dict[str, Decimal]:
145
+ """本片每标的一根 Bar 的 close(稳定构造;矛盾重复 Bar 即失败)"""
146
+ prices: dict[str, Decimal] = {}
147
+ for event in time_slice.events:
148
+ if isinstance(event, BarEvent):
149
+ bar = event.payload
150
+ if bar.instrument_id in prices:
151
+ raise ValueError(f"同一时间片出现多根 {bar.instrument_id} Bar,属矛盾输入")
152
+ prices[bar.instrument_id] = bar.close
153
+ return prices
154
+
155
+ def _apply_corporate_actions(self, time_slice: MarketTimeSlice) -> None:
156
+ """Fill 后、估值前应用权益事件;登记日收盘成交可获得当日权益。"""
157
+ actions = sorted(
158
+ (event for event in time_slice.events if isinstance(event, CorporateActionEvent)),
159
+ key=lambda event: (event.payload.action.action_id, event.payload.phase.value),
160
+ )
161
+ for event in actions:
162
+ self._portfolio.apply_corporate_action(event.payload, time_slice.event_time)
163
+
164
+ def _validate_signal_times(self, signals: list, slice_time: datetime) -> tuple:
165
+ for signal in signals:
166
+ if signal.event_time != slice_time:
167
+ raise ValueError(
168
+ f"Signal.event_time {signal.event_time} 必须等于 slice_time "
169
+ f"{slice_time}(signal_id={signal.signal_id})"
170
+ )
171
+ return tuple(signals)
172
+
173
+ def _validate_intent_times(self, intents: tuple, slice_time: datetime) -> tuple:
174
+ for intent in intents:
175
+ if intent.creation_event_time != slice_time:
176
+ raise ValueError(
177
+ f"OrderIntent.creation_event_time {intent.creation_event_time} "
178
+ f"必须等于 slice_time {slice_time}"
179
+ f"(intent_id={intent.intent_id});不 submit"
180
+ )
181
+ return intents
182
+
183
+ def _validate_ack_times(self, acks: tuple, slice_time: datetime) -> tuple:
184
+ for ack in acks:
185
+ if ack.event_time != slice_time:
186
+ raise ValueError(
187
+ f"OrderAck.event_time {ack.event_time} 必须等于 slice_time "
188
+ f"{slice_time}(intent_id={ack.intent_id})"
189
+ )
190
+ return acks
191
+
192
+ def _build_trace_entries(
193
+ self,
194
+ slice_time: datetime,
195
+ fills: tuple[Fill, ...],
196
+ attempts: tuple[ExecutionAttempt, ...],
197
+ order_updates: tuple[OrderTransition, ...],
198
+ signals: tuple,
199
+ requests: tuple[OrderRequest, ...],
200
+ intents: tuple[OrderIntent, ...],
201
+ acks: tuple[OrderAck, ...],
202
+ ) -> tuple[TraceEntry, ...]:
203
+ """确定性顺序的审计条目(分类固定;detail 仅人类可读,不驱动行为)"""
204
+ entries: list[TraceEntry] = []
205
+ for fill in fills:
206
+ entries.append(
207
+ TraceEntry(
208
+ event_time=slice_time,
209
+ kind="fill",
210
+ detail=(
211
+ f"fill_id={fill.fill_id} order_id={fill.order_id} "
212
+ f"{fill.instrument_id} {fill.side.value} "
213
+ f"{fill.quantity}@{fill.price}"
214
+ ),
215
+ )
216
+ )
217
+ for attempt in attempts:
218
+ if attempt.outcome is ExecutionAttemptOutcome.DEFERRED:
219
+ entries.append(
220
+ TraceEntry(
221
+ event_time=slice_time,
222
+ kind="execution_deferred",
223
+ detail=(
224
+ f"attempt_id={attempt.attempt_id} order_id={attempt.order_id} "
225
+ f"reason={attempt.defer_reason.value if attempt.defer_reason else None}"
226
+ ),
227
+ )
228
+ )
229
+ for update in order_updates:
230
+ entries.append(
231
+ TraceEntry(
232
+ event_time=slice_time,
233
+ kind="order_update",
234
+ detail=(
235
+ f"order_id={update.order_id} status={update.current_status.value} "
236
+ f"reason={update.reason_code.value}"
237
+ ),
238
+ )
239
+ )
240
+ for signal in signals:
241
+ entries.append(
242
+ TraceEntry(
243
+ event_time=slice_time,
244
+ kind="signal",
245
+ detail=(
246
+ f"signal_id={signal.signal_id} {signal.instrument_id} "
247
+ f"{signal.signal_type.value} {signal.side.value}"
248
+ ),
249
+ )
250
+ )
251
+ for request in requests:
252
+ entries.append(
253
+ TraceEntry(
254
+ event_time=slice_time,
255
+ kind="strategy_order_request",
256
+ detail=(
257
+ f"request_id={request.request_id} strategy_id={request.strategy_id} "
258
+ f"{request.instrument_id} {request.request_type.value}"
259
+ ),
260
+ )
261
+ )
262
+ for intent in intents:
263
+ entries.append(
264
+ TraceEntry(
265
+ event_time=slice_time,
266
+ kind="order_intent",
267
+ detail=(
268
+ f"intent_id={intent.intent_id} {intent.instrument_id} "
269
+ f"{intent.side.value} {intent.quantity}"
270
+ ),
271
+ )
272
+ )
273
+ for ack in acks:
274
+ reason = ack.rejection_reason.value if ack.rejection_reason else None
275
+ entries.append(
276
+ TraceEntry(
277
+ event_time=slice_time,
278
+ kind="order_ack",
279
+ detail=(
280
+ f"intent_id={ack.intent_id} status={ack.status.value} "
281
+ f"order_id={ack.order_id} rejection_reason={reason}"
282
+ ),
283
+ )
284
+ )
285
+ return tuple(entries)
286
+
287
+ async def _step(self, time_slice: MarketTimeSlice) -> RuntimeStepResult:
288
+ # 1. 时间边界(失败零下游调用)
289
+ self._validate_time(time_slice.event_time)
290
+ t = time_slice.event_time
291
+
292
+ # 2. 市场状态投影(先于撮合);交易所运行状态投影(T4R 唯一 SSOT)
293
+ market_states = self._resolver.apply_slice(time_slice)
294
+ exchange_states = self._exchange_resolver.apply_slice(time_slice)
295
+
296
+ # 3. 新交易日结算(显式 SESSION_OPEN;先于旧单撮合)
297
+ self._settle_if_new_trading_day(time_slice)
298
+
299
+ # 4. 旧订单撮合(本片新单绝不参与;account view 即同一 Portfolio;
300
+ # 交易所运行状态由 Resolver 投影注入,Execution 不自行解释 Session 事件)
301
+ processing_result = self._execution.process_market_slice(
302
+ time_slice,
303
+ market_states,
304
+ exchange_states,
305
+ self._portfolio,
306
+ )
307
+
308
+ # 6. 公司行动:登记日权益基于本片成交后的持仓;除权/到账在估值前生效。
309
+ self._apply_corporate_actions(time_slice)
310
+
311
+ # 7. 本片 Bar Close 估值(空映射仍调用,使 snapshot.as_of 推进到 T)
312
+ prices = self._bar_close_prices(time_slice)
313
+ self._portfolio.mark_to_market(prices, t)
314
+
315
+ self._order_api.begin_slice(time_slice, prices, exchange_states)
316
+ signals = await self._strategy_runtime.run_slice(time_slice)
317
+ signal_batch = self._validate_signal_times(signals, t)
318
+ requests = tuple(self._order_api.requests)
319
+ intent_batch = self._validate_intent_times(tuple(self._order_api.intents), t)
320
+ ack_batch = self._validate_ack_times(tuple(self._order_api.acks), t)
321
+ order_updates = processing_result.order_updates + tuple(self._order_api.cancellations)
322
+ matches = self._order_api.matches
323
+ fills = processing_result.fills + tuple(fill for batch in matches for fill in batch.fills)
324
+ attempts = processing_result.execution_attempts + tuple(
325
+ attempt for batch in matches for attempt in batch.execution_attempts
326
+ )
327
+ order_updates += tuple(update for batch in matches for update in batch.order_updates)
328
+ order_updates += self._execution.finish_slice(time_slice)
329
+ self._portfolio.mark_to_market(prices, t)
330
+ snapshot = self._portfolio.snapshot()
331
+
332
+ # 15-16. 组装 trace 与 StepResult@T(构造失败即 step 失败)
333
+ trace_entries = self._build_trace_entries(
334
+ t,
335
+ fills,
336
+ attempts,
337
+ order_updates,
338
+ signal_batch,
339
+ requests,
340
+ intent_batch,
341
+ ack_batch,
342
+ )
343
+ result = RuntimeStepResult(
344
+ slice_time=t,
345
+ fills=fills,
346
+ execution_attempts=attempts,
347
+ order_acks=ack_batch,
348
+ order_updates=order_updates,
349
+ signals=signal_batch,
350
+ portfolio_decisions=self._strategy_runtime.decisions,
351
+ strategy_order_requests=requests,
352
+ order_intents=intent_batch,
353
+ portfolio_snapshot=snapshot,
354
+ exchange_states=tuple(exchange_states[k] for k in sorted(exchange_states)),
355
+ trace_entries=trace_entries,
356
+ )
357
+
358
+ # 17. 提交 last successful slice time(仅在完整 StepResult 构造成功后)
359
+ self._state = RuntimeSessionState.READY
360
+ self._last_slice_time = t
361
+ return result
@@ -0,0 +1,58 @@
1
+ """Portfolio 同步只读能力到 Strategy 异步查询的适配。
2
+
3
+ 每次委托当前账户,确保策略读取的是本片成交和估值后的真实状态。
4
+ 既有 CashView/PositionView 保持原语义;组合构建另通过公开快照与持仓批次
5
+ 读取市值、取得时间和剩余数量。适配器不缓存、推断或修改资产事实。
6
+ """
7
+
8
+ from typing import Protocol
9
+
10
+ from quant_core.app.portfolio.interface.public.portfolio import IPortfolioReader
11
+ from quant_core.app.portfolio.ledger_schema import PositionLotSnapshot
12
+ from quant_core.app.portfolio.portfolio_schema import PortfolioSnapshot
13
+ from quant_core.app.strategy.interface.public.strategy_portfolio_reader import (
14
+ IStrategyPortfolioReader,
15
+ )
16
+ from quant_core.app.strategy.schema.portfolio_schema import CashView, PositionView
17
+
18
+
19
+ class StrategyPortfolioSource(IPortfolioReader, Protocol):
20
+ """适配器只依赖账户读取和成交批次事实。"""
21
+
22
+ def snapshot(self) -> PortfolioSnapshot:
23
+ """读取当前真实估值快照。"""
24
+ ...
25
+
26
+ def position_lots(self) -> tuple[PositionLotSnapshot, ...]:
27
+ """读取成交形成的持仓批次。"""
28
+ ...
29
+
30
+
31
+ class StrategyPortfolioReaderAdapter(IStrategyPortfolioReader):
32
+ """Portfolio 同步只读 Protocol → Strategy async 只读 Protocol 的桥
33
+
34
+ 显式继承 Strategy 域 Protocol(不重新定义第二套形状)。
35
+ """
36
+
37
+ def __init__(self, portfolio: StrategyPortfolioSource) -> None:
38
+ self._portfolio = portfolio
39
+
40
+ async def get_position(self, instrument_id: str) -> PositionView | None:
41
+ """实时读取当前持仓并翻译为 PositionView(无持仓返回 None)"""
42
+ position = self._portfolio.get_position(instrument_id)
43
+ if position is None:
44
+ return None
45
+ return PositionView(instrument_id=position.instrument_id, quantity=position.quantity)
46
+
47
+ async def get_cash(self) -> CashView:
48
+ """实时读取当前资金并翻译为 CashView(available_cash → cash)"""
49
+ cash = self._portfolio.get_cash()
50
+ return CashView(cash=cash.available_cash)
51
+
52
+ async def get_snapshot(self) -> PortfolioSnapshot:
53
+ """读取账户估值,不缓存或修改资产事实。"""
54
+ return self._portfolio.snapshot()
55
+
56
+ async def get_position_lots(self) -> tuple[PositionLotSnapshot, ...]:
57
+ """读取真实持仓批次,供持有期约束使用。"""
58
+ return self._portfolio.position_lots()
@@ -0,0 +1,37 @@
1
+ """交易规则的时间有效性解析。"""
2
+
3
+ from collections.abc import Iterable
4
+ from datetime import date, datetime
5
+
6
+ from quant_core.app.data.schema.market_schema import TradingRuleResponse
7
+
8
+
9
+ class EffectiveTradingRuleBook:
10
+ """不可变规则簿;缺失或重叠均失败关闭。"""
11
+
12
+ def __init__(self, rules: Iterable[TradingRuleResponse]) -> None:
13
+ grouped: dict[str, list[TradingRuleResponse]] = {}
14
+ for rule in rules:
15
+ grouped.setdefault(rule.instrument_id, []).append(rule)
16
+ normalized: dict[str, tuple[TradingRuleResponse, ...]] = {}
17
+ for instrument_id, rows in grouped.items():
18
+ ordered = tuple(sorted(rows, key=lambda item: item.effective_from))
19
+ for previous, current in zip(ordered, ordered[1:], strict=False):
20
+ if previous.effective_to is None or previous.effective_to >= current.effective_from:
21
+ raise ValueError(f"{instrument_id} TradingRule 有重叠有效期")
22
+ normalized[instrument_id] = ordered
23
+ self._rules = normalized
24
+
25
+ def resolve(self, instrument_id: str, at: date | datetime) -> TradingRuleResponse:
26
+ business_date = at.date() if isinstance(at, datetime) else at
27
+ matches = tuple(
28
+ rule
29
+ for rule in self._rules.get(instrument_id, ())
30
+ if rule.effective_from <= business_date
31
+ and (rule.effective_to is None or business_date <= rule.effective_to)
32
+ )
33
+ if len(matches) != 1:
34
+ raise ValueError(
35
+ f"{instrument_id} 在 {business_date} 必须恰有一条 TradingRule,实际 {len(matches)}"
36
+ )
37
+ return matches[0]
@@ -0,0 +1,12 @@
1
+ """订单规划消费的最小交易规则接口。"""
2
+
3
+ from datetime import date, datetime
4
+ from typing import Protocol
5
+
6
+ from quant_core.app.data.schema.market_schema import TradingRuleResponse
7
+
8
+
9
+ class ITradingRuleQuery(Protocol):
10
+ def resolve(self, instrument_id: str, at: date | datetime) -> TradingRuleResponse:
11
+ """返回业务时点唯一有效的交易规则,缺失或重叠应失败。"""
12
+ ...
File without changes
File without changes
@@ -0,0 +1,89 @@
1
+ """
2
+ 通用异常机制基类 - Common owns mechanism, Domain owns semantics
3
+
4
+ 本模块只定义机制:AppException 基类 + 通用抽象子类(不存在/冲突/未认证等)。
5
+ 具体 Domain 的业务错误(OrderNotFound / UserDisabled 等)由各 Domain 的
6
+ app/{module}/exceptions.py 自定义并继承这里的基类,common 不存放业务语义。
7
+
8
+ 设计约束:本模块不得 import fastapi。Service 层只依赖异常基类,
9
+ 协议层(HTTP / WebSocket / CLI / Task Worker)各自负责翻译。
10
+
11
+ 命名约定:
12
+ - error_code: 业务语义错误码字符串,跨入口复用,供 WS / MQ / 前端编程判断。
13
+ - http_status: HTTP 协议状态码,仅 HTTP 入口翻译使用。
14
+
15
+ 两个入口完全分离:HTTP 响应体放 http_status,WS 响应放 error_code,
16
+ 不要让两者共用一个名叫 code 的字段。
17
+
18
+ 新增异常类型:继承 AppException 或其子类,声明 error_code / http_status 两个类属性即可,
19
+ 无需修改构造签名。
20
+ """
21
+
22
+ from typing import ClassVar
23
+
24
+
25
+ class AppException(Exception):
26
+ """应用业务异常基类。
27
+
28
+ Attributes:
29
+ message: 面向用户的错误描述(默认中文)。
30
+ error_code: 稳定的业务错误码字符串,跨入口复用,供 WS / MQ / 前端编程判断。
31
+ http_status: 建议的 HTTP 状态码,供 HTTP handler 翻译;默认 400。
32
+ """
33
+
34
+ error_code: ClassVar[str] = "APP_ERROR"
35
+ http_status: ClassVar[int] = 400
36
+
37
+ def __init__(self, message: str = "业务处理失败"):
38
+ self.message = message
39
+ super().__init__(self.message)
40
+
41
+
42
+ class NotFoundException(AppException):
43
+ """资源不存在"""
44
+
45
+ error_code = "NOT_FOUND"
46
+ http_status = 404
47
+
48
+ def __init__(self, message: str = "资源不存在"):
49
+ super().__init__(message)
50
+
51
+
52
+ class UnauthorizedException(AppException):
53
+ """未认证或认证失败(凭证无效、账户禁用等)"""
54
+
55
+ error_code = "UNAUTHORIZED"
56
+ http_status = 401
57
+
58
+ def __init__(self, message: str = "认证失败"):
59
+ super().__init__(message)
60
+
61
+
62
+ class PermissionDeniedException(AppException):
63
+ """无权限访问"""
64
+
65
+ error_code = "PERMISSION_DENIED"
66
+ http_status = 403
67
+
68
+ def __init__(self, message: str = "无权访问"):
69
+ super().__init__(message)
70
+
71
+
72
+ class ConflictException(AppException):
73
+ """资源冲突,例如唯一约束冲突、状态非法变更"""
74
+
75
+ error_code = "CONFLICT"
76
+ http_status = 409
77
+
78
+ def __init__(self, message: str = "资源冲突"):
79
+ super().__init__(message)
80
+
81
+
82
+ class ValidationException(AppException):
83
+ """业务规则校验失败(区别于 FastAPI 的请求体格式校验)"""
84
+
85
+ error_code = "VALIDATION_ERROR"
86
+ http_status = 422
87
+
88
+ def __init__(self, message: str = "请求参数校验失败"):
89
+ super().__init__(message)
@@ -0,0 +1,45 @@
1
+ """本地执行记录的协议无关信封,不包含领域结果模型。"""
2
+
3
+ import hashlib
4
+ import json
5
+ from datetime import UTC, datetime
6
+ from enum import Enum
7
+ from typing import Any
8
+ from uuid import UUID, uuid4
9
+
10
+ from pydantic import BaseModel, Field, model_validator
11
+
12
+
13
+ class RunStatus(str, Enum):
14
+ COMPLETED = "completed"
15
+ FAILED = "failed"
16
+
17
+
18
+ class LocalRunRecord(BaseModel):
19
+ """执行来源与结果索引;领域结果由所属模块解释。"""
20
+
21
+ uuid: UUID = Field(default_factory=uuid4)
22
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
23
+ status: RunStatus = RunStatus.COMPLETED
24
+ request: dict[str, Any]
25
+ provenance: dict[str, Any] = Field(default_factory=dict)
26
+ result: dict[str, Any] = Field(default_factory=dict)
27
+ error: str | None = None
28
+
29
+ @model_validator(mode="after")
30
+ def validate_record(self) -> "LocalRunRecord":
31
+ if self.created_at.utcoffset() is None:
32
+ raise ValueError("运行记录创建时间必须带时区")
33
+ if (self.status == RunStatus.FAILED) != (self.error is not None):
34
+ raise ValueError("运行状态与错误信息不一致")
35
+ return self
36
+
37
+
38
+ def content_digest(value: str) -> str:
39
+ """稳定内容摘要,不用于身份认证。"""
40
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
41
+
42
+
43
+ def json_value(value: Any) -> Any:
44
+ """将数值与日期保留为精确文本,供 JSON 产物使用。"""
45
+ return json.loads(json.dumps(value, default=str, ensure_ascii=False, allow_nan=False))