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,187 @@
1
+ from quant_core.app.data.schema.history_schema import HistoryFacts
2
+
3
+ """通过 SDK 准备固定版本的本地回放数据;完成后关闭网络客户端。"""
4
+
5
+ import hashlib
6
+ import json
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, time, timedelta
9
+ from pathlib import Path
10
+
11
+ from pqdata import MarketClient
12
+ from pqdata.schema.bar_schema import BarFrequency, BarQuery
13
+ from pqdata.schema.history_schema import HistoryFactsQuery
14
+ from pydantic import TypeAdapter
15
+ from quant_core.app.data.schema.adjustment_schema import AdjustmentType
16
+ from quant_core.app.data.schema.market_schema import BarResponse, InstrumentResponse
17
+ from quant_core.app.data.service.prepared_data_verification_service import PreparedDataVerification
18
+ from quant_core.app.data.service.prepared_market_service import PreparedMarketData
19
+
20
+ from quant_cli.app.data.repository.parquet_repository import atomic_json, write_parquet
21
+ from quant_cli.app.data.service.bar_cache_service import (
22
+ BarCacheQuery,
23
+ bars_frame,
24
+ load_bar_snapshot,
25
+ prepare_bar_cache,
26
+ )
27
+ from quant_cli.app.data.service.dataset_load_service import load_dataset, manifest_digest
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class PreparedDataset:
32
+ market: PreparedMarketData
33
+ verification: PreparedDataVerification
34
+ manifest_path: Path
35
+
36
+
37
+ async def prepare_dataset(
38
+ client: MarketClient,
39
+ *,
40
+ instrument_ids: tuple[str, ...],
41
+ frequency: str,
42
+ execution_frequency: str,
43
+ start: datetime,
44
+ end: datetime,
45
+ adjustment_type: AdjustmentType,
46
+ cache_dir: Path,
47
+ lookback_days: int,
48
+ max_rows: int,
49
+ revision: str | None = None,
50
+ ) -> PreparedDataset:
51
+ """先固定事实版本,再校验并下载;版本冲突或超出容量时直接失败。"""
52
+ history_start = start - timedelta(days=lookback_days)
53
+ first = await client.get_history_facts(
54
+ HistoryFactsQuery(
55
+ instrument_id=instrument_ids[0],
56
+ start=history_start.date(),
57
+ end=end.date(),
58
+ revision=revision,
59
+ )
60
+ )
61
+ market = PreparedMarketData(history_start, end, first.revision)
62
+ instruments = await client.search_instruments(limit=10000)
63
+ if len(instruments) >= 10000:
64
+ raise ValueError("标的目录达到单次读取上限,无法证明目录完整")
65
+ market.instruments = [InstrumentResponse.model_validate(row) for row in instruments]
66
+ verification = PreparedDataVerification()
67
+ cache_key = hashlib.sha256(
68
+ json.dumps(
69
+ {
70
+ "format_version": 2,
71
+ "source": client.source_id,
72
+ "revision": first.revision,
73
+ "instruments": sorted(instrument_ids),
74
+ "frequency": frequency,
75
+ "execution_frequency": execution_frequency,
76
+ "start": start.isoformat(),
77
+ "end": end.isoformat(),
78
+ "lookback_days": lookback_days,
79
+ "adjustment_type": adjustment_type.value,
80
+ },
81
+ sort_keys=True,
82
+ ).encode()
83
+ ).hexdigest()
84
+ directory = cache_dir / cache_key
85
+ manifest_path = directory / "manifest.json"
86
+ if manifest_path.exists():
87
+ prepared, evidence = load_dataset(manifest_path, max_rows)
88
+ return PreparedDataset(prepared, evidence, manifest_path)
89
+ directory.mkdir(parents=True, exist_ok=True)
90
+ manifest = {
91
+ "format_version": 2,
92
+ "source": client.source_id,
93
+ "frequencies": sorted({frequency, execution_frequency}),
94
+ "revision": first.revision,
95
+ "start": history_start.isoformat(),
96
+ "end": end.isoformat(),
97
+ "files": [],
98
+ "facts": {},
99
+ "health": [],
100
+ }
101
+ total_rows = 0
102
+ cached_bars = {}
103
+ for value in sorted({frequency, execution_frequency}):
104
+ path = await prepare_bar_cache(
105
+ client,
106
+ BarCacheQuery(
107
+ instrument_ids=list(instrument_ids),
108
+ frequency=TypeAdapter(BarFrequency).validate_python(value),
109
+ start=history_start,
110
+ end=end,
111
+ revision=first.revision,
112
+ ),
113
+ cache_dir,
114
+ max_rows,
115
+ )
116
+ _, rows = load_bar_snapshot(path, max_rows)
117
+ cached_bars[value] = rows
118
+ total_rows += len(rows)
119
+ if total_rows > max_rows:
120
+ raise ValueError("回放数据超过配置行数上限")
121
+ for instrument_id in instrument_ids:
122
+ facts = await client.get_history_facts(
123
+ HistoryFactsQuery(
124
+ instrument_id=instrument_id,
125
+ start=history_start.date(),
126
+ end=end.date(),
127
+ revision=first.revision,
128
+ )
129
+ )
130
+ market.facts[instrument_id] = HistoryFacts.model_validate(facts.model_dump())
131
+ manifest["facts"][instrument_id] = facts.model_dump(mode="json")
132
+ verification_start = start
133
+ if facts.instrument.listed_date is not None and facts.instrument.listed_date > start.date():
134
+ verification_start = datetime.combine(
135
+ facts.instrument.listed_date, time(9, 30), tzinfo=start.tzinfo
136
+ )
137
+ if verification_start <= end:
138
+ for value in sorted({frequency, execution_frequency}):
139
+ result = await client.verify_bars(instrument_id, value, verification_start, end)
140
+ verification.add("bar", instrument_id, value, verification_start, end, result)
141
+ manifest["health"].append(result.model_dump(mode="json"))
142
+ if adjustment_type is not AdjustmentType.NONE:
143
+ result = await client.verify_adjustment_factors(
144
+ instrument_id, frequency, verification_start, end
145
+ )
146
+ verification.add(
147
+ "adjustment_factor", instrument_id, frequency, verification_start, end, result
148
+ )
149
+ manifest["health"].append(result.model_dump(mode="json"))
150
+ result = await client.verify_corporate_actions(instrument_id, verification_start, end)
151
+ verification.add(
152
+ "corporate_action", instrument_id, "1d", verification_start, end, result
153
+ )
154
+ manifest["health"].append(result.model_dump(mode="json"))
155
+ for value in sorted({frequency, execution_frequency}):
156
+ query = BarQuery(
157
+ instrument_ids=[instrument_id],
158
+ frequency=TypeAdapter(BarFrequency).validate_python(value),
159
+ start=history_start,
160
+ end=end,
161
+ revision=first.revision,
162
+ )
163
+ selected = [row for row in cached_bars[value] if row.instrument_id == instrument_id]
164
+ receipt = write_parquet(
165
+ directory,
166
+ bars_frame(selected),
167
+ dataset_id="bar.raw",
168
+ primary_key=("timestamp", "instrument_id", "frequency"),
169
+ )
170
+ bars = [BarResponse.model_validate(row) for row in selected]
171
+ market.add_bars(instrument_id, value, bars)
172
+ manifest["files"].append(
173
+ {"query": query.model_dump(mode="json"), **receipt.model_dump(mode="json")}
174
+ )
175
+ # 校验元数据、覆盖校验和最后一页之间没有并发提交。
176
+ await client.get_history_facts(
177
+ HistoryFactsQuery(
178
+ instrument_id=instrument_ids[0],
179
+ start=history_start.date(),
180
+ end=end.date(),
181
+ revision=first.revision,
182
+ )
183
+ )
184
+ manifest["instruments"] = [row.model_dump(mode="json") for row in market.instruments]
185
+ manifest["content_digest"] = manifest_digest(manifest)
186
+ atomic_json(manifest_path, manifest)
187
+ return PreparedDataset(market, verification, manifest_path)
File without changes
File without changes
@@ -0,0 +1,81 @@
1
+ """因子定义发布和下载命令;与本地运行记录命令分组。"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+ from urllib.parse import quote
7
+
8
+ import typer
9
+ from quant_core.app.factor.schema.factor_computation_schema import FactorComputationSpec
10
+ from quant_core.app.factor.schema.factor_publication_schema import PublishedFactor
11
+
12
+ from quant_cli.common.http.definition_client import request_definition, save_definition
13
+ from quant_cli.common.utils.cli_utils import cli_errors, output
14
+
15
+ app = typer.Typer(help="因子定义发布、查询与获取", no_args_is_help=True)
16
+ Server = Annotated[str, typer.Option(envvar="PQ_QUANT_URL")]
17
+ Token = Annotated[str, typer.Option(envvar="PQ_QUANT_TOKEN", help="建议由环境变量提供令牌")]
18
+
19
+
20
+ @app.command("publish")
21
+ @cli_errors
22
+ def publish(
23
+ config: Annotated[Path, typer.Option(exists=True)],
24
+ server: Server,
25
+ token: Token,
26
+ source: Annotated[Path | None, typer.Option(exists=True)] = None,
27
+ ) -> None:
28
+ """发布 JSON 定义,支持从 Python 文件读取源码,不执行它。"""
29
+ payload = json.loads(config.read_text(encoding="utf-8"))
30
+ if source is not None:
31
+ payload["python_source"] = source.read_text(encoding="utf-8")
32
+ spec = FactorComputationSpec.model_validate(payload)
33
+ result = PublishedFactor.model_validate(
34
+ request_definition(
35
+ server, token, "POST", "/factor/definitions", payload=spec.model_dump(mode="json")
36
+ )
37
+ )
38
+ output(result.model_dump(mode="json"))
39
+
40
+
41
+ @app.command("list")
42
+ @cli_errors
43
+ def list_definitions(
44
+ server: Server, token: Token, name: str | None = None, limit: int = 100, offset: int = 0
45
+ ) -> None:
46
+ """分页列出发布定义。"""
47
+ params: dict[str, str | int] = {"limit": limit, "offset": offset}
48
+ if name is not None:
49
+ params["name"] = name
50
+ output(request_definition(server, token, "GET", "/factor/definitions", params=params))
51
+
52
+
53
+ @app.command("fetch")
54
+ @cli_errors
55
+ def fetch(
56
+ name: str,
57
+ server: Server,
58
+ token: Token,
59
+ destination: Annotated[Path, typer.Option()],
60
+ version: str = "latest",
61
+ source: str = "local-python",
62
+ frequency: str = "1d",
63
+ ) -> None:
64
+ """获取并校验完整身份的定义文件,latest 在此固定。"""
65
+ result = PublishedFactor.model_validate(
66
+ request_definition(
67
+ server,
68
+ token,
69
+ "GET",
70
+ f"/factor/definitions/{quote(name, safe='')}/{quote(version, safe='')}",
71
+ params={"source": source, "frequency": frequency},
72
+ )
73
+ )
74
+ save_definition(destination, result.model_dump(mode="json"))
75
+ output(
76
+ {
77
+ "path": str(destination.resolve()),
78
+ "identity": result.definition.identity.model_dump(mode="json"),
79
+ "content_digest": result.content_digest,
80
+ }
81
+ )
@@ -0,0 +1,102 @@
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
+ from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
10
+
11
+ from quant_cli.app.factor.cli.factor_definition_cli import app as definitions
12
+ from quant_cli.app.factor.cli.local_selection_cli import register_selection_commands
13
+ from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
14
+ from quant_cli.app.factor.service.local_factor_service import run_local_factor
15
+ from quant_cli.common.constants import DEFAULT_DATABASE_PATH
16
+ from quant_cli.common.utils.cli_utils import cli_errors, output
17
+
18
+ app = typer.Typer(help="本地因子计算与归档", no_args_is_help=True)
19
+
20
+ app.add_typer(definitions, name="definition")
21
+ register_selection_commands(app)
22
+
23
+
24
+ @app.command("compute")
25
+ @cli_errors
26
+ def compute(
27
+ config: Annotated[Path, typer.Option(exists=True)],
28
+ source: Annotated[Path | None, typer.Option(exists=True)] = None,
29
+ definition: Annotated[list[Path] | None, typer.Option(exists=True)] = None,
30
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
31
+ ) -> None:
32
+ """从 JSON 请求和源码或下载定义执行本地因子,并默认归档。"""
33
+ import json
34
+
35
+ payload = json.loads(config.read_text(encoding="utf-8"))
36
+ if source is not None:
37
+ payload["source"] = source.read_text(encoding="utf-8")
38
+ if definition:
39
+ from quant_core.app.factor.schema.factor_publication_schema import PublishedFactor
40
+
41
+ if payload.get("source") or payload.get("definitions"):
42
+ raise ValueError("发布定义与源码/内嵌定义不能同时提供")
43
+ payload["definitions"] = [
44
+ PublishedFactor.model_validate_json(path.read_text(encoding="utf-8")).model_dump(
45
+ mode="json"
46
+ )
47
+ for path in definition
48
+ ]
49
+ request = LocalFactorRequest.model_validate(payload)
50
+ output(asyncio.run(run_local_factor(request, build_local_factor_store(database))))
51
+
52
+
53
+ @app.command("get")
54
+ @cli_errors
55
+ def get(
56
+ run: Annotated[UUID, typer.Option()],
57
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
58
+ ) -> None:
59
+ output(build_local_factor_store(database).get(run))
60
+
61
+
62
+ @app.command("list")
63
+ @cli_errors
64
+ def list_runs(
65
+ limit: int = 20,
66
+ offset: int = 0,
67
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
68
+ ) -> None:
69
+ output(
70
+ [
71
+ row.model_dump(mode="json")
72
+ for row in build_local_factor_store(database).list_runs(limit, offset)
73
+ ]
74
+ )
75
+
76
+
77
+ @app.command("import")
78
+ @cli_errors
79
+ def import_artifact(
80
+ artifact: Annotated[Path, typer.Option(exists=True)],
81
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
82
+ ) -> None:
83
+ """由单一归档入口导入独立 Worker 产物。"""
84
+ from quant_cli.app.factor.service.local_factor_service import import_factor_artifact
85
+
86
+ output(
87
+ import_factor_artifact(
88
+ artifact.read_text(encoding="utf-8"), build_local_factor_store(database)
89
+ )
90
+ )
91
+
92
+
93
+ @app.command("values")
94
+ @cli_errors
95
+ def values(
96
+ run: Annotated[UUID, typer.Option()],
97
+ limit: int = 100,
98
+ offset: int = 0,
99
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
100
+ ) -> None:
101
+ """分页查询归档因子值。"""
102
+ output(build_local_factor_store(database).list_values(run, limit, offset))
@@ -0,0 +1,58 @@
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.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 LocalSelectionRequest
15
+ from quant_cli.app.factor.service.local_selection_service import run_local_selection
16
+ from quant_cli.common.constants import DEFAULT_DATABASE_PATH
17
+ from quant_cli.common.utils.cli_utils import cli_errors, output
18
+
19
+
20
+ def register_selection_commands(app: typer.Typer) -> None:
21
+ @app.command("select")
22
+ @cli_errors
23
+ def select(
24
+ config: Annotated[Path, typer.Option(exists=True)],
25
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
26
+ ) -> None:
27
+ request = LocalSelectionRequest.model_validate_json(config.read_text(encoding="utf-8"))
28
+ output(
29
+ asyncio.run(
30
+ run_local_selection(
31
+ request,
32
+ build_local_factor_store(database),
33
+ build_local_selection_store(database),
34
+ )
35
+ )
36
+ )
37
+
38
+ @app.command("selection-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_selection_store(database).get(run))
45
+
46
+ @app.command("selection-list")
47
+ @cli_errors
48
+ def list_runs(
49
+ limit: int = 20,
50
+ offset: int = 0,
51
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
52
+ ) -> None:
53
+ output(
54
+ [
55
+ row.model_dump(mode="json")
56
+ for row in build_local_selection_store(database).list_runs(limit, offset)
57
+ ]
58
+ )
File without changes
@@ -0,0 +1,12 @@
1
+ """本地选股运行存取协议。"""
2
+
3
+ from typing import Protocol
4
+ from uuid import UUID
5
+
6
+ from quant_core.common.local_run import LocalRunRecord
7
+
8
+
9
+ class LocalSelectionStore(Protocol):
10
+ def save(self, record: LocalRunRecord) -> None: ...
11
+ def get(self, run_uuid: UUID) -> LocalRunRecord: ...
12
+ def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]: ...
@@ -0,0 +1,38 @@
1
+ """本地因子组装,不加载远端存储实现。"""
2
+
3
+ from pathlib import Path
4
+
5
+ from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
6
+
7
+ from quant_cli.app.factor.interface.local_selection_store import LocalSelectionStore
8
+ from quant_cli.app.factor.repository.factor_local_repository import FactorLocalRepository
9
+
10
+
11
+ def build_local_factor_store(path: Path) -> LocalFactorStore:
12
+ """返回本地因子结果存取。"""
13
+ return FactorLocalRepository(path)
14
+
15
+
16
+ # 公开函数入口不包一层重复转发;内部实现只由本域组装模块暴露。
17
+ from quant_cli.app.factor.service.local_factor_query_service import (
18
+ build_local_factor_query as build_local_factor_query,
19
+ )
20
+ from quant_cli.app.factor.service.local_factor_service import (
21
+ compute_local_factor as compute_local_factor,
22
+ )
23
+ from quant_cli.app.factor.service.local_factor_service import (
24
+ decode_factor_artifact as decode_factor_artifact,
25
+ )
26
+ from quant_cli.app.factor.service.local_factor_service import (
27
+ import_factor_artifact as import_factor_artifact,
28
+ )
29
+ from quant_cli.app.factor.service.local_factor_service import (
30
+ load_factor_result as load_factor_result,
31
+ )
32
+
33
+
34
+ def build_local_selection_store(path: Path) -> LocalSelectionStore:
35
+ """按显式路径组装选股归档能力。"""
36
+ from quant_cli.app.factor.repository.selection_local_repository import SelectionLocalRepository
37
+
38
+ return SelectionLocalRepository(path)
File without changes
@@ -0,0 +1,154 @@
1
+ """factor 本地运行存取;短事务由本地连接上下文管理。"""
2
+
3
+ from datetime import UTC
4
+ from pathlib import Path
5
+ from uuid import UUID
6
+
7
+ from quant_core.common.local_run import LocalRunRecord
8
+
9
+ from quant_cli.common.database.duckdb_connection import local_transaction
10
+
11
+
12
+ class FactorLocalRepository:
13
+ """只维护本领域的运行记录,查询使用数据库分页。"""
14
+
15
+ def __init__(self, path: Path) -> None:
16
+ self.path = path
17
+
18
+ def save(self, record: LocalRunRecord, values: dict | None = None) -> None:
19
+ with local_transaction(self.path) as connection:
20
+ connection.execute("""CREATE TABLE IF NOT EXISTS factor_local_run (
21
+ id BIGINT PRIMARY KEY, uuid UUID UNIQUE NOT NULL,
22
+ created_at TIMESTAMPTZ NOT NULL, document JSON NOT NULL)""")
23
+ existing = connection.execute(
24
+ "SELECT document FROM factor_local_run WHERE uuid = ?", [str(record.uuid)]
25
+ ).fetchone()
26
+ if existing is not None:
27
+ if LocalRunRecord.model_validate_json(existing[0]) != record:
28
+ raise ValueError("同一运行 UUID 的记录不可覆盖")
29
+ from decimal import Decimal
30
+
31
+ stored = connection.execute(
32
+ """SELECT name, as_of AT TIME ZONE 'UTC', instrument_id, value
33
+ FROM factor_local_value AS v JOIN factor_local_run AS r ON r.id = v.run_id
34
+ WHERE r.uuid = ?""",
35
+ [str(record.uuid)],
36
+ ).fetchall()
37
+ actual = {
38
+ (name, timestamp.replace(tzinfo=UTC), instrument): Decimal(value)
39
+ if value is not None
40
+ else None
41
+ for name, timestamp, instrument, value in stored
42
+ }
43
+ expected = {
44
+ (name, timestamp, instrument): value
45
+ for name, cells in (values or {}).items()
46
+ for (timestamp, instrument), value in cells.items()
47
+ }
48
+ if actual != expected:
49
+ raise ValueError("同一运行 UUID 的因子值不可覆盖")
50
+ return
51
+ connection.execute(
52
+ """INSERT INTO factor_local_run
53
+ SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM factor_local_run""",
54
+ [str(record.uuid), record.created_at, record.model_dump_json()],
55
+ )
56
+
57
+ connection.execute("""CREATE TABLE IF NOT EXISTS factor_local_value (
58
+ run_id BIGINT NOT NULL REFERENCES factor_local_run(id),
59
+ name VARCHAR NOT NULL, as_of TIMESTAMPTZ NOT NULL,
60
+ instrument_id VARCHAR NOT NULL, value VARCHAR,
61
+ PRIMARY KEY (run_id, name, as_of, instrument_id))""")
62
+ identity = connection.execute(
63
+ "SELECT id FROM factor_local_run WHERE uuid = ?", [str(record.uuid)]
64
+ ).fetchone()
65
+ assert identity is not None
66
+ if values:
67
+ connection.executemany(
68
+ "INSERT INTO factor_local_value VALUES (?, ?, ?, ?, ?)",
69
+ [
70
+ (
71
+ identity[0],
72
+ name,
73
+ timestamp,
74
+ instrument,
75
+ None if value is None else str(value),
76
+ )
77
+ for name, cells in values.items()
78
+ for (timestamp, instrument), value in cells.items()
79
+ ],
80
+ )
81
+
82
+ def get(self, run_uuid: UUID) -> LocalRunRecord:
83
+ if not self.path.is_file():
84
+ raise ValueError("运行记录不存在")
85
+ with local_transaction(self.path, read_only=True) as connection:
86
+ if not connection.execute(
87
+ "SELECT 1 FROM information_schema.tables WHERE table_name = ?",
88
+ ["factor_local_run"],
89
+ ).fetchall():
90
+ raise ValueError("运行记录不存在")
91
+ row = connection.execute(
92
+ "SELECT document FROM factor_local_run WHERE uuid = ?", [str(run_uuid)]
93
+ ).fetchone()
94
+ if row is None:
95
+ raise ValueError("运行记录不存在")
96
+ return LocalRunRecord.model_validate_json(row[0])
97
+
98
+ def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]:
99
+ if not 1 <= limit <= 1000 or offset < 0:
100
+ raise ValueError("分页参数非法")
101
+ if not self.path.is_file():
102
+ return []
103
+ with local_transaction(self.path, read_only=True) as connection:
104
+ if not connection.execute(
105
+ "SELECT 1 FROM information_schema.tables WHERE table_name = ?",
106
+ ["factor_local_run"],
107
+ ).fetchall():
108
+ return []
109
+ rows = connection.execute(
110
+ """SELECT document FROM factor_local_run
111
+ ORDER BY id DESC LIMIT ? OFFSET ?""",
112
+ [limit, offset],
113
+ ).fetchall()
114
+ return [LocalRunRecord.model_validate_json(row[0]) for row in rows]
115
+
116
+ def load_values(self, run_uuid: UUID) -> dict:
117
+ from decimal import Decimal
118
+
119
+ values: dict = {}
120
+ with local_transaction(self.path, read_only=True) as connection:
121
+ rows = connection.execute(
122
+ """SELECT name, as_of AT TIME ZONE 'UTC', instrument_id, value
123
+ FROM factor_local_value AS v JOIN factor_local_run AS r ON r.id = v.run_id
124
+ WHERE r.uuid = ? ORDER BY as_of, instrument_id""",
125
+ [str(run_uuid)],
126
+ ).fetchall()
127
+ for name, timestamp, instrument, value in rows:
128
+ values.setdefault(name, {})[(timestamp.replace(tzinfo=UTC), instrument)] = (
129
+ Decimal(value) if value is not None else None
130
+ )
131
+ return values
132
+
133
+ def list_values(self, run_uuid: UUID, limit: int = 100, offset: int = 0) -> list[dict]:
134
+ """分页返回因子值;不在 CLI 中一次展开全市场结果。"""
135
+ if not 1 <= limit <= 10000 or offset < 0:
136
+ raise ValueError("因子值分页参数非法")
137
+ self.get(run_uuid)
138
+ with local_transaction(self.path, read_only=True) as connection:
139
+ rows = connection.execute(
140
+ """SELECT name, as_of AT TIME ZONE 'UTC', instrument_id, value
141
+ FROM factor_local_value AS v JOIN factor_local_run AS r ON r.id = v.run_id
142
+ WHERE r.uuid = ? ORDER BY name, as_of, instrument_id
143
+ LIMIT ? OFFSET ?""",
144
+ [str(run_uuid), limit, offset],
145
+ ).fetchall()
146
+ return [
147
+ {
148
+ "name": name,
149
+ "as_of": timestamp.replace(tzinfo=UTC).isoformat(),
150
+ "instrument_id": instrument,
151
+ "value": value,
152
+ }
153
+ for name, timestamp, instrument, value in rows
154
+ ]