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,116 @@
1
+ """面向 CLI/Agent 的版本化选股报告契约。"""
2
+
3
+ import hashlib
4
+ import json
5
+ from datetime import datetime
6
+ from decimal import Decimal
7
+
8
+ from pydantic import BaseModel, ConfigDict
9
+
10
+ from quant_core.app.factor.schema.factor_contract import FactorIdentity, FactorSnapshot
11
+ from quant_core.app.factor.schema.selection_types import SelectionDefinition, SelectionResult
12
+
13
+ SCHEMA_VERSION = "power-quant.selection.v1"
14
+
15
+
16
+ class ContributionReport(BaseModel):
17
+ model_config = ConfigDict(frozen=True)
18
+
19
+ factor: FactorIdentity
20
+ value: Decimal
21
+
22
+
23
+ class SelectedReport(BaseModel):
24
+ model_config = ConfigDict(frozen=True)
25
+
26
+ instrument_id: str
27
+ rank: int
28
+ score: Decimal
29
+ contributions: tuple[ContributionReport, ...]
30
+
31
+
32
+ class ExcludedReport(BaseModel):
33
+ model_config = ConfigDict(frozen=True)
34
+
35
+ instrument_id: str
36
+ reason: str
37
+ factor: FactorIdentity | None = None
38
+
39
+
40
+ class SelectionReport(BaseModel):
41
+ model_config = ConfigDict(frozen=True)
42
+
43
+ schema_version: str = SCHEMA_VERSION
44
+ selection_id: str
45
+ decision_time: datetime
46
+ universe_size: int
47
+ selected_count: int
48
+ excluded_count: int
49
+ dataset_revisions: tuple[str, ...]
50
+ selected: tuple[SelectedReport, ...]
51
+ excluded: tuple[ExcludedReport, ...]
52
+
53
+
54
+ def build_selection_report(
55
+ definition: SelectionDefinition,
56
+ universe: tuple[str, ...] | list[str],
57
+ snapshot: FactorSnapshot,
58
+ result: SelectionResult,
59
+ ) -> SelectionReport:
60
+ payload = {
61
+ "factors": [
62
+ {
63
+ "identity": factor.identity.model_dump(mode="json"),
64
+ "weight": str(factor.weight),
65
+ "direction": factor.direction.value,
66
+ "transform": {
67
+ "winsorization": factor.transform.winsorization.value,
68
+ "normalization": factor.transform.normalization.value,
69
+ "neutralization": factor.transform.neutralization.value,
70
+ },
71
+ }
72
+ for factor in definition.factors
73
+ ],
74
+ "top_n": definition.top_n,
75
+ "missing_policy": definition.missing_policy.value,
76
+ }
77
+ selection_id = hashlib.sha256(
78
+ json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
79
+ ).hexdigest()[:24]
80
+ revisions = tuple(
81
+ sorted(
82
+ {
83
+ row.provenance.dataset_revision
84
+ for row in snapshot.values
85
+ if row.provenance.dataset_revision is not None
86
+ }
87
+ )
88
+ )
89
+ return SelectionReport(
90
+ selection_id=selection_id,
91
+ decision_time=snapshot.decision_time,
92
+ universe_size=len(set(universe)),
93
+ selected_count=len(result.selected),
94
+ excluded_count=len(result.excluded),
95
+ dataset_revisions=revisions,
96
+ selected=tuple(
97
+ SelectedReport(
98
+ instrument_id=row.instrument_id,
99
+ rank=row.rank,
100
+ score=row.score,
101
+ contributions=tuple(
102
+ ContributionReport(factor=factor, value=value)
103
+ for factor, value in row.contributions
104
+ ),
105
+ )
106
+ for row in result.selected
107
+ ),
108
+ excluded=tuple(
109
+ ExcludedReport(
110
+ instrument_id=row.instrument_id,
111
+ reason=row.reason,
112
+ factor=row.factor,
113
+ )
114
+ for row in result.excluded
115
+ ),
116
+ )
@@ -0,0 +1,135 @@
1
+ """T5 显式配置与可审计结果;不承载 Provider/订单语义。"""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from decimal import Decimal
6
+ from enum import Enum
7
+
8
+ from quant_core.app.factor.schema.factor_contract import FactorIdentity
9
+
10
+
11
+ class SortDirection(str, Enum):
12
+ ASC = "asc"
13
+ DESC = "desc"
14
+
15
+
16
+ class MissingPolicy(str, Enum):
17
+ EXCLUDE = "exclude"
18
+ ERROR = "error"
19
+
20
+
21
+ class NormalizationMethod(str, Enum):
22
+ NONE = "none"
23
+ RANK = "rank"
24
+ ZSCORE = "zscore"
25
+
26
+
27
+ class WinsorizationMethod(str, Enum):
28
+ NONE = "none"
29
+ MAD = "mad"
30
+ PERCENTILE = "percentile"
31
+
32
+
33
+ class NeutralizationMethod(str, Enum):
34
+ NONE = "none"
35
+ INDUSTRY = "industry"
36
+ SIZE = "size"
37
+ INDUSTRY_SIZE = "industry_size"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class SelectionExposure:
42
+ industry: str | None = None
43
+ size: Decimal | None = None
44
+ available_at: datetime | None = None
45
+ dataset_revision: str | None = None
46
+
47
+ def __post_init__(self) -> None:
48
+ if self.available_at is not None and self.available_at.utcoffset() is None:
49
+ raise ValueError("exposure 可见时间必须带时区")
50
+ if self.size is not None and (not self.size.is_finite() or self.size <= 0):
51
+ raise ValueError("exposure 市值必须为有限正值")
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class FactorTransform:
56
+ """单个因子的横截面预处理;默认保持原始值,避免隐式改变旧口径。"""
57
+
58
+ winsorization: WinsorizationMethod = WinsorizationMethod.NONE
59
+ normalization: NormalizationMethod = NormalizationMethod.NONE
60
+ lower_quantile: Decimal = Decimal("0.01")
61
+ upper_quantile: Decimal = Decimal("0.99")
62
+ mad_scale: Decimal = Decimal("3")
63
+ neutralization: NeutralizationMethod = NeutralizationMethod.NONE
64
+
65
+ def __post_init__(self) -> None:
66
+ if not Decimal(0) <= self.lower_quantile < self.upper_quantile <= Decimal(1):
67
+ raise ValueError("winsorize quantile 必须满足 0 <= lower < upper <= 1")
68
+ if self.mad_scale <= 0:
69
+ raise ValueError("mad_scale 必须大于 0")
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class FactorWeight:
74
+ identity: FactorIdentity
75
+ weight: Decimal
76
+ direction: SortDirection = SortDirection.DESC
77
+ transform: FactorTransform = FactorTransform()
78
+
79
+ def __post_init__(self) -> None:
80
+ if not self.weight.is_finite() or self.weight <= 0:
81
+ raise ValueError("factor weight 必须为正的有限 Decimal")
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class FactorFilter:
86
+ identity: FactorIdentity
87
+ minimum: Decimal | None = None
88
+ maximum: Decimal | None = None
89
+
90
+ def __post_init__(self) -> None:
91
+ if self.minimum is None and self.maximum is None:
92
+ raise ValueError("filter 至少指定 minimum 或 maximum")
93
+ if self.minimum is not None and self.maximum is not None and self.minimum > self.maximum:
94
+ raise ValueError("filter minimum 不得大于 maximum")
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class SelectionDefinition:
99
+ factors: tuple[FactorWeight, ...]
100
+ top_n: int
101
+ filters: tuple[FactorFilter, ...] = ()
102
+ missing_policy: MissingPolicy = MissingPolicy.EXCLUDE
103
+
104
+ def __post_init__(self) -> None:
105
+ if not self.factors or self.top_n < 1:
106
+ raise ValueError("selection 必须有因子且 top_n >= 1")
107
+ keys = [factor.identity.logical_key for factor in self.factors]
108
+ if len(keys) != len(set(keys)):
109
+ raise ValueError("duplicate factor identity in selection")
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class SelectedInstrument:
114
+ instrument_id: str
115
+ score: Decimal
116
+ contributions: tuple[tuple[FactorIdentity, Decimal], ...]
117
+ rank: int
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class ExcludedInstrument:
122
+ instrument_id: str
123
+ reason: str
124
+ factor: FactorIdentity | None = None
125
+ score: Decimal | None = None
126
+ contributions: tuple[tuple[FactorIdentity, Decimal], ...] = ()
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class SelectionResult:
131
+ selected: tuple[SelectedInstrument, ...]
132
+ excluded: tuple[ExcludedInstrument, ...]
133
+
134
+ def excluded_by_id(self, instrument_id: str) -> ExcludedInstrument:
135
+ return next(row for row in self.excluded if row.instrument_id == instrument_id)
@@ -0,0 +1 @@
1
+ """Factor Service - 因子同步与查询编排(T3/T4)"""
@@ -0,0 +1,140 @@
1
+ """数据库因子定义驱动的 Canonical 因子值生产。"""
2
+
3
+ import asyncio
4
+ from datetime import date, datetime
5
+
6
+ from quant_core.app.data.schema.market_schema import BarResponse
7
+ from quant_core.app.factor.constants import (
8
+ LOCAL_DATASET_REVISION,
9
+ LOCAL_FACTOR_SOURCE,
10
+ FactorFrequency,
11
+ FactorSourceKind,
12
+ FactorStatus,
13
+ )
14
+ from quant_core.app.factor.exceptions import (
15
+ FactorNotRegisteredException,
16
+ InsufficientFactorHistoryError,
17
+ )
18
+ from quant_core.app.factor.interface.factor_execution import WindowExecutor
19
+ from quant_core.app.factor.schema.factor_compute_request import FactorComputeRequest
20
+ from quant_core.app.factor.schema.factor_contract import (
21
+ FactorDefinition,
22
+ FactorIdentity,
23
+ FactorProvenance,
24
+ FactorValue,
25
+ )
26
+ from quant_core.app.factor.visibility import next_trading_day_open
27
+
28
+
29
+ class FactorComputeService:
30
+ """只接收已持久化定义;源码执行方式由注入的 Runner 决定。"""
31
+
32
+ def __init__(
33
+ self,
34
+ trading_days: list[date],
35
+ definitions: list[FactorDefinition],
36
+ runner: WindowExecutor,
37
+ ) -> None:
38
+ self._trading_days = sorted(trading_days)
39
+ self._definitions = {item.identity: item for item in definitions}
40
+ if len(self._definitions) != len(definitions):
41
+ raise ValueError("同次计算不能含重复的完整因子身份")
42
+ self._runner = runner
43
+
44
+ def get(self, name: str | FactorIdentity) -> FactorDefinition:
45
+ """按完整身份解析;旧名称入口仅允许唯一匹配。"""
46
+ if isinstance(name, FactorIdentity):
47
+ definition = self._definitions.get(name)
48
+ else:
49
+ matches = [item for key, item in self._definitions.items() if key.name == name]
50
+ if len(matches) > 1:
51
+ raise ValueError(f"因子名称存在歧义,必须指定完整身份: {name}")
52
+ definition = matches[0] if matches else None
53
+ if definition is None:
54
+ raise FactorNotRegisteredException(str(name))
55
+ return definition
56
+
57
+ def definitions(self) -> list[FactorDefinition]:
58
+ return list(self._definitions.values())
59
+
60
+ async def compute(
61
+ self,
62
+ factor_name: str,
63
+ instrument_id: str,
64
+ event_time: datetime,
65
+ bars: list[BarResponse],
66
+ *,
67
+ dataset_revision: str = LOCAL_DATASET_REVISION,
68
+ input_digest: str | None = None,
69
+ ) -> FactorValue:
70
+ result = await self.compute_many(
71
+ [FactorComputeRequest(factor_name, instrument_id, event_time, bars, input_digest)],
72
+ dataset_revision=dataset_revision,
73
+ )
74
+ return result[0]
75
+
76
+ async def compute_many(
77
+ self,
78
+ requests: list[FactorComputeRequest],
79
+ *,
80
+ dataset_revision: str,
81
+ ) -> list[FactorValue]:
82
+ """先完成时点/频率校验,再将所有可计算格打包交给 Runner。"""
83
+ prepared: list[tuple[FactorComputeRequest, FactorDefinition, datetime, int | None]] = []
84
+ calls: list[tuple[str, list[BarResponse], dict]] = []
85
+ for request in requests:
86
+ definition = self.get(request.identity or request.factor_name)
87
+ spec = definition.computation_spec
88
+ if spec is None or definition.provenance_template is None:
89
+ raise ValueError("因子定义缺少 Python 源码或摘要")
90
+ if definition.identity.source != LOCAL_FACTOR_SOURCE:
91
+ raise ValueError("只执行本地 Python 因子")
92
+ if spec.input_kind != "window":
93
+ raise ValueError("面板因子必须按完整横截面计算")
94
+ if spec.frequency is not FactorFrequency.DAILY:
95
+ raise NotImplementedError("分钟因子计算尚未实现;不能套用日频可见口径")
96
+ if any(bar.frequency != spec.frequency.value for bar in request.bars):
97
+ raise ValueError("输入 Bar 频率与因子定义不一致")
98
+ available_at = next_trading_day_open(request.event_time.date(), self._trading_days)
99
+ if available_at is None:
100
+ raise InsufficientFactorHistoryError(
101
+ f"交易日历无 {request.event_time.date()} 的下一交易日"
102
+ )
103
+ call_index = None
104
+ if len(request.bars) >= spec.lookback:
105
+ call_index = len(calls)
106
+ calls.append((spec.python_source, list(request.bars), spec.parameters))
107
+ prepared.append((request, definition, available_at, call_index))
108
+ computed = await asyncio.to_thread(self._runner, calls) if calls else []
109
+ if len(computed) != len(calls):
110
+ raise ValueError("因子执行结果数与请求数不一致")
111
+ results: list[FactorValue] = []
112
+ for request, definition, available_at, call_index in prepared:
113
+ value = computed[call_index] if call_index is not None else None
114
+ template = definition.provenance_template
115
+ assert template is not None
116
+ results.append(
117
+ FactorValue(
118
+ identity=definition.identity,
119
+ instrument_id=request.instrument_id,
120
+ as_of=request.event_time,
121
+ available_at=available_at,
122
+ value=value,
123
+ status=FactorStatus.AVAILABLE if value is not None else FactorStatus.MISSING,
124
+ provenance=FactorProvenance(
125
+ source_kind=FactorSourceKind.LOCAL_CALCULATOR,
126
+ provider_id=LOCAL_FACTOR_SOURCE,
127
+ retrieval_ref="python:compute",
128
+ content_digest=template.content_digest,
129
+ dataset_revision=dataset_revision,
130
+ note=(
131
+ f"input_sha256={request.input_digest}"
132
+ if request.input_digest is not None
133
+ else f"bar@{request.bars[-1].timestamp.isoformat()}"
134
+ if request.bars
135
+ else None
136
+ ),
137
+ ),
138
+ )
139
+ )
140
+ return results
@@ -0,0 +1,134 @@
1
+ """以真实 PIT 截面和复权历史价构造可解释的因子研究样本。"""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date, datetime, time, timedelta
5
+ from decimal import Decimal
6
+ from zoneinfo import ZoneInfo
7
+
8
+ from quant_core.app.data.interface.market_data_query import IMarketDataQuery
9
+ from quant_core.app.data.schema.adjustment_schema import AdjustmentType, BarViewRequest
10
+ from quant_core.app.factor.constants import FactorStatus
11
+ from quant_core.app.factor.interface.public.factor_query import IFactorQuery
12
+ from quant_core.app.factor.schema.diagnostic_types import CrossSection, FactorDiagnostics
13
+ from quant_core.app.factor.schema.factor_contract import FactorIdentity
14
+ from quant_core.app.factor.service.factor_diagnostics_service import FactorDiagnosticsService
15
+
16
+ CN = ZoneInfo("Asia/Shanghai")
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DiagnosticHorizon:
21
+ trading_days: int
22
+ result: FactorDiagnostics
23
+ paired_samples: int
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FactorDiagnosticRun:
28
+ factor: FactorIdentity
29
+ start: date
30
+ end: date
31
+ universe_size: int
32
+ factor_cells: int
33
+ available_cells: int
34
+ price_basis: str
35
+ horizons: tuple[DiagnosticHorizon, ...]
36
+
37
+
38
+ class FactorDiagnosticRunner:
39
+ """只编排 Market/Factor 公共查询;IC 数学仍由原诊断服务完成。"""
40
+
41
+ def __init__(self, market: IMarketDataQuery, factors: IFactorQuery) -> None:
42
+ self._market = market
43
+ self._factors = factors
44
+
45
+ async def run(
46
+ self,
47
+ identity: FactorIdentity,
48
+ universe: tuple[str, ...],
49
+ start: date,
50
+ end: date,
51
+ horizons: tuple[int, ...] = (1, 5, 10),
52
+ ) -> FactorDiagnosticRun:
53
+ if start > end or not universe or not horizons or any(h < 1 for h in horizons):
54
+ raise ValueError("诊断需要非空股票池、有效日期区间和正的观察窗口")
55
+ calendar = await self._market.list_trading_days(
56
+ "XSHG", start, end + timedelta(days=max(horizons) * 3 + 15)
57
+ )
58
+ days = [row.trade_date for row in calendar if row.is_open]
59
+ anchors = [day for day in days if start <= day <= end]
60
+ positions = {day: index for index, day in enumerate(days)}
61
+ if not anchors or positions[anchors[-1]] + max(horizons) + 1 >= len(days):
62
+ raise ValueError("交易日历不足以覆盖因子诊断的前瞻收益窗口")
63
+ normalized_universe = tuple(sorted(set(universe)))
64
+ eligible_by_day = {}
65
+ for day in anchors:
66
+ listed = {row.instrument_id for row in await self._market.list_instruments_on(day)}
67
+ eligible_by_day[day] = tuple(
68
+ instrument_id for instrument_id in normalized_universe if instrument_id in listed
69
+ )
70
+ prices: dict[str, dict[date, Decimal]] = {}
71
+ last_day = days[positions[anchors[-1]] + max(horizons) + 1]
72
+ for instrument_id in sorted({stock for pool in eligible_by_day.values() for stock in pool}):
73
+ bars = await self._market.get_adjusted_bars(
74
+ instrument_id,
75
+ "1d",
76
+ BarViewRequest(adjustment_type=AdjustmentType.POST),
77
+ datetime.combine(start, time.min, tzinfo=CN),
78
+ datetime.combine(last_day, time.max, tzinfo=CN),
79
+ 100000,
80
+ )
81
+ prices[instrument_id] = {
82
+ bar.timestamp.astimezone(CN).date(): bar.close for bar in bars.bars
83
+ }
84
+ sections = {h: [] for h in horizons}
85
+ available_cells = 0
86
+ factor_cells = 0
87
+ for day in anchors:
88
+ position = positions[day]
89
+ visible_day = days[position + 1]
90
+ decision_time = datetime.combine(visible_day, time(9, 31), tzinfo=CN)
91
+ eligible = eligible_by_day[day]
92
+ factor_cells += len(eligible)
93
+ snapshot = await self._factors.get_snapshot(
94
+ [identity], list(eligible), decision_time, target_as_of=day
95
+ )
96
+ factor_values = {
97
+ row.instrument_id: row.value
98
+ for row in snapshot.values
99
+ if row.status is FactorStatus.AVAILABLE and row.value is not None
100
+ }
101
+ available_cells += len(factor_values)
102
+ for horizon in horizons:
103
+ end_day = days[position + 1 + horizon]
104
+ returns = {}
105
+ for instrument_id in factor_values:
106
+ base = prices[instrument_id].get(visible_day)
107
+ final = prices[instrument_id].get(end_day)
108
+ if base is not None and final is not None and base > 0:
109
+ returns[instrument_id] = final / base - Decimal(1)
110
+ sections[horizon].append(CrossSection(day, factor_values, returns))
111
+ analyzer = FactorDiagnosticsService()
112
+ return FactorDiagnosticRun(
113
+ factor=identity,
114
+ start=start,
115
+ end=end,
116
+ universe_size=len(normalized_universe),
117
+ factor_cells=factor_cells,
118
+ available_cells=available_cells,
119
+ price_basis=(
120
+ "后复权收盘价:因子 T 日收盘归属、T+1 开盘可见;"
121
+ "收益从 T+1 收盘至 T+1+N 收盘;仅供诊断,不代表成交"
122
+ ),
123
+ horizons=tuple(
124
+ DiagnosticHorizon(
125
+ trading_days=horizon,
126
+ result=analyzer.analyze(tuple(sections[horizon])),
127
+ paired_samples=sum(
128
+ len(section.factor_values.keys() & section.forward_returns.keys())
129
+ for section in sections[horizon]
130
+ ),
131
+ )
132
+ for horizon in horizons
133
+ ),
134
+ )
@@ -0,0 +1,140 @@
1
+ """确定性的纯因子诊断计算;数据提取由调用方负责。"""
2
+
3
+ from decimal import Decimal, localcontext
4
+
5
+ from quant_core.app.factor.schema.diagnostic_types import (
6
+ CrossSection,
7
+ DecayPoint,
8
+ FactorDiagnostics,
9
+ IcPoint,
10
+ QuantileReturn,
11
+ )
12
+
13
+
14
+ class FactorDiagnosticsService:
15
+ def analyze(
16
+ self,
17
+ sections: tuple[CrossSection, ...],
18
+ *,
19
+ quantiles: int = 5,
20
+ selections: tuple[tuple[str, ...], ...] = (),
21
+ comparison_values: dict[str, Decimal] | None = None,
22
+ horizon_sections: dict[int, tuple[CrossSection, ...]] | None = None,
23
+ ) -> FactorDiagnostics:
24
+ if quantiles < 2:
25
+ raise ValueError("quantiles 必须至少为 2")
26
+ points = tuple(
27
+ self._ic_point(section) for section in sorted(sections, key=lambda x: x.as_of)
28
+ )
29
+ return FactorDiagnostics(
30
+ ic_series=points,
31
+ quantile_returns=self._quantile_returns(sections, quantiles),
32
+ decay=self._decay(horizon_sections or {}),
33
+ selection_turnover=self._turnover(selections),
34
+ factor_correlation=(
35
+ _correlation(sections[-1].factor_values, comparison_values)
36
+ if sections and comparison_values is not None
37
+ else None
38
+ ),
39
+ )
40
+
41
+ @staticmethod
42
+ def _ic_point(section: CrossSection) -> IcPoint:
43
+ common = sorted(section.factor_values.keys() & section.forward_returns.keys())
44
+ factor = {key: section.factor_values[key] for key in common}
45
+ returns = {key: section.forward_returns[key] for key in common}
46
+ return IcPoint(
47
+ as_of=section.as_of,
48
+ ic=_correlation(factor, returns),
49
+ rank_ic=_correlation(_ranks(factor), _ranks(returns)),
50
+ sample_size=len(common),
51
+ )
52
+
53
+ @staticmethod
54
+ def _quantile_returns(
55
+ sections: tuple[CrossSection, ...], quantiles: int
56
+ ) -> tuple[QuantileReturn, ...]:
57
+ buckets: list[list[Decimal]] = [[] for _ in range(quantiles)]
58
+ for section in sections:
59
+ common = sorted(
60
+ section.factor_values.keys() & section.forward_returns.keys(),
61
+ key=lambda key: (section.factor_values[key], key),
62
+ )
63
+ for index, instrument_id in enumerate(common):
64
+ bucket = min(index * quantiles // len(common), quantiles - 1) if common else 0
65
+ buckets[bucket].append(section.forward_returns[instrument_id])
66
+ return tuple(
67
+ QuantileReturn(
68
+ quantile=index + 1,
69
+ mean_return=(sum(values, Decimal(0)) / Decimal(len(values)) if values else None),
70
+ sample_size=len(values),
71
+ )
72
+ for index, values in enumerate(buckets)
73
+ )
74
+
75
+ @staticmethod
76
+ def _decay(
77
+ horizon_sections: dict[int, tuple[CrossSection, ...]],
78
+ ) -> tuple[DecayPoint, ...]:
79
+ result: list[DecayPoint] = []
80
+ for horizon, sections in sorted(horizon_sections.items()):
81
+ if horizon < 1:
82
+ raise ValueError("decay horizon 必须大于 0")
83
+ values = [
84
+ point.rank_ic
85
+ for point in (FactorDiagnosticsService._ic_point(section) for section in sections)
86
+ if point.rank_ic is not None
87
+ ]
88
+ result.append(
89
+ DecayPoint(
90
+ lag=horizon,
91
+ mean_rank_ic=(
92
+ sum(values, Decimal(0)) / Decimal(len(values)) if values else None
93
+ ),
94
+ )
95
+ )
96
+ return tuple(result)
97
+
98
+ @staticmethod
99
+ def _turnover(selections: tuple[tuple[str, ...], ...]) -> Decimal | None:
100
+ if len(selections) < 2:
101
+ return None
102
+ rates: list[Decimal] = []
103
+ for previous, current in zip(selections, selections[1:], strict=False):
104
+ prior = set(previous)
105
+ now = set(current)
106
+ denominator = max(len(prior), len(now))
107
+ rates.append(Decimal(1) - Decimal(len(prior & now)) / Decimal(denominator or 1))
108
+ return sum(rates, Decimal(0)) / Decimal(len(rates))
109
+
110
+
111
+ def _ranks(values: dict[str, Decimal]) -> dict[str, Decimal]:
112
+ groups: dict[Decimal, list[str]] = {}
113
+ for key, value in values.items():
114
+ groups.setdefault(value, []).append(key)
115
+ result: dict[str, Decimal] = {}
116
+ cursor = 1
117
+ for value in sorted(groups):
118
+ members = groups[value]
119
+ rank = (Decimal(cursor) + Decimal(cursor + len(members) - 1)) / Decimal(2)
120
+ result.update({key: rank for key in members})
121
+ cursor += len(members)
122
+ return result
123
+
124
+
125
+ def _correlation(left: dict[str, Decimal], right: dict[str, Decimal]) -> Decimal | None:
126
+ keys = sorted(left.keys() & right.keys())
127
+ if len(keys) < 2:
128
+ return None
129
+ left_mean = sum((left[key] for key in keys), Decimal(0)) / Decimal(len(keys))
130
+ right_mean = sum((right[key] for key in keys), Decimal(0)) / Decimal(len(keys))
131
+ covariance = sum(
132
+ ((left[key] - left_mean) * (right[key] - right_mean) for key in keys), Decimal(0)
133
+ )
134
+ left_variance = sum(((left[key] - left_mean) ** 2 for key in keys), Decimal(0))
135
+ right_variance = sum(((right[key] - right_mean) ** 2 for key in keys), Decimal(0))
136
+ if not left_variance or not right_variance:
137
+ return None
138
+ with localcontext() as context:
139
+ context.prec = 28
140
+ return covariance / (left_variance * right_variance).sqrt()
@@ -0,0 +1,24 @@
1
+ """通过 Market 已有 PIT 基础指标读取市值暴露;不猜测历史行业分类。"""
2
+
3
+ import hashlib
4
+ from datetime import datetime
5
+
6
+ from quant_core.app.data.interface.market_data_query import IMarketDataQuery
7
+ from quant_core.app.factor.schema.selection_types import SelectionExposure
8
+
9
+
10
+ async def query_exposures(
11
+ market: IMarketDataQuery, instrument_ids: tuple[str, ...], decision_time: datetime
12
+ ) -> dict[str, SelectionExposure]:
13
+ """查询指定时点可见市值,行业缺失保持缺值,不推测历史分类。"""
14
+ rows = await market.list_daily_basic_at(decision_time)
15
+ selected = set(instrument_ids)
16
+ return {
17
+ row.instrument_id: SelectionExposure(
18
+ size=row.total_market_cap,
19
+ available_at=decision_time,
20
+ dataset_revision=hashlib.sha256(row.model_dump_json().encode()).hexdigest(),
21
+ )
22
+ for row in rows
23
+ if row.instrument_id in selected
24
+ }