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,189 @@
1
+ """全市场收盘后因子试算;只通过 SDK 读取,不发布因子、不下单。
2
+
3
+ 在 apps/quant_cli 运行:python -m examples.full_market_screen --day YYYY-MM-DD
4
+ 该示例使用当前修订的数据,不构成历史 PIT 回测或可成交性保证。
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ import csv
10
+ import hashlib
11
+ import json
12
+ from collections import Counter
13
+ from datetime import date, datetime, time, timedelta
14
+ from decimal import Decimal
15
+ from pathlib import Path
16
+ from zoneinfo import ZoneInfo
17
+
18
+ from pqdata.schema.bar_schema import BarQuery
19
+ from quant_core.app.factor.computation.python_factor_executor import execute_python_panel
20
+ from quant_core.app.factor.schema.factor_computation_schema import FactorComputationSpec
21
+ from quant_core.app.factor.schema.factor_panel import FactorPanel, FactorPanelRow
22
+
23
+ from quant_cli.app.data.data_dependency import build_market_query
24
+ from quant_cli.common.utils.platform_utils import configure_utf8_output
25
+
26
+ SOURCE = """def compute_panel(panel, parameters):
27
+ result = {}
28
+ for row in panel.rows:
29
+ f = row.fields
30
+ result[(row.as_of, row.instrument_id)] = (
31
+ f["close"] / f["open"] - 1 if row.eligible else None
32
+ )
33
+ return result
34
+ """
35
+
36
+
37
+ async def run(day: date, output: Path, minimum_amount: Decimal, top: int) -> dict:
38
+ """固定行情修订批量读取,调用现有面板执行器并保存完整审计结果。"""
39
+ cn = ZoneInfo("Asia/Shanghai")
40
+ start = datetime.combine(day, time.min, cn)
41
+ end = datetime.combine(day, time.max, cn)
42
+ observed_at = datetime.now(cn)
43
+ if observed_at <= end:
44
+ raise ValueError("只支持已结束的历史交易日")
45
+ async with build_market_query() as client:
46
+ revision = (await client.snapshot(day, day))["canonical"]
47
+ catalog = await client.search_instruments(limit=10000)
48
+ if len(catalog) >= 10000:
49
+ raise ValueError("标的目录可能截断,拒绝冒充全市场")
50
+ stocks = [i for i in catalog if i.instrument_type == "stock"]
51
+ unknown_listing = [i.instrument_id for i in stocks if i.listed_date is None]
52
+ universe = {
53
+ i.instrument_id: i
54
+ for i in stocks
55
+ if i.listed_date is not None
56
+ and i.listed_date <= day
57
+ and (i.delisted_date is None or i.delisted_date >= day)
58
+ }
59
+ ids = sorted(universe)
60
+ bars = {}
61
+ for offset in range(0, len(ids), 1000):
62
+ query = BarQuery(
63
+ instrument_ids=ids[offset : offset + 1000],
64
+ frequency="1d",
65
+ start=start,
66
+ end=end,
67
+ revision=revision,
68
+ )
69
+ async for page in client.iter_bar_pages(query):
70
+ for bar in page.items:
71
+ if bar.instrument_id in bars:
72
+ raise ValueError("同一证券出现重复日线")
73
+ bars[bar.instrument_id] = bar
74
+ print(f"已读取 {min(offset + 1000, len(ids))}/{len(ids)} 只候选", flush=True)
75
+ if (await client.snapshot(day, day))["canonical"] != revision:
76
+ raise ValueError("读取期间数据修订变化,请重新运行")
77
+ if not bars:
78
+ raise ValueError("当日无日线数据")
79
+ reasons = dict.fromkeys(unknown_listing, "unknown_listing_date")
80
+ rows = []
81
+ for instrument_id in ids:
82
+ instrument = universe[instrument_id]
83
+ bar = bars.get(instrument_id)
84
+ reason = "eligible"
85
+ if bar is None:
86
+ reason = "missing_bar"
87
+ elif bar.suspended or bar.volume <= 0:
88
+ reason = "suspended_or_no_volume"
89
+ elif (
90
+ bar.open <= 0
91
+ or bar.low <= 0
92
+ or not (bar.low <= min(bar.open, bar.close) <= max(bar.open, bar.close) <= bar.high)
93
+ ):
94
+ reason = "invalid_ohlc"
95
+ elif day - instrument.listed_date < timedelta(days=120):
96
+ reason = "listed_less_than_120_calendar_days"
97
+ elif bar.amount is None or bar.amount < minimum_amount:
98
+ reason = "insufficient_amount"
99
+ reasons[instrument_id] = reason
100
+ rows.append(
101
+ FactorPanelRow(
102
+ instrument_id=instrument_id,
103
+ as_of=datetime.combine(day, time(15), cn),
104
+ available_at=observed_at,
105
+ eligible=reason == "eligible",
106
+ fields={"open": bar.open if bar else None, "close": bar.close if bar else None},
107
+ )
108
+ )
109
+ universe_digest = hashlib.sha256(
110
+ json.dumps(
111
+ [universe[i].model_dump(mode="json") for i in ids],
112
+ sort_keys=True,
113
+ ).encode()
114
+ ).hexdigest()
115
+ panel = FactorPanel(
116
+ decision_time=observed_at,
117
+ price_basis="raw_same_day_ratio",
118
+ universe_ref="listed_a_share_catalog",
119
+ universe_revision=universe_digest,
120
+ market_revision=revision,
121
+ units={"open": "CNY", "close": "CNY"},
122
+ rows=tuple(rows),
123
+ )
124
+ spec = FactorComputationSpec(
125
+ name="intraday_strength",
126
+ input_kind="panel",
127
+ inputs=["bar/1d"],
128
+ python_source=SOURCE,
129
+ description="收盘价 / 开盘价 - 1;成交额与上市时间过滤",
130
+ )
131
+ values = execute_python_panel(spec, panel)
132
+ ranked = sorted(
133
+ [(i, score) for (_, i), score in values.items() if score is not None],
134
+ key=lambda item: (-item[1], item[0]),
135
+ )
136
+ output.mkdir(parents=True, exist_ok=True)
137
+ with (output / "ranking.csv").open("w", encoding="utf-8-sig", newline="") as stream:
138
+ writer = csv.writer(stream)
139
+ writer.writerow(["rank", "instrument_id", "name", "intraday_return", "amount_cny"])
140
+ for rank, (i, score) in enumerate(ranked, 1):
141
+ writer.writerow([rank, i, universe[i].name, score, bars[i].amount])
142
+ audit = {
143
+ "day": str(day),
144
+ "observed_at": observed_at.isoformat(),
145
+ "revision": revision,
146
+ "input_digest": panel.input_digest,
147
+ "definition_digest": spec.content_digest,
148
+ "catalog_stocks": len(stocks),
149
+ "universe": len(ids),
150
+ "bars": len(bars),
151
+ "filter_counts": dict(Counter(reasons.values())),
152
+ "minimum_amount": str(minimum_amount),
153
+ "top": [{"id": i, "name": universe[i].name, "score": str(s)} for i, s in ranked[:top]],
154
+ "limitations": [
155
+ "当前修订快照,不是历史PIT回测",
156
+ "未过滤历史ST和涨跌停,非可成交名单",
157
+ "仅验证选股链路,未验证收益预测能力",
158
+ ],
159
+ }
160
+ for name, content in {
161
+ "summary.json": audit,
162
+ "exclusions.json": {i: r for i, r in reasons.items() if r != "eligible"},
163
+ "bars.json": [bars[i].model_dump(mode="json") for i in sorted(bars)],
164
+ "universe.json": [universe[i].model_dump(mode="json") for i in ids],
165
+ "factor.json": spec.model_dump(mode="json"),
166
+ }.items():
167
+ (output / name).write_text(
168
+ json.dumps(content, ensure_ascii=False, indent=2), encoding="utf-8"
169
+ )
170
+ return audit
171
+
172
+
173
+ if __name__ == "__main__":
174
+ configure_utf8_output()
175
+ parser = argparse.ArgumentParser(description=__doc__)
176
+ parser.add_argument("--day", type=date.fromisoformat, required=True)
177
+ parser.add_argument("--output", type=Path, default=Path(".cache/full_market_screen"))
178
+ parser.add_argument("--minimum-amount", type=Decimal, default=Decimal("20000000"))
179
+ parser.add_argument("--top", type=int, default=20)
180
+ args = parser.parse_args()
181
+ if args.minimum_amount < 0 or args.top < 1:
182
+ parser.error("成交额不能为负,top 必须为正数")
183
+ print(
184
+ json.dumps(
185
+ asyncio.run(run(args.day, args.output, args.minimum_amount, args.top)),
186
+ ensure_ascii=False,
187
+ indent=2,
188
+ )
189
+ )
@@ -0,0 +1,196 @@
1
+ """显式本地快照验证:因子独立复算、研究归档、真实交易回放与重启读取。"""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import sys
7
+ from datetime import date, datetime, time
8
+ from decimal import Decimal
9
+ from pathlib import Path
10
+ from unittest.mock import patch
11
+ from zoneinfo import ZoneInfo
12
+
13
+ from quant_core.app.backtest.schema import BacktestConfig, BacktestResult
14
+ from quant_core.app.factor.factor_dependency import build_factor_diagnostics
15
+ from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
16
+ from quant_core.app.research.schema.dataset_schema import ResearchLabel
17
+ from quant_core.common.local_run import RunStatus
18
+
19
+ from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
20
+ from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
21
+ from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
22
+ from quant_cli.app.data.service.dataset_load_service import load_dataset
23
+ from quant_cli.app.factor.local_factor_dependency import (
24
+ build_local_factor_query,
25
+ build_local_factor_store,
26
+ )
27
+ from quant_cli.app.factor.service.local_factor_service import run_local_factor
28
+ from quant_cli.app.research.local_research_dependency import build_local_research_store
29
+ from quant_cli.app.research.schema.local_research_schema import LocalEvaluationRequest
30
+ from quant_cli.app.research.service.local_research_service import (
31
+ compare_local_research,
32
+ evaluate_local_factor,
33
+ )
34
+
35
+ CN = ZoneInfo("Asia/Shanghai")
36
+ SOURCE = (
37
+ "from quant_core.app.factor.computation.factor_expression import MA, Factor\n"
38
+ "factors = {'reversal5': -MA(Factor('close') / Factor('open') - 1, 5)}"
39
+ )
40
+
41
+
42
+ async def validate(manifest: Path, database: Path, backtest_manifest: Path) -> dict:
43
+ assert "quant_backend.common.database.database_session" not in sys.modules
44
+ factor_store = build_local_factor_store(database)
45
+ research_store = build_local_research_store(database)
46
+ with patch("socket.socket", side_effect=AssertionError("离线计算禁止网络")):
47
+ market, _ = load_dataset(manifest, 1000000)
48
+ ids = tuple(sorted(market.facts))
49
+ request = LocalFactorRequest(
50
+ source=SOURCE,
51
+ manifest_path=manifest,
52
+ instrument_ids=ids,
53
+ start=date(2026, 9, 8),
54
+ end=date(2026, 9, 24),
55
+ )
56
+ factor_run = await run_local_factor(request, factor_store)
57
+ assert factor_run.status == RunStatus.COMPLETED, factor_run.error
58
+ values = factor_store.load_values(factor_run.uuid)["reversal5"]
59
+ maximum_error = Decimal(0)
60
+ labels = []
61
+ for instrument in ids:
62
+ info = await market.get_instrument(instrument)
63
+ calendar = await market.list_trading_days(
64
+ info.exchange, market.start.date(), market.end.date()
65
+ )
66
+ days = sorted(x.trade_date for x in calendar if x.is_open)
67
+ bars = await market.list_bars(instrument, "1d", market.start, market.end, 10000)
68
+ lookup = {bar.timestamp.astimezone(CN).date(): bar for bar in bars}
69
+ for (ts, key), actual in values.items():
70
+ if key != instrument:
71
+ continue
72
+ day = ts.astimezone(CN).date()
73
+ index = days.index(day)
74
+ window = [lookup.get(d) for d in days[index - 4 : index + 1]]
75
+ expected = None
76
+ if len(window) == 5 and all(b and not b.suspended and b.open > 0 for b in window):
77
+ expected = -sum((b.close / b.open - 1 for b in window), Decimal(0)) / 5
78
+ assert (expected is None) == (actual is None)
79
+ if expected is not None:
80
+ maximum_error = max(maximum_error, abs(expected - actual))
81
+ assert abs(expected - actual) < Decimal("1e-12")
82
+ if index + 2 < len(days):
83
+ buy, sell = lookup.get(days[index + 1]), lookup.get(days[index + 2])
84
+ # 显式原始价格标签,只用于流程验证,不宣称投资有效性。
85
+ value = (
86
+ sell.close / buy.close - 1
87
+ if buy and sell and not buy.suspended and not sell.suspended
88
+ else None
89
+ )
90
+ labels.append(
91
+ ResearchLabel(
92
+ ts,
93
+ instrument,
94
+ datetime.combine(days[index + 2], time(15), CN),
95
+ value,
96
+ "raw-next-session-close-to-following-close",
97
+ market.revision,
98
+ )
99
+ )
100
+ evaluation = LocalEvaluationRequest(
101
+ factor_run_uuid=factor_run.uuid, factor_name="reversal5", labels=tuple(labels)
102
+ )
103
+ research_runs = []
104
+ for _ in range(2):
105
+ record = evaluate_local_factor(evaluation, factor_store, build_factor_diagnostics())
106
+ assert record.status == RunStatus.COMPLETED, record.error
107
+ research_store.save(record)
108
+ research_runs.append(record.uuid)
109
+ assert compare_local_research(research_store, tuple(research_runs))["comparable"]
110
+ last = max(ts for ts, _ in values)
111
+ top = tuple(
112
+ key
113
+ for key, value in sorted(
114
+ [
115
+ (key, value)
116
+ for (ts, key), value in values.items()
117
+ if ts == last and value is not None
118
+ ],
119
+ key=lambda item: (-item[1], item[0]),
120
+ )[:5]
121
+ )
122
+ query, aliases = await build_local_factor_query(
123
+ factor_run, {"reversal5": values}, market, market.end.date()
124
+ )
125
+ before = await query.get_snapshot(list(aliases.values()), list(ids), last)
126
+ assert all(row.as_of < last for row in before.values)
127
+ after = await query.get_snapshot(
128
+ list(aliases.values()), list(ids), datetime(2026, 9, 28, 9, 30, tzinfo=CN)
129
+ )
130
+ assert after.values and all(row.available_at <= after.decision_time for row in after.values)
131
+ source = """def handle_bar(context, bars):
132
+ if context.event_time.date().isoformat() == "2026-09-28":
133
+ for key in context.universe:
134
+ context.order_shares(key, 200)
135
+ elif context.event_time.date().isoformat() == "2026-09-30":
136
+ for key in context.universe:
137
+ context.order_target(key, 0)
138
+ """
139
+ backtest_request = LocalBacktestRequest(
140
+ source=source,
141
+ manifest_path=backtest_manifest,
142
+ parameters={"selection_run_uuid": str(factor_run.uuid)},
143
+ config=BacktestConfig(
144
+ strategy_id="local_validation",
145
+ instrument_ids=top,
146
+ frequency="1d",
147
+ start=datetime(2026, 9, 28, 9, 30, tzinfo=CN),
148
+ end=datetime(2026, 9, 30, 15, tzinfo=CN),
149
+ initial_cash=Decimal(1000000),
150
+ strategy_adjustment_type="none",
151
+ ),
152
+ )
153
+ backtest = await run_local_backtest(backtest_request, factor_store)
154
+ assert backtest.status == RunStatus.COMPLETED, backtest.error
155
+ build_local_backtest_store(database).save(backtest)
156
+ result = BacktestResult.model_validate(
157
+ build_local_backtest_store(database).get(backtest.uuid).result
158
+ )
159
+ assert len(result.fills) == 10
160
+ expected_cash = Decimal(1000000) + sum(
161
+ (
162
+ (1 if f.side.value == "sell" else -1) * f.price * f.quantity - f.fees.total
163
+ for f in result.fills
164
+ ),
165
+ Decimal(0),
166
+ )
167
+ assert result.final_portfolio_snapshot.cash.total_cash == expected_cash
168
+ assert "quant_backend.common.database.database_session" not in sys.modules
169
+ return {
170
+ "status": "passed",
171
+ "factor_run": str(factor_run.uuid),
172
+ "research_runs": [str(x) for x in research_runs],
173
+ "backtest_run": str(backtest.uuid),
174
+ "instruments": len(ids),
175
+ "cells": len(values),
176
+ "maximum_error": str(maximum_error),
177
+ "fills": len(result.fills),
178
+ "final_cash": str(expected_cash),
179
+ "database": str(database),
180
+ "factor_market_revision": factor_run.provenance["market_revision"],
181
+ "backtest_market_revision": backtest.provenance["market_revision"],
182
+ "labels": "raw-next-session-close-to-following-close; pipeline validation only",
183
+ }
184
+
185
+
186
+ if __name__ == "__main__":
187
+ parser = argparse.ArgumentParser()
188
+ parser.add_argument("--manifest", type=Path, required=True)
189
+ parser.add_argument("--database", type=Path, required=True)
190
+ parser.add_argument("--backtest-manifest", type=Path, required=True)
191
+ args = parser.parse_args()
192
+ report = asyncio.run(validate(args.manifest, args.database, args.backtest_manifest))
193
+ (args.database.parent / "validation_report.json").write_text(
194
+ json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
195
+ )
196
+ print(json.dumps(report, ensure_ascii=False))
@@ -0,0 +1,219 @@
1
+ """固定五股样本:下载定义后,断网 CLI 计算、依赖复用与独立数值/成交复核。"""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from datetime import date, datetime
9
+ from decimal import Decimal
10
+ from pathlib import Path
11
+ from zoneinfo import ZoneInfo
12
+
13
+ from quant_core.app.backtest.schema import BacktestResult
14
+
15
+ from quant_cli.app.data.service.dataset_load_service import load_dataset
16
+
17
+ CN = ZoneInfo("Asia/Shanghai")
18
+ BOOTSTRAP = """
19
+ import asyncio, sys
20
+ from importlib.abc import MetaPathFinder
21
+ from unittest.mock import patch
22
+ class Guard(MetaPathFinder):
23
+ def find_spec(self, fullname, path=None, target=None):
24
+ if fullname.startswith('quant_backend'):
25
+ raise AssertionError('Remote infrastructure import: ' + fullname)
26
+ sys.meta_path.insert(0, Guard())
27
+ from quant_cli.app.quant_cli import main
28
+ with asyncio.Runner() as runner:
29
+ runner.get_loop()
30
+ with (
31
+ patch('socket.socket', side_effect=AssertionError('Offline network access')),
32
+ patch('asyncio.run', runner.run),
33
+ ):
34
+ main()
35
+ """
36
+
37
+
38
+ def validate(manifest: Path, factor: Path, strategy: Path, output: Path) -> dict:
39
+ output.mkdir(parents=True, exist_ok=True)
40
+ database = str(output / "research.duckdb")
41
+ market, _ = load_dataset(manifest, 1000000)
42
+ ids = sorted(market.facts)
43
+
44
+ def command(*args, expected=0):
45
+ result = subprocess.run(
46
+ [os.environ.get("PQ_VALIDATION_CLIENT_PYTHON", sys.executable), "-c", BOOTSTRAP, *args],
47
+ cwd=output,
48
+ capture_output=True,
49
+ text=True,
50
+ encoding="utf-8",
51
+ timeout=180,
52
+ env={**os.environ, "PYTHONIOENCODING": "utf-8"},
53
+ )
54
+ assert result.returncode == expected, result.stdout + result.stderr
55
+ return json.loads(result.stdout)
56
+
57
+ def config(name, payload):
58
+ path = output / f"{name}.json"
59
+ path.write_text(
60
+ json.dumps(payload, default=str, ensure_ascii=False, indent=2), encoding="utf-8"
61
+ )
62
+ return str(path)
63
+
64
+ factor_request = {
65
+ "manifest_path": str(manifest),
66
+ "instrument_ids": ids,
67
+ "start": "2026-09-08",
68
+ "end": "2026-09-30",
69
+ }
70
+ computed = command(
71
+ "factor",
72
+ "compute",
73
+ "--database",
74
+ database,
75
+ "--definition",
76
+ str(factor),
77
+ "--config",
78
+ config("factor-request", factor_request),
79
+ )
80
+ cells = command(
81
+ "factor", "values", "--run", computed["uuid"], "--limit", "10000", "--database", database
82
+ )
83
+ error, selection = Decimal(0), []
84
+ for cell in cells:
85
+ instrument = cell["instrument_id"]
86
+ day = datetime.fromisoformat(cell["as_of"]).astimezone(CN).date()
87
+ days = sorted(row.trade_date for row in market.facts[instrument].calendar if row.is_open)
88
+ bars = {bar.timestamp.astimezone(CN).date(): bar for bar in market.bars[(instrument, "1d")]}
89
+ index = days.index(day)
90
+ window = [bars.get(d) for d in days[max(0, index - 4) : index + 1]]
91
+ expected = None
92
+ if len(window) == 5 and all(bar and not bar.suspended and bar.open > 0 for bar in window):
93
+ expected = -sum((bar.close / bar.open - 1 for bar in window), Decimal(0)) / 5
94
+ actual = None if cell["value"] is None else Decimal(cell["value"])
95
+ assert (actual is None) == (expected is None)
96
+ if expected is not None:
97
+ error = max(error, abs(actual - expected))
98
+ assert abs(actual - expected) < Decimal("1e-12")
99
+ if day == date(2026, 9, 24) and expected is not None:
100
+ selection.append((instrument, expected))
101
+ top = [key for key, value in sorted(selection, key=lambda row: (-row[1], row[0]))[:3]]
102
+ bundle = json.loads(strategy.read_text(encoding="utf-8"))
103
+ backtest_request = {
104
+ "manifest_path": str(manifest),
105
+ "config": {
106
+ "strategy_id": bundle["definition"]["strategy_id"],
107
+ "instrument_ids": ids,
108
+ "frequency": "1d",
109
+ "start": "2026-09-28T09:30:00+08:00",
110
+ "end": "2026-09-30T15:00:00+08:00",
111
+ "initial_cash": "1000000",
112
+ "strategy_adjustment_type": "none",
113
+ },
114
+ }
115
+ automatic = command(
116
+ "strategy",
117
+ "run",
118
+ "--database",
119
+ database,
120
+ "--definition",
121
+ str(strategy),
122
+ "--config",
123
+ config("automatic-request", backtest_request),
124
+ )
125
+ reused = command(
126
+ "strategy",
127
+ "run",
128
+ "--database",
129
+ database,
130
+ "--definition",
131
+ str(strategy),
132
+ "--config",
133
+ config(
134
+ "reuse-request",
135
+ {
136
+ **backtest_request,
137
+ "factor_run_uuid": computed["uuid"],
138
+ },
139
+ ),
140
+ )
141
+ result = BacktestResult.model_validate(automatic["result"])
142
+ again = BacktestResult.model_validate(reused["result"])
143
+ assert len(result.fills) == 6
144
+ assert sorted(
145
+ fill.instrument_id for fill in result.fills if fill.side.value == "buy"
146
+ ) == sorted(top)
147
+ cash = Decimal(1000000)
148
+ for fill in result.fills:
149
+ bars = {
150
+ bar.timestamp.astimezone(CN).date(): bar
151
+ for bar in market.bars[(fill.instrument_id, "1d")]
152
+ }
153
+ day = date(2026, 9, 28) if fill.side.value == "buy" else date(2026, 9, 30)
154
+ assert fill.quantity == 200
155
+ assert fill.price == bars[day].close
156
+ notional = fill.price * 200
157
+ fees = max(notional * Decimal("0.0003"), Decimal(5))
158
+ if fill.side.value == "sell":
159
+ fees += notional * Decimal("0.0005")
160
+ assert fill.fees.total == fees
161
+ cash += (1 if fill.side.value == "sell" else -1) * notional - fees
162
+ assert result.final_portfolio_snapshot.cash.total_cash == cash
163
+ assert again.final_portfolio_snapshot.cash.total_cash == cash
164
+
165
+ def business(r):
166
+ return [(f.instrument_id, f.side, f.quantity, f.price, f.fees.total) for f in r.fills]
167
+
168
+ assert business(result) == business(again)
169
+ for kind, record in (("factor", computed), ("backtest", automatic), ("backtest", reused)):
170
+ assert command(kind, "get", "--run", record["uuid"], "--database", database) == record
171
+ # 用同一包再次执行;进程重启后固定版本和业务结果仍稳定。
172
+ repeated = command(
173
+ "strategy",
174
+ "run",
175
+ "--database",
176
+ database,
177
+ "--definition",
178
+ str(strategy),
179
+ "--config",
180
+ str(output / "automatic-request.json"),
181
+ )
182
+ assert business(BacktestResult.model_validate(repeated["result"])) == business(result)
183
+ report = {
184
+ "status": "passed",
185
+ "environment": computed["provenance"]["environment"],
186
+ "offline": True,
187
+ "manifest": str(manifest),
188
+ "factor_version": computed["provenance"]["published_definitions"],
189
+ "strategy": automatic["provenance"]["published_strategy"],
190
+ "cells": len(cells),
191
+ "maximum_error": str(error),
192
+ "selected": top,
193
+ "fills": len(result.fills),
194
+ "final_cash": str(cash),
195
+ "automatic": automatic["uuid"],
196
+ "reuse": reused["uuid"],
197
+ "repeat": repeated["uuid"],
198
+ "limitations": ["五股固定工程样本,非全市场或投资收益验收"],
199
+ }
200
+ config("validation-report", report)
201
+ return report
202
+
203
+
204
+ if __name__ == "__main__":
205
+ parser = argparse.ArgumentParser()
206
+ for name in ("manifest", "factor", "strategy", "output"):
207
+ parser.add_argument(f"--{name}", required=True, type=Path)
208
+ args = parser.parse_args()
209
+ print(
210
+ json.dumps(
211
+ validate(
212
+ args.manifest.resolve(),
213
+ args.factor.resolve(),
214
+ args.strategy.resolve(),
215
+ args.output.resolve(),
216
+ ),
217
+ ensure_ascii=False,
218
+ )
219
+ )
@@ -0,0 +1,87 @@
1
+ """研究因子历史值与可见时间的覆盖,不推断收益预测能力。"""
2
+
3
+ from datetime import datetime, time
4
+ from zoneinfo import ZoneInfo
5
+
6
+ from quant_core.app.factor.constants import FactorFrequency, FactorStatus
7
+ from quant_core.app.factor.schema.factor_contract import FactorIdentity
8
+
9
+ CN = ZoneInfo("Asia/Shanghai")
10
+ NAMES = ("momentum_20d", "volume_ratio_5d", "ma_close_10")
11
+ STOCKS = (
12
+ "CN.XSHE.000001",
13
+ "CN.XSHE.300750",
14
+ "CN.XSHG.600519",
15
+ "CN.XSHG.688981",
16
+ )
17
+
18
+
19
+ async def run(context, parameters):
20
+ if context.factors is None:
21
+ raise ValueError("研究运行未注入因子查询能力")
22
+ start = datetime.combine(context.start, time.min, CN)
23
+ end = datetime.combine(context.end, time.max, CN)
24
+ decision_time = datetime.combine(context.end, time(15), CN)
25
+ observations = []
26
+ exclusions = []
27
+ values_total = 0
28
+ for name in NAMES:
29
+ identity = FactorIdentity(
30
+ name=name,
31
+ source="local-python",
32
+ definition_version="1",
33
+ frequency=FactorFrequency.DAILY,
34
+ )
35
+ for instrument_id in STOCKS:
36
+ values = await context.factors.get_series(
37
+ identity, instrument_id, start, end, decision_time
38
+ )
39
+ available = [row for row in values if row.status is FactorStatus.AVAILABLE]
40
+ if not available:
41
+ exclusions.append(
42
+ {
43
+ "factor": name,
44
+ "instrument_id": instrument_id,
45
+ "reason": "决策时点无可见因子值",
46
+ }
47
+ )
48
+ continue
49
+ assert all(row.available_at <= decision_time for row in values)
50
+ values_total += len(available)
51
+ observations.append(
52
+ {
53
+ "factor": name,
54
+ "instrument_id": instrument_id,
55
+ "available_count": len(available),
56
+ "first_as_of": min(row.as_of for row in available).isoformat(),
57
+ "last_as_of": max(row.as_of for row in available).isoformat(),
58
+ "latest_available_at": max(row.available_at for row in available).isoformat(),
59
+ }
60
+ )
61
+ return {
62
+ "question": "本地三因子对四只股票的 PIT 历史覆盖是否足以开展收益研究?",
63
+ "conclusion": (
64
+ f"12 个因子×股票组合中 {len(observations)} 个有可见值,"
65
+ f"共 {values_total} 个可见因子值;当前只能验收覆盖和可见时间,"
66
+ "样本不足以判断预测能力。"
67
+ ),
68
+ "methodology": "按因子身份和股票分别读取截至样本结束日 15:00 已可见的历史值;不填补缺失。",
69
+ "sample_start": context.start,
70
+ "sample_end": context.end,
71
+ "candidate_count": len(NAMES) * len(STOCKS),
72
+ "observations": observations,
73
+ "exclusions": exclusions,
74
+ "metrics": [
75
+ {
76
+ "key": "available_values",
77
+ "label": "可见因子值行数",
78
+ "value": values_total,
79
+ "unit": "count",
80
+ "explanation": "按因子身份×标的×归属日期统计;不是独立收益样本数。",
81
+ }
82
+ ],
83
+ "limitations": [
84
+ "只检查三因子、四只股票和短窗口,不能外推至全市场。",
85
+ "未计算未来收益、IC、分层表现或样本外结果,不构成因子有效性证明。",
86
+ ],
87
+ }