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,139 @@
1
+ """离线选股用例:资格与快照校验在应用层,评分只调用核心。"""
2
+
3
+ from datetime import datetime, time
4
+ from zoneinfo import ZoneInfo
5
+
6
+ from quant_core.app.factor.factor_dependency import build_factor_selection
7
+ from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
8
+ from quant_core.app.factor.schema.selection_report import build_selection_report
9
+ from quant_core.app.factor.schema.selection_types import (
10
+ FactorWeight,
11
+ NeutralizationMethod,
12
+ SelectionDefinition,
13
+ )
14
+ from quant_core.common.local_run import LocalRunRecord, RunStatus, content_digest
15
+
16
+ from quant_cli.app.data.service.dataset_load_service import load_dataset
17
+ from quant_cli.app.factor.interface.local_selection_store import LocalSelectionStore
18
+ from quant_cli.app.factor.schema.local_selection_schema import LocalSelectionRequest
19
+ from quant_cli.app.factor.service.local_factor_query_service import build_local_factor_query
20
+ from quant_cli.app.factor.service.local_factor_service import load_factor_result
21
+
22
+
23
+ async def compute_local_selection(
24
+ request: LocalSelectionRequest,
25
+ factor_store: LocalFactorStore,
26
+ ) -> LocalRunRecord:
27
+ """计算不创建连接;使用单一运行和单一完整行情快照。"""
28
+ record = LocalRunRecord(request=request.model_dump(mode="json"))
29
+ try:
30
+ market, _ = load_dataset(request.manifest_path, request.max_rows)
31
+ digest = content_digest(request.manifest_path.read_text(encoding="utf-8"))
32
+ factor_run, values = load_factor_result(factor_store, request.factor_run_uuid)
33
+ if factor_run.provenance.get("manifest_digest") != digest:
34
+ raise ValueError("因子与选股输入快照不同")
35
+ if factor_run.provenance.get("market_revision") != market.revision:
36
+ raise ValueError("因子与选股行情修订不同")
37
+ if not set(request.instrument_ids).issubset(factor_run.request.get("instrument_ids", ())):
38
+ raise ValueError("选股股票池不属于因子运行")
39
+ if (
40
+ not market.start.astimezone(ZoneInfo("Asia/Shanghai")).date()
41
+ <= request.target_as_of
42
+ <= market.end.astimezone(ZoneInfo("Asia/Shanghai")).date()
43
+ ):
44
+ raise ValueError("选股日期超出已准备快照")
45
+ query, aliases = await build_local_factor_query(
46
+ factor_run, values, market, market.end.date()
47
+ )
48
+ if any(f.name not in aliases for f in request.factors):
49
+ raise ValueError("因子运行不包含所需名称")
50
+ if any(f.transform.neutralization != NeutralizationMethod.NONE for f in request.factors):
51
+ raise ValueError("当前快照未准备行业/市值暴露,不能执行中性化")
52
+ definition = SelectionDefinition(
53
+ factors=tuple(
54
+ FactorWeight(aliases[f.name], f.weight, f.direction, f.transform)
55
+ for f in request.factors
56
+ ),
57
+ top_n=request.top_n,
58
+ missing_policy=request.missing_policy,
59
+ )
60
+ universe = []
61
+ exclusions = []
62
+ for iid in sorted(request.instrument_ids):
63
+ info = await market.get_instrument(iid)
64
+ reason = None
65
+ if info.listed_date is None:
66
+ reason = "unknown_listing"
67
+ elif info.listed_date > request.target_as_of or (
68
+ info.delisted_date and info.delisted_date < request.target_as_of
69
+ ):
70
+ reason = "not_listed"
71
+ facts = await market.list_daily_facts(iid, request.target_as_of, request.target_as_of)
72
+ if len(facts) != 1:
73
+ reason = reason or "unknown_status"
74
+ else:
75
+ fact = facts[0]
76
+ if request.exclude_st:
77
+ if fact.special_type is None:
78
+ reason = reason or "unknown_special_treatment"
79
+ elif fact.is_st:
80
+ reason = reason or "special_treatment"
81
+ if request.exclude_suspended:
82
+ if fact.suspension_status is None:
83
+ reason = reason or "unknown_suspension"
84
+ elif fact.suspension_status != "none":
85
+ reason = reason or "suspended"
86
+ if request.exclude_limit_up:
87
+ cn = ZoneInfo("Asia/Shanghai")
88
+ bars = await market.list_bars(
89
+ iid,
90
+ "1d",
91
+ max(market.start, datetime.combine(request.target_as_of, time.min, cn)),
92
+ min(market.end, datetime.combine(request.target_as_of, time.max, cn)),
93
+ limit=2,
94
+ )
95
+ if fact.limit_up is None or len(bars) != 1:
96
+ reason = reason or "unknown_price_limit"
97
+ elif bars[0].close >= fact.limit_up:
98
+ reason = reason or "at_limit_up"
99
+ if reason:
100
+ exclusions.append({"instrument_id": iid, "reason": reason})
101
+ else:
102
+ universe.append(iid)
103
+ snapshot = await query.get_snapshot(
104
+ [f.identity for f in definition.factors],
105
+ universe,
106
+ request.decision_time,
107
+ target_as_of=request.target_as_of,
108
+ dataset_revision=str(factor_run.uuid),
109
+ )
110
+ result = build_factor_selection().select(definition, universe, snapshot)
111
+ report = build_selection_report(definition, universe, snapshot, result).model_dump(
112
+ mode="json"
113
+ )
114
+ report["qualification_excluded"] = exclusions
115
+ report["requested_universe_size"] = len(request.instrument_ids)
116
+ record.provenance = {
117
+ "manifest_digest": digest,
118
+ "market_revision": market.revision,
119
+ "factor_run_uuid": str(factor_run.uuid),
120
+ "input_digest": factor_run.provenance["input_digest"],
121
+ "definition_digest": factor_run.provenance["definition_digest"],
122
+ "selection_request_digest": content_digest(request.model_dump_json()),
123
+ "facts_basis": "current_revision_history; historical knowledge revisions unavailable",
124
+ }
125
+ record.result = report
126
+ except Exception as exc:
127
+ record.status, record.error = RunStatus.FAILED, str(exc)
128
+ return record
129
+
130
+
131
+ async def run_local_selection(
132
+ request: LocalSelectionRequest,
133
+ factor_store: LocalFactorStore,
134
+ selection_store: LocalSelectionStore,
135
+ ) -> LocalRunRecord:
136
+ """完成计算后短事务归档;保存失败向调用方传播。"""
137
+ record = await compute_local_selection(request, factor_store)
138
+ selection_store.save(record)
139
+ return record
File without changes
File without changes
@@ -0,0 +1,69 @@
1
+ """独立 Worker 命令,不实现调度中心。"""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from quant_core.common.local_run import RunStatus
9
+
10
+ from quant_cli.app.job.schema.local_job_schema import LocalJobRequest
11
+ from quant_cli.app.job.service.local_job_service import execute_local_job
12
+ from quant_cli.common.constants import DEFAULT_DATABASE_PATH
13
+ from quant_cli.common.utils.cli_utils import cli_errors, output
14
+
15
+ app = typer.Typer(help="独立 Worker 执行与产物输出", no_args_is_help=True)
16
+
17
+
18
+ @app.command("execute")
19
+ @cli_errors
20
+ def execute(
21
+ config: Annotated[Path, typer.Option(exists=True)], output_dir: Annotated[Path, typer.Option()]
22
+ ) -> None:
23
+ record, path = asyncio.run(
24
+ execute_local_job(
25
+ LocalJobRequest.model_validate_json(config.read_text(encoding="utf-8")), output_dir
26
+ )
27
+ )
28
+ output({"run_uuid": str(record.uuid), "status": record.status.value, "artifact": str(path)})
29
+ if record.status == RunStatus.FAILED:
30
+ raise typer.Exit(1)
31
+
32
+
33
+ @app.command("import")
34
+ @cli_errors
35
+ def import_artifact(
36
+ artifact: Annotated[Path, typer.Option(exists=True)],
37
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
38
+ ) -> None:
39
+ """单一归档进程导入 Worker 结果,拒绝损坏或未知格式。"""
40
+ import json
41
+
42
+ from quant_core.common.local_run import LocalRunRecord, content_digest
43
+
44
+ from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
45
+ from quant_cli.app.factor.local_factor_dependency import (
46
+ build_local_factor_store,
47
+ import_factor_artifact,
48
+ )
49
+ from quant_cli.app.research.local_research_dependency import build_local_research_store
50
+
51
+ text = artifact.read_text(encoding="utf-8")
52
+ envelope = json.loads(text)
53
+ if envelope.get("format_version") != 1:
54
+ raise ValueError("不支持的产物版本")
55
+ canonical = json.dumps(envelope["payload"], sort_keys=True, ensure_ascii=False, allow_nan=False)
56
+ if envelope.get("sha256") != content_digest(canonical):
57
+ raise ValueError("产物摘要不匹配")
58
+ kind = envelope.get("kind")
59
+ if kind == "factor":
60
+ record = import_factor_artifact(text, build_local_factor_store(database))
61
+ elif kind in ("research", "backtest"):
62
+ record = LocalRunRecord.model_validate(envelope["payload"]["record"])
63
+ if kind == "research":
64
+ build_local_research_store(database).save(record)
65
+ else:
66
+ build_local_backtest_store(database).save(record)
67
+ else:
68
+ raise ValueError("不支持的产物类型")
69
+ output(record)
File without changes
@@ -0,0 +1,11 @@
1
+ """调度无关的本地 Worker 请求。"""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class LocalJobRequest(BaseModel):
9
+ model_config = ConfigDict(extra="forbid")
10
+ kind: Literal["factor", "research", "backtest"]
11
+ request: dict
File without changes
@@ -0,0 +1,60 @@
1
+ """调度无关的 Worker 执行入口;只写独立产物,不写共享 DuckDB。"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from quant_core.app.factor.schema.factor_artifact_schema import FactorArtifact, FactorArtifactValue
7
+ from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
8
+ from quant_core.common.local_run import LocalRunRecord, content_digest
9
+
10
+ from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
11
+ from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
12
+ from quant_cli.app.factor.local_factor_dependency import compute_local_factor
13
+ from quant_cli.app.job.schema.local_job_schema import LocalJobRequest
14
+ from quant_cli.app.research.schema.local_research_schema import LocalPythonResearchRequest
15
+ from quant_cli.app.research.service.local_research_execution_service import run_local_research
16
+
17
+
18
+ async def execute_local_job(job: LocalJobRequest, output_dir: Path) -> tuple[LocalRunRecord, Path]:
19
+ """执行独立任务并原子发布产物;上层调度负责重试、领取与状态登记。"""
20
+ if job.kind == "factor":
21
+ record, values = await compute_local_factor(LocalFactorRequest.model_validate(job.request))
22
+ payload = FactorArtifact(
23
+ record=record,
24
+ values=tuple(
25
+ FactorArtifactValue(
26
+ name=name, as_of=timestamp, instrument_id=instrument, value=value
27
+ )
28
+ for name, cells in values.items()
29
+ for (timestamp, instrument), value in cells.items()
30
+ ),
31
+ ).model_dump(mode="json")
32
+ elif job.kind == "research":
33
+ research = LocalPythonResearchRequest.model_validate(job.request)
34
+ record = await run_local_research(
35
+ research.research,
36
+ research.manifest_path,
37
+ research.max_rows,
38
+ factor_artifact_path=research.factor_artifact_path,
39
+ )
40
+ payload = {"record": record.model_dump(mode="json")}
41
+ else:
42
+ request = LocalBacktestRequest.model_validate(job.request)
43
+ if request.factor_run_uuid is not None:
44
+ raise ValueError("离线 Worker 回测需先提供独立因子产物,不能读取共享研究库")
45
+ record = await run_local_backtest(request)
46
+ payload = {"record": record.model_dump(mode="json")}
47
+ text = json.dumps(payload, sort_keys=True, ensure_ascii=False, allow_nan=False)
48
+ envelope = {
49
+ "format_version": 1,
50
+ "kind": job.kind,
51
+ "sha256": content_digest(text),
52
+ "payload": payload,
53
+ }
54
+ output_dir.mkdir(parents=True, exist_ok=True)
55
+ target = output_dir / f"{record.uuid}.json"
56
+ temporary = target.with_suffix(".tmp")
57
+ with temporary.open("x", encoding="utf-8") as stream:
58
+ json.dump(envelope, stream, ensure_ascii=False, allow_nan=False)
59
+ temporary.replace(target)
60
+ return record, target
@@ -0,0 +1,49 @@
1
+ """Quant 统一命令注册;领域业务逻辑留在所属模块。"""
2
+
3
+ import shutil
4
+ from importlib.resources import files
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from quant_cli.app.backtest.cli.local_backtest_cli import app as backtest
11
+ from quant_cli.app.data.cli.data_cli import app as data
12
+ from quant_cli.app.factor.cli.local_factor_cli import app as factor
13
+ from quant_cli.app.job.cli.local_job_cli import app as job
14
+ from quant_cli.app.research.cli.local_research_cli import app as research
15
+ from quant_cli.app.strategy.cli.strategy_cli import app as strategy
16
+ from quant_cli.common.utils.platform_utils import configure_utf8_output
17
+
18
+ app = typer.Typer(help="Power Quant 本地研究与计算", no_args_is_help=True)
19
+ app.add_typer(data, name="data")
20
+ app.add_typer(job, name="job")
21
+ app.add_typer(factor, name="factor")
22
+ app.add_typer(research, name="research")
23
+ app.add_typer(backtest, name="backtest")
24
+ app.add_typer(strategy, name="strategy")
25
+
26
+
27
+ @app.command()
28
+ def examples(
29
+ directory: Annotated[Path, typer.Option("--directory")] = Path("powerquant-examples"),
30
+ ) -> None:
31
+ """导出随包示例到新目录,保留现有文件避免覆盖用户配置。"""
32
+ if directory.exists():
33
+ raise typer.BadParameter("目标目录已存在,请指定新目录")
34
+ source = files("quant_cli").joinpath("examples")
35
+ if not source.is_dir():
36
+ # 开发源码入口与 wheel 都引用同一份 examples,不生成重复示例。
37
+ source = Path(__file__).resolve().parents[3] / "examples"
38
+ shutil.copytree(str(source), directory)
39
+ typer.echo(str(directory.resolve()))
40
+
41
+
42
+ def main() -> None:
43
+ """安装后的 pq 命令入口。"""
44
+ configure_utf8_output()
45
+ app()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
File without changes
File without changes
@@ -0,0 +1,93 @@
1
+ """本地研究检验与比较命令。"""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+ from uuid import UUID
6
+
7
+ import typer
8
+ from quant_core.app.factor.factor_dependency import build_factor_diagnostics
9
+
10
+ from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
11
+ from quant_cli.app.research.local_research_dependency import build_local_research_store
12
+ from quant_cli.app.research.schema.local_research_schema import LocalEvaluationRequest
13
+ from quant_cli.app.research.service.local_research_service import (
14
+ compare_local_research,
15
+ evaluate_local_factor,
16
+ )
17
+ from quant_cli.common.constants import DEFAULT_DATABASE_PATH
18
+ from quant_cli.common.utils.cli_utils import cli_errors, output
19
+
20
+ app = typer.Typer(help="本地研究检验与比较", no_args_is_help=True)
21
+
22
+
23
+ @app.command("evaluate")
24
+ @cli_errors
25
+ def evaluate(
26
+ config: Annotated[Path, typer.Option(exists=True)],
27
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
28
+ ) -> None:
29
+ request = LocalEvaluationRequest.model_validate_json(config.read_text(encoding="utf-8"))
30
+ record = evaluate_local_factor(
31
+ request, build_local_factor_store(database), build_factor_diagnostics()
32
+ )
33
+ build_local_research_store(database).save(record)
34
+ output(record)
35
+
36
+
37
+ @app.command("compare")
38
+ @cli_errors
39
+ def compare(
40
+ run: Annotated[list[UUID], typer.Option()],
41
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
42
+ ) -> None:
43
+ output(compare_local_research(build_local_research_store(database), tuple(run)))
44
+
45
+
46
+ @app.command("get")
47
+ @cli_errors
48
+ def get(
49
+ run: Annotated[UUID, typer.Option()],
50
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
51
+ ) -> None:
52
+ output(build_local_research_store(database).get(run))
53
+
54
+
55
+ @app.command("list")
56
+ @cli_errors
57
+ def list_runs(
58
+ limit: int = 20,
59
+ offset: int = 0,
60
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
61
+ ) -> None:
62
+ output(
63
+ [
64
+ row.model_dump(mode="json")
65
+ for row in build_local_research_store(database).list_runs(limit, offset)
66
+ ]
67
+ )
68
+
69
+
70
+ @app.command("run")
71
+ @cli_errors
72
+ def run(
73
+ config: Annotated[Path, typer.Option(exists=True)],
74
+ manifest: Annotated[Path, typer.Option(exists=True)],
75
+ factor_run: Annotated[UUID | None, typer.Option()] = None,
76
+ max_rows: int = 1000000,
77
+ database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
78
+ ) -> None:
79
+ """执行可信研究 Python 请求,默认归档结果。"""
80
+ import asyncio
81
+
82
+ from quant_core.app.research.schema.research_schema import ResearchRunRequest
83
+
84
+ from quant_cli.app.research.service.local_research_execution_service import run_local_research
85
+
86
+ request = ResearchRunRequest.model_validate_json(config.read_text(encoding="utf-8"))
87
+ record = asyncio.run(
88
+ run_local_research(
89
+ request, manifest, max_rows, build_local_factor_store(database), factor_run
90
+ )
91
+ )
92
+ build_local_research_store(database).save(record)
93
+ output(record)
@@ -0,0 +1,10 @@
1
+ """本地研究组装。"""
2
+
3
+ from pathlib import Path
4
+
5
+ from quant_cli.app.research.repository.research_local_repository import ResearchLocalRepository
6
+
7
+
8
+ def build_local_research_store(path: Path) -> ResearchLocalRepository:
9
+ """返回本地研究存取。"""
10
+ return ResearchLocalRepository(path)
File without changes
@@ -0,0 +1,67 @@
1
+ """research 本地运行存取;短事务由本地连接上下文管理。"""
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 ResearchLocalRepository:
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 research_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 research_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 research_local_run
31
+ SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM research_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
+ ["research_local_run"],
42
+ ).fetchall():
43
+ raise ValueError("运行记录不存在")
44
+ row = connection.execute(
45
+ "SELECT document FROM research_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
+ ["research_local_run"],
60
+ ).fetchall():
61
+ return []
62
+ rows = connection.execute(
63
+ """SELECT document FROM research_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
@@ -0,0 +1,26 @@
1
+ """本地研究输入:引用已归档因子运行,标签独立传入。"""
2
+
3
+ from pathlib import Path
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+ from quant_core.app.research.schema.dataset_schema import ResearchLabel
8
+ from quant_core.app.research.schema.research_schema import ResearchRunRequest
9
+
10
+
11
+ class LocalEvaluationRequest(BaseModel):
12
+ model_config = ConfigDict(extra="forbid")
13
+ factor_run_uuid: UUID
14
+ factor_name: str
15
+ labels: tuple[ResearchLabel, ...]
16
+ quantiles: int = Field(default=5, ge=2, le=100)
17
+
18
+
19
+ class LocalPythonResearchRequest(BaseModel):
20
+ """独立 Worker 的 Python 研究请求。"""
21
+
22
+ model_config = ConfigDict(extra="forbid")
23
+ research: ResearchRunRequest
24
+ manifest_path: Path
25
+ max_rows: int = Field(default=1000000, ge=1)
26
+ factor_artifact_path: Path | None = None
File without changes
@@ -0,0 +1,105 @@
1
+ """本地 Python 研究运行,复用管理入口的研究执行函数。"""
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+ from uuid import UUID
6
+
7
+ from quant_core.app.factor.factor_dependency import build_factor_diagnostics, build_factor_selection
8
+ from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
9
+ from quant_core.app.research.adapter.market_label_query import MarketLabelQuery
10
+ from quant_core.app.research.schema.research_schema import ResearchRunRequest
11
+ from quant_core.app.research.service.research_dataset_service import ResearchDatasetService
12
+ from quant_core.app.research.service.research_evaluation_service import ResearchEvaluationService
13
+ from quant_core.app.research.service.research_service import execute_research
14
+ from quant_core.common.local_run import LocalRunRecord, RunStatus, content_digest
15
+
16
+ from quant_cli.app.data.service.dataset_load_service import load_dataset
17
+ from quant_cli.app.factor.local_factor_dependency import (
18
+ build_local_factor_query,
19
+ decode_factor_artifact,
20
+ load_factor_result,
21
+ )
22
+ from quant_cli.common.utils.runtime_environment import runtime_versions
23
+
24
+
25
+ class SnapshotRevision:
26
+ """已加载快照的修订查询,不访问外部系统。"""
27
+
28
+ def __init__(self, revision: str, first: date, last: date) -> None:
29
+ self.revision, self.first, self.last = revision, first, last
30
+
31
+ async def snapshot(self, start: date, end: date) -> dict[str, str]:
32
+ if start < self.first or end > self.last:
33
+ raise ValueError("研究范围超出输入快照")
34
+ return {"market": self.revision}
35
+
36
+
37
+ async def run_local_research(
38
+ request: ResearchRunRequest,
39
+ manifest: Path,
40
+ max_rows: int,
41
+ factor_store: LocalFactorStore | None = None,
42
+ factor_run_uuid: UUID | None = None,
43
+ factor_artifact_path: Path | None = None,
44
+ ) -> LocalRunRecord:
45
+ """显式研究源码、市场快照和可选因子运行,返回待归档记录。"""
46
+ manifest = manifest.expanduser().resolve()
47
+ record = LocalRunRecord(
48
+ request={
49
+ **request.model_dump(mode="json"),
50
+ "manifest_path": str(manifest),
51
+ "max_rows": max_rows,
52
+ "factor_artifact_path": str(factor_artifact_path.resolve())
53
+ if factor_artifact_path
54
+ else None,
55
+ "factor_run_uuid": str(factor_run_uuid) if factor_run_uuid else None,
56
+ },
57
+ provenance={"environment": runtime_versions()},
58
+ )
59
+ try:
60
+ market, _ = load_dataset(manifest, max_rows)
61
+ record.provenance.update(
62
+ manifest_digest=content_digest(manifest.read_text(encoding="utf-8")),
63
+ market_revision=market.revision,
64
+ )
65
+ factors = None
66
+ if factor_run_uuid and factor_artifact_path:
67
+ raise ValueError("只允许一个因子结果来源")
68
+ if factor_run_uuid or factor_artifact_path:
69
+ if factor_artifact_path:
70
+ factor_record, values = decode_factor_artifact(
71
+ factor_artifact_path.read_text(encoding="utf-8")
72
+ )
73
+ if factor_record.status != RunStatus.COMPLETED:
74
+ raise ValueError("因子产物未成功")
75
+ else:
76
+ if factor_store is None or factor_run_uuid is None:
77
+ raise ValueError("因子运行需要显式读取能力")
78
+ factor_record, values = load_factor_result(factor_store, factor_run_uuid)
79
+ record.provenance["factor_run_uuid"] = str(factor_record.uuid)
80
+ if factor_record.provenance["market_revision"] != market.revision:
81
+ raise ValueError("研究与因子使用的行情修订不同")
82
+ factors, _ = await build_local_factor_query(
83
+ factor_record, values, market, market.end.date()
84
+ )
85
+ result = await execute_research(
86
+ request,
87
+ market,
88
+ SnapshotRevision(market.revision, market.start.date(), market.end.date()),
89
+ factors_query=factors,
90
+ dataset_factory=lambda factors: ResearchDatasetService(
91
+ factors, MarketLabelQuery(market)
92
+ ),
93
+ evaluation=ResearchEvaluationService(
94
+ build_factor_selection(), build_factor_diagnostics()
95
+ ),
96
+ )
97
+ record.result = result.model_dump(mode="json")
98
+ if result.error:
99
+ record.status, record.error = RunStatus.FAILED, result.error
100
+ except Exception as exc:
101
+ import logging
102
+
103
+ logging.getLogger(__name__).exception("本地研究执行失败")
104
+ record.status, record.error = RunStatus.FAILED, str(exc)
105
+ return record