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,342 @@
1
+ # Derived from vn.py fa5206fe63836f3f8cd1ebd7168fbd19a5e2ff09.
2
+ # Copyright (c) 2015-present, Xiaoyou Chen. MIT; see app/factor/LICENSE.vnpy.
3
+ """Time Series Operators"""
4
+
5
+ from typing import cast
6
+
7
+ import numpy as np
8
+ import polars as pl
9
+ from scipy import stats
10
+
11
+ from quant_core.app.factor.computation.panel_expression import PanelExpression
12
+
13
+
14
+ def ts_delay(feature: PanelExpression, window: int) -> PanelExpression:
15
+ """Get the value from a fixed time in the past"""
16
+ df: pl.DataFrame = feature.df.select(
17
+ pl.col("datetime"), pl.col("vt_symbol"), pl.col("data").shift(window).over("vt_symbol")
18
+ )
19
+ return PanelExpression(df)
20
+
21
+
22
+ def ts_min(feature: PanelExpression, window: int) -> PanelExpression:
23
+ """Calculate the minimum value over a rolling window"""
24
+ df: pl.DataFrame = feature.df.select(
25
+ pl.col("datetime"),
26
+ pl.col("vt_symbol"),
27
+ pl.col("data").rolling_min(window, min_samples=1).over("vt_symbol"),
28
+ )
29
+ return PanelExpression(df)
30
+
31
+
32
+ def ts_max(feature: PanelExpression, window: int) -> PanelExpression:
33
+ """Calculate the maximum value over a rolling window"""
34
+ df: pl.DataFrame = feature.df.select(
35
+ pl.col("datetime"),
36
+ pl.col("vt_symbol"),
37
+ pl.col("data").rolling_max(window, min_samples=1).over("vt_symbol"),
38
+ )
39
+ return PanelExpression(df)
40
+
41
+
42
+ def ts_argmax(feature: PanelExpression, window: int) -> PanelExpression:
43
+ """Return the index of the maximum value over a rolling window"""
44
+ df: pl.DataFrame = feature.df.select(
45
+ pl.col("datetime"),
46
+ pl.col("vt_symbol"),
47
+ pl.col("data").rolling_map(lambda s: cast(int, s.arg_max()) + 1, window).over("vt_symbol"),
48
+ )
49
+ return PanelExpression(df)
50
+
51
+
52
+ def ts_argmin(feature: PanelExpression, window: int) -> PanelExpression:
53
+ """Return the index of the minimum value over a rolling window"""
54
+ df: pl.DataFrame = feature.df.select(
55
+ pl.col("datetime"),
56
+ pl.col("vt_symbol"),
57
+ pl.col("data").rolling_map(lambda s: cast(int, s.arg_min()) + 1, window).over("vt_symbol"),
58
+ )
59
+ return PanelExpression(df)
60
+
61
+
62
+ def ts_rank(feature: PanelExpression, window: int) -> PanelExpression:
63
+ """Calculate the percentile rank of the current value within the window"""
64
+ df: pl.DataFrame = feature.df.select(
65
+ pl.col("datetime"),
66
+ pl.col("vt_symbol"),
67
+ pl.col("data")
68
+ .rolling_map(lambda s: stats.percentileofscore(s, s[-1]) / 100, window)
69
+ .over("vt_symbol"),
70
+ )
71
+ return PanelExpression(df)
72
+
73
+
74
+ def ts_sum(feature: PanelExpression, window: int) -> PanelExpression:
75
+ """Calculate the sum over a rolling window"""
76
+ df: pl.DataFrame = feature.df.select(
77
+ pl.col("datetime"),
78
+ pl.col("vt_symbol"),
79
+ pl.col("data").rolling_sum(window).over("vt_symbol"),
80
+ )
81
+ return PanelExpression(df)
82
+
83
+
84
+ def ts_mean(feature: PanelExpression, window: int) -> PanelExpression:
85
+ """Calculate the mean over a rolling window"""
86
+ df: pl.DataFrame = feature.df.select(
87
+ pl.col("datetime"),
88
+ pl.col("vt_symbol"),
89
+ pl.col("data")
90
+ .rolling_map(lambda s: np.nanmean(s), window, min_samples=1)
91
+ .over("vt_symbol"),
92
+ )
93
+ return PanelExpression(df)
94
+
95
+
96
+ def ts_std(feature: PanelExpression, window: int) -> PanelExpression:
97
+ """Calculate the standard deviation over a rolling window"""
98
+ df: pl.DataFrame = feature.df.select(
99
+ pl.col("datetime"),
100
+ pl.col("vt_symbol"),
101
+ pl.col("data")
102
+ .rolling_map(lambda s: np.nanstd(s, ddof=0), window, min_samples=1)
103
+ .over("vt_symbol"),
104
+ )
105
+ return PanelExpression(df)
106
+
107
+
108
+ def ts_slope(feature: PanelExpression, window: int) -> PanelExpression:
109
+ """Calculate the slope of linear regression over a rolling window (optimized)"""
110
+ # 预计算 x 相关的常数 (x = 0, 1, 2, ..., window-1)
111
+ n = window
112
+ sum_x = n * (n - 1) / 2 # 等差数列求和
113
+ sum_x2 = (n - 1) * n * (2 * n - 1) / 6 # 平方和公式
114
+ denominator = n * sum_x2 - sum_x * sum_x
115
+
116
+ # 计算 sum(i * y[t-window+1+i]) for i in 0..window-1
117
+ # 等价于 sum((window-1-j) * y[t-j]) for j in 0..window-1
118
+ sum_xy_expr: pl.Expr = pl.sum_horizontal(
119
+ [(window - 1 - j) * pl.col("data").shift(j) for j in range(window)]
120
+ )
121
+
122
+ df: pl.DataFrame = feature.df.with_columns(
123
+ [
124
+ pl.col("data").rolling_sum(window, min_samples=window).over("vt_symbol").alias("sum_y"),
125
+ sum_xy_expr.over("vt_symbol").alias("sum_xy"),
126
+ ]
127
+ )
128
+
129
+ df = df.select(
130
+ pl.col("datetime"),
131
+ pl.col("vt_symbol"),
132
+ ((n * pl.col("sum_xy") - sum_x * pl.col("sum_y")) / denominator).alias("data"),
133
+ )
134
+ return PanelExpression(df)
135
+
136
+
137
+ def ts_quantile(feature: PanelExpression, window: int, quantile: float) -> PanelExpression:
138
+ """Calculate the quantile value over a rolling window"""
139
+ df: pl.DataFrame = feature.df.select(
140
+ pl.col("datetime"),
141
+ pl.col("vt_symbol"),
142
+ pl.col("data")
143
+ .rolling_map(lambda s: s.quantile(quantile=quantile, interpolation="linear"), window)
144
+ .over("vt_symbol"),
145
+ )
146
+ return PanelExpression(df)
147
+
148
+
149
+ def ts_rsquare(feature: PanelExpression, window: int) -> PanelExpression:
150
+ """Calculate the R-squared value of linear regression over a rolling window (optimized)"""
151
+ # 预计算 x 相关的常数 (x = 0, 1, 2, ..., window-1)
152
+ n = window
153
+ sum_x2 = (n - 1) * n * (2 * n - 1) / 6 # 平方和公式
154
+ mean_x = (n - 1) / 2
155
+ var_x = sum_x2 / n - mean_x * mean_x # 总体方差
156
+
157
+ # 计算 sum(i * y[t-window+1+i]) for i in 0..window-1
158
+ sum_xy_expr: pl.Expr = pl.sum_horizontal(
159
+ [(window - 1 - j) * pl.col("data").shift(j) for j in range(window)]
160
+ )
161
+
162
+ df: pl.DataFrame = feature.df.with_columns(
163
+ [
164
+ pl.col("data").rolling_sum(window, min_samples=window).over("vt_symbol").alias("sum_y"),
165
+ pl.col("data")
166
+ .rolling_var(window, min_samples=window, ddof=0)
167
+ .over("vt_symbol")
168
+ .alias("var_y"),
169
+ sum_xy_expr.over("vt_symbol").alias("sum_xy"),
170
+ ]
171
+ )
172
+
173
+ # mean_y 和 cov(x, y) = E(xy) - E(x)E(y)
174
+ df = df.with_columns(
175
+ [
176
+ (pl.col("sum_y") / n).alias("mean_y"),
177
+ ]
178
+ )
179
+
180
+ df = df.with_columns([(pl.col("sum_xy") / n - mean_x * pl.col("mean_y")).alias("cov_xy")])
181
+
182
+ # r = cov(x,y) / (std_x * std_y), r^2 = cov(x,y)^2 / (var_x * var_y)
183
+ df = df.select(
184
+ pl.col("datetime"),
185
+ pl.col("vt_symbol"),
186
+ (pl.col("cov_xy").pow(2) / (var_x * pl.col("var_y"))).alias("data"),
187
+ )
188
+
189
+ df = df.with_columns(
190
+ pl.when(pl.col("data").is_infinite() | pl.col("data").is_nan())
191
+ .then(None)
192
+ .otherwise(pl.col("data"))
193
+ .alias("data")
194
+ )
195
+
196
+ return PanelExpression(df)
197
+
198
+
199
+ def ts_resi(feature: PanelExpression, window: int) -> PanelExpression:
200
+ """Calculate the residual of linear regression over a rolling window (optimized)"""
201
+ # 预计算 x 相关的常数 (x = 0, 1, 2, ..., window-1)
202
+ n = window
203
+ sum_x = n * (n - 1) / 2 # 等差数列求和
204
+ sum_x2 = (n - 1) * n * (2 * n - 1) / 6 # 平方和公式
205
+ mean_x = (n - 1) / 2
206
+ denominator = n * sum_x2 - sum_x * sum_x
207
+
208
+ # 计算 sum(i * y[t-window+1+i]) for i in 0..window-1
209
+ sum_xy_expr: pl.Expr = pl.sum_horizontal(
210
+ [(window - 1 - j) * pl.col("data").shift(j) for j in range(window)]
211
+ )
212
+
213
+ df: pl.DataFrame = feature.df.with_columns(
214
+ [
215
+ pl.col("data").rolling_sum(window, min_samples=window).over("vt_symbol").alias("sum_y"),
216
+ sum_xy_expr.over("vt_symbol").alias("sum_xy"),
217
+ ]
218
+ )
219
+
220
+ # 计算 slope 和 intercept
221
+ df = df.with_columns(
222
+ [
223
+ ((n * pl.col("sum_xy") - sum_x * pl.col("sum_y")) / denominator).alias("slope"),
224
+ (pl.col("sum_y") / n).alias("mean_y"),
225
+ ]
226
+ )
227
+
228
+ df = df.with_columns([(pl.col("mean_y") - pl.col("slope") * mean_x).alias("intercept")])
229
+
230
+ # residual = y - (slope * (n-1) + intercept),最后一个点的 x = n-1
231
+ df = df.select(
232
+ pl.col("datetime"),
233
+ pl.col("vt_symbol"),
234
+ (pl.col("data") - (pl.col("slope") * (n - 1) + pl.col("intercept"))).alias("data"),
235
+ )
236
+
237
+ return PanelExpression(df)
238
+
239
+
240
+ def ts_corr(feature1: PanelExpression, feature2: PanelExpression, window: int) -> PanelExpression:
241
+ """Calculate the correlation between two features over a rolling window"""
242
+ df_merged: pl.DataFrame = feature1.df.join(feature2.df, on=["datetime", "vt_symbol"])
243
+
244
+ df: pl.DataFrame = df_merged.select(
245
+ pl.col("datetime"),
246
+ pl.col("vt_symbol"),
247
+ pl.rolling_corr("data", "data_right", window_size=window, min_samples=1)
248
+ .over("vt_symbol")
249
+ .alias("data"),
250
+ )
251
+
252
+ df = df.with_columns(
253
+ pl.when(pl.col("data").is_infinite()).then(None).otherwise(pl.col("data")).alias("data")
254
+ )
255
+
256
+ return PanelExpression(df)
257
+
258
+
259
+ def ts_less(feature1: PanelExpression, feature2: PanelExpression | float) -> PanelExpression:
260
+ """Return the minimum value between two features"""
261
+ if isinstance(feature2, PanelExpression):
262
+ df_merged: pl.DataFrame = feature1.df.join(feature2.df, on=["datetime", "vt_symbol"])
263
+ else:
264
+ df_merged = feature1.df.with_columns(pl.lit(feature2).alias("data_right"))
265
+
266
+ df: pl.DataFrame = df_merged.select(
267
+ pl.col("datetime"),
268
+ pl.col("vt_symbol"),
269
+ pl.min_horizontal("data", "data_right").over("vt_symbol").alias("data"),
270
+ )
271
+
272
+ return PanelExpression(df)
273
+
274
+
275
+ def ts_greater(feature1: PanelExpression, feature2: PanelExpression | float) -> PanelExpression:
276
+ """Return the maximum value between two features"""
277
+ if isinstance(feature2, PanelExpression):
278
+ df_merged: pl.DataFrame = feature1.df.join(feature2.df, on=["datetime", "vt_symbol"])
279
+
280
+ else:
281
+ df_merged = feature1.df.with_columns(pl.lit(feature2).alias("data_right"))
282
+
283
+ df: pl.DataFrame = df_merged.select(
284
+ pl.col("datetime"),
285
+ pl.col("vt_symbol"),
286
+ pl.max_horizontal("data", "data_right").over("vt_symbol").alias("data"),
287
+ )
288
+
289
+ return PanelExpression(df)
290
+
291
+
292
+ def ts_log(feature: PanelExpression) -> PanelExpression:
293
+ """Calculate the natural logarithm of the feature"""
294
+ df: pl.DataFrame = feature.df.select(
295
+ pl.col("datetime"), pl.col("vt_symbol"), pl.col("data").log().over("vt_symbol")
296
+ )
297
+ return PanelExpression(df)
298
+
299
+
300
+ def ts_abs(feature: PanelExpression) -> PanelExpression:
301
+ """Calculate the absolute value of the feature"""
302
+ df: pl.DataFrame = feature.df.select(
303
+ pl.col("datetime"), pl.col("vt_symbol"), pl.col("data").abs().over("vt_symbol")
304
+ )
305
+ return PanelExpression(df)
306
+
307
+
308
+ def ts_delta(feature: PanelExpression, window: int) -> PanelExpression:
309
+ """Calculate difference between current value and value from window periods ago"""
310
+ return feature - ts_delay(feature, window)
311
+
312
+
313
+ def ts_cov(feature1: PanelExpression, feature2: PanelExpression, window: int) -> PanelExpression:
314
+ """Calculate covariance between two features over a rolling window"""
315
+ return ts_corr(feature1, feature2, window) * ts_std(feature1, window) * ts_std(feature2, window)
316
+
317
+
318
+ def ts_decay_linear(feature: PanelExpression, window: int) -> PanelExpression:
319
+ """Calculate linear decay weighted average"""
320
+
321
+ def decay_func(s: pl.Series) -> float:
322
+ """Calculate linear decay weighted average for a series"""
323
+ weights = pl.Series(range(window, 0, -1))
324
+ denominator: int = window * (window + 1) // 2
325
+ return float((s * weights).sum() / denominator)
326
+
327
+ df: pl.DataFrame = feature.df.select(
328
+ pl.col("datetime"),
329
+ pl.col("vt_symbol"),
330
+ pl.col("data").rolling_map(lambda s: decay_func(s), window).over("vt_symbol"),
331
+ )
332
+ return PanelExpression(df)
333
+
334
+
335
+ def ts_product(feature: PanelExpression, window: int) -> PanelExpression:
336
+ """Calculate the product over a rolling window"""
337
+ df: pl.DataFrame = feature.df.select(
338
+ pl.col("datetime"),
339
+ pl.col("vt_symbol"),
340
+ pl.col("data").rolling_map(lambda s: s.product(), window).over("vt_symbol"),
341
+ )
342
+ return PanelExpression(df)
@@ -0,0 +1,59 @@
1
+ """可信本地 Python 面板执行器,统一输出数值与键校验。"""
2
+
3
+ from collections import deque
4
+ from collections.abc import Callable
5
+ from decimal import Decimal
6
+ from typing import Any
7
+
8
+ import polars as pl
9
+
10
+ from quant_core.app.factor.computation.panel_expression import PanelExpression
11
+ from quant_core.app.factor.computation.universe_filter import ELIGIBLE_KEYS
12
+ from quant_core.app.factor.schema.factor_panel import FactorPanel
13
+ from quant_core.app.factor.schema.factor_result_schema import PanelValues
14
+
15
+
16
+ def evaluate_panel(
17
+ panel: FactorPanel,
18
+ recipe: Callable[[dict[str, PanelExpression]], PanelExpression],
19
+ lookback: int,
20
+ required: tuple[str, ...],
21
+ ) -> PanelValues:
22
+ """运行固定 Python 配方;缺字段、零成交和预热不足保持显式缺值。"""
23
+ rows = sorted(panel.rows, key=lambda row: (row.as_of, row.instrument_id))
24
+ if not rows:
25
+ return {}
26
+ columns: dict[str, Any] = {
27
+ "datetime": [row.as_of for row in rows],
28
+ "vt_symbol": [row.instrument_id for row in rows],
29
+ }
30
+ for field in required:
31
+ columns[field] = [
32
+ float(value) if (value := row.fields.get(field)) is not None else None for row in rows
33
+ ]
34
+ frame = pl.DataFrame(columns, schema_overrides={field: pl.Float64 for field in required})
35
+ fields = {
36
+ field: PanelExpression(frame.select("datetime", "vt_symbol", field)) for field in required
37
+ }
38
+ keys = frozenset((row.as_of, row.instrument_id) for row in rows if row.eligible)
39
+ token = ELIGIBLE_KEYS.set(keys)
40
+ try:
41
+ result = recipe(fields).df
42
+ finally:
43
+ ELIGIBLE_KEYS.reset(token)
44
+ history: dict[str, int] = {}
45
+ validity: dict[str, deque[bool]] = {}
46
+ values: PanelValues = {}
47
+ source = {(row.as_of, row.instrument_id): row for row in rows}
48
+ for timestamp, instrument_id, raw in result.iter_rows():
49
+ key = (timestamp, instrument_id)
50
+ history[instrument_id] = history.get(instrument_id, 0) + 1
51
+ window = validity.setdefault(instrument_id, deque(maxlen=lookback))
52
+ window.append(all(source[key].fields.get(field) is not None for field in required))
53
+ value = Decimal(str(raw)) if raw is not None else None
54
+ if not source[key].eligible or history[instrument_id] < lookback or not all(window):
55
+ value = None
56
+ if value is not None and not value.is_finite():
57
+ value = None
58
+ values[key] = value
59
+ return values
@@ -0,0 +1,193 @@
1
+ """向量算子的数据代理;不包含表达式求值器。"""
2
+ # Derived from vn.py fa5206fe63836f3f8cd1ebd7168fbd19a5e2ff09.
3
+ # Copyright (c) 2015-present, Xiaoyou Chen. MIT; see app/factor/LICENSE.vnpy.
4
+
5
+ from numbers import Real
6
+ from typing import Union, cast
7
+
8
+ import polars as pl
9
+
10
+
11
+ class PanelExpression:
12
+ """Feature data proxy"""
13
+
14
+ def __init__(self, df: pl.DataFrame) -> None:
15
+ """Constructor"""
16
+ self.name: str = df.columns[-1]
17
+ self.df: pl.DataFrame = df.rename({self.name: "data"})
18
+
19
+ # Note that for numerical expressions, variables should be placed before numbers. e.g. a * 2
20
+
21
+ @staticmethod
22
+ def _as_series(value: object) -> pl.Series:
23
+ """Normalize an operator result to a Polars series."""
24
+ if isinstance(value, pl.Series):
25
+ return value
26
+
27
+ return cast(pl.Series, value)
28
+
29
+ def _comparison_series(self, value: object) -> pl.Series:
30
+ """Normalize comparison results to an Int32 series."""
31
+ if isinstance(value, pl.Series):
32
+ return value.cast(pl.Int32)
33
+
34
+ if isinstance(value, bool):
35
+ return pl.Series(name="data", values=[int(value)] * len(self.df))
36
+
37
+ if isinstance(value, Real):
38
+ return pl.Series(name="data", values=[int(bool(value))] * len(self.df))
39
+
40
+ raise TypeError(f"Unsupported comparison result type: {type(value)!r}")
41
+
42
+ def result(self, s: pl.Series) -> "PanelExpression":
43
+ """Convert series data to feature object"""
44
+ result: pl.DataFrame = self.df[["datetime", "vt_symbol"]]
45
+ result = result.with_columns(other=s)
46
+
47
+ return PanelExpression(result)
48
+
49
+ def __add__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
50
+ """Addition operation"""
51
+ if isinstance(other, PanelExpression):
52
+ s = self._as_series(self.df["data"] + other.df["data"])
53
+ else:
54
+ s = self._as_series(self.df["data"] + other)
55
+ return self.result(s)
56
+
57
+ def __radd__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
58
+ """Right addition operation"""
59
+ if isinstance(other, PanelExpression):
60
+ s = self._as_series(other.df["data"] + self.df["data"])
61
+ else:
62
+ s = self._as_series(other + self.df["data"])
63
+ return self.result(s)
64
+
65
+ def __sub__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
66
+ """Subtraction operation"""
67
+ if isinstance(other, PanelExpression):
68
+ s = self._as_series(self.df["data"] - other.df["data"])
69
+ else:
70
+ s = self._as_series(self.df["data"] - other)
71
+ return self.result(s)
72
+
73
+ def __rsub__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
74
+ """Right subtraction operation"""
75
+ if isinstance(other, PanelExpression):
76
+ s = self._as_series(other.df["data"] - self.df["data"])
77
+ else:
78
+ s = self._as_series(other - self.df["data"])
79
+ return self.result(s)
80
+
81
+ def __mul__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
82
+ """Multiplication operation"""
83
+ if isinstance(other, PanelExpression):
84
+ s = self._as_series(self.df["data"] * other.df["data"])
85
+ else:
86
+ s = self._as_series(self.df["data"] * other)
87
+ return self.result(s)
88
+
89
+ def __rmul__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
90
+ """Right multiplication operation"""
91
+ if isinstance(other, PanelExpression):
92
+ s = self._as_series(self.df["data"] * other.df["data"])
93
+ else:
94
+ s = self._as_series(self.df["data"] * other)
95
+ return self.result(s)
96
+
97
+ def __truediv__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
98
+ """Division operation"""
99
+ if isinstance(other, PanelExpression):
100
+ s = self._as_series(self.df["data"] / other.df["data"])
101
+ else:
102
+ s = self._as_series(self.df["data"] / other)
103
+ return self.result(s)
104
+
105
+ def __rtruediv__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
106
+ """Right division operation"""
107
+ if isinstance(other, PanelExpression):
108
+ s = self._as_series(other.df["data"] / self.df["data"])
109
+ else:
110
+ s = self._as_series(other / self.df["data"])
111
+ return self.result(s)
112
+
113
+ def __floordiv__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
114
+ """Floor division operation"""
115
+ if isinstance(other, PanelExpression):
116
+ s = self._as_series(self.df["data"] // other.df["data"])
117
+ else:
118
+ s = self._as_series(self.df["data"] // other)
119
+ return self.result(s)
120
+
121
+ def __mod__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
122
+ """Modulo operation"""
123
+ if isinstance(other, PanelExpression):
124
+ s = self._as_series(self.df["data"] % other.df["data"])
125
+ else:
126
+ s = self._as_series(self.df["data"] % other)
127
+ return self.result(s)
128
+
129
+ def __pow__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
130
+ """Power operation"""
131
+ if isinstance(other, PanelExpression):
132
+ s = self._as_series(self.df["data"].pow(other.df["data"]))
133
+ else:
134
+ s = self._as_series(self.df["data"].pow(cast(int | float, other)))
135
+ return self.result(s)
136
+
137
+ def __abs__(self) -> "PanelExpression":
138
+ """Get absolute value"""
139
+ s: pl.Series = self.df["data"].abs()
140
+ return self.result(s)
141
+
142
+ def __neg__(self) -> "PanelExpression":
143
+ """Negation operation"""
144
+ s: pl.Series = -self.df["data"]
145
+ return self.result(s)
146
+
147
+ def __gt__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
148
+ """Greater than comparison"""
149
+ if isinstance(other, PanelExpression):
150
+ s: object = self.df["data"] > other.df["data"]
151
+ else:
152
+ s = self.df["data"] > other
153
+ return self.result(self._comparison_series(s))
154
+
155
+ def __ge__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
156
+ """Greater than or equal comparison"""
157
+ if isinstance(other, PanelExpression):
158
+ s: object = self.df["data"] >= other.df["data"]
159
+ else:
160
+ s = self.df["data"] >= other
161
+ return self.result(self._comparison_series(s))
162
+
163
+ def __lt__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
164
+ """Less than comparison"""
165
+ if isinstance(other, PanelExpression):
166
+ s: object = self.df["data"] < other.df["data"]
167
+ else:
168
+ s = self.df["data"] < other
169
+ return self.result(self._comparison_series(s))
170
+
171
+ def __le__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression":
172
+ """Less than or equal comparison"""
173
+ if isinstance(other, PanelExpression):
174
+ s: object = self.df["data"] <= other.df["data"]
175
+ else:
176
+ s = self.df["data"] <= other
177
+ return self.result(self._comparison_series(s))
178
+
179
+ def __eq__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression": # type: ignore[override]
180
+ """Equal comparison"""
181
+ if isinstance(other, PanelExpression):
182
+ s: object = self.df["data"] == other.df["data"]
183
+ else:
184
+ s = self.df["data"] == other
185
+ return self.result(self._comparison_series(s))
186
+
187
+ def __ne__(self, other: Union["PanelExpression", Real, int, float]) -> "PanelExpression": # type: ignore[override]
188
+ """Not equal comparison"""
189
+ if isinstance(other, PanelExpression):
190
+ s: object = self.df["data"] != other.df["data"]
191
+ else:
192
+ s = self.df["data"] != other
193
+ return self.result(self._comparison_series(s))
@@ -0,0 +1,87 @@
1
+ """Trusted local Python execution for user-authored factors."""
2
+
3
+ from collections.abc import Callable
4
+ from decimal import Decimal, InvalidOperation
5
+ from types import CodeType
6
+ from typing import Any
7
+
8
+ from quant_core.app.data.schema.market_schema import BarResponse
9
+ from quant_core.app.factor.schema.factor_computation_schema import FactorComputationSpec
10
+ from quant_core.app.factor.schema.factor_panel import FactorPanel
11
+ from quant_core.app.factor.schema.factor_result_schema import PanelValues
12
+
13
+
14
+ def _load_function(source: str, name: str, compiled: dict[str, CodeType]) -> Callable[..., Any]:
15
+ """复用编译结果,每次执行仍使用独立命名空间;仅用于可信代码。"""
16
+ if len(source.encode("utf-8")) > 65536:
17
+ raise ValueError("因子源码超过 64 KiB")
18
+ if source not in compiled:
19
+ compiled[source] = compile(source, "<factor>", "exec")
20
+ namespace: dict[str, Any] = {
21
+ "__name__": "factor_panel_user_code" if name == "compute_panel" else "factor_user_code"
22
+ }
23
+ exec(compiled[source], namespace)
24
+ function = namespace.get(name)
25
+ if not callable(function):
26
+ raise ValueError(f"因子源码必须定义 {name}")
27
+ return function
28
+
29
+
30
+ def _factor_number(value: Any) -> Decimal | None:
31
+ if value is None:
32
+ return None
33
+ try:
34
+ number = Decimal(str(value))
35
+ except (InvalidOperation, ValueError) as exc:
36
+ raise ValueError("因子计算结果不是数值") from exc
37
+ if not number.is_finite():
38
+ raise ValueError("因子计算结果必须为有限数值")
39
+ return number
40
+
41
+
42
+ def execute_python_windows(
43
+ calls: list[tuple[str, list[BarResponse], dict[str, Any]]],
44
+ ) -> list[Decimal | None]:
45
+ """执行独立证券窗口函数,保留 Decimal 精度与逐次全局变量隔离。"""
46
+ compiled: dict[str, CodeType] = {}
47
+ results: list[Decimal | None] = []
48
+ for source, bars, parameters in calls:
49
+ compute = _load_function(source, "compute", compiled)
50
+ input_bars = [
51
+ {
52
+ "timestamp": bar.timestamp.isoformat(),
53
+ "open": str(bar.open),
54
+ "high": str(bar.high),
55
+ "low": str(bar.low),
56
+ "close": str(bar.close),
57
+ "volume": str(bar.volume),
58
+ "amount": str(bar.amount) if bar.amount is not None else None,
59
+ "previous_close": (
60
+ str(bar.previous_close) if bar.previous_close is not None else None
61
+ ),
62
+ "suspended": bar.suspended,
63
+ }
64
+ for bar in bars
65
+ ]
66
+ results.append(_factor_number(compute(input_bars, dict(parameters))))
67
+ return results
68
+
69
+
70
+ def execute_python_panel(spec: FactorComputationSpec, panel: FactorPanel) -> PanelValues:
71
+ """执行完整横截面函数并校验结果键和有限数值。"""
72
+ problems = spec.validate_definition()
73
+ if spec.input_kind != "panel" or problems:
74
+ raise ValueError(f"面板计算规格非法: {problems}")
75
+ if spec.frequency.value != panel.frequency:
76
+ raise ValueError("面板输入与因子频率不一致")
77
+ compute = _load_function(spec.python_source, "compute_panel", {})
78
+ result = compute(panel, dict(spec.parameters))
79
+ if not isinstance(result, dict):
80
+ raise ValueError("compute_panel 必须返回 (as_of, instrument_id) 到数值的映射")
81
+ expected = {(row.as_of, row.instrument_id) for row in panel.rows}
82
+ if set(result) != expected:
83
+ raise ValueError("面板结果必须完整覆盖输入键,不允许多出或遗漏")
84
+ values: PanelValues = {}
85
+ for key, raw in result.items():
86
+ values[key] = _factor_number(raw)
87
+ return values