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,67 @@
1
+ """selection 本地运行存取;短事务由本地连接上下文管理。"""
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 SelectionLocalRepository:
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 selection_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 selection_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 selection_local_run
31
+ SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM selection_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
+ ["selection_local_run"],
42
+ ).fetchall():
43
+ raise ValueError("运行记录不存在")
44
+ row = connection.execute(
45
+ "SELECT document FROM selection_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
+ ["selection_local_run"],
60
+ ).fetchall():
61
+ return []
62
+ rows = connection.execute(
63
+ """SELECT document FROM selection_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]
@@ -0,0 +1,56 @@
1
+ """显式单次因子运行的本地选股请求。"""
2
+
3
+ from datetime import date, datetime
4
+ from decimal import Decimal
5
+ from pathlib import Path
6
+ from uuid import UUID
7
+ from zoneinfo import ZoneInfo
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
10
+ from quant_core.app.factor.schema.selection_types import (
11
+ FactorTransform,
12
+ MissingPolicy,
13
+ SortDirection,
14
+ )
15
+
16
+
17
+ class LocalSelectionFactor(BaseModel):
18
+ model_config = ConfigDict(extra="forbid", frozen=True)
19
+ name: str = Field(min_length=1)
20
+ weight: Decimal = Field(gt=0, allow_inf_nan=False)
21
+ direction: SortDirection = SortDirection.DESC
22
+ transform: FactorTransform = Field(default_factory=FactorTransform)
23
+
24
+
25
+ class LocalSelectionRequest(BaseModel):
26
+ model_config = ConfigDict(extra="forbid", frozen=True)
27
+ manifest_path: Path
28
+ factor_run_uuid: UUID
29
+ instrument_ids: tuple[str, ...] = Field(min_length=1)
30
+ factors: tuple[LocalSelectionFactor, ...] = Field(min_length=1)
31
+ decision_time: datetime
32
+ target_as_of: date
33
+ top_n: int = Field(default=10, ge=1)
34
+ missing_policy: MissingPolicy = MissingPolicy.EXCLUDE
35
+ exclude_st: bool = True
36
+ exclude_suspended: bool = True
37
+ exclude_limit_up: bool = True
38
+ max_rows: int = Field(default=1000000, ge=1)
39
+
40
+ @field_validator("manifest_path")
41
+ @classmethod
42
+ def absolute_path(cls, value: Path) -> Path:
43
+ return value.expanduser().resolve()
44
+
45
+ @model_validator(mode="after")
46
+ def validate_request(self) -> "LocalSelectionRequest":
47
+ if self.decision_time.utcoffset() is None:
48
+ raise ValueError("decision_time 必须带时区")
49
+ if self.target_as_of > self.decision_time.astimezone(ZoneInfo("Asia/Shanghai")).date():
50
+ raise ValueError("选股日期在决策时点之后")
51
+ if len(set(self.instrument_ids)) != len(self.instrument_ids):
52
+ raise ValueError("股票池重复")
53
+ names = [factor.name for factor in self.factors]
54
+ if len(set(names)) != len(names):
55
+ raise ValueError("选股因子重复")
56
+ return self
File without changes
@@ -0,0 +1,181 @@
1
+ """PQFactor 本地入口:显式快照、交易时间轴与历史参与池,计算不联网。"""
2
+
3
+ import hashlib
4
+ import json
5
+ from datetime import date, datetime, time, timedelta
6
+ from decimal import Decimal
7
+ from pathlib import Path
8
+ from zoneinfo import ZoneInfo
9
+
10
+ from pqdata import MarketClient
11
+ from pqdata.schema.history_schema import HistoryFactsQuery
12
+ from quant_core.app.data.schema.adjustment_schema import BarViewRequest
13
+ from quant_core.app.data.service.calendar_coverage_service import build_calendar_coverage
14
+ from quant_core.app.factor.computation.factor_batch_evaluator import (
15
+ FactorBatchResult,
16
+ execute_factors,
17
+ )
18
+ from quant_core.app.factor.computation.factor_expression import FactorExpression
19
+ from quant_core.app.factor.schema.factor_input_schema import FactorInputRequest
20
+ from quant_core.app.factor.schema.factor_panel import FactorPanel, FactorPanelRow
21
+ from quant_core.app.factor.schema.panel_policy import PANEL_UNITS
22
+
23
+ from quant_cli.app.data.service.bar_cache_service import load_bar_snapshot
24
+ from quant_cli.app.data.service.dataset_prepare_service import PreparedDataset, prepare_dataset
25
+
26
+
27
+ async def prepare_local_factor_data(
28
+ factors: dict[str, FactorExpression],
29
+ client: MarketClient,
30
+ *,
31
+ instrument_ids: tuple[str, ...],
32
+ start: date,
33
+ end: date,
34
+ view: BarViewRequest,
35
+ cache_dir: Path,
36
+ max_rows: int,
37
+ max_history_days: int = 3660,
38
+ ) -> PreparedDataset:
39
+ """显式在线准备:按交易日历解析预热,复用回测数据准备和本地缓存。"""
40
+ if not factors or not instrument_ids or start > end or max_history_days < 1:
41
+ raise ValueError("因子数据准备参数非法")
42
+ lookback = max(factor.lookback for factor in factors.values())
43
+ fields = tuple(sorted(set().union(*(factor.required_fields for factor in factors.values()))))
44
+ FactorInputRequest(
45
+ instrument_ids=instrument_ids,
46
+ start=start,
47
+ end=end,
48
+ fields=fields,
49
+ lookback=lookback,
50
+ view=view,
51
+ )
52
+ if set(fields) - PANEL_UNITS.keys() or view.known_at is not None:
53
+ raise ValueError("当前快照仅支持量价字段和当前修订")
54
+ span = min(max_history_days, lookback * 3 + 7)
55
+ revision = None
56
+ while True:
57
+ lower = start - timedelta(days=span)
58
+ facts = await client.get_history_facts(
59
+ HistoryFactsQuery(
60
+ instrument_id=instrument_ids[0],
61
+ start=lower,
62
+ end=end,
63
+ revision=revision,
64
+ )
65
+ )
66
+ if revision is not None and facts.revision != revision:
67
+ raise ValueError("准备期间数据修订发生变化")
68
+ revision = facts.revision
69
+ coverage = build_calendar_coverage(
70
+ facts.instrument.exchange, lower, end, list(facts.calendar)
71
+ )
72
+ coverage.require_complete()
73
+ days = coverage.open_days
74
+ output = [day for day in days if start <= day <= end]
75
+ if not output:
76
+ raise ValueError("输出区间没有交易日")
77
+ first = days.index(output[0])
78
+ if first >= lookback - 1:
79
+ history_start = days[first - lookback + 1]
80
+ break
81
+ if span >= max_history_days:
82
+ raise ValueError("已达到历史查询上限,预热交易日仍不足")
83
+ span = min(span * 2, max_history_days)
84
+ cn = ZoneInfo("Asia/Shanghai")
85
+ if view.adjustment_type.value == "pre" and (
86
+ view.anchor_mode is None
87
+ or view.anchor_mode.value != "fixed_date"
88
+ or view.anchor_date is None
89
+ or not history_start <= view.anchor_date <= end
90
+ ):
91
+ raise ValueError("前复权需要准备范围内的固定日期锚点")
92
+ prepared = await prepare_dataset(
93
+ client,
94
+ instrument_ids=instrument_ids,
95
+ frequency="1d",
96
+ execution_frequency="1d",
97
+ start=datetime.combine(history_start, time.min, cn),
98
+ end=datetime.combine(end, time.max, cn),
99
+ adjustment_type=view.adjustment_type,
100
+ cache_dir=cache_dir,
101
+ lookback_days=0,
102
+ max_rows=max_rows,
103
+ revision=revision,
104
+ )
105
+ return prepared
106
+
107
+
108
+ def execute_cached_factors(
109
+ factors: dict[str, FactorExpression],
110
+ manifest_path: Path,
111
+ *,
112
+ calendar: tuple[datetime, ...],
113
+ universe: dict[datetime, tuple[str, ...]],
114
+ start: datetime,
115
+ end: datetime,
116
+ max_rows: int,
117
+ output_instruments: tuple[str, ...] | None = None,
118
+ ) -> FactorBatchResult:
119
+ """原始价格快照试算;不声称复原供应商修订之前的历史知识状态。"""
120
+ snapshot, bars = load_bar_snapshot(manifest_path, max_rows)
121
+ query = snapshot.query
122
+ if query.frequency != "1d":
123
+ raise ValueError("首批本地公式入口仅支持日线")
124
+ if not calendar or tuple(sorted(set(calendar))) != calendar or set(calendar) != set(universe):
125
+ raise ValueError("必须提供有序完整交易日轴和逐日参与池")
126
+ if (
127
+ calendar[0] < query.start
128
+ or calendar[-1] > query.end
129
+ or start < query.start
130
+ or end > query.end
131
+ ):
132
+ raise ValueError("计算时间范围超出已准备快照")
133
+ ids = set(query.instrument_ids)
134
+ if any(not set(members).issubset(ids) for members in universe.values()):
135
+ raise ValueError("历史参与池包含快照未准备的证券")
136
+ required = set().union(*(factor.required_fields for factor in factors.values()))
137
+ if not required.issubset(PANEL_UNITS):
138
+ raise ValueError("本地行情快照未提供所需非量价字段")
139
+ lookup = {(bar.timestamp, bar.instrument_id): bar for bar in bars}
140
+ if any(timestamp not in universe for timestamp, _ in lookup):
141
+ raise ValueError("行情时间不属于提供的交易日轴")
142
+ rows = []
143
+ for timestamp in calendar:
144
+ for instrument_id in sorted(ids):
145
+ bar = lookup.get((timestamp, instrument_id))
146
+ fields: dict[str, Decimal | None] = dict.fromkeys(required)
147
+ if bar is not None and not bar.suspended:
148
+ for field in required:
149
+ if field == "vwap":
150
+ fields[field] = (
151
+ bar.amount / bar.volume
152
+ if bar.amount is not None and bar.volume > 0
153
+ else None
154
+ )
155
+ else:
156
+ value = getattr(bar, field)
157
+ fields[field] = Decimal(value) if value is not None else None
158
+ rows.append(
159
+ FactorPanelRow(
160
+ instrument_id=instrument_id,
161
+ as_of=timestamp,
162
+ available_at=query.end,
163
+ eligible=instrument_id in universe[timestamp],
164
+ fields=fields,
165
+ )
166
+ )
167
+ evidence = {key.isoformat(): sorted(value) for key, value in sorted(universe.items())}
168
+ panel = FactorPanel(
169
+ decision_time=query.end,
170
+ price_basis="raw",
171
+ universe_ref="explicit-historical-universe",
172
+ universe_revision=hashlib.sha256(json.dumps(evidence, sort_keys=True).encode()).hexdigest(),
173
+ market_revision=hashlib.sha256(
174
+ json.dumps(snapshot.model_dump(mode="json"), sort_keys=True).encode()
175
+ ).hexdigest(),
176
+ units={field: PANEL_UNITS[field] for field in required},
177
+ rows=tuple(rows),
178
+ )
179
+ return execute_factors(
180
+ factors, panel, start=start, end=end, output_instruments=output_instruments
181
+ )
@@ -0,0 +1,146 @@
1
+ """固定本地结果的 PIT 查询;不访问数据库或补算因子。"""
2
+
3
+ from datetime import date, datetime, time
4
+ from zoneinfo import ZoneInfo
5
+
6
+ from quant_core.app.data.interface.market_data_query import IMarketDataQuery
7
+ from quant_core.app.factor.constants import FactorFreshness, FactorSourceKind, FactorStatus
8
+ from quant_core.app.factor.schema.factor_contract import (
9
+ FactorIdentity,
10
+ FactorProvenance,
11
+ FactorSnapshot,
12
+ FactorSnapshotSelection,
13
+ FactorValue,
14
+ )
15
+ from quant_core.common.local_run import LocalRunRecord
16
+
17
+ _CN = ZoneInfo("Asia/Shanghai")
18
+
19
+
20
+ class LocalFactorQuery:
21
+ """持有一次运行的不可变行,按显式交易日历过滤可见性。"""
22
+
23
+ def __init__(
24
+ self, rows: tuple[FactorValue, ...], revision: str, trading_days: tuple[date, ...]
25
+ ) -> None:
26
+ self.rows, self.revision, self.trading_days = rows, revision, trading_days
27
+
28
+ def _check_revision(self, revision: str | None) -> None:
29
+ if revision is not None and revision != self.revision:
30
+ raise ValueError("本地因子运行批次不匹配")
31
+
32
+ async def get_snapshot(
33
+ self,
34
+ identities: list[FactorIdentity],
35
+ instrument_ids: list[str],
36
+ decision_time: datetime,
37
+ *,
38
+ target_as_of: date | None = None,
39
+ freshness: FactorFreshness = FactorFreshness.EXACT,
40
+ dataset_revision: str | None = None,
41
+ ) -> FactorSnapshot:
42
+ self._check_revision(dataset_revision)
43
+ decision_day = decision_time.astimezone(_CN).date()
44
+ target = target_as_of or max(
45
+ (day for day in self.trading_days if day < decision_day), default=decision_day
46
+ )
47
+ eligible = [
48
+ row
49
+ for row in self.rows
50
+ if row.identity in identities
51
+ and row.instrument_id in instrument_ids
52
+ and row.available_at <= decision_time
53
+ and row.as_of.astimezone(_CN).date() <= target
54
+ ]
55
+ if freshness == FactorFreshness.EXACT:
56
+ eligible = [row for row in eligible if row.as_of.astimezone(_CN).date() == target]
57
+ latest = {}
58
+ for row in sorted(eligible, key=lambda item: item.as_of):
59
+ latest[(row.identity.logical_key, row.instrument_id)] = row
60
+ return FactorSnapshot(
61
+ decision_time=decision_time,
62
+ values=tuple(latest.values()),
63
+ selection=FactorSnapshotSelection(
64
+ identities=tuple(identities),
65
+ instrument_ids=tuple(instrument_ids),
66
+ target_as_of=target,
67
+ freshness=freshness,
68
+ dataset_revision=self.revision,
69
+ ),
70
+ )
71
+
72
+ async def get_series(
73
+ self,
74
+ identity: FactorIdentity,
75
+ instrument_id: str,
76
+ start: datetime,
77
+ end: datetime,
78
+ decision_time: datetime,
79
+ *,
80
+ dataset_revision: str | None = None,
81
+ ) -> tuple[FactorValue, ...]:
82
+ self._check_revision(dataset_revision)
83
+ return tuple(
84
+ row
85
+ for row in self.rows
86
+ if row.identity == identity
87
+ and row.instrument_id == instrument_id
88
+ and start <= row.as_of <= end
89
+ and row.available_at <= decision_time
90
+ )
91
+
92
+
93
+ async def build_local_factor_query(
94
+ record: LocalRunRecord, values: dict, market: IMarketDataQuery, calendar_end: date
95
+ ) -> tuple[LocalFactorQuery, dict[str, FactorIdentity]]:
96
+ """日线结果保守采用下一交易日开盘可见;缺后续日历时拒绝推测。"""
97
+ revision = str(record.uuid)
98
+ aliases = {
99
+ name: FactorIdentity(
100
+ name=name,
101
+ source="local-python",
102
+ definition_version=record.provenance["definition_digest"],
103
+ )
104
+ for name in values
105
+ }
106
+ if record.provenance.get("published_definitions"):
107
+ aliases = {
108
+ name: FactorIdentity.model_validate(identity)
109
+ for name, identity in record.provenance["published_definitions"].items()
110
+ }
111
+ if set(aliases) != set(values):
112
+ raise ValueError("发布因子身份与结果名称不匹配")
113
+ rows = []
114
+ calendars: dict[str, list[date]] = {}
115
+ for name, cells in values.items():
116
+ for (timestamp, instrument), value in sorted(cells.items()):
117
+ day = timestamp.astimezone(_CN).date()
118
+ if instrument not in calendars:
119
+ info = await market.get_instrument(instrument)
120
+ calendar = await market.list_trading_days(info.exchange, day, calendar_end)
121
+ calendars[instrument] = sorted(
122
+ {item.trade_date for item in calendar if item.is_open}
123
+ )
124
+ future = [item for item in calendars[instrument] if item > day]
125
+ if not future:
126
+ # 最后一个快照交易日的结果不能凭空生成后续可见时间。
127
+ continue
128
+ rows.append(
129
+ FactorValue(
130
+ identity=aliases[name],
131
+ instrument_id=instrument,
132
+ as_of=timestamp,
133
+ available_at=datetime.combine(future[0], time(9, 30), _CN),
134
+ value=value,
135
+ status=FactorStatus.AVAILABLE if value is not None else FactorStatus.MISSING,
136
+ provenance=FactorProvenance(
137
+ source_kind=FactorSourceKind.LOCAL_CALCULATOR,
138
+ provider_id="local-factor",
139
+ dataset_revision=revision,
140
+ content_digest=record.provenance["definition_digest"],
141
+ ),
142
+ )
143
+ )
144
+ return LocalFactorQuery(
145
+ tuple(rows), revision, tuple(sorted({day for days in calendars.values() for day in days}))
146
+ ), aliases
@@ -0,0 +1,120 @@
1
+ """本地因子公开用例:计算不打开数据库,归档依赖显式存取协议。"""
2
+
3
+ import logging
4
+ from uuid import UUID
5
+
6
+ from quant_core.app.factor.computation.factor_source_loader import load_formulas
7
+ from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
8
+ from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
9
+ from quant_core.app.factor.service.factor_local_execution_service import execute_local_factors
10
+ from quant_core.common.local_run import LocalRunRecord, RunStatus, content_digest
11
+
12
+ from quant_cli.app.data.service.dataset_load_service import load_dataset
13
+ from quant_cli.common.utils.runtime_environment import runtime_versions
14
+
15
+
16
+ async def compute_local_factor(request: LocalFactorRequest) -> tuple[LocalRunRecord, dict]:
17
+ """复用既有输入与算子,返回结果和来源;计算失败形成可归档记录。"""
18
+ record = LocalRunRecord(
19
+ request=request.model_dump(mode="json"),
20
+ provenance={
21
+ "source_digest": content_digest(request.source),
22
+ "environment": runtime_versions(),
23
+ },
24
+ )
25
+ try:
26
+ manifest = request.manifest_path.read_text(encoding="utf-8")
27
+ market, _ = load_dataset(request.manifest_path, request.max_rows)
28
+ record.provenance.update(
29
+ manifest_digest=content_digest(manifest), market_revision=market.revision
30
+ )
31
+ if request.definitions:
32
+ from quant_core.app.factor.service.published_factor_execution_service import (
33
+ execute_published_factors,
34
+ )
35
+
36
+ result = await execute_published_factors(request, market)
37
+ record.provenance["published_definitions"] = {
38
+ item.definition.identity.name: item.definition.identity.model_dump(mode="json")
39
+ for item in request.definitions
40
+ }
41
+ record.provenance["published_digests"] = {
42
+ item.definition.identity.name: item.content_digest for item in request.definitions
43
+ }
44
+ else:
45
+ result = await execute_local_factors(
46
+ load_formulas(request.source),
47
+ market,
48
+ instrument_ids=request.instrument_ids,
49
+ start=request.start,
50
+ end=request.end,
51
+ view=request.view,
52
+ exclude_st=request.exclude_st,
53
+ )
54
+ record.provenance.update(
55
+ input_digest=result.input_digest, definition_digest=result.definition_digest
56
+ )
57
+ record.result = {
58
+ "factors": list(result.values),
59
+ "lookback": result.lookback,
60
+ "cells": sum(len(cells) for cells in result.values.values()),
61
+ }
62
+ return record, result.values
63
+ except Exception as exc:
64
+ logging.getLogger(__name__).exception("本地计算失败")
65
+ record.status, record.error = RunStatus.FAILED, str(exc)
66
+ return record, {}
67
+
68
+
69
+ async def run_local_factor(request: LocalFactorRequest, store: LocalFactorStore) -> LocalRunRecord:
70
+ """先计算、后短事务归档;归档失败直接传播,不宣称计算任务成功。"""
71
+ record, values = await compute_local_factor(request)
72
+ store.save(record, values)
73
+ return record
74
+
75
+
76
+ def load_factor_result(store: LocalFactorStore, run_uuid: UUID) -> tuple[LocalRunRecord, dict]:
77
+ """研究与回测通过本公开用例读取因子产物。"""
78
+ record = store.get(run_uuid)
79
+ if record.status != RunStatus.COMPLETED:
80
+ raise ValueError("因子运行未成功")
81
+ return record, store.load_values(run_uuid)
82
+
83
+
84
+ def decode_factor_artifact(text: str) -> tuple[LocalRunRecord, dict]:
85
+ """校验 Worker 产物后由单写入入口归档,不信任外部文件元数据。"""
86
+ import json
87
+
88
+ from quant_core.app.factor.schema.factor_artifact_schema import FactorArtifact
89
+
90
+ envelope = json.loads(text)
91
+ if envelope.get("format_version") != 1 or envelope.get("kind") != "factor":
92
+ raise ValueError("不支持的因子产物格式")
93
+ payload = envelope["payload"]
94
+ canonical = json.dumps(payload, sort_keys=True, ensure_ascii=False, allow_nan=False)
95
+ if envelope.get("sha256") != content_digest(canonical):
96
+ raise ValueError("因子产物摘要不匹配")
97
+ artifact = FactorArtifact.model_validate(payload)
98
+ values: dict = {}
99
+ for row in artifact.values:
100
+ cells = values.setdefault(row.name, {})
101
+ key = (row.as_of, row.instrument_id)
102
+ if key in cells:
103
+ raise ValueError("因子产物包含重复单元")
104
+ cells[key] = row.value
105
+ if artifact.record.status == RunStatus.COMPLETED:
106
+ if (
107
+ set(values) != set(artifact.record.result["factors"])
108
+ or sum(len(cells) for cells in values.values()) != artifact.record.result["cells"]
109
+ ):
110
+ raise ValueError("因子产物与运行摘要不一致")
111
+ elif values:
112
+ raise ValueError("失败产物不得包含成功因子值")
113
+ return artifact.record, values
114
+
115
+
116
+ def import_factor_artifact(text: str, store: LocalFactorStore) -> LocalRunRecord:
117
+ """由单一归档入口导入已校验产物。"""
118
+ record, values = decode_factor_artifact(text)
119
+ store.save(record, values)
120
+ return record