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,511 @@
1
+ """Portfolio 聚合根:拥有账户可变状态和内部实体集合。"""
2
+
3
+ from collections.abc import Mapping
4
+ from datetime import date, datetime
5
+ from decimal import ROUND_FLOOR, Decimal
6
+
7
+ from quant_core.app.order_execution.constants import OrderSide
8
+ from quant_core.app.order_execution.domain.order_event import Fill
9
+ from quant_core.app.portfolio.domain.entitlement_entity import DividendEntitlement
10
+ from quant_core.app.portfolio.domain.portfolio_event import (
11
+ ClosedLot,
12
+ CorporateActionApplied,
13
+ FillApplied,
14
+ ReservationAction,
15
+ ReservationChanged,
16
+ )
17
+ from quant_core.app.portfolio.domain.portfolio_value import (
18
+ CorporateActionInstruction,
19
+ CorporateActionPhase,
20
+ )
21
+ from quant_core.app.portfolio.domain.position_entity import Position, PositionLot
22
+ from quant_core.app.portfolio.domain.reservation_entity import OrderReservation
23
+
24
+
25
+ def finite_decimal(value: object, field: str) -> Decimal:
26
+ """领域金额边界:禁止 float 和非有限 Decimal。"""
27
+ if isinstance(value, float):
28
+ raise ValueError(f"{field} 必须使用 Decimal,禁止 float(避免二进制浮点误差)")
29
+ if not isinstance(value, (Decimal, int, str)):
30
+ raise ValueError(f"{field} 必须是 Decimal、整数或数字字符串")
31
+ converted = value if isinstance(value, Decimal) else Decimal(value)
32
+ if not converted.is_finite():
33
+ raise ValueError(f"{field} 必须是有限 Decimal,禁止 NaN / Infinity")
34
+ return converted
35
+
36
+
37
+ class Portfolio:
38
+ """资金、持仓、冻结、结算与公司行动状态的唯一聚合根。"""
39
+
40
+ def __init__(
41
+ self,
42
+ account_id: str,
43
+ initial_cash: Decimal,
44
+ initial_market_time: datetime,
45
+ ) -> None:
46
+ if (
47
+ not account_id
48
+ or account_id.strip() != account_id
49
+ or any(char.isspace() for char in account_id)
50
+ ):
51
+ raise ValueError("account_id 不允许为空或包含空白字符")
52
+ if initial_market_time.tzinfo is None:
53
+ raise ValueError("initial_market_time 必须 timezone-aware")
54
+ cash = finite_decimal(initial_cash, "initial_cash")
55
+ if cash < 0:
56
+ raise ValueError(f"initial_cash 必须 >= 0,实际 {cash}")
57
+ self._account_id = account_id
58
+ self._cash = cash
59
+ self._frozen_cash = Decimal("0")
60
+ self._dividend_receivable = Decimal("0")
61
+ self._positions: dict[str, Position] = {}
62
+ self._realized_pnl = Decimal("0")
63
+ self._applied_fill_ids: set[str] = set()
64
+ self._position_lots: list[PositionLot] = []
65
+ self._order_reservations: dict[str, OrderReservation] = {}
66
+ self._dividend_entitlements: list[DividendEntitlement] = []
67
+ self._state_time = initial_market_time
68
+ self._last_settled_day: date | None = None
69
+ self._corporate_action_entitlements: dict[str, int] = {}
70
+ self._corporate_action_receivables: dict[str, Decimal] = {}
71
+ self._corporate_action_bonuses: dict[str, int] = {}
72
+ self._corporate_action_bonus_lots: dict[str, tuple[str, ...]] = {}
73
+ self._locked_bonus_by_instrument: dict[str, int] = {}
74
+ self._applied_corporate_action_phases: set[tuple[str, str]] = set()
75
+
76
+ def apply_fill(self, fill: Fill) -> FillApplied:
77
+ """原子应用成交到现金、持仓、批次、冻结与股息税状态。"""
78
+ if fill.fill_id in self._applied_fill_ids:
79
+ raise ValueError(f"重复 Fill 被拒绝:fill_id {fill.fill_id} 已成功应用过一次")
80
+ self._check_event_time(fill.fill_time, "fill_time")
81
+ fee_total = finite_decimal(fill.fees.total, "fees.total")
82
+ gross = finite_decimal(fill.price, "price") * Decimal(fill.quantity)
83
+ reservation = self._order_reservations.get(fill.order_id)
84
+ if reservation is not None:
85
+ reservation.ensure_matches(fill.instrument_id, fill.side)
86
+ reservation_cash_delta = Decimal("0")
87
+ reservation_quantity_delta = 0
88
+
89
+ if fill.side is OrderSide.BUY:
90
+ cost = gross + fee_total
91
+ released_cash = (
92
+ (
93
+ reservation.remaining_cash
94
+ * Decimal(fill.quantity)
95
+ / Decimal(reservation.remaining_quantity)
96
+ )
97
+ if reservation is not None
98
+ else Decimal(0)
99
+ )
100
+ spendable = self._cash + released_cash
101
+ if cost > spendable:
102
+ raise ValueError(f"可用资金不足以买入:需要 {cost},可用 {spendable}")
103
+ if reservation is None:
104
+ self._cash -= cost
105
+ else:
106
+ reservation.consume(fill.quantity, released_cash)
107
+ self._frozen_cash -= released_cash
108
+ self._cash += released_cash - cost
109
+ reservation_cash_delta = -released_cash
110
+ reservation_quantity_delta = -fill.quantity
111
+ unit_cost = cost / Decimal(fill.quantity)
112
+ self._position_lots.append(
113
+ PositionLot(
114
+ lot_id=f"lot:{fill.fill_id}",
115
+ instrument_id=fill.instrument_id,
116
+ source_id=fill.fill_id,
117
+ acquired_at=fill.fill_time,
118
+ quantity=fill.quantity,
119
+ unit_cost=unit_cost,
120
+ )
121
+ )
122
+ position = self._positions.get(fill.instrument_id)
123
+ if position is None:
124
+ self._positions[fill.instrument_id] = Position(fill.quantity, fill.price, fee_total)
125
+ else:
126
+ position.apply_buy(fill.quantity, fill.price, cost)
127
+ closed_lots: tuple[ClosedLot, ...] = ()
128
+ dividend_tax = Decimal("0")
129
+ else:
130
+ position = self._positions.get(fill.instrument_id)
131
+ if position is None:
132
+ raise ValueError(f"卖出被拒绝:{fill.instrument_id} 无持仓")
133
+ if fill.quantity > position.available_quantity:
134
+ raise ValueError(
135
+ f"卖出被拒绝:数量 {fill.quantity} 超过可卖 "
136
+ f"{position.available_quantity}(T+1)"
137
+ )
138
+ cash_after = self._cash + gross - fee_total
139
+ if cash_after < 0:
140
+ raise ValueError(f"卖出被拒绝:记账后现金 {cash_after} 为负")
141
+ planned = self._plan_closed_lots(fill, gross, fee_total)
142
+ if reservation is not None and fill.quantity > reservation.remaining_quantity:
143
+ raise ValueError("卖出 Fill 超过订单冻结证券数量")
144
+ dividend_tax, consumptions = self._plan_dividend_tax(planned)
145
+ cash_after -= dividend_tax
146
+ if cash_after < 0:
147
+ raise ValueError(f"卖出及股息税记账后现金 {cash_after} 为负")
148
+ self._cash = cash_after
149
+ if reservation is not None:
150
+ reservation.consume(fill.quantity)
151
+ reservation_quantity_delta = -fill.quantity
152
+ realized = sum((trade.realized_pnl for trade in planned), Decimal("0"))
153
+ position.apply_sell(fill.quantity, fill.price, realized)
154
+ self._realized_pnl += realized
155
+ for trade in planned:
156
+ lot = self._lot_by_id(trade.position_lot_id)
157
+ lot.close(trade.quantity)
158
+ for entitlement, quantity in consumptions:
159
+ entitlement.consume(quantity)
160
+ if position.quantity == 0:
161
+ del self._positions[fill.instrument_id]
162
+ closed_lots = tuple(planned)
163
+
164
+ self._state_time = fill.fill_time
165
+ self._applied_fill_ids.add(fill.fill_id)
166
+ return FillApplied(
167
+ gross=gross,
168
+ closed_lots=closed_lots,
169
+ dividend_tax=dividend_tax,
170
+ reservation_cash_delta=reservation_cash_delta,
171
+ reservation_quantity_delta=reservation_quantity_delta,
172
+ )
173
+
174
+ def apply_corporate_action(
175
+ self, instruction: CorporateActionInstruction, at: datetime
176
+ ) -> CorporateActionApplied | None:
177
+ """按登记、除权、红股上市和派息阶段推进公司行动状态机。"""
178
+ self._check_event_time(at, "corporate_action_time")
179
+ key = (instruction.action_id, instruction.phase.value)
180
+ if key in self._applied_corporate_action_phases:
181
+ return None
182
+ action_id = instruction.action_id
183
+ instrument_id = instruction.instrument_id
184
+ position = self._positions.get(instrument_id)
185
+ receivable_delta = Decimal("0")
186
+ cash_delta = Decimal("0")
187
+
188
+ if instruction.phase is CorporateActionPhase.RECORD:
189
+ self._corporate_action_entitlements[action_id] = (
190
+ 0 if position is None else position.quantity
191
+ )
192
+ for lot in self._open_lots(instrument_id):
193
+ self._dividend_entitlements.append(
194
+ DividendEntitlement(
195
+ action_id,
196
+ lot.lot_id,
197
+ lot.remaining_quantity,
198
+ instruction.cash_per_share,
199
+ )
200
+ )
201
+ else:
202
+ if action_id not in self._corporate_action_entitlements:
203
+ raise ValueError(f"公司行动 {action_id} 缺少股权登记阶段,禁止推断权益")
204
+ entitled = self._corporate_action_entitlements[action_id]
205
+ if instruction.phase is CorporateActionPhase.EX_DATE:
206
+ receivable = Decimal(entitled) * instruction.cash_per_share
207
+ bonus_decimal = Decimal(entitled) * instruction.stock_ratio
208
+ bonus = int(bonus_decimal)
209
+ if Decimal(bonus) != bonus_decimal:
210
+ raise ValueError(
211
+ f"公司行动 {action_id} 产生非整数送转股 {bonus_decimal},MVP 禁止静默取整"
212
+ )
213
+ if bonus and position is None:
214
+ raise ValueError("登记日有权益但除权日持仓缺失,状态矛盾")
215
+ if instruction.cash_per_share:
216
+ self._dividend_receivable += receivable
217
+ self._corporate_action_receivables[action_id] = receivable
218
+ receivable_delta = receivable
219
+ if bonus:
220
+ assert position is not None
221
+ entitlements = [
222
+ item for item in self._dividend_entitlements if item.action_id == action_id
223
+ ]
224
+ exact = [
225
+ Decimal(item.remaining_quantity) * instruction.stock_ratio
226
+ for item in entitlements
227
+ ]
228
+ allocations = [int(v.to_integral_value(rounding=ROUND_FLOOR)) for v in exact]
229
+ remainder = bonus - sum(allocations)
230
+ order = sorted(
231
+ range(len(entitlements)),
232
+ key=lambda index: (exact[index] - Decimal(allocations[index]), -index),
233
+ reverse=True,
234
+ )
235
+ for index in order[:remainder]:
236
+ allocations[index] += 1
237
+ bonus_lot_ids: list[str] = []
238
+ for entitlement, allocated in zip(entitlements, allocations, strict=True):
239
+ if not allocated:
240
+ continue
241
+ original = self._lot_by_id(entitlement.lot_id)
242
+ adjusted_cost = original.apply_bonus_cost(allocated)
243
+ lot_id = f"lot:{action_id}:bonus:{original.lot_id}"
244
+ self._position_lots.append(
245
+ PositionLot(
246
+ lot_id=lot_id,
247
+ instrument_id=instrument_id,
248
+ source_id=action_id,
249
+ acquired_at=original.acquired_at,
250
+ quantity=allocated,
251
+ unit_cost=adjusted_cost,
252
+ )
253
+ )
254
+ bonus_lot_ids.append(lot_id)
255
+ position.apply_bonus(bonus)
256
+ self._corporate_action_bonus_lots[action_id] = tuple(bonus_lot_ids)
257
+ if instruction.stock_ratio:
258
+ self._corporate_action_bonuses[action_id] = bonus
259
+ self._locked_bonus_by_instrument[instrument_id] = (
260
+ self._locked_bonus_by_instrument.get(instrument_id, 0) + bonus
261
+ )
262
+ elif instruction.phase is CorporateActionPhase.LISTING_DATE:
263
+ bonus = self._corporate_action_bonuses.pop(action_id, None)
264
+ if bonus is None:
265
+ raise ValueError(f"公司行动 {action_id} 缺少除权阶段送转股")
266
+ if bonus:
267
+ if position is None:
268
+ raise ValueError("红股上市日持仓缺失,状态矛盾")
269
+ locked = self._locked_bonus_by_instrument.get(instrument_id, 0)
270
+ if locked < bonus or position.pending_settlement_quantity < bonus:
271
+ raise ValueError("红股锁定数量与持仓待结算数量矛盾")
272
+ position.release_bonus(bonus)
273
+ for lot_id in self._corporate_action_bonus_lots.pop(action_id, ()):
274
+ lot = self._lot_by_id(lot_id)
275
+ lot.release(at.date())
276
+ remaining = locked - bonus
277
+ if remaining:
278
+ self._locked_bonus_by_instrument[instrument_id] = remaining
279
+ else:
280
+ self._locked_bonus_by_instrument.pop(instrument_id, None)
281
+ elif instruction.phase is CorporateActionPhase.PAY_DATE:
282
+ receivable = self._corporate_action_receivables.pop(action_id, None)
283
+ if receivable is None:
284
+ raise ValueError(f"公司行动 {action_id} 缺少除权阶段应收股息")
285
+ self._dividend_receivable -= receivable
286
+ self._cash += receivable
287
+ receivable_delta = -receivable
288
+ cash_delta = receivable
289
+ self._state_time = at
290
+ self._applied_corporate_action_phases.add(key)
291
+ return CorporateActionApplied(
292
+ action_id, instrument_id, instruction.phase.value, receivable_delta, cash_delta
293
+ )
294
+
295
+ def mark_to_market(self, prices: Mapping[str, Decimal], at: datetime) -> None:
296
+ """原子校验估值输入后,只更新当前持仓并推进组合时间。"""
297
+ self._check_event_time(at, "at")
298
+ normalized: dict[str, Decimal] = {}
299
+ for instrument_id, price in prices.items():
300
+ canonical = _canonical_instrument_id(instrument_id)
301
+ checked = finite_decimal(price, f"price[{instrument_id}]")
302
+ if checked <= 0:
303
+ raise ValueError(f"估值价格必须 > 0,实际 {checked}")
304
+ normalized[canonical] = checked
305
+ for canonical, price in normalized.items():
306
+ position = self._positions.get(canonical)
307
+ if position is not None:
308
+ position.mark(price)
309
+ self._state_time = at
310
+
311
+ def settle_trading_day(self, trading_day: date) -> None:
312
+ """释放普通 T+1 持仓;红股继续锁定到上市阶段。"""
313
+ if self._last_settled_day is not None and trading_day < self._last_settled_day:
314
+ raise ValueError(f"交易日不允许回退:{trading_day} < 已结算 {self._last_settled_day}")
315
+ if self._last_settled_day == trading_day:
316
+ return
317
+ for instrument_id, position in self._positions.items():
318
+ position.settle(self._locked_bonus_by_instrument.get(instrument_id, 0))
319
+ locked_bonus_lot_ids = {
320
+ lot_id for lot_ids in self._corporate_action_bonus_lots.values() for lot_id in lot_ids
321
+ }
322
+ for lot in self._position_lots:
323
+ if (
324
+ lot.remaining_quantity
325
+ and lot.available_quantity < lot.remaining_quantity
326
+ and lot.lot_id not in locked_bonus_lot_ids
327
+ ):
328
+ lot.release(trading_day)
329
+ self._last_settled_day = trading_day
330
+
331
+ def available_cash(self) -> Decimal:
332
+ return self._cash
333
+
334
+ def available_quantity(self, instrument_id: str) -> int:
335
+ canonical = _canonical_instrument_id(instrument_id)
336
+ position = self._positions.get(canonical)
337
+ if position is None:
338
+ return 0
339
+ frozen = sum(
340
+ reservation.remaining_quantity
341
+ for reservation in self._order_reservations.values()
342
+ if reservation.instrument_id == canonical and reservation.side is OrderSide.SELL
343
+ )
344
+ return position.available_quantity - frozen
345
+
346
+ def reserved_cash(self, order_id: str) -> Decimal:
347
+ reservation = self._order_reservations.get(order_id)
348
+ return Decimal("0") if reservation is None else reservation.remaining_cash
349
+
350
+ def reserved_quantity(self, order_id: str) -> int:
351
+ reservation = self._order_reservations.get(order_id)
352
+ return 0 if reservation is None else reservation.remaining_quantity
353
+
354
+ def reserve_order_resources(
355
+ self,
356
+ order_id: str,
357
+ instrument_id: str,
358
+ side: OrderSide,
359
+ quantity: int,
360
+ cash_amount: Decimal,
361
+ event_time: datetime,
362
+ ) -> ReservationChanged | None:
363
+ """原子冻结订单资源;失败不改变聚合。"""
364
+ self._check_event_time(event_time, "reservation_time")
365
+ canonical = _canonical_instrument_id(instrument_id)
366
+ amount = finite_decimal(cash_amount, "cash_amount")
367
+ if quantity <= 0 or order_id in self._order_reservations:
368
+ return None
369
+ if side is OrderSide.BUY:
370
+ if amount <= 0 or amount > self._cash:
371
+ return None
372
+ elif side is OrderSide.SELL:
373
+ if amount != 0 or quantity > self.available_quantity(canonical):
374
+ return None
375
+ else:
376
+ raise ValueError("side 必须是 OrderSide")
377
+ reservation = OrderReservation(
378
+ order_id=order_id,
379
+ instrument_id=canonical,
380
+ side=side,
381
+ remaining_quantity=quantity,
382
+ remaining_cash=amount,
383
+ )
384
+ self._order_reservations[order_id] = reservation
385
+ if amount:
386
+ self._cash -= amount
387
+ self._frozen_cash += amount
388
+ return ReservationChanged(
389
+ order_id,
390
+ canonical,
391
+ side,
392
+ ReservationAction.RESERVE,
393
+ amount,
394
+ quantity,
395
+ event_time,
396
+ )
397
+
398
+ def release_order_resources(
399
+ self, order_id: str, event_time: datetime
400
+ ) -> ReservationChanged | None:
401
+ """释放订单剩余冻结资源;不存在时幂等返回 None。"""
402
+ self._check_event_time(event_time, "reservation_release_time")
403
+ reservation = self._order_reservations.pop(order_id, None)
404
+ if reservation is None:
405
+ return None
406
+ cash = reservation.remaining_cash
407
+ quantity = reservation.remaining_quantity
408
+ if cash:
409
+ self._frozen_cash -= cash
410
+ self._cash += cash
411
+ return ReservationChanged(
412
+ reservation.order_id,
413
+ reservation.instrument_id,
414
+ reservation.side,
415
+ ReservationAction.RELEASE,
416
+ -cash,
417
+ -quantity,
418
+ event_time,
419
+ )
420
+
421
+ def _open_lots(self, instrument_id: str) -> list[PositionLot]:
422
+ return [
423
+ lot
424
+ for lot in self._position_lots
425
+ if lot.instrument_id == instrument_id and lot.remaining_quantity > 0
426
+ ]
427
+
428
+ def _lot_by_id(self, lot_id: str) -> PositionLot:
429
+ return next(lot for lot in self._position_lots if lot.lot_id == lot_id)
430
+
431
+ def _plan_closed_lots(self, fill: Fill, gross: Decimal, fee_total: Decimal) -> list[ClosedLot]:
432
+ remaining = fill.quantity
433
+ planned: list[ClosedLot] = []
434
+ for lot in self._open_lots(fill.instrument_id):
435
+ if remaining == 0:
436
+ break
437
+ quantity = min(remaining, lot.available_quantity)
438
+ if quantity == 0:
439
+ continue
440
+ ratio = Decimal(quantity) / Decimal(fill.quantity)
441
+ proceeds = gross * ratio
442
+ sell_fees = fee_total * ratio
443
+ cost_basis = lot.unit_cost * Decimal(quantity)
444
+ planned.append(
445
+ ClosedLot(
446
+ trade_id=f"trade:{fill.fill_id}:{lot.lot_id}",
447
+ instrument_id=fill.instrument_id,
448
+ position_lot_id=lot.lot_id,
449
+ buy_source_id=lot.source_id,
450
+ sell_fill_id=fill.fill_id,
451
+ quantity=quantity,
452
+ opened_at=lot.acquired_at,
453
+ closed_at=fill.fill_time,
454
+ cost_basis=cost_basis,
455
+ proceeds=proceeds,
456
+ sell_fees=sell_fees,
457
+ realized_pnl=proceeds - sell_fees - cost_basis,
458
+ holding_days=(fill.fill_time.date() - lot.acquired_at.date()).days,
459
+ )
460
+ )
461
+ remaining -= quantity
462
+ if remaining:
463
+ raise ValueError(f"可用 PositionLot 不足:尚缺 {remaining}")
464
+ return planned
465
+
466
+ def _plan_dividend_tax(
467
+ self, closed_lots: list[ClosedLot]
468
+ ) -> tuple[Decimal, list[tuple[DividendEntitlement, int]]]:
469
+ tax = Decimal("0")
470
+ consumptions: list[tuple[DividendEntitlement, int]] = []
471
+ planned_by_entitlement: dict[int, int] = {}
472
+ for trade in closed_lots:
473
+ for entitlement in self._dividend_entitlements:
474
+ if entitlement.lot_id != trade.position_lot_id:
475
+ continue
476
+ already_planned = planned_by_entitlement.get(id(entitlement), 0)
477
+ quantity = min(
478
+ entitlement.remaining_quantity - already_planned,
479
+ trade.quantity,
480
+ )
481
+ if quantity == 0:
482
+ continue
483
+ rate = (
484
+ Decimal("0.20")
485
+ if trade.holding_days <= 30
486
+ else Decimal("0.10")
487
+ if trade.holding_days <= 365
488
+ else Decimal("0")
489
+ )
490
+ tax += entitlement.gross_cash_for(quantity) * rate
491
+ planned_by_entitlement[id(entitlement)] = already_planned + quantity
492
+ consumptions.append((entitlement, quantity))
493
+ return tax, consumptions
494
+
495
+ def _check_event_time(self, event_time: datetime, field: str) -> None:
496
+ if event_time.tzinfo is None:
497
+ raise ValueError(f"{field} 必须 timezone-aware")
498
+ if event_time < self._state_time:
499
+ raise ValueError(f"{field} {event_time} 不允许早于当前状态时间 {self._state_time}")
500
+
501
+
502
+ def _canonical_instrument_id(value: str) -> str:
503
+ parts = value.split(".")
504
+ if (
505
+ len(parts) != 3
506
+ or any(not part for part in parts)
507
+ or parts[0] != parts[0].upper()
508
+ or parts[1] != parts[1].upper()
509
+ ):
510
+ raise ValueError(f"非法 Canonical instrument_id:{value!r}")
511
+ return value
@@ -0,0 +1,59 @@
1
+ """Portfolio 聚合产生的不可变领域事实。"""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from decimal import Decimal
6
+ from enum import Enum
7
+
8
+ from quant_core.app.order_execution.constants import OrderSide
9
+
10
+
11
+ class ReservationAction(str, Enum):
12
+ RESERVE = "reserve"
13
+ RELEASE = "release"
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ReservationChanged:
18
+ order_id: str
19
+ instrument_id: str
20
+ side: OrderSide
21
+ action: ReservationAction
22
+ cash_delta: Decimal
23
+ quantity_delta: int
24
+ event_time: datetime
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class ClosedLot:
29
+ trade_id: str
30
+ instrument_id: str
31
+ position_lot_id: str
32
+ buy_source_id: str
33
+ sell_fill_id: str
34
+ quantity: int
35
+ opened_at: datetime
36
+ closed_at: datetime
37
+ cost_basis: Decimal
38
+ proceeds: Decimal
39
+ sell_fees: Decimal
40
+ realized_pnl: Decimal
41
+ holding_days: int
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class FillApplied:
46
+ gross: Decimal
47
+ closed_lots: tuple[ClosedLot, ...] = ()
48
+ dividend_tax: Decimal = Decimal("0")
49
+ reservation_cash_delta: Decimal = Decimal("0")
50
+ reservation_quantity_delta: int = 0
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class CorporateActionApplied:
55
+ action_id: str
56
+ instrument_id: str
57
+ phase: str
58
+ receivable_delta: Decimal = Decimal("0")
59
+ cash_delta: Decimal = Decimal("0")
@@ -0,0 +1,21 @@
1
+ """Portfolio 领域的不可变输入值。"""
2
+
3
+ from dataclasses import dataclass
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+
7
+
8
+ class CorporateActionPhase(str, Enum):
9
+ RECORD = "record"
10
+ EX_DATE = "ex_date"
11
+ LISTING_DATE = "listing_date"
12
+ PAY_DATE = "pay_date"
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class CorporateActionInstruction:
17
+ action_id: str
18
+ instrument_id: str
19
+ phase: CorporateActionPhase
20
+ cash_per_share: Decimal
21
+ stock_ratio: Decimal