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
pqdata/client.py ADDED
@@ -0,0 +1,389 @@
1
+ """有界、强类型行情客户端;不引入 ORM、供应商 SDK 或应用配置。"""
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncIterator
5
+ from datetime import date, datetime
6
+ from hashlib import sha256
7
+ from types import TracebackType
8
+ from typing import Any, Self
9
+ from urllib.parse import quote
10
+
11
+ import httpx
12
+ from pydantic import TypeAdapter, ValidationError
13
+
14
+ from pqdata.exceptions import MarketAPIError, MarketProtocolError
15
+ from pqdata.schema.adjustment_schema import AdjustedBarSeries, BarViewRequest
16
+ from pqdata.schema.bar_content_schema import BarContent
17
+ from pqdata.schema.bar_schema import BarFrequency, BarPage, BarQuery, BarResponse
18
+ from pqdata.schema.history_schema import HistoryFacts, HistoryFactsQuery
19
+ from pqdata.schema.pit_daily_window import PITDailyWindow
20
+ from pqdata.schema.query_schema import (
21
+ BarWindowQuery,
22
+ DataHealthQuery,
23
+ InstrumentRangeQuery,
24
+ PITWindowQuery,
25
+ )
26
+ from pqdata.schema.quote_schema import DailyBasicResponse
27
+ from pqdata.schema.reference_schema import (
28
+ InstrumentResponse,
29
+ TradingCalendarDayResponse,
30
+ TradingRuleResponse,
31
+ )
32
+ from pqdata.schema.reliability_schema import DatasetHealthResult
33
+
34
+
35
+ class MarketClient:
36
+ """持有一个连接池;通过 async with 或 aclose 显式释放连接。"""
37
+
38
+ def __init__(
39
+ self,
40
+ base_url: str,
41
+ *,
42
+ token: str | None = None,
43
+ timeout: float = 60.0,
44
+ max_retries: int = 2,
45
+ transport: httpx.AsyncBaseTransport | None = None,
46
+ ) -> None:
47
+ if not 0 <= max_retries <= 5:
48
+ raise ValueError("max_retries must be between 0 and 5")
49
+ self._max_retries = max_retries
50
+ self._http = httpx.AsyncClient(
51
+ base_url=base_url.rstrip("/") + "/",
52
+ headers={"Authorization": f"Bearer {token}"} if token else {},
53
+ timeout=timeout,
54
+ transport=transport,
55
+ follow_redirects=False,
56
+ )
57
+
58
+ async def __aenter__(self) -> Self:
59
+ return self
60
+
61
+ async def __aexit__(
62
+ self,
63
+ exc_type: type[BaseException] | None,
64
+ exc: BaseException | None,
65
+ traceback: TracebackType | None,
66
+ ) -> None:
67
+ await self.aclose()
68
+
69
+ async def aclose(self) -> None:
70
+ await self._http.aclose()
71
+
72
+ @property
73
+ def source_id(self) -> str:
74
+ """服务地址的稳定摘要,用于隔离不同数据源的本地缓存。"""
75
+ return sha256(str(self._http.base_url).encode("utf-8")).hexdigest()
76
+
77
+ async def _request(
78
+ self,
79
+ method: str,
80
+ path: str,
81
+ response_type: Any,
82
+ *,
83
+ params: dict[str, Any] | None = None,
84
+ body: Any = None,
85
+ ) -> Any:
86
+ # 所有当前端点均为只读查询;未来写入端点不能复用此重试路径。
87
+ for attempt in range(self._max_retries + 1):
88
+ try:
89
+ response = await self._http.request(method, path, params=params, json=body)
90
+ except httpx.TransportError:
91
+ if attempt == self._max_retries:
92
+ raise
93
+ await asyncio.sleep(min(0.25 * 2**attempt, 2.0))
94
+ continue
95
+ if response.status_code not in {429, 502, 503, 504} or attempt == self._max_retries:
96
+ break
97
+ delay = min(0.25 * 2**attempt, 2.0)
98
+ retry_after = response.headers.get("retry-after", "")
99
+ if retry_after.isdecimal():
100
+ delay = min(max(delay, int(retry_after)), 5.0)
101
+ await response.aclose()
102
+ await asyncio.sleep(delay)
103
+ if not response.is_success:
104
+ raise MarketAPIError(response.status_code, response.headers.get("x-request-id"))
105
+ try:
106
+ return TypeAdapter(response_type).validate_json(response.content)
107
+ except ValidationError as error:
108
+ raise MarketProtocolError("Invalid market response") from error
109
+
110
+ async def get_instrument(self, instrument_id: str) -> InstrumentResponse:
111
+ return await self._request(
112
+ "GET",
113
+ f"instruments/{quote(instrument_id, safe='')}",
114
+ InstrumentResponse,
115
+ )
116
+
117
+ async def get_history_facts(self, query: HistoryFactsQuery) -> HistoryFacts:
118
+ result = await self._request(
119
+ "POST", "history/facts", HistoryFacts, body=query.model_dump(mode="json")
120
+ )
121
+ if query.revision is not None and result.revision != query.revision:
122
+ raise MarketProtocolError("History facts revision changed")
123
+ if result.instrument.instrument_id != query.instrument_id:
124
+ raise MarketProtocolError("History facts instrument differs from query")
125
+ return result
126
+
127
+ async def get_bars(
128
+ self,
129
+ instrument_id: str,
130
+ frequency: str,
131
+ start: datetime | None = None,
132
+ end: datetime | None = None,
133
+ limit: int = 1000,
134
+ ) -> list[BarResponse]:
135
+ query = BarWindowQuery(
136
+ instrument_id=instrument_id,
137
+ frequency=TypeAdapter(BarFrequency).validate_python(frequency),
138
+ start=start,
139
+ end=end,
140
+ limit=limit,
141
+ )
142
+ return await self._request(
143
+ "POST", "bars/window", list[BarResponse], body=query.model_dump(mode="json")
144
+ )
145
+
146
+ async def get_adjusted_bars(
147
+ self,
148
+ instrument_id: str,
149
+ frequency: str,
150
+ view: BarViewRequest,
151
+ start: datetime | None = None,
152
+ end: datetime | None = None,
153
+ limit: int = 1000,
154
+ ) -> AdjustedBarSeries:
155
+ query = BarWindowQuery(
156
+ instrument_id=instrument_id,
157
+ frequency=TypeAdapter(BarFrequency).validate_python(frequency),
158
+ start=start,
159
+ end=end,
160
+ limit=limit,
161
+ view=BarViewRequest.model_validate(view.model_dump(mode="json")),
162
+ )
163
+ return await self._request(
164
+ "POST", "bars/view", AdjustedBarSeries, body=query.model_dump(mode="json")
165
+ )
166
+
167
+ async def verify_bars(
168
+ self,
169
+ instrument_id: str,
170
+ frequency: str,
171
+ start: datetime,
172
+ end: datetime,
173
+ ) -> DatasetHealthResult:
174
+ query = DataHealthQuery(
175
+ instrument_id=instrument_id,
176
+ frequency=TypeAdapter(BarFrequency).validate_python(frequency),
177
+ start=start,
178
+ end=end,
179
+ )
180
+ return await self._request(
181
+ "POST", "verify/bars", DatasetHealthResult, body=query.model_dump(mode="json")
182
+ )
183
+
184
+ async def verify_adjustment_factors(
185
+ self,
186
+ instrument_id: str,
187
+ frequency: str,
188
+ start: datetime,
189
+ end: datetime,
190
+ ) -> DatasetHealthResult:
191
+ query = DataHealthQuery(
192
+ instrument_id=instrument_id,
193
+ frequency=TypeAdapter(BarFrequency).validate_python(frequency),
194
+ start=start,
195
+ end=end,
196
+ )
197
+ return await self._request(
198
+ "POST",
199
+ "verify/adjustment-factors",
200
+ DatasetHealthResult,
201
+ body=query.model_dump(mode="json"),
202
+ )
203
+
204
+ async def verify_corporate_actions(
205
+ self,
206
+ instrument_id: str,
207
+ start: datetime,
208
+ end: datetime,
209
+ ) -> DatasetHealthResult:
210
+ query = DataHealthQuery(instrument_id=instrument_id, start=start, end=end)
211
+ return await self._request(
212
+ "POST",
213
+ "verify/corporate-actions",
214
+ DatasetHealthResult,
215
+ body=query.model_dump(mode="json"),
216
+ )
217
+
218
+ async def snapshot(self, start: date, end: date) -> dict[str, str]:
219
+ return await self._request(
220
+ "GET",
221
+ "revisions",
222
+ dict[str, str],
223
+ params={
224
+ "start": start.isoformat(),
225
+ "end": end.isoformat(),
226
+ },
227
+ )
228
+
229
+ async def search_instruments(
230
+ self,
231
+ keyword: str | None = None,
232
+ instrument_type: str | None = None,
233
+ exchange: str | None = None,
234
+ limit: int = 100,
235
+ ) -> list[InstrumentResponse]:
236
+ params = {
237
+ key: value
238
+ for key, value in {
239
+ "keyword": keyword,
240
+ "instrument_type": instrument_type,
241
+ "exchange": exchange,
242
+ "limit": limit,
243
+ }.items()
244
+ if value is not None
245
+ }
246
+ return await self._request("GET", "instruments", list[InstrumentResponse], params=params)
247
+
248
+ async def list_instruments_on(
249
+ self, on_date: date, limit: int = 10000
250
+ ) -> list[InstrumentResponse]:
251
+ return await self._request(
252
+ "GET",
253
+ "universe",
254
+ list[InstrumentResponse],
255
+ params={
256
+ "on_date": on_date.isoformat(),
257
+ "limit": limit,
258
+ },
259
+ )
260
+
261
+ async def list_trading_rules(
262
+ self,
263
+ instrument_ids: list[str],
264
+ start: date,
265
+ end: date,
266
+ ) -> list[TradingRuleResponse]:
267
+ query = InstrumentRangeQuery(instrument_ids=instrument_ids, start=start, end=end)
268
+ return await self._request(
269
+ "POST",
270
+ "trading-rules/query",
271
+ list[TradingRuleResponse],
272
+ body=query.model_dump(mode="json"),
273
+ )
274
+
275
+ async def list_trading_days(
276
+ self,
277
+ exchange: str,
278
+ start: date,
279
+ end: date,
280
+ ) -> list[TradingCalendarDayResponse]:
281
+ return await self._request(
282
+ "GET",
283
+ "calendar",
284
+ list[TradingCalendarDayResponse],
285
+ params={
286
+ "exchange": exchange,
287
+ "start": start.isoformat(),
288
+ "end": end.isoformat(),
289
+ },
290
+ )
291
+
292
+ async def is_trading_day(self, exchange: str, trade_date: date) -> bool:
293
+ return await self._request(
294
+ "GET",
295
+ "calendar/is-open",
296
+ bool,
297
+ params={
298
+ "exchange": exchange,
299
+ "trade_date": trade_date.isoformat(),
300
+ },
301
+ )
302
+
303
+ async def list_daily_basic_at(
304
+ self,
305
+ decision_time: datetime,
306
+ limit: int = 10000,
307
+ ) -> list[DailyBasicResponse]:
308
+ return await self._request(
309
+ "GET",
310
+ "daily-basic/visible",
311
+ list[DailyBasicResponse],
312
+ params={
313
+ "decision_time": decision_time.isoformat(),
314
+ "limit": limit,
315
+ },
316
+ )
317
+
318
+ async def get_pit_daily_windows(
319
+ self,
320
+ instrument_id: str,
321
+ days: list[date],
322
+ next_days: list[date],
323
+ lookback: int,
324
+ ) -> tuple[PITDailyWindow, ...]:
325
+ query = PITWindowQuery(
326
+ instrument_id=instrument_id,
327
+ days=days,
328
+ next_days=next_days,
329
+ lookback=lookback,
330
+ )
331
+ return await self._request(
332
+ "POST",
333
+ "bars/pit-windows",
334
+ tuple[PITDailyWindow, ...],
335
+ body=query.model_dump(mode="json"),
336
+ )
337
+
338
+ async def query_bars(self, query: BarQuery) -> BarPage:
339
+ """读取一页数据;错误内容不混入行情结果。"""
340
+ page = await self._request(
341
+ "POST", "bars/query", BarPage, body=query.model_dump(mode="json")
342
+ )
343
+ if query.revision is not None and page.revision != query.revision:
344
+ raise MarketProtocolError("Market revision changed during pagination")
345
+ if len(page.items) > query.page_size:
346
+ raise MarketProtocolError("Server exceeded requested page size")
347
+ previous: tuple | None = None
348
+ for bar in page.items:
349
+ key = (bar.timestamp, bar.instrument_id)
350
+ if (
351
+ bar.instrument_id not in query.instrument_ids
352
+ or bar.frequency != query.frequency
353
+ or not query.start <= bar.timestamp <= query.end
354
+ or (previous is not None and key <= previous)
355
+ ):
356
+ raise MarketProtocolError("Bar page violates query range or ordering")
357
+ previous = key
358
+ return page
359
+
360
+ async def describe_bars(self, query: BarQuery) -> BarContent:
361
+ """只传输范围摘要;服务端仍需扫描数据,不能视为常数成本的查询。"""
362
+ if query.cursor is not None:
363
+ raise ValueError("Content queries must start at the first page")
364
+ result = await self._request(
365
+ "POST", "bars/content", BarContent, body=query.model_dump(mode="json")
366
+ )
367
+ if query.revision is not None and result.revision != query.revision:
368
+ raise MarketProtocolError("Content revision differs from query")
369
+ return result
370
+
371
+ async def iter_bar_pages(self, query: BarQuery) -> AsyncIterator[BarPage]:
372
+ """逐页消费,固定 revision,检测重复游标与跨页重复数据。"""
373
+ seen: set[str] = {query.cursor} if query.cursor else set()
374
+ previous: tuple | None = None
375
+ while True:
376
+ page = await self.query_bars(query)
377
+ if previous is not None and page.items:
378
+ first = page.items[0]
379
+ if (first.timestamp, first.instrument_id) <= previous:
380
+ raise MarketProtocolError("Bar pages overlap or are out of order")
381
+ if page.next_cursor and (page.next_cursor in seen or not page.items):
382
+ raise MarketProtocolError("Pagination did not advance")
383
+ yield page
384
+ if page.next_cursor is None:
385
+ return
386
+ last = page.items[-1]
387
+ previous = (last.timestamp, last.instrument_id)
388
+ seen.add(page.next_cursor)
389
+ query = query.model_copy(update={"cursor": page.next_cursor, "revision": page.revision})
pqdata/download.py ADDED
@@ -0,0 +1,60 @@
1
+ """流式落盘;完整下载后才替换目标文件,失败保留原文件。"""
2
+
3
+ import hashlib
4
+ import os
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from pqdata.client import MarketClient
11
+ from pqdata.schema.bar_schema import BarQuery
12
+
13
+
14
+ class DownloadReceipt(BaseModel):
15
+ """供本地缓存记录查询条件、版本及内容校验和。"""
16
+
17
+ query: BarQuery
18
+ revision: str
19
+ row_count: int = Field(ge=0)
20
+ byte_count: int = Field(ge=0)
21
+ sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
22
+
23
+
24
+ async def download_bars(
25
+ client: MarketClient, query: BarQuery, destination: Path
26
+ ) -> DownloadReceipt:
27
+ """完整范围以 JSON Lines 落盘;内存上限为一页数据。"""
28
+ if query.cursor is not None:
29
+ raise ValueError("Downloads must start at the first page")
30
+ destination.parent.mkdir(parents=True, exist_ok=True)
31
+ temporary: Path | None = None
32
+ digest = hashlib.sha256()
33
+ row_count = byte_count = 0
34
+ revision = ""
35
+ try:
36
+ with tempfile.NamedTemporaryFile(
37
+ mode="wb", dir=destination.parent, prefix=".market-", suffix=".part", delete=False
38
+ ) as output:
39
+ temporary = Path(output.name)
40
+ async for page in client.iter_bar_pages(query):
41
+ revision = page.revision
42
+ for bar in page.items:
43
+ line = (bar.model_dump_json() + "\n").encode("utf-8")
44
+ output.write(line)
45
+ digest.update(line)
46
+ row_count += 1
47
+ byte_count += len(line)
48
+ output.flush()
49
+ os.fsync(output.fileno())
50
+ os.replace(temporary, destination)
51
+ finally:
52
+ if temporary is not None:
53
+ temporary.unlink(missing_ok=True)
54
+ return DownloadReceipt(
55
+ query=query,
56
+ revision=revision,
57
+ row_count=row_count,
58
+ byte_count=byte_count,
59
+ sha256=digest.hexdigest(),
60
+ )
pqdata/exceptions.py ADDED
@@ -0,0 +1,14 @@
1
+ """客户端错误不依赖服务端异常实现。"""
2
+
3
+
4
+ class MarketAPIError(Exception):
5
+ """服务拒绝请求;保留状态码和可追踪的请求 ID,不暴露凭据。"""
6
+
7
+ def __init__(self, status_code: int, request_id: str | None = None) -> None:
8
+ self.status_code = status_code
9
+ self.request_id = request_id
10
+ super().__init__(f"Market request failed (HTTP {status_code}, request_id={request_id})")
11
+
12
+
13
+ class MarketProtocolError(Exception):
14
+ """响应不符合契约或下载内容不一致,禁止继续消费。"""
@@ -0,0 +1 @@
1
+ """客户端公开数据契约。"""
@@ -0,0 +1,67 @@
1
+ """复权因子与价格视图契约。"""
2
+
3
+ from datetime import date, datetime
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from pqdata.schema.bar_schema import BarResponse
10
+
11
+
12
+ class AdjustmentType(str, Enum):
13
+ NONE = "none"
14
+ PRE = "pre"
15
+ POST = "post"
16
+
17
+
18
+ class AdjustmentAnchorMode(str, Enum):
19
+ FIXED_DATE = "fixed_date"
20
+ LATEST = "latest"
21
+ DECISION_TIME = "decision_time"
22
+
23
+
24
+ class AdjustmentFactorResponse(BaseModel):
25
+ model_config = ConfigDict(from_attributes=True)
26
+
27
+ instrument_id: str
28
+ trade_date: date
29
+ factor: Decimal = Field(gt=0)
30
+ dataset_revision: str
31
+ source_provider: str
32
+ source_updated_at: datetime | None = None
33
+ recorded_at: datetime
34
+
35
+
36
+ class AdjustedBarResponse(BarResponse):
37
+ """BarResponse 的合成视图;仅放宽 volume 为可含小数的 Decimal。"""
38
+
39
+ volume: Decimal
40
+
41
+
42
+ class BarViewRequest(BaseModel):
43
+ adjustment_type: AdjustmentType = AdjustmentType.NONE
44
+ anchor_mode: AdjustmentAnchorMode | None = None
45
+ anchor_date: date | None = None
46
+ decision_time: datetime | None = None
47
+ known_at: datetime | None = None
48
+
49
+ @model_validator(mode="after")
50
+ def validate_anchor(self) -> "BarViewRequest":
51
+ if self.adjustment_type in {AdjustmentType.NONE, AdjustmentType.POST}:
52
+ return self
53
+ if self.anchor_mode is None:
54
+ raise ValueError("复权查询必须指定 anchor_mode")
55
+ if self.anchor_mode is AdjustmentAnchorMode.FIXED_DATE and self.anchor_date is None:
56
+ raise ValueError("fixed_date 必须指定 anchor_date")
57
+ if self.anchor_mode is AdjustmentAnchorMode.DECISION_TIME and self.decision_time is None:
58
+ raise ValueError("decision_time 模式必须指定 decision_time")
59
+ return self
60
+
61
+
62
+ class AdjustedBarSeries(BaseModel):
63
+ instrument_id: str
64
+ adjustment_type: AdjustmentType
65
+ anchor_date: date | None
66
+ factor_revision: str | None
67
+ bars: list[AdjustedBarResponse | BarResponse]
@@ -0,0 +1,24 @@
1
+ """范围内容凭据;摘要包含完整行,能识别更正、补数和删除。"""
2
+
3
+ import hashlib
4
+ from collections.abc import Iterable
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from pqdata.schema.bar_schema import BarResponse
9
+
10
+
11
+ class BarContent(BaseModel):
12
+ """查询范围的内容身份,不宣称该范围已完成行情质量验收。"""
13
+
14
+ revision: str
15
+ row_count: int = Field(ge=0)
16
+ sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
17
+
18
+
19
+ def bar_content_digest(rows: Iterable[BarResponse]) -> str:
20
+ """以分页规范的行顺序计算摘要,避免浮点转换损失价格精度。"""
21
+ digest = hashlib.sha256()
22
+ for row in rows:
23
+ digest.update((row.model_dump_json() + "\n").encode("utf-8"))
24
+ return digest.hexdigest()
@@ -0,0 +1,56 @@
1
+ """标准行情与有界批量读取契约;时间戳表示 Bar 结束时间。"""
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Literal
6
+
7
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
8
+
9
+ BarFrequency = Literal["1m", "5m", "15m", "30m", "1h", "1d", "1w"]
10
+
11
+
12
+ class BarResponse(BaseModel):
13
+ model_config = ConfigDict(from_attributes=True, frozen=True)
14
+
15
+ instrument_id: str
16
+ timestamp: AwareDatetime
17
+ frequency: BarFrequency
18
+ open: Decimal
19
+ high: Decimal
20
+ low: Decimal
21
+ close: Decimal
22
+ volume: int = Field(ge=0)
23
+ amount: Decimal | None = None
24
+ previous_close: Decimal | None = None
25
+ suspended: bool
26
+
27
+
28
+ class BarQuery(BaseModel):
29
+ """批量范围与游标;每页按 timestamp、instrument_id 稳定排序。"""
30
+
31
+ model_config = ConfigDict(frozen=True, extra="forbid")
32
+
33
+ instrument_ids: list[str] = Field(min_length=1, max_length=1000)
34
+ frequency: BarFrequency
35
+ start: AwareDatetime
36
+ end: AwareDatetime
37
+ page_size: int = Field(default=10000, ge=1, le=100000)
38
+ cursor: str | None = Field(default=None, max_length=8192)
39
+ revision: str | None = None
40
+
41
+ @model_validator(mode="after")
42
+ def validate_range(self) -> "BarQuery":
43
+ if self.start > self.end:
44
+ raise ValueError("start must not be after end")
45
+ if len(set(self.instrument_ids)) != len(self.instrument_ids):
46
+ raise ValueError("instrument_ids must be unique")
47
+ return self
48
+
49
+
50
+ class BarPage(BaseModel):
51
+ """revision 在分页期间必须稳定;内容被修订时服务返回冲突。"""
52
+
53
+ items: list[BarResponse]
54
+ revision: str = Field(min_length=1)
55
+ next_cursor: str | None = None
56
+ generated_at: datetime
@@ -0,0 +1,46 @@
1
+ """公司行动 Canonical Fact 与运行时阶段事件。"""
2
+
3
+ from datetime import date
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+
10
+ class CorporateActionPhase(str, Enum):
11
+ RECORD = "record"
12
+ EX_DATE = "ex_date"
13
+ LISTING_DATE = "listing_date"
14
+ PAY_DATE = "pay_date"
15
+
16
+
17
+ class CorporateActionResponse(BaseModel):
18
+ model_config = ConfigDict(from_attributes=True)
19
+
20
+ action_id: str
21
+ instrument_id: str
22
+ record_date: date
23
+ ex_date: date
24
+ pay_date: date | None = None
25
+ listing_date: date | None = None
26
+ cash_per_share: Decimal = Field(Decimal(0), ge=0)
27
+ stock_ratio: Decimal = Field(Decimal(0), ge=0)
28
+ dataset_revision: str
29
+ source_provider: str
30
+
31
+ @model_validator(mode="after")
32
+ def validate_dates_and_value(self) -> "CorporateActionResponse":
33
+ if self.record_date > self.ex_date:
34
+ raise ValueError("公司行动日期必须满足 record_date <= ex_date")
35
+ if self.cash_per_share == 0 and self.stock_ratio == 0:
36
+ raise ValueError("公司行动必须至少包含现金或送转比例")
37
+ if self.cash_per_share > 0 and (self.pay_date is None or self.pay_date < self.ex_date):
38
+ raise ValueError("现金分红必须有不早于 ex_date 的 pay_date")
39
+ if self.stock_ratio > 0 and (self.listing_date is None or self.listing_date < self.ex_date):
40
+ raise ValueError("送转股必须有不早于 ex_date 的 listing_date")
41
+ return self
42
+
43
+
44
+ class CorporateActionEventPayload(BaseModel):
45
+ action: CorporateActionResponse
46
+ phase: CorporateActionPhase