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,204 @@
1
+ """
2
+ Portfolio Schema - 组合只读投影契约(CashSnapshot / PositionSnapshot / PortfolioSnapshot)
3
+
4
+ 语义 SSOT 见 `app/portfolio/SPEC.md`(Fill Rule §5、Runtime Isolation §6)。
5
+
6
+ 不变量(构造边界自维护,不依赖调用者自律):
7
+ - 全部金额/价格严格 Decimal(mode="before" 拒绝 float,不静默转换)。
8
+ - 数量非负;available + pending_settlement 不超过 quantity。
9
+ - Cash:total == available + frozen,分项均 >= 0。
10
+ - Position:Canonical instrument_id;market_value == quantity * last_price。
11
+ - PortfolioSnapshot:positions 按 instrument_id 稳定排序且不重复;
12
+ exposure == Σ market_value;total_value == total_cash + exposure;
13
+ unrealized_pnl == Σ position.unrealized_pnl(realized 可含已平仓历史,
14
+ 不强制等于当前 positions 之和)。
15
+ - Snapshot 深度不可变(frozen + tuple)。本轮不修改 Strategy 的 PositionView。
16
+ """
17
+
18
+ from datetime import datetime
19
+ from decimal import Decimal
20
+
21
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
22
+
23
+ from quant_core.app.data.schema.trading_state_schema import validate_canonical_instrument_id
24
+
25
+
26
+ def _check_tz_aware(value: datetime, field: str) -> datetime:
27
+ if value.tzinfo is None:
28
+ raise ValueError(f"{field} 必须 timezone-aware(Canonical 市场时间带时区)")
29
+ return value
30
+
31
+
32
+ def _reject_float(value: object, field: str) -> object:
33
+ """组合域内金额/价格共享校验:严格拒绝 float 与非有限 Decimal
34
+
35
+ int 字面量可无损转 Decimal,允许;统一规则 value.is_finite() 必须为 True
36
+ (拒绝 NaN / ±Infinity / sNaN,含无范围约束的 PnL 字段)。
37
+ """
38
+ if isinstance(value, float):
39
+ raise ValueError(f"{field} 必须使用 Decimal,禁止 float(避免二进制浮点误差)")
40
+ if isinstance(value, Decimal) and not value.is_finite():
41
+ raise ValueError(f"{field} 必须是有限 Decimal,禁止 NaN / Infinity / sNaN")
42
+ return value
43
+
44
+
45
+ def _validate_identifier(value: object, field: str) -> str:
46
+ """组合域内标识校验:拒绝非字符串、空串、纯空白、含任何空白字符"""
47
+ if not isinstance(value, str):
48
+ raise ValueError(f"{field} 必须是非空字符串标识")
49
+ if not value or value.strip() != value or any(ch.isspace() for ch in value):
50
+ raise ValueError(f"{field} 不允许为空或包含空白字符:{value!r}")
51
+ return value
52
+
53
+
54
+ def _ensure_finite(model: object, fields: tuple[str, ...]) -> None:
55
+ """Decimal 解析完成后的二次统一 finite 校验(mode="after")。
56
+
57
+ 字符串 "NaN" / "Infinity" 在 before 阶段还不是 Decimal,会在
58
+ Pydantic 解析后变成非有限 Decimal;此处在解析后统一收口拒绝,
59
+ 与 before 阶段 `_reject_float` 使用同一条 is_finite() 规则。
60
+ """
61
+ for name in fields:
62
+ value = getattr(model, name)
63
+ if isinstance(value, Decimal) and not value.is_finite():
64
+ raise ValueError(f"{name} 必须是有限 Decimal,禁止字符串形式的 NaN / Infinity / sNaN")
65
+
66
+
67
+ class CashSnapshot(BaseModel):
68
+ """资金只读投影(分项均 >= 0 且与 total 一致)"""
69
+
70
+ model_config = ConfigDict(frozen=True)
71
+
72
+ available_cash: Decimal = Field(..., ge=0, description="可用资金(Decimal,>= 0)")
73
+ frozen_cash: Decimal = Field(..., ge=0, description="冻结资金(Decimal,>= 0)")
74
+ dividend_receivable: Decimal = Field(
75
+ Decimal("0"), ge=0, description="已除权但尚未到账的应收股息"
76
+ )
77
+ total_cash: Decimal = Field(
78
+ ..., ge=0, description="资金合计(= available + frozen + receivable,>= 0)"
79
+ )
80
+
81
+ @field_validator(
82
+ "available_cash", "frozen_cash", "dividend_receivable", "total_cash", mode="before"
83
+ )
84
+ @classmethod
85
+ def _reject_float_amounts(cls, value: object) -> object:
86
+ return _reject_float(value, "资金字段")
87
+
88
+ @model_validator(mode="after")
89
+ def _check_total_cash(self) -> "CashSnapshot":
90
+ _ensure_finite(self, ("available_cash", "frozen_cash", "dividend_receivable", "total_cash"))
91
+ expected = self.available_cash + self.frozen_cash + self.dividend_receivable
92
+ if self.total_cash != expected:
93
+ raise ValueError(
94
+ f"total_cash {self.total_cash} 必须等于 available + frozen + receivable {expected}"
95
+ )
96
+ return self
97
+
98
+
99
+ class PositionSnapshot(BaseModel):
100
+ """单持仓只读投影(数量/市值不变量在构造边界自维护)"""
101
+
102
+ model_config = ConfigDict(frozen=True)
103
+
104
+ instrument_id: str = Field(..., description="Canonical 标识(CN.XSHE.000001)")
105
+ quantity: int = Field(..., ge=0, description="总持仓(share,>= 0)")
106
+ available_quantity: int = Field(..., ge=0, description="可卖数量(T+1,>= 0)")
107
+ pending_settlement_quantity: int = Field(..., ge=0, description="待结算数量(>= 0)")
108
+ average_cost: Decimal = Field(..., ge=0, description="持仓均价(Decimal,>= 0)")
109
+ last_price: Decimal = Field(..., gt=0, description="最新价(Decimal,> 0)")
110
+ market_value: Decimal = Field(..., description="市值(= quantity * last_price)")
111
+ realized_pnl: Decimal = Field(..., description="已实现盈亏(Decimal)")
112
+ unrealized_pnl: Decimal = Field(..., description="未实现盈亏(Decimal)")
113
+
114
+ _validate_id = field_validator("instrument_id")(
115
+ classmethod(lambda cls, value: validate_canonical_instrument_id(value))
116
+ )
117
+ _validate_amounts = field_validator(
118
+ "average_cost",
119
+ "last_price",
120
+ "market_value",
121
+ "realized_pnl",
122
+ "unrealized_pnl",
123
+ mode="before",
124
+ )(classmethod(lambda cls, value: _reject_float(value, "持仓金额字段")))
125
+
126
+ @model_validator(mode="after")
127
+ def _check_quantity_invariants(self) -> "PositionSnapshot":
128
+ _ensure_finite(
129
+ self,
130
+ ("average_cost", "last_price", "market_value", "realized_pnl", "unrealized_pnl"),
131
+ )
132
+ if self.available_quantity + self.pending_settlement_quantity > self.quantity:
133
+ raise ValueError(
134
+ f"available {self.available_quantity} + pending_settlement "
135
+ f"{self.pending_settlement_quantity} 不得超过总持仓 {self.quantity}"
136
+ )
137
+ expected_value = Decimal(self.quantity) * self.last_price
138
+ if self.market_value != expected_value:
139
+ raise ValueError(
140
+ f"market_value {self.market_value} 必须等于 quantity * last_price {expected_value}"
141
+ )
142
+ return self
143
+
144
+
145
+ class PortfolioSnapshot(BaseModel):
146
+ """组合 point-in-time 只读投影(跨域唯一读取形态;Strategy 只读)"""
147
+
148
+ model_config = ConfigDict(frozen=True)
149
+
150
+ account_id: str = Field(..., description="运行账户标识(Backtest/Paper/Live 隔离)")
151
+
152
+ _validate_account_id = field_validator("account_id")(
153
+ classmethod(lambda cls, value: _validate_identifier(value, "account_id"))
154
+ )
155
+
156
+ as_of: datetime = Field(..., description="估值市场时间(timezone-aware)")
157
+ cash: CashSnapshot = Field(..., description="资金投影")
158
+ positions: tuple[PositionSnapshot, ...] = Field(
159
+ ..., description="持仓投影(按 instrument_id 稳定排序,无重复)"
160
+ )
161
+ total_value: Decimal = Field(..., description="组合总值(= total_cash + exposure)")
162
+ realized_pnl: Decimal = Field(..., description="已实现盈亏合计(可含已平仓历史)")
163
+ unrealized_pnl: Decimal = Field(..., description="未实现盈亏合计(= Σ position)")
164
+ exposure: Decimal = Field(..., ge=0, description="持仓敞口(= Σ market_value,>= 0)")
165
+
166
+ _validate_time = field_validator("as_of")(
167
+ classmethod(lambda cls, value: _check_tz_aware(value, "as_of"))
168
+ )
169
+ _validate_amounts = field_validator(
170
+ "total_value", "realized_pnl", "unrealized_pnl", "exposure", mode="before"
171
+ )(classmethod(lambda cls, value: _reject_float(value, "组合金额字段")))
172
+
173
+ @field_validator("positions")
174
+ @classmethod
175
+ def _stably_sort_positions(
176
+ cls, positions: tuple[PositionSnapshot, ...]
177
+ ) -> tuple[PositionSnapshot, ...]:
178
+ """构造期按 instrument_id 稳定排序,并拒绝重复持仓"""
179
+ ids = [p.instrument_id for p in positions]
180
+ if len(ids) != len(set(ids)):
181
+ raise ValueError(f"同一 instrument_id 不允许重复 position:{ids}")
182
+ return tuple(sorted(positions, key=lambda p: p.instrument_id))
183
+
184
+ @model_validator(mode="after")
185
+ def _check_aggregate_invariants(self) -> "PortfolioSnapshot":
186
+ _ensure_finite(self, ("total_value", "realized_pnl", "unrealized_pnl", "exposure"))
187
+ expected_exposure = sum((p.market_value for p in self.positions), Decimal("0"))
188
+ if self.exposure != expected_exposure:
189
+ raise ValueError(
190
+ f"exposure {self.exposure} 必须等于 Σ position.market_value {expected_exposure}"
191
+ )
192
+ expected_total = self.cash.total_cash + self.exposure
193
+ if self.total_value != expected_total:
194
+ raise ValueError(
195
+ f"total_value {self.total_value} 必须等于 "
196
+ f"cash.total_cash + exposure {expected_total}"
197
+ )
198
+ expected_unrealized = sum((p.unrealized_pnl for p in self.positions), Decimal("0"))
199
+ if self.unrealized_pnl != expected_unrealized:
200
+ raise ValueError(
201
+ f"unrealized_pnl {self.unrealized_pnl} 必须等于 Σ position.unrealized_pnl "
202
+ f"{expected_unrealized}"
203
+ )
204
+ return self
@@ -0,0 +1,427 @@
1
+ """Simulated Portfolio - 内存模拟组合(T3,确定性可回放)
2
+
3
+ 语义 SSOT 见 `app/portfolio/SPEC.md` §5 Fill Rule、T3 Task 及用户裁定
4
+ 2026-09-04:
5
+ - BUY Fill 的全部 ``FeeBreakdown.total`` 计入 CostBasis;
6
+ - SELL Fill 的费用在计算已实现盈亏时扣除;
7
+ - 清仓(quantity == 0)后删除 active Position,已实现盈亏保留在账户级累计;
8
+ - Portfolio 不做任何货币舍入(费用精度由 FeeModel 产出的 FeeBreakdown 负责)。
9
+
10
+ 核心不变量:
11
+ - 唯一交易输入是 Fill(Fill Rule);只有 apply_fill 改变现金/持仓状态。
12
+ - BUY:现金减少 ``q*P + F``;数量与 pending_settlement 增加 q;当日不可卖。
13
+ - SELL:不允许超过 available;数量与 available 同步减少 q;
14
+ 现金增加 ``q*P - F``;已实现 ``q*P - F - q*C``(卖出费用计入已实现)。
15
+ - settle_trading_day:新交易日首片前只释放普通买入 pending;红股锁定数量
16
+ 必须等待 listing_date 事件,禁止被普通 T+1 提前释放;
17
+ 同日重复调用幂等;交易日显式注入,不读墙钟。
18
+ - mark_to_market 只更新估值(last_price/market_value/unrealized),
19
+ 不改数量;接受市场价格超集(用户裁定 2026-09-06):先校验全部输入,
20
+ 只更新与持仓的交集;缺价持仓延续上次估值;交集为空仍推进 as_of。
21
+ - 全程 Decimal 精确计算,无 float / 非有限值(含字符串形式的 "NaN"/"Infinity")。
22
+ - 同一 fill_id 只能成功应用一次;失败的 Fill 不占用该 ID。
23
+ - SELL 记账后现金必须 >= 0(MVP 不允许账户现金为负)。
24
+ - apply_fill 失败原子:先完整校验,任一失败即抛出且不产生部分状态。
25
+ """
26
+
27
+ from datetime import datetime
28
+ from decimal import Decimal
29
+
30
+ from quant_core.app.data.schema.corporate_action_schema import (
31
+ CorporateActionEventPayload,
32
+ )
33
+ from quant_core.app.data.schema.trading_state_schema import validate_canonical_instrument_id
34
+ from quant_core.app.order_execution.constants import OrderSide
35
+ from quant_core.app.order_execution.domain.order_event import Fill
36
+ from quant_core.app.portfolio.domain.portfolio_entity import Portfolio, finite_decimal
37
+ from quant_core.app.portfolio.domain.portfolio_value import (
38
+ CorporateActionInstruction,
39
+ )
40
+ from quant_core.app.portfolio.domain.portfolio_value import (
41
+ CorporateActionPhase as PortfolioCorporateActionPhase,
42
+ )
43
+ from quant_core.app.portfolio.domain.reservation_entity import OrderReservation
44
+ from quant_core.app.portfolio.ledger_schema import (
45
+ CashLedgerAccount,
46
+ CashLedgerEntry,
47
+ CashLedgerEntryType,
48
+ ClosedTrade,
49
+ OrderReservationLedgerEntry,
50
+ PositionLotSnapshot,
51
+ ReservationLedgerAction,
52
+ )
53
+ from quant_core.app.portfolio.portfolio_schema import (
54
+ CashSnapshot,
55
+ PortfolioSnapshot,
56
+ PositionSnapshot,
57
+ )
58
+
59
+
60
+ def _check_finite_decimal(value: object, field: str) -> Decimal:
61
+ """组合域共享金额校验:拒绝 float;转换后统一 is_finite() 收口
62
+
63
+ 字符串 "NaN" / "Infinity" / "sNaN" 等在转换前不是 Decimal,必须在
64
+ Decimal 转换完成后再次检查 is_finite(),不允许任何非有限值进入内部状态。
65
+ """
66
+ return finite_decimal(value, field)
67
+
68
+
69
+ class SimulatedPortfolio(Portfolio):
70
+ """确定性可回放的内存组合;结构化满足:
71
+
72
+ - IPortfolioUpdate(apply_fill / mark_to_market / settle_trading_day / snapshot)
73
+ - IPortfolioReader(get_cash / get_position)
74
+ - IExecutionAccountView(available_cash / available_quantity,只读视图)
75
+ """
76
+
77
+ def __init__(
78
+ self, account_id: str, initial_cash: Decimal, initial_market_time: datetime
79
+ ) -> None:
80
+ super().__init__(account_id, initial_cash, initial_market_time)
81
+ cash = self._cash
82
+ self._cash_ledger: list[CashLedgerEntry] = []
83
+ self._closed_trades: list[ClosedTrade] = []
84
+ self._cash_source_keys: set[str] = set()
85
+ self._reservation_ledger: list[OrderReservationLedgerEntry] = []
86
+ self._append_cash_entry(
87
+ account=CashLedgerAccount.CASH,
88
+ entry_type=CashLedgerEntryType.INITIAL_BALANCE,
89
+ amount=cash,
90
+ event_time=initial_market_time,
91
+ source_key=f"initial:{self._account_id}",
92
+ )
93
+
94
+ # ------------------------------------------------------------------
95
+ # IPortfolioUpdate
96
+ # ------------------------------------------------------------------
97
+
98
+ def apply_fill(self, fill: Fill) -> None:
99
+ """由聚合应用成交;本适配层只生成账本投影。"""
100
+ reservation = self._order_reservations.get(fill.order_id)
101
+ result = super().apply_fill(fill)
102
+ if reservation is not None and (
103
+ result.reservation_cash_delta or result.reservation_quantity_delta
104
+ ):
105
+ self._append_reservation_entry(
106
+ reservation,
107
+ ReservationLedgerAction.CONSUME,
108
+ result.reservation_cash_delta,
109
+ result.reservation_quantity_delta,
110
+ fill.fill_time,
111
+ )
112
+ if result.reservation_cash_delta:
113
+ released = -result.reservation_cash_delta
114
+ for account, amount, suffix in (
115
+ (CashLedgerAccount.CASH, released, "cash"),
116
+ (CashLedgerAccount.FROZEN_CASH, -released, "frozen"),
117
+ ):
118
+ self._append_cash_entry(
119
+ account=account,
120
+ entry_type=CashLedgerEntryType.ORDER_RELEASE,
121
+ amount=amount,
122
+ event_time=fill.fill_time,
123
+ source_key=f"fill:{fill.fill_id}:release:{suffix}",
124
+ instrument_id=fill.instrument_id,
125
+ )
126
+ self._append_trade_cash_entries(fill, result.gross)
127
+ projected = [
128
+ ClosedTrade(
129
+ trade_id=trade.trade_id,
130
+ instrument_id=trade.instrument_id,
131
+ position_lot_id=trade.position_lot_id,
132
+ buy_source_id=trade.buy_source_id,
133
+ sell_fill_id=trade.sell_fill_id,
134
+ quantity=trade.quantity,
135
+ opened_at=trade.opened_at,
136
+ closed_at=trade.closed_at,
137
+ cost_basis=trade.cost_basis,
138
+ proceeds=trade.proceeds,
139
+ sell_fees=trade.sell_fees,
140
+ realized_pnl=trade.realized_pnl,
141
+ holding_days=trade.holding_days,
142
+ )
143
+ for trade in result.closed_lots
144
+ ]
145
+ self._closed_trades.extend(projected)
146
+ if result.dividend_tax:
147
+ self._append_cash_entry(
148
+ account=CashLedgerAccount.CASH,
149
+ entry_type=CashLedgerEntryType.DIVIDEND_TAX,
150
+ amount=-result.dividend_tax,
151
+ event_time=fill.fill_time,
152
+ source_key=f"dividend-tax:{fill.fill_id}",
153
+ instrument_id=fill.instrument_id,
154
+ )
155
+
156
+ def apply_corporate_action(self, event: CorporateActionEventPayload, at: datetime) -> None:
157
+ """按登记/除权/红股上市/到账阶段处理权益,重复阶段幂等。"""
158
+ action = event.action
159
+ applied = super().apply_corporate_action(
160
+ CorporateActionInstruction(
161
+ action_id=action.action_id,
162
+ instrument_id=action.instrument_id,
163
+ phase=PortfolioCorporateActionPhase(event.phase.value),
164
+ cash_per_share=action.cash_per_share,
165
+ stock_ratio=action.stock_ratio,
166
+ ),
167
+ at,
168
+ )
169
+ if applied is None:
170
+ return
171
+ if applied.receivable_delta > 0:
172
+ self._append_cash_entry(
173
+ account=CashLedgerAccount.DIVIDEND_RECEIVABLE,
174
+ entry_type=CashLedgerEntryType.DIVIDEND_RECEIVABLE,
175
+ amount=applied.receivable_delta,
176
+ event_time=at,
177
+ source_key=f"corporate-action:{action.action_id}:receivable",
178
+ instrument_id=action.instrument_id,
179
+ )
180
+ elif applied.receivable_delta < 0:
181
+ self._append_cash_entry(
182
+ account=CashLedgerAccount.DIVIDEND_RECEIVABLE,
183
+ entry_type=CashLedgerEntryType.DIVIDEND_PAYMENT,
184
+ amount=applied.receivable_delta,
185
+ event_time=at,
186
+ source_key=f"corporate-action:{action.action_id}:receivable-paid",
187
+ instrument_id=action.instrument_id,
188
+ )
189
+ if applied.cash_delta:
190
+ self._append_cash_entry(
191
+ account=CashLedgerAccount.CASH,
192
+ entry_type=CashLedgerEntryType.DIVIDEND_PAYMENT,
193
+ amount=applied.cash_delta,
194
+ event_time=at,
195
+ source_key=f"corporate-action:{action.action_id}:cash-paid",
196
+ instrument_id=action.instrument_id,
197
+ )
198
+
199
+ def snapshot(self) -> PortfolioSnapshot:
200
+ """当前 point-in-time 只读投影(持仓按 instrument_id 稳定排序)"""
201
+ position_snapshots = tuple(
202
+ PositionSnapshot(
203
+ instrument_id=instrument_id,
204
+ quantity=position.quantity,
205
+ available_quantity=position.available_quantity,
206
+ pending_settlement_quantity=position.pending_settlement_quantity,
207
+ average_cost=position.average_cost,
208
+ last_price=position.last_price,
209
+ market_value=Decimal(position.quantity) * position.last_price,
210
+ realized_pnl=position.realized_pnl,
211
+ unrealized_pnl=position.unrealized_pnl(),
212
+ )
213
+ for instrument_id, position in sorted(self._positions.items())
214
+ )
215
+ exposure = sum((p.market_value for p in position_snapshots), Decimal("0"))
216
+ total_cash = self._cash + self._frozen_cash + self._dividend_receivable
217
+ unrealized = sum((p.unrealized_pnl for p in position_snapshots), Decimal("0"))
218
+ return PortfolioSnapshot(
219
+ account_id=self._account_id,
220
+ as_of=self._state_time,
221
+ cash=CashSnapshot(
222
+ available_cash=self._cash,
223
+ frozen_cash=self._frozen_cash,
224
+ dividend_receivable=self._dividend_receivable,
225
+ total_cash=total_cash,
226
+ ),
227
+ positions=position_snapshots,
228
+ total_value=total_cash + exposure,
229
+ realized_pnl=self._realized_pnl,
230
+ unrealized_pnl=unrealized,
231
+ exposure=exposure,
232
+ )
233
+
234
+ # ------------------------------------------------------------------
235
+ # IPortfolioReader
236
+ # ------------------------------------------------------------------
237
+
238
+ def get_cash(self) -> CashSnapshot:
239
+ return self.snapshot().cash
240
+
241
+ def get_position(self, instrument_id: str) -> PositionSnapshot | None:
242
+ canonical = validate_canonical_instrument_id(instrument_id)
243
+ position = self._positions.get(canonical)
244
+ if position is None:
245
+ return None
246
+ return next(p for p in self.snapshot().positions if p.instrument_id == canonical)
247
+
248
+ # ------------------------------------------------------------------
249
+ # IExecutionAccountView(执行期只读视图)
250
+ # ------------------------------------------------------------------
251
+
252
+ def reserve_order(
253
+ self,
254
+ order_id: str,
255
+ instrument_id: str,
256
+ side: OrderSide,
257
+ quantity: int,
258
+ cash_amount: Decimal,
259
+ event_time: datetime,
260
+ ) -> bool:
261
+ """由聚合冻结资源,并把领域事实投影到账本。"""
262
+ event = self.reserve_order_resources(
263
+ order_id, instrument_id, side, quantity, cash_amount, event_time
264
+ )
265
+ if event is None:
266
+ return False
267
+ reservation = self._order_reservations[order_id]
268
+ self._append_reservation_entry(
269
+ reservation,
270
+ ReservationLedgerAction.RESERVE,
271
+ event.cash_delta,
272
+ quantity,
273
+ event_time,
274
+ )
275
+ if event.cash_delta:
276
+ self._append_reservation_cash_transfer(
277
+ order_id,
278
+ event.cash_delta,
279
+ event_time,
280
+ event.instrument_id,
281
+ release=False,
282
+ )
283
+ return True
284
+
285
+ def release_order_reservation(self, order_id: str, event_time: datetime) -> None:
286
+ reservation = self._order_reservations.get(order_id)
287
+ event = self.release_order_resources(order_id, event_time)
288
+ if event is None or reservation is None:
289
+ return
290
+ self._append_reservation_entry(
291
+ reservation,
292
+ ReservationLedgerAction.RELEASE,
293
+ event.cash_delta,
294
+ event.quantity_delta,
295
+ event_time,
296
+ )
297
+ if event.cash_delta:
298
+ self._append_reservation_cash_transfer(
299
+ order_id,
300
+ -event.cash_delta,
301
+ event_time,
302
+ reservation.instrument_id,
303
+ release=True,
304
+ )
305
+
306
+ def _append_reservation_entry(
307
+ self,
308
+ reservation: OrderReservation,
309
+ action: ReservationLedgerAction,
310
+ cash_delta: Decimal,
311
+ quantity_delta: int,
312
+ event_time: datetime,
313
+ ) -> None:
314
+ self._reservation_ledger.append(
315
+ OrderReservationLedgerEntry(
316
+ entry_id=f"reservation:{len(self._reservation_ledger)}",
317
+ order_id=reservation.order_id,
318
+ instrument_id=reservation.instrument_id,
319
+ side=reservation.side.value,
320
+ action=action,
321
+ cash_delta=cash_delta,
322
+ quantity_delta=quantity_delta,
323
+ event_time=event_time,
324
+ )
325
+ )
326
+
327
+ def _append_reservation_cash_transfer(
328
+ self,
329
+ order_id: str,
330
+ amount: Decimal,
331
+ event_time: datetime,
332
+ instrument_id: str,
333
+ *,
334
+ release: bool,
335
+ ) -> None:
336
+ entry_type = (
337
+ CashLedgerEntryType.ORDER_RELEASE if release else CashLedgerEntryType.ORDER_RESERVE
338
+ )
339
+ direction = Decimal("1") if release else Decimal("-1")
340
+ suffix = "release" if release else "reserve"
341
+ self._append_cash_entry(
342
+ account=CashLedgerAccount.CASH,
343
+ entry_type=entry_type,
344
+ amount=direction * amount,
345
+ event_time=event_time,
346
+ source_key=f"order:{order_id}:{suffix}:cash",
347
+ instrument_id=instrument_id,
348
+ )
349
+ self._append_cash_entry(
350
+ account=CashLedgerAccount.FROZEN_CASH,
351
+ entry_type=entry_type,
352
+ amount=-direction * amount,
353
+ event_time=event_time,
354
+ source_key=f"order:{order_id}:{suffix}:frozen",
355
+ instrument_id=instrument_id,
356
+ )
357
+
358
+ def position_lots(self) -> tuple[PositionLotSnapshot, ...]:
359
+ """返回包含已耗尽批次的稳定持仓批次投影。"""
360
+ return tuple(
361
+ PositionLotSnapshot(
362
+ lot_id=lot.lot_id,
363
+ instrument_id=lot.instrument_id,
364
+ source_id=lot.source_id,
365
+ acquired_at=lot.acquired_at,
366
+ available_on=lot.available_on,
367
+ original_quantity=lot.original_quantity,
368
+ remaining_quantity=lot.remaining_quantity,
369
+ available_quantity=lot.available_quantity,
370
+ unit_cost=lot.unit_cost,
371
+ )
372
+ for lot in self._position_lots
373
+ )
374
+
375
+ def cash_ledger_entries(self) -> tuple[CashLedgerEntry, ...]:
376
+ return tuple(self._cash_ledger)
377
+
378
+ def closed_trades(self) -> tuple[ClosedTrade, ...]:
379
+ return tuple(self._closed_trades)
380
+
381
+ def reservation_ledger_entries(self) -> tuple[OrderReservationLedgerEntry, ...]:
382
+ return tuple(self._reservation_ledger)
383
+
384
+ def _append_trade_cash_entries(self, fill: Fill, gross: Decimal) -> None:
385
+ direction = Decimal("-1") if fill.side is OrderSide.BUY else Decimal("1")
386
+ self._append_cash_entry(
387
+ account=CashLedgerAccount.CASH,
388
+ entry_type=CashLedgerEntryType.TRADE_GROSS,
389
+ amount=direction * gross,
390
+ event_time=fill.fill_time,
391
+ source_key=f"fill:{fill.fill_id}:gross",
392
+ instrument_id=fill.instrument_id,
393
+ )
394
+ self._append_cash_entry(
395
+ account=CashLedgerAccount.CASH,
396
+ entry_type=CashLedgerEntryType.TRADE_FEE,
397
+ amount=-fill.fees.total,
398
+ event_time=fill.fill_time,
399
+ source_key=f"fill:{fill.fill_id}:fee",
400
+ instrument_id=fill.instrument_id,
401
+ )
402
+
403
+ def _append_cash_entry(
404
+ self,
405
+ *,
406
+ account: CashLedgerAccount,
407
+ entry_type: CashLedgerEntryType,
408
+ amount: Decimal,
409
+ event_time: datetime,
410
+ source_key: str,
411
+ instrument_id: str | None = None,
412
+ ) -> None:
413
+ if source_key in self._cash_source_keys:
414
+ raise ValueError(f"重复现金账来源:{source_key}")
415
+ self._cash_ledger.append(
416
+ CashLedgerEntry(
417
+ entry_id=f"cash:{source_key}",
418
+ account_id=self._account_id,
419
+ account=account,
420
+ entry_type=entry_type,
421
+ amount=amount,
422
+ event_time=event_time,
423
+ source_key=source_key,
424
+ instrument_id=instrument_id,
425
+ )
426
+ )
427
+ self._cash_source_keys.add(source_key)
@@ -0,0 +1,57 @@
1
+ # Research SPEC
2
+
3
+ ## 1. Domain Purpose
4
+
5
+ `research` 负责把交易想法转化为可复现、可比较、可追溯的历史证据,为 Strategy Candidate 的形成和调整提供依据。
6
+
7
+ ## 2. Responsibilities
8
+
9
+ - 将 Trading Idea 结构化为可验证的 TradingHypothesis。
10
+ - 定义并运行 Historical Experiment。
11
+ - 研究因子、形态、事件和市场环境。
12
+ - 统计样本结果、稳健性、局限与反例。
13
+ - 比较假设、参数和 Strategy Candidate。
14
+ - 产出结构化 ResearchEvidence。
15
+
16
+ ## 3. Non-Responsibilities
17
+
18
+ - 不定义最终可执行 Strategy Contract。
19
+ - 不运行完整账户级历史回测。
20
+ - 不撮合订单或维护 Portfolio。
21
+ - 不参与实时事件循环。
22
+ - 不直接调用通知渠道。
23
+
24
+ ## 4. Main Concepts
25
+
26
+ ```text
27
+ Trading Idea
28
+ ↓
29
+ TradingHypothesis
30
+ ↓
31
+ ResearchExperiment
32
+ ↓
33
+ ResearchEvidence
34
+ ↓
35
+ Strategy Candidate
36
+ ```
37
+
38
+ - `TradingHypothesis`:观察条件、预期结果、观察窗口、股票范围与关键假设。
39
+ - `ResearchExperiment`:数据范围、参数、样本定义、分组和可复现配置。
40
+ - `ResearchEvidence`:样本量、收益分布、胜率、MFE/MAE、稳定性、局限和数据来源。
41
+ - `Comparison`:跨参数、时期、股票范围或市场环境比较实验结果。
42
+
43
+ ## 5. Research 与 Backtest
44
+
45
+ ```text
46
+ Research 研究“这个现象是否存在、证据是否稳健”
47
+ Backtest 验证“完整策略考虑时间、成交、费用和资金后是否可交易”
48
+ ```
49
+
50
+ Research 可以比较 Strategy Candidate,但账户级执行对象是 Backtest 中的可执行 StrategyDefinition。
51
+
52
+ ## 6. Cross-Domain Dependencies
53
+
54
+ - 通过 Market 的公开查询契约读取 point-in-time 正确的历史数据。
55
+ - 可以读取 Strategy Candidate 定义与 BacktestResult 做比较,但不得复制其逻辑。
56
+ - Notification 可以消费 ResearchEvidence 或研究报告,本域不得直接调用渠道 SDK。
57
+ - Agent/LLM 是研究执行能力之一,必须输出结构化、可追溯结果,不能成为自由文本旁路。
File without changes
File without changes