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,61 @@
1
+ """回放准备所需的市场事实;事件与时间片仍由量化端组织。"""
2
+
3
+ from datetime import date
4
+ from decimal import Decimal
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict, model_validator
8
+
9
+ from pqdata.schema.adjustment_schema import AdjustmentFactorResponse
10
+ from pqdata.schema.corporate_action_schema import CorporateActionResponse
11
+ from pqdata.schema.reference_schema import (
12
+ InstrumentResponse,
13
+ TradingCalendarDayResponse,
14
+ TradingRuleResponse,
15
+ )
16
+
17
+
18
+ class DailyFactResponse(BaseModel):
19
+ model_config = ConfigDict(from_attributes=True)
20
+ instrument_id: str
21
+ trade_date: date
22
+ suspension_status: Literal["none", "full_day", "intraday"] | None
23
+ special_type: Literal["Normal", "ST", "StarST"] | None = None
24
+ special_type_source: str | None = None
25
+ limit_up: Decimal | None = None
26
+ limit_dn: Decimal | None = None
27
+ extra: dict[str, Any] | None = None
28
+ source_provider: str
29
+
30
+ @property
31
+ def is_st(self) -> bool | None:
32
+ """未知保持 None,调用方不能把缺少数据当成正常。"""
33
+ return None if self.special_type is None else self.special_type != "Normal"
34
+
35
+ @property
36
+ def is_suspended(self) -> bool | None:
37
+ """只判断全天停牌;False 不表示全天每一分钟都可交易。"""
38
+ return None if self.suspension_status is None else self.suspension_status == "full_day"
39
+
40
+
41
+ class HistoryFactsQuery(BaseModel):
42
+ instrument_id: str
43
+ start: date
44
+ end: date
45
+ revision: str | None = None
46
+
47
+ @model_validator(mode="after")
48
+ def validate_range(self) -> "HistoryFactsQuery":
49
+ if not 0 <= (self.end - self.start).days <= 3660:
50
+ raise ValueError("History facts range must be between zero and ten years")
51
+ return self
52
+
53
+
54
+ class HistoryFacts(BaseModel):
55
+ revision: str
56
+ instrument: InstrumentResponse
57
+ calendar: list[TradingCalendarDayResponse]
58
+ trading_rules: list[TradingRuleResponse]
59
+ daily_facts: list[DailyFactResponse]
60
+ adjustment_factors: list[AdjustmentFactorResponse]
61
+ corporate_actions: list[CorporateActionResponse]
@@ -0,0 +1,16 @@
1
+ """Market-owned daily price windows for downstream PIT calculations."""
2
+
3
+ from datetime import date
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from pqdata.schema.adjustment_schema import AdjustedBarResponse
8
+
9
+
10
+ class PITDailyWindow(BaseModel):
11
+ model_config = ConfigDict(frozen=True)
12
+
13
+ as_of: date
14
+ anchor_date: date
15
+ bars: tuple[AdjustedBarResponse, ...]
16
+ input_digest: str
@@ -0,0 +1,53 @@
1
+ """研究与历史数据读取的显式请求契约。"""
2
+
3
+ from datetime import date
4
+
5
+ from pydantic import AwareDatetime, BaseModel, Field, model_validator
6
+
7
+ from pqdata.schema.adjustment_schema import BarViewRequest
8
+ from pqdata.schema.bar_schema import BarFrequency
9
+
10
+
11
+ class InstrumentRangeQuery(BaseModel):
12
+ instrument_ids: list[str] = Field(min_length=1, max_length=10000)
13
+ start: date
14
+ end: date
15
+
16
+ @model_validator(mode="after")
17
+ def validate_range(self) -> "InstrumentRangeQuery":
18
+ if self.start > self.end:
19
+ raise ValueError("start must not be after end")
20
+ return self
21
+
22
+
23
+ class BarWindowQuery(BaseModel):
24
+ instrument_id: str
25
+ frequency: BarFrequency
26
+ start: AwareDatetime | None = None
27
+ end: AwareDatetime | None = None
28
+ limit: int = Field(default=1000, ge=1, le=100000)
29
+ view: BarViewRequest | None = None
30
+
31
+
32
+ class PITWindowQuery(BaseModel):
33
+ instrument_id: str
34
+ days: list[date] = Field(min_length=1, max_length=2000)
35
+ next_days: list[date] = Field(min_length=1, max_length=2000)
36
+ lookback: int = Field(ge=1, le=2000)
37
+
38
+ @model_validator(mode="after")
39
+ def validate_days(self) -> "PITWindowQuery":
40
+ if len(self.days) != len(self.next_days):
41
+ raise ValueError("days and next_days must have equal length")
42
+ if self.days != sorted(set(self.days)):
43
+ raise ValueError("days must be strictly increasing")
44
+ if any(next_day <= day for day, next_day in zip(self.days, self.next_days, strict=True)):
45
+ raise ValueError("next_days must follow corresponding days")
46
+ return self
47
+
48
+
49
+ class DataHealthQuery(BaseModel):
50
+ instrument_id: str
51
+ frequency: BarFrequency = "1d"
52
+ start: AwareDatetime
53
+ end: AwareDatetime
@@ -0,0 +1,73 @@
1
+ """
2
+ Market Schema - Quote / DailyBasic / Sector 的响应模型
3
+ """
4
+
5
+ from datetime import date, datetime
6
+ from decimal import Decimal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class QuoteResponse(BaseModel):
12
+ """最新行情快照响应"""
13
+
14
+ model_config = ConfigDict(from_attributes=True)
15
+
16
+ instrument_id: str = Field(..., description="Canonical标的ID")
17
+ timestamp: datetime = Field(..., description="快照时间(事件时间)")
18
+ last: Decimal = Field(..., description="最新价")
19
+ open: Decimal | None = Field(None, description="今日开盘价")
20
+ high: Decimal | None = Field(None, description="今日最高价")
21
+ low: Decimal | None = Field(None, description="今日最低价")
22
+ previous_close: Decimal | None = Field(None, description="昨收价")
23
+ volume: int = Field(..., description="累计成交量(share)")
24
+ amount: Decimal | None = Field(None, description="累计成交额(CNY)")
25
+ bid_price_1: Decimal | None = Field(None, description="买一价")
26
+ bid_volume_1: int | None = Field(None, description="买一量(share)")
27
+ ask_price_1: Decimal | None = Field(None, description="卖一价")
28
+ ask_volume_1: int | None = Field(None, description="卖一量(share)")
29
+ status: str | None = Field(None, description="交易状态")
30
+
31
+
32
+ class DailyBasicResponse(BaseModel):
33
+ """日频基础指标响应"""
34
+
35
+ model_config = ConfigDict(from_attributes=True)
36
+
37
+ instrument_id: str = Field(..., description="Canonical标的ID")
38
+ trade_date: date = Field(..., description="交易日")
39
+ turnover_rate: Decimal | None = Field(None, description="换手率(%,1.23=1.23%)")
40
+ turnover_rate_free: Decimal | None = Field(None, description="自由流通换手率(%)")
41
+ volume_ratio: Decimal | None = Field(None, description="量比")
42
+ pe: Decimal | None = Field(None, description="市盈率(NULL=不可计算,如亏损)")
43
+ pe_ttm: Decimal | None = Field(None, description="市盈率TTM")
44
+ pb: Decimal | None = Field(None, description="市净率")
45
+ ps: Decimal | None = Field(None, description="市销率")
46
+ ps_ttm: Decimal | None = Field(None, description="市销率TTM")
47
+ dividend_yield: Decimal | None = Field(None, description="股息率(%)")
48
+ dividend_yield_ttm: Decimal | None = Field(None, description="股息率TTM(%)")
49
+ total_shares: Decimal | None = Field(None, description="总股本(share)")
50
+ float_shares: Decimal | None = Field(None, description="流通股本(share)")
51
+ free_float_shares: Decimal | None = Field(None, description="自由流通股本(share)")
52
+ total_market_cap: Decimal | None = Field(None, description="总市值(CNY)")
53
+ circulating_market_cap: Decimal | None = Field(None, description="流通市值(CNY)")
54
+
55
+
56
+ class SectorResponse(BaseModel):
57
+ """板块响应"""
58
+
59
+ model_config = ConfigDict(from_attributes=True)
60
+
61
+ sector_id: str = Field(..., description="Canonical板块ID")
62
+ name: str = Field(..., description="名称")
63
+ sector_type: str = Field(..., description="类型")
64
+ source: str = Field(..., description="分类体系来源")
65
+
66
+
67
+ class SectorMembershipResponse(BaseModel):
68
+ """标的-板块关系响应"""
69
+
70
+ instrument_id: str = Field(..., description="Canonical标的ID")
71
+ sector_id: str = Field(..., description="Canonical板块ID")
72
+ effective_from: date = Field(..., description="生效日期(含)")
73
+ effective_to: date | None = Field(None, description="失效日期(含);NULL=当前有效")
@@ -0,0 +1,48 @@
1
+ """标的、交易规则与日历的公开数据响应。"""
2
+
3
+ from datetime import date
4
+ from decimal import Decimal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ class InstrumentResponse(BaseModel):
10
+ """标的响应"""
11
+
12
+ model_config = ConfigDict(from_attributes=True)
13
+ instrument_id: str = Field(..., description="Canonical标的ID,如CN.XSHE.000001")
14
+ symbol: str = Field(..., description="交易代码")
15
+ name: str = Field(..., description="名称")
16
+ instrument_type: str = Field(..., description="标的类型")
17
+ exchange: str = Field(..., description="交易所")
18
+ board: str | None = Field(None, description="板块")
19
+ currency: str = Field(..., description="计价货币")
20
+ status: str = Field(..., description="状态")
21
+ listed_date: date | None = Field(None, description="上市日期")
22
+ delisted_date: date | None = Field(None, description="退市日期")
23
+
24
+
25
+ class TradingRuleResponse(BaseModel):
26
+ """某标的在一个业务日期区间内生效的交易规则。"""
27
+
28
+ model_config = ConfigDict(from_attributes=True, frozen=True)
29
+ instrument_id: str
30
+ effective_from: date
31
+ effective_to: date | None = None
32
+ min_buy_quantity: int = Field(..., gt=0)
33
+ buy_quantity_step: int = Field(..., gt=0)
34
+ min_sell_quantity: int = Field(..., gt=0)
35
+ sell_quantity_step: int = Field(..., gt=0)
36
+ tick_size: Decimal = Field(..., gt=0)
37
+ settlement_days: int = Field(..., ge=0)
38
+
39
+
40
+ class TradingCalendarDayResponse(BaseModel):
41
+ """交易日历响应"""
42
+
43
+ model_config = ConfigDict(from_attributes=True)
44
+ exchange: str = Field(..., description="交易所")
45
+ trade_date: date = Field(..., description="日期")
46
+ is_open: bool = Field(..., description="是否交易日")
47
+ previous_open_date: date | None = Field(None, description="上一个交易日")
48
+ next_open_date: date | None = Field(None, description="下一个交易日")
@@ -0,0 +1,152 @@
1
+ """
2
+ Market Data Reliability Schema - Sync 结果与 Dataset 健康契约
3
+
4
+ SSOT 语义见 SPEC「Market Data 可靠性」与 DATA-DICTIONARY §5。
5
+ 核心原则:
6
+ - "同步成功" != Provider 请求成功;必须 = 转换成功 + 数据合法 + 无重复 +
7
+ 目标区间完整 + 结果可追溯(MarketSyncResult 结构化表达)。
8
+ - Verify 只证明 "Observed Canonical Data 是否覆盖 Expected Timeline",
9
+ 不负责猜测 Missing 的业务原因(停牌/零成交属 Trading Status,future)。
10
+ """
11
+
12
+ from datetime import date, datetime
13
+ from enum import Enum
14
+ from typing import Any
15
+
16
+ from pydantic import BaseModel, Field
17
+
18
+
19
+ class SyncIssueCode(str, Enum):
20
+ """结构化问题码(issues 摘要用,禁止只返回 "sync success" 文本)"""
21
+
22
+ INVALID_OHLC = "INVALID_OHLC"
23
+ NON_POSITIVE_PRICE = "NON_POSITIVE_PRICE"
24
+ NEGATIVE_VOLUME = "NEGATIVE_VOLUME"
25
+ NEGATIVE_AMOUNT = "NEGATIVE_AMOUNT"
26
+ INVALID_TIMESTAMP = "INVALID_TIMESTAMP"
27
+ INVALID_INSTRUMENT_ID = "INVALID_INSTRUMENT_ID"
28
+ INVALID_TRADING_DAY = "INVALID_TRADING_DAY" # 交易日语义违反 TradingCalendar
29
+ MISSING_BAR = "MISSING_BAR" # Expected Timeline 上的缺口(unexplained)
30
+ MISSING_ADJUSTMENT_FACTOR = "MISSING_ADJUSTMENT_FACTOR"
31
+ MISSING_DATASET_COVERAGE = "MISSING_DATASET_COVERAGE"
32
+ DUPLICATE_BAR = "DUPLICATE_BAR" # 同批内重复(唯一键冲突,跳过)
33
+ PROVIDER_ERROR = "PROVIDER_ERROR"
34
+
35
+
36
+ class SyncStatus(str, Enum):
37
+ """Sync 结果状态
38
+
39
+ SUCCESS = 目标同步区间成功且验证通过;
40
+ PARTIAL = 有数据写入,但存在缺口或非法数据;
41
+ FAILED = 同步任务无法形成可用结果。
42
+ 不因为写入了一部分数据就返回 SUCCESS。
43
+ """
44
+
45
+ SUCCESS = "SUCCESS"
46
+ PARTIAL = "PARTIAL"
47
+ FAILED = "FAILED"
48
+
49
+
50
+ class SyncIssue(BaseModel):
51
+ """单条结构化问题摘要"""
52
+
53
+ code: SyncIssueCode
54
+ instrument_id: str | None = None
55
+ timestamp: datetime | None = None
56
+ detail: str | None = Field(None, description="补充说明(如失败原因原文)")
57
+
58
+
59
+ class MarketSyncResult(BaseModel):
60
+ """Sync 稳定结果对象(可追溯:请求区间 + 各阶段计数 + 状态 + 问题摘要)
61
+
62
+ 字段按真实实现裁剪:V1 Provider 为 ON CONFLICT DO NOTHING 幂等 upsert,
63
+ updated_count 恒为 0(重复数据走 skipped)。
64
+ """
65
+
66
+ dataset: str
67
+ provider: str | None = None
68
+ instrument_id: str | None = Field(None, description="单标的同步时有值")
69
+ frequency: str | None = None
70
+ requested_start: date | None = None
71
+ requested_end: date | None = None
72
+ fetched_count: int = Field(0, description="Provider 返回条数")
73
+ normalized_count: int = Field(0, description="Mapper 转换成功条数")
74
+ inserted_count: int = Field(0, description="新插入条数")
75
+ updated_count: int = Field(0, description="更新条数(V1 幂等 upsert 恒为 0)")
76
+ skipped_count: int = Field(0, description="重复/无需写入条数")
77
+ invalid_count: int = Field(0, description="校验拒绝条数")
78
+ missing_count: int = Field(0, description="Completeness Check 发现的缺口数")
79
+ started_at: datetime
80
+ finished_at: datetime | None = None
81
+ status: SyncStatus = SyncStatus.FAILED
82
+ issues: list[SyncIssue] = Field(default_factory=list, description="结构化问题摘要(限量)")
83
+
84
+
85
+ class DatasetHealth(str, Enum):
86
+ """Dataset 健康状态(Verify 产出)
87
+
88
+ HEALTHY = 请求区间完整且合法,可以消费;
89
+ PARTIAL = 有数据但目标区间存在缺口(unexplained missing,含疑似停牌日);
90
+ STALE = 结构正确且完整,但未更新到要求的新鲜度(V1 不产出,预留枚举位);
91
+ INVALID = 存在违反 Canonical 数据规则的数据;
92
+ NOT_SYNCED = 请求区间基本没有本地数据。
93
+ """
94
+
95
+ HEALTHY = "HEALTHY"
96
+ PARTIAL = "PARTIAL"
97
+ STALE = "STALE"
98
+ INVALID = "INVALID"
99
+ NOT_SYNCED = "NOT_SYNCED"
100
+
101
+
102
+ class DatasetHealthResult(BaseModel):
103
+ """Verify 结果(只检查,绝不触发 Sync)
104
+
105
+ 完整性验证单位:instrument_id + frequency + requested time range。
106
+ missing 明细只返回限量样本(missing_count 是权威计数)。
107
+ """
108
+
109
+ dataset: str
110
+ instrument_id: str
111
+ frequency: str
112
+ requested_start: datetime
113
+ requested_end: datetime
114
+ health: DatasetHealth
115
+ expected_count: int = Field(0, description="Expected Timeline 应有 Bar 数")
116
+ actual_count: int = Field(0, description="本地实际 Bar 数")
117
+ missing_count: int = 0
118
+ invalid_count: int = 0
119
+ latest_timestamp: datetime | None = None
120
+ dataset_revision: str | None = Field(
121
+ None, description="本次验证所见内容版本;必须进入缓存键/回测结果"
122
+ )
123
+ issues: list[SyncIssue] = Field(default_factory=list)
124
+
125
+
126
+ def issue_code_from_reason(reason: str) -> SyncIssueCode:
127
+ """Validator 失败原因字符串 → 结构化问题码(未知原因归入 INVALID_OHLC)"""
128
+ mapping: dict[str, SyncIssueCode] = {
129
+ "high_lt_max_open_close": SyncIssueCode.INVALID_OHLC,
130
+ "low_gt_min_open_close": SyncIssueCode.INVALID_OHLC,
131
+ "non_positive_price": SyncIssueCode.NON_POSITIVE_PRICE,
132
+ "negative_volume": SyncIssueCode.NEGATIVE_VOLUME,
133
+ "negative_amount": SyncIssueCode.NEGATIVE_AMOUNT,
134
+ "naive_timestamp": SyncIssueCode.INVALID_TIMESTAMP,
135
+ "invalid_instrument_id": SyncIssueCode.INVALID_INSTRUMENT_ID,
136
+ }
137
+ return mapping.get(reason, SyncIssueCode.INVALID_OHLC)
138
+
139
+
140
+ def to_issue(reason: str, instrument_id: str, timestamp: datetime | None) -> SyncIssue:
141
+ """Validator 失败原因 → SyncIssue 的统一入口"""
142
+ return SyncIssue(
143
+ code=issue_code_from_reason(reason),
144
+ instrument_id=instrument_id,
145
+ timestamp=timestamp,
146
+ detail=reason,
147
+ )
148
+
149
+
150
+ def health_payload(result: DatasetHealthResult) -> dict[str, Any]:
151
+ """DatasetHealthResult 的可 JSON 化视图(API/日志复用)"""
152
+ return result.model_dump(mode="json")
quant_cli/__init__.py ADDED
File without changes
quant_cli/api.py ADDED
@@ -0,0 +1,39 @@
1
+ """Stable local Python use cases; importing performs no I/O."""
2
+
3
+ from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
4
+
5
+ from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
6
+ from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
7
+ from quant_cli.app.data.schema.dataset_request_schema import DatasetPrepareRequest
8
+ from quant_cli.app.data.service.dataset_load_service import load_dataset
9
+ from quant_cli.app.data.service.dataset_prepare_service import prepare_dataset
10
+ from quant_cli.app.factor.local_factor_dependency import (
11
+ build_local_factor_store,
12
+ build_local_selection_store,
13
+ )
14
+ from quant_cli.app.factor.schema.local_selection_schema import (
15
+ LocalSelectionFactor,
16
+ LocalSelectionRequest,
17
+ )
18
+ from quant_cli.app.factor.service.local_factor_service import compute_local_factor, run_local_factor
19
+ from quant_cli.app.factor.service.local_selection_service import (
20
+ compute_local_selection,
21
+ run_local_selection,
22
+ )
23
+
24
+ __all__ = [
25
+ "build_local_factor_store",
26
+ "build_local_selection_store",
27
+ "DatasetPrepareRequest",
28
+ "LocalBacktestRequest",
29
+ "prepare_dataset",
30
+ "load_dataset",
31
+ "LocalFactorRequest",
32
+ "LocalSelectionFactor",
33
+ "LocalSelectionRequest",
34
+ "compute_local_factor",
35
+ "run_local_factor",
36
+ "compute_local_selection",
37
+ "run_local_selection",
38
+ "run_local_backtest",
39
+ ]
File without changes
File without changes
File without changes
@@ -0,0 +1,72 @@
1
+ """本地策略回测命令;不加载管理数据库入口。"""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+ from uuid import UUID
7
+
8
+ import typer
9
+
10
+ from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
11
+ from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
12
+ from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
13
+ from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
14
+ from quant_cli.common.constants import DEFAULT_DATABASE_PATH
15
+ from quant_cli.common.utils.cli_utils import cli_errors, output
16
+
17
+ app = typer.Typer(help="本地策略回测", no_args_is_help=True)
18
+
19
+
20
+ @app.command("run")
21
+ @cli_errors
22
+ def run(
23
+ config: Annotated[Path, typer.Option(exists=True)],
24
+ source: Annotated[Path | None, typer.Option(exists=True)] = None,
25
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
26
+ ) -> None:
27
+ import json
28
+
29
+ payload = json.loads(config.read_text(encoding="utf-8"))
30
+ if source is not None:
31
+ payload["source"] = source.read_text(encoding="utf-8")
32
+ request = LocalBacktestRequest.model_validate(payload)
33
+ record = asyncio.run(run_local_backtest(request, build_local_factor_store(database)))
34
+ build_local_backtest_store(database).save(record)
35
+ output(record)
36
+
37
+
38
+ @app.command("get")
39
+ @cli_errors
40
+ def get(
41
+ run: Annotated[UUID, typer.Option()],
42
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
43
+ ) -> None:
44
+ output(build_local_backtest_store(database).get(run))
45
+
46
+
47
+ @app.command("list")
48
+ @cli_errors
49
+ def list_runs(
50
+ limit: int = 20,
51
+ offset: int = 0,
52
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
53
+ ) -> None:
54
+ output(
55
+ [
56
+ row.model_dump(mode="json")
57
+ for row in build_local_backtest_store(database).list_runs(limit, offset)
58
+ ]
59
+ )
60
+
61
+
62
+ @app.command("compare")
63
+ @cli_errors
64
+ def compare(
65
+ run: Annotated[list[UUID], typer.Option()],
66
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
67
+ ) -> None:
68
+ """比较多次本地回测。"""
69
+ from quant_cli.app.backtest.service.local_backtest_service import compare_backtest_records
70
+
71
+ store = build_local_backtest_store(database)
72
+ output(compare_backtest_records(tuple(store.get(key) for key in run)))
@@ -0,0 +1,10 @@
1
+ """本地回测归档组装。"""
2
+
3
+ from pathlib import Path
4
+
5
+ from quant_cli.app.backtest.repository.backtest_local_repository import BacktestLocalRepository
6
+
7
+
8
+ def build_local_backtest_store(path: Path) -> BacktestLocalRepository:
9
+ """返回本地回测存取。"""
10
+ return BacktestLocalRepository(path)
@@ -0,0 +1,49 @@
1
+ """本地回测输入,策略源码与数据快照显式提供。"""
2
+
3
+ from decimal import Decimal
4
+ from pathlib import Path
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
8
+ from quant_core.app.backtest.schema import BacktestConfig
9
+ from quant_core.app.data.schema.adjustment_schema import BarViewRequest
10
+ from quant_core.app.strategy.schema.strategy_publication_schema import PublishedStrategy
11
+
12
+
13
+ class LocalBacktestRequest(BaseModel):
14
+ model_config = ConfigDict(extra="forbid")
15
+ config: BacktestConfig
16
+ source: str = ""
17
+ publication: PublishedStrategy | None = None
18
+ factor_view: BarViewRequest = Field(default_factory=BarViewRequest)
19
+ exclude_st: bool = False
20
+ manifest_path: Path
21
+ parameters: dict = Field(default_factory=dict)
22
+ factor_run_uuid: UUID | None = None
23
+ factor_artifact_path: Path | None = None
24
+ slippage_bps: Decimal = Field(default=Decimal(0), ge=0, allow_inf_nan=False)
25
+ max_rows: int = Field(default=1000000, ge=1)
26
+
27
+ @model_validator(mode="after")
28
+ def one_factor_source(self) -> "LocalBacktestRequest":
29
+ if bool(self.source) == (self.publication is not None):
30
+ raise ValueError("必须且只能提供本地策略源码或已发布策略")
31
+ if self.publication is not None:
32
+ spec = self.publication.definition
33
+ if spec.strategy_id != self.config.strategy_id:
34
+ raise ValueError("回测策略 ID 与发布定义不同")
35
+ if self.parameters and self.parameters != spec.parameters:
36
+ raise ValueError("发布策略参数不可静默覆盖,请发布新版本")
37
+ if spec.universe and set(spec.universe) != set(self.config.instrument_ids):
38
+ raise ValueError("股票池与发布策略不同")
39
+ if not spec.factor_refs and (self.factor_run_uuid or self.factor_artifact_path):
40
+ raise ValueError("发布策略未声明因子依赖,不能注入因子结果")
41
+ if self.factor_run_uuid and self.factor_artifact_path:
42
+ raise ValueError("因子运行和独立产物只能指定一种")
43
+ return self
44
+
45
+ @field_validator("manifest_path")
46
+ @classmethod
47
+ def absolute_manifest(cls, value: Path) -> Path:
48
+ """归档固定路径,避免重启后工作目录变化影响定位。"""
49
+ return value.expanduser().resolve()
File without changes
@@ -0,0 +1,67 @@
1
+ """backtest 本地运行存取;短事务由本地连接上下文管理。"""
2
+
3
+ from pathlib import Path
4
+ from uuid import UUID
5
+
6
+ from quant_core.common.local_run import LocalRunRecord
7
+
8
+ from quant_cli.common.database.duckdb_connection import local_transaction
9
+
10
+
11
+ class BacktestLocalRepository:
12
+ """只维护本领域的运行记录,查询使用数据库分页。"""
13
+
14
+ def __init__(self, path: Path) -> None:
15
+ self.path = path
16
+
17
+ def save(self, record: LocalRunRecord) -> None:
18
+ with local_transaction(self.path) as connection:
19
+ connection.execute("""CREATE TABLE IF NOT EXISTS backtest_local_run (
20
+ id BIGINT PRIMARY KEY, uuid UUID UNIQUE NOT NULL,
21
+ created_at TIMESTAMPTZ NOT NULL, document JSON NOT NULL)""")
22
+ existing = connection.execute(
23
+ "SELECT document FROM backtest_local_run WHERE uuid = ?", [str(record.uuid)]
24
+ ).fetchone()
25
+ if existing is not None:
26
+ if LocalRunRecord.model_validate_json(existing[0]) != record:
27
+ raise ValueError("同一运行 UUID 的记录不可覆盖")
28
+ return
29
+ connection.execute(
30
+ """INSERT INTO backtest_local_run
31
+ SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM backtest_local_run""",
32
+ [str(record.uuid), record.created_at, record.model_dump_json()],
33
+ )
34
+
35
+ def get(self, run_uuid: UUID) -> LocalRunRecord:
36
+ if not self.path.is_file():
37
+ raise ValueError("运行记录不存在")
38
+ with local_transaction(self.path, read_only=True) as connection:
39
+ if not connection.execute(
40
+ "SELECT 1 FROM information_schema.tables WHERE table_name = ?",
41
+ ["backtest_local_run"],
42
+ ).fetchall():
43
+ raise ValueError("运行记录不存在")
44
+ row = connection.execute(
45
+ "SELECT document FROM backtest_local_run WHERE uuid = ?", [str(run_uuid)]
46
+ ).fetchone()
47
+ if row is None:
48
+ raise ValueError("运行记录不存在")
49
+ return LocalRunRecord.model_validate_json(row[0])
50
+
51
+ def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]:
52
+ if not 1 <= limit <= 1000 or offset < 0:
53
+ raise ValueError("分页参数非法")
54
+ if not self.path.is_file():
55
+ return []
56
+ with local_transaction(self.path, read_only=True) as connection:
57
+ if not connection.execute(
58
+ "SELECT 1 FROM information_schema.tables WHERE table_name = ?",
59
+ ["backtest_local_run"],
60
+ ).fetchall():
61
+ return []
62
+ rows = connection.execute(
63
+ """SELECT document FROM backtest_local_run
64
+ ORDER BY id DESC LIMIT ? OFFSET ?""",
65
+ [limit, offset],
66
+ ).fetchall()
67
+ return [LocalRunRecord.model_validate_json(row[0]) for row in rows]
File without changes