quantark 0.1.0__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 (399) hide show
  1. quantark/__init__.py +3 -0
  2. quantark/_compat.py +150 -0
  3. quantark/asset/__init__.py +8 -0
  4. quantark/asset/bond/__init__.py +2 -0
  5. quantark/asset/bond/engine/__init__.py +44 -0
  6. quantark/asset/bond/engine/analytical/__init__.py +12 -0
  7. quantark/asset/bond/engine/analytical/black_engine.py +583 -0
  8. quantark/asset/bond/engine/analytical/bond_forward_engine.py +390 -0
  9. quantark/asset/bond/engine/analytical/bond_futures_engine.py +569 -0
  10. quantark/asset/bond/engine/convertible/__init__.py +12 -0
  11. quantark/asset/bond/engine/convertible/convertible_bond_engine.py +800 -0
  12. quantark/asset/bond/engine/discount/__init__.py +10 -0
  13. quantark/asset/bond/engine/discount/bond_discount_engine.py +517 -0
  14. quantark/asset/bond/engine/discount/frn_engine.py +913 -0
  15. quantark/asset/bond/engine/pde/__init__.py +14 -0
  16. quantark/asset/bond/engine/pde/convertible/__init__.py +21 -0
  17. quantark/asset/bond/engine/pde/convertible/jump_diffusion_engine.py +603 -0
  18. quantark/asset/bond/engine/pde/convertible/pde_params.py +59 -0
  19. quantark/asset/bond/engine/pde/convertible/tf_engine.py +546 -0
  20. quantark/asset/bond/engine/tree/__init__.py +14 -0
  21. quantark/asset/bond/engine/tree/convertible/__init__.py +21 -0
  22. quantark/asset/bond/engine/tree/convertible/binomial_engine.py +488 -0
  23. quantark/asset/bond/engine/tree/convertible/tree_params.py +72 -0
  24. quantark/asset/bond/engine/tree/convertible/trinomial_engine.py +1341 -0
  25. quantark/asset/bond/product/__init__.py +37 -0
  26. quantark/asset/bond/product/base_bond_product.py +114 -0
  27. quantark/asset/bond/product/convertible/__init__.py +16 -0
  28. quantark/asset/bond/product/convertible/convertible_bond.py +595 -0
  29. quantark/asset/bond/product/couponbond/__init__.py +12 -0
  30. quantark/asset/bond/product/couponbond/fixed_bond.py +285 -0
  31. quantark/asset/bond/product/couponbond/frn.py +538 -0
  32. quantark/asset/bond/product/forward/__init__.py +9 -0
  33. quantark/asset/bond/product/forward/base_bond_forward.py +92 -0
  34. quantark/asset/bond/product/forward/bond_forward.py +335 -0
  35. quantark/asset/bond/product/futures/__init__.py +8 -0
  36. quantark/asset/bond/product/futures/bond_futures.py +532 -0
  37. quantark/asset/bond/product/option/__init__.py +9 -0
  38. quantark/asset/bond/product/option/euro_short_term_bond_option.py +231 -0
  39. quantark/asset/bond/riskmeasures/__init__.py +13 -0
  40. quantark/asset/bond/riskmeasures/bond_greeks_calculator.py +484 -0
  41. quantark/asset/bond/schedule/__init__.py +21 -0
  42. quantark/asset/bond/schedule/cashflow.py +595 -0
  43. quantark/asset/equity/__init__.py +11 -0
  44. quantark/asset/equity/analysis/__init__.py +4 -0
  45. quantark/asset/equity/analysis/autocallable_path_analyzer.py +257 -0
  46. quantark/asset/equity/engine/__init__.py +84 -0
  47. quantark/asset/equity/engine/analytical/__init__.py +37 -0
  48. quantark/asset/equity/engine/analytical/american_option_engine.py +682 -0
  49. quantark/asset/equity/engine/analytical/asian_option_analytical_engine.py +1102 -0
  50. quantark/asset/equity/engine/analytical/barrier_analytical_engine.py +455 -0
  51. quantark/asset/equity/engine/analytical/black_scholes_engine.py +322 -0
  52. quantark/asset/equity/engine/analytical/deltaone_engine.py +340 -0
  53. quantark/asset/equity/engine/analytical/digital_option_engine.py +168 -0
  54. quantark/asset/equity/engine/analytical/double_barrier_option_engine.py +481 -0
  55. quantark/asset/equity/engine/analytical/double_sharkfin_option_analytical_engine.py +508 -0
  56. quantark/asset/equity/engine/analytical/one_touch_analytical_engine.py +302 -0
  57. quantark/asset/equity/engine/analytical/range_accrual_analytical_engine.py +396 -0
  58. quantark/asset/equity/engine/analytical/single_sharkfin_option_analytical_engine.py +229 -0
  59. quantark/asset/equity/engine/base_engine.py +137 -0
  60. quantark/asset/equity/engine/event_stats.py +85 -0
  61. quantark/asset/equity/engine/mc/__init__.py +31 -0
  62. quantark/asset/equity/engine/mc/american_option_mc_engine.py +485 -0
  63. quantark/asset/equity/engine/mc/asian_option_mc_engine.py +678 -0
  64. quantark/asset/equity/engine/mc/barrier_option_mc_engine.py +726 -0
  65. quantark/asset/equity/engine/mc/digital_option_mc_engine.py +419 -0
  66. quantark/asset/equity/engine/mc/double_sharkfin_option_mc_engine.py +676 -0
  67. quantark/asset/equity/engine/mc/euro_mc_engine.py +423 -0
  68. quantark/asset/equity/engine/mc/phoenix_mc_engine.py +1206 -0
  69. quantark/asset/equity/engine/mc/range_accrual_mc_engine.py +738 -0
  70. quantark/asset/equity/engine/mc/single_sharkfin_option_mc_engine.py +549 -0
  71. quantark/asset/equity/engine/mc/snowball_mc_engine.py +2250 -0
  72. quantark/asset/equity/engine/pde/__init__.py +36 -0
  73. quantark/asset/equity/engine/pde/american_pde_solver.py +211 -0
  74. quantark/asset/equity/engine/pde/barrier_pde_solver.py +692 -0
  75. quantark/asset/equity/engine/pde/base_pde_solver.py +994 -0
  76. quantark/asset/equity/engine/pde/double_barrier_pde_solver.py +510 -0
  77. quantark/asset/equity/engine/pde/double_one_touch_pde_solver.py +435 -0
  78. quantark/asset/equity/engine/pde/european_pde_solver.py +170 -0
  79. quantark/asset/equity/engine/pde/ko_reset_snowball_pde_solver.py +477 -0
  80. quantark/asset/equity/engine/pde/one_touch_pde_solver.py +439 -0
  81. quantark/asset/equity/engine/pde/phoenix_pde_solver.py +613 -0
  82. quantark/asset/equity/engine/pde/snowball_pde_solver.py +1810 -0
  83. quantark/asset/equity/engine/pde/spatial_grid.py +750 -0
  84. quantark/asset/equity/engine/pde/time_grid.py +308 -0
  85. quantark/asset/equity/engine/pde_engine.py +238 -0
  86. quantark/asset/equity/engine/quad/__init__.py +23 -0
  87. quantark/asset/equity/engine/quad/discrete_quad_engine.py +106 -0
  88. quantark/asset/equity/engine/quad/european_quad_engine.py +325 -0
  89. quantark/asset/equity/engine/quad/ko_reset_snowball_quad_engine.py +362 -0
  90. quantark/asset/equity/engine/quad/phoenix_quad_engine.py +614 -0
  91. quantark/asset/equity/engine/quad/quad_adapters.py +1260 -0
  92. quantark/asset/equity/engine/quad/quad_core.py +513 -0
  93. quantark/asset/equity/engine/quad/quad_math.py +219 -0
  94. quantark/asset/equity/engine/quad/snowball_quad_engine.py +1137 -0
  95. quantark/asset/equity/engine/validation/script/benchmark_check_american_analytical.py +117 -0
  96. quantark/asset/equity/engine/validation/script/benchmark_check_american_pde.py +114 -0
  97. quantark/asset/equity/engine/validation/script/benchmark_check_asian_analytical.py +440 -0
  98. quantark/asset/equity/engine/validation/script/benchmark_check_barrier_analytical.py +269 -0
  99. quantark/asset/equity/engine/validation/script/benchmark_check_barrier_pde_solver.py +636 -0
  100. quantark/asset/equity/engine/validation/script/benchmark_check_digital_option.py +256 -0
  101. quantark/asset/equity/engine/validation/script/benchmark_check_snowball_pde_solver.py +807 -0
  102. quantark/asset/equity/engine/validation/script/boundary_check_american_analytical.py +290 -0
  103. quantark/asset/equity/engine/validation/script/boundary_check_american_pde.py +242 -0
  104. quantark/asset/equity/engine/validation/script/boundary_check_asian_analytical.py +612 -0
  105. quantark/asset/equity/engine/validation/script/boundary_check_barrier_analytical.py +434 -0
  106. quantark/asset/equity/engine/validation/script/boundary_check_barrier_pde_solver.py +748 -0
  107. quantark/asset/equity/engine/validation/script/boundary_check_digital_option.py +575 -0
  108. quantark/asset/equity/engine/validation/script/boundary_check_snowball_pde_solver.py +1101 -0
  109. quantark/asset/equity/engine/validation/script/greeks_check_digital_option.py +349 -0
  110. quantark/asset/equity/engine/validation/script/mc_comparison_barrier_pde.py +270 -0
  111. quantark/asset/equity/engine/validation/script/quick_mc_compare.py +51 -0
  112. quantark/asset/equity/engine/validation/script/validation_stepdown_improved.py +97 -0
  113. quantark/asset/equity/param/__init__.py +24 -0
  114. quantark/asset/equity/param/engine_param_profiles.py +325 -0
  115. quantark/asset/equity/param/engine_params.py +728 -0
  116. quantark/asset/equity/process/__init__.py +7 -0
  117. quantark/asset/equity/process/bsm/__init__.py +7 -0
  118. quantark/asset/equity/process/bsm/bsm_process.py +108 -0
  119. quantark/asset/equity/process/bsm/qmc_brownian_bridge.py +401 -0
  120. quantark/asset/equity/process/bsm/qmc_path_generator.py +694 -0
  121. quantark/asset/equity/process/bsm/qmc_rqmc_driver.py +163 -0
  122. quantark/asset/equity/process/bsm/qmc_sobol.py +195 -0
  123. quantark/asset/equity/process/bsm/qmc_variance_reduction.py +292 -0
  124. quantark/asset/equity/product/__init__.py +8 -0
  125. quantark/asset/equity/product/base_equity_product.py +72 -0
  126. quantark/asset/equity/product/deltaone/__init__.py +22 -0
  127. quantark/asset/equity/product/deltaone/base_deltaone_product.py +147 -0
  128. quantark/asset/equity/product/deltaone/futures.py +485 -0
  129. quantark/asset/equity/product/deltaone/spot_instrument.py +118 -0
  130. quantark/asset/equity/product/option/__init__.py +104 -0
  131. quantark/asset/equity/product/option/american_option.py +114 -0
  132. quantark/asset/equity/product/option/asian_option.py +531 -0
  133. quantark/asset/equity/product/option/barrier_option.py +289 -0
  134. quantark/asset/equity/product/option/base_equity_option.py +659 -0
  135. quantark/asset/equity/product/option/digital_option.py +102 -0
  136. quantark/asset/equity/product/option/double_barrier_option.py +286 -0
  137. quantark/asset/equity/product/option/double_one_touch_option.py +310 -0
  138. quantark/asset/equity/product/option/double_sharkfin_option.py +466 -0
  139. quantark/asset/equity/product/option/european_vanilla_option.py +103 -0
  140. quantark/asset/equity/product/option/ko_reset_snowball_option.py +563 -0
  141. quantark/asset/equity/product/option/observation_schedule.py +530 -0
  142. quantark/asset/equity/product/option/one_touch_option.py +287 -0
  143. quantark/asset/equity/product/option/phoenix_config.py +116 -0
  144. quantark/asset/equity/product/option/phoenix_helpers.py +576 -0
  145. quantark/asset/equity/product/option/phoenix_option.py +1167 -0
  146. quantark/asset/equity/product/option/range_accrual_config.py +288 -0
  147. quantark/asset/equity/product/option/range_accrual_helpers.py +608 -0
  148. quantark/asset/equity/product/option/range_accrual_option.py +526 -0
  149. quantark/asset/equity/product/option/single_sharkfin_option.py +420 -0
  150. quantark/asset/equity/product/option/snowball_config.py +261 -0
  151. quantark/asset/equity/product/option/snowball_helpers.py +977 -0
  152. quantark/asset/equity/product/option/snowball_option.py +1242 -0
  153. quantark/asset/equity/report/__init__.py +15 -0
  154. quantark/asset/equity/report/autocallable_risk_report.py +2118 -0
  155. quantark/asset/equity/report/plotting.py +87 -0
  156. quantark/asset/equity/report/snowball_risk_comparison_report.py +2230 -0
  157. quantark/asset/equity/report/surfaces.py +123 -0
  158. quantark/asset/equity/report/term_structure.py +126 -0
  159. quantark/asset/equity/riskmeasures/__init__.py +7 -0
  160. quantark/asset/equity/riskmeasures/greeks_calculator.py +1204 -0
  161. quantark/asset/rate/__init__.py +58 -0
  162. quantark/asset/rate/engine/__init__.py +25 -0
  163. quantark/asset/rate/engine/cap_floor_engine.py +514 -0
  164. quantark/asset/rate/engine/fra_engine.py +286 -0
  165. quantark/asset/rate/engine/irs_discount_engine.py +891 -0
  166. quantark/asset/rate/engine/swaption_engine.py +587 -0
  167. quantark/asset/rate/product/__init__.py +67 -0
  168. quantark/asset/rate/product/cap_floor.py +550 -0
  169. quantark/asset/rate/product/fra.py +219 -0
  170. quantark/asset/rate/product/irs.py +1223 -0
  171. quantark/asset/rate/product/swaption.py +372 -0
  172. quantark/backtest/__init__.py +153 -0
  173. quantark/backtest/base.py +263 -0
  174. quantark/backtest/dashboard.py +874 -0
  175. quantark/backtest/equity/__init__.py +35 -0
  176. quantark/backtest/equity/config.py +118 -0
  177. quantark/backtest/equity/engine.py +408 -0
  178. quantark/backtest/equity/hedge_executor.py +374 -0
  179. quantark/backtest/equity/metrics.py +396 -0
  180. quantark/backtest/equity/results.py +232 -0
  181. quantark/backtest/equity/state.py +252 -0
  182. quantark/backtest/examples/__init__.py +4 -0
  183. quantark/backtest/examples/advanced_backtest.py +345 -0
  184. quantark/backtest/examples/basic_delta_hedge.py +246 -0
  185. quantark/backtest/examples/fi_dv01_hedge.py +267 -0
  186. quantark/backtest/fi/__init__.py +30 -0
  187. quantark/backtest/fi/config.py +114 -0
  188. quantark/backtest/fi/engine.py +378 -0
  189. quantark/backtest/fi/hedge_executor.py +254 -0
  190. quantark/backtest/fi/metrics.py +308 -0
  191. quantark/backtest/fi/results.py +193 -0
  192. quantark/backtest/fi/state.py +212 -0
  193. quantark/backtest/logger.py +393 -0
  194. quantark/backtest/otc/__init__.py +74 -0
  195. quantark/backtest/otc/_replay.py +637 -0
  196. quantark/backtest/otc/book_engine.py +587 -0
  197. quantark/backtest/otc/config.py +175 -0
  198. quantark/backtest/otc/dashboard.py +1006 -0
  199. quantark/backtest/otc/engine.py +420 -0
  200. quantark/backtest/otc/engine_factory.py +138 -0
  201. quantark/backtest/otc/market.py +216 -0
  202. quantark/backtest/otc/results.py +107 -0
  203. quantark/backtest/otc/state.py +166 -0
  204. quantark/backtest/report_generator.py +608 -0
  205. quantark/backtest/strategy/__init__.py +28 -0
  206. quantark/backtest/strategy/base_strategy.py +235 -0
  207. quantark/backtest/strategy/convexity_neutral_strategy.py +247 -0
  208. quantark/backtest/strategy/delta_neutral_strategy.py +283 -0
  209. quantark/backtest/strategy/dv01_neutral_strategy.py +283 -0
  210. quantark/backtest/transaction_costs.py +485 -0
  211. quantark/backtest/visualizer.py +1019 -0
  212. quantark/cashleg/__init__.py +31 -0
  213. quantark/cashleg/accrual_leg.py +120 -0
  214. quantark/cashleg/base.py +48 -0
  215. quantark/cashleg/base_amount.py +60 -0
  216. quantark/cashleg/deterministic_leg.py +39 -0
  217. quantark/cashleg/event_distribution.py +262 -0
  218. quantark/cashleg/fixed_payoff_leg.py +92 -0
  219. quantark/cashleg/leg_schedule.py +95 -0
  220. quantark/cashleg/leg_valuator.py +40 -0
  221. quantark/dynamicscenario/__init__.py +97 -0
  222. quantark/dynamicscenario/base.py +297 -0
  223. quantark/dynamicscenario/config.py +122 -0
  224. quantark/dynamicscenario/engine.py +703 -0
  225. quantark/dynamicscenario/equity/__init__.py +14 -0
  226. quantark/dynamicscenario/fi/__init__.py +24 -0
  227. quantark/dynamicscenario/fi/config.py +149 -0
  228. quantark/dynamicscenario/fi/engine.py +500 -0
  229. quantark/dynamicscenario/fi/results.py +503 -0
  230. quantark/dynamicscenario/path/__init__.py +17 -0
  231. quantark/dynamicscenario/path/day_path.py +397 -0
  232. quantark/dynamicscenario/path/fi_path_library.py +488 -0
  233. quantark/dynamicscenario/path/path_builder.py +726 -0
  234. quantark/dynamicscenario/path/path_library.py +620 -0
  235. quantark/dynamicscenario/report/__init__.py +12 -0
  236. quantark/dynamicscenario/report/dynamic_report.py +1175 -0
  237. quantark/dynamicscenario/report/visualizer.py +1586 -0
  238. quantark/dynamicscenario/results/__init__.py +19 -0
  239. quantark/dynamicscenario/results/dynamic_results.py +579 -0
  240. quantark/dynamicscenario/results/result_exporter.py +438 -0
  241. quantark/param/__init__.py +75 -0
  242. quantark/param/basis/__init__.py +19 -0
  243. quantark/param/basis/basis_yield.py +301 -0
  244. quantark/param/div/__init__.py +16 -0
  245. quantark/param/div/dividend_yield.py +123 -0
  246. quantark/param/index/__init__.py +52 -0
  247. quantark/param/index/rate_index.py +568 -0
  248. quantark/param/quote/__init__.py +7 -0
  249. quantark/param/quote/spot_quote.py +35 -0
  250. quantark/param/rrf/__init__.py +22 -0
  251. quantark/param/rrf/rate_curve.py +436 -0
  252. quantark/param/vol/__init__.py +6 -0
  253. quantark/param/vol/vol_surface.py +118 -0
  254. quantark/portfolio/__init__.py +61 -0
  255. quantark/portfolio/base.py +203 -0
  256. quantark/portfolio/equity/__init__.py +17 -0
  257. quantark/portfolio/equity/portfolio.py +391 -0
  258. quantark/portfolio/equity/position.py +368 -0
  259. quantark/portfolio/fi/__init__.py +14 -0
  260. quantark/portfolio/fi/portfolio.py +424 -0
  261. quantark/portfolio/fi/position.py +272 -0
  262. quantark/portfolio/portfolio_snapshot.py +221 -0
  263. quantark/portfolio/portfolio_storage.py +414 -0
  264. quantark/priceenv/__init__.py +7 -0
  265. quantark/priceenv/pricing_environment.py +196 -0
  266. quantark/rfq/__init__.py +32 -0
  267. quantark/rfq/builders.py +102 -0
  268. quantark/rfq/models.py +214 -0
  269. quantark/rfq/registry.py +611 -0
  270. quantark/rfq/service.py +237 -0
  271. quantark/simm/__init__.py +155 -0
  272. quantark/simm/calibration/__init__.py +206 -0
  273. quantark/simm/calibration/accessors.py +439 -0
  274. quantark/simm/calibration/commodity.py +156 -0
  275. quantark/simm/calibration/credit_non_qualifying.py +79 -0
  276. quantark/simm/calibration/credit_qualifying.py +130 -0
  277. quantark/simm/calibration/cross_risk.py +39 -0
  278. quantark/simm/calibration/equity.py +125 -0
  279. quantark/simm/calibration/fx.py +92 -0
  280. quantark/simm/calibration/ir.py +152 -0
  281. quantark/simm/calibration/version.py +33 -0
  282. quantark/simm/config.py +186 -0
  283. quantark/simm/crif/__init__.py +35 -0
  284. quantark/simm/crif/models.py +230 -0
  285. quantark/simm/crif/parser.py +585 -0
  286. quantark/simm/engines/__init__.py +62 -0
  287. quantark/simm/engines/aggregation/__init__.py +67 -0
  288. quantark/simm/engines/aggregation/addon.py +141 -0
  289. quantark/simm/engines/aggregation/bucket_aggregator.py +298 -0
  290. quantark/simm/engines/aggregation/concentration.py +349 -0
  291. quantark/simm/engines/aggregation/product_class_aggregator.py +183 -0
  292. quantark/simm/engines/aggregation/risk_class_aggregator.py +403 -0
  293. quantark/simm/engines/aggregation/simm_calculator.py +430 -0
  294. quantark/simm/engines/aggregation/weighted_sensitivity.py +272 -0
  295. quantark/simm/engines/base.py +231 -0
  296. quantark/simm/engines/classification/__init__.py +10 -0
  297. quantark/simm/engines/classification/bucket_mapper.py +347 -0
  298. quantark/simm/engines/factory.py +137 -0
  299. quantark/simm/engines/portfolio_adapter.py +336 -0
  300. quantark/simm/engines/result.py +176 -0
  301. quantark/simm/engines/risk_class/__init__.py +18 -0
  302. quantark/simm/engines/risk_class/equity_engine.py +263 -0
  303. quantark/simm/engines/risk_class/ir_engine.py +264 -0
  304. quantark/simm/report/__init__.py +17 -0
  305. quantark/simm/report/crif_export.py +284 -0
  306. quantark/simm/report/excel_generator.py +401 -0
  307. quantark/simm/report/html_generator.py +840 -0
  308. quantark/simm/results/__init__.py +38 -0
  309. quantark/simm/results/attribution.py +313 -0
  310. quantark/simm/results/simm_result.py +339 -0
  311. quantark/simm/results/whatif.py +268 -0
  312. quantark/simm/sensitivity.py +533 -0
  313. quantark/simm/taxonomy.py +416 -0
  314. quantark/stresstest/__init__.py +67 -0
  315. quantark/stresstest/base.py +116 -0
  316. quantark/stresstest/config.py +5 -0
  317. quantark/stresstest/engine.py +5 -0
  318. quantark/stresstest/equity/__init__.py +17 -0
  319. quantark/stresstest/equity/config.py +69 -0
  320. quantark/stresstest/equity/engine.py +272 -0
  321. quantark/stresstest/equity/report/__init__.py +7 -0
  322. quantark/stresstest/equity/report/report_generator.py +423 -0
  323. quantark/stresstest/equity/report/visualizer.py +328 -0
  324. quantark/stresstest/equity/results.py +145 -0
  325. quantark/stresstest/fi/__init__.py +15 -0
  326. quantark/stresstest/fi/config.py +59 -0
  327. quantark/stresstest/fi/engine.py +213 -0
  328. quantark/stresstest/fi/metrics.py +60 -0
  329. quantark/stresstest/fi/results.py +64 -0
  330. quantark/stresstest/report/__init__.py +12 -0
  331. quantark/stresstest/report/report_generator.py +5 -0
  332. quantark/stresstest/report/visualizer.py +5 -0
  333. quantark/stresstest/results/__init__.py +16 -0
  334. quantark/stresstest/results/result_aggregator.py +325 -0
  335. quantark/stresstest/results/result_exporter.py +286 -0
  336. quantark/stresstest/results/stress_results.py +5 -0
  337. quantark/stresstest/scenario/__init__.py +13 -0
  338. quantark/stresstest/scenario/scenario.py +242 -0
  339. quantark/stresstest/scenario/scenario_builder.py +376 -0
  340. quantark/stresstest/scenario/scenario_library.py +435 -0
  341. quantark/stresstest/scenario/scenario_storage.py +224 -0
  342. quantark/stresstest/stress/__init__.py +13 -0
  343. quantark/stresstest/stress/stress_applicator.py +590 -0
  344. quantark/stresstest/stress/stress_types.py +142 -0
  345. quantark/util/__init__.py +23 -0
  346. quantark/util/barrier_shift.py +44 -0
  347. quantark/util/calendar/__init__.py +27 -0
  348. quantark/util/calendar/business_calendar.py +584 -0
  349. quantark/util/calendar/day_counter.py +517 -0
  350. quantark/util/calendar/holidayfile/china.csv +1920 -0
  351. quantark/util/calendar/holidayfile/china_sse.csv +1462 -0
  352. quantark/util/enum/__init__.py +81 -0
  353. quantark/util/enum/bond_enums.py +112 -0
  354. quantark/util/enum/deltaone_enums.py +16 -0
  355. quantark/util/enum/engine_enums.py +137 -0
  356. quantark/util/enum/greeks_enums.py +29 -0
  357. quantark/util/enum/option_enums.py +221 -0
  358. quantark/util/exceptions.py +66 -0
  359. quantark/util/marketdata/__init__.py +39 -0
  360. quantark/util/marketdata/adapter/base_adapter.py +203 -0
  361. quantark/util/marketdata/adapter/mock_adapter.py +265 -0
  362. quantark/util/marketdata/converter.py +289 -0
  363. quantark/util/marketdata/example_usage.py +314 -0
  364. quantark/util/marketdata/generator/__init__.py +7 -0
  365. quantark/util/marketdata/generator/mock_generator.py +466 -0
  366. quantark/util/marketdata/models.py +358 -0
  367. quantark/util/marketdata/storage/__init__.py +7 -0
  368. quantark/util/marketdata/storage/parquet_storage.py +340 -0
  369. quantark/util/numerical/__init__.py +98 -0
  370. quantark/util/numerical/comparison.py +219 -0
  371. quantark/util/numerical/constants.py +98 -0
  372. quantark/util/numerical/formatting.py +380 -0
  373. quantark/util/numerical/pnl.py +17 -0
  374. quantark/util/numerical/safe_math.py +238 -0
  375. quantark/util/numerical/validation.py +315 -0
  376. quantark/var/__init__.py +39 -0
  377. quantark/var/attribution.py +398 -0
  378. quantark/var/backtest/__init__.py +7 -0
  379. quantark/var/backtest/var_backtester.py +309 -0
  380. quantark/var/base.py +63 -0
  381. quantark/var/config.py +219 -0
  382. quantark/var/engines/__init__.py +13 -0
  383. quantark/var/engines/historical.py +925 -0
  384. quantark/var/engines/monte_carlo.py +870 -0
  385. quantark/var/engines/parametric.py +1199 -0
  386. quantark/var/results/__init__.py +16 -0
  387. quantark/var/results/incremental_var_result.py +131 -0
  388. quantark/var/results/var_report.py +346 -0
  389. quantark/var/results/var_result.py +134 -0
  390. quantark/var/risk_factors/__init__.py +22 -0
  391. quantark/var/risk_factors/base.py +41 -0
  392. quantark/var/risk_factors/equity_factors.py +158 -0
  393. quantark/var/risk_factors/fi_factors.py +99 -0
  394. quantark-0.1.0.dist-info/METADATA +351 -0
  395. quantark-0.1.0.dist-info/RECORD +399 -0
  396. quantark-0.1.0.dist-info/WHEEL +4 -0
  397. quantark-0.1.0.dist-info/licenses/LICENSE +202 -0
  398. quantark-0.1.0.dist-info/licenses/NOTICE +2 -0
  399. quantark_compat.pth +1 -0
@@ -0,0 +1,637 @@
1
+ """
2
+ Per-product replay helpers for OTC autocallable backtests.
3
+
4
+ ``ProductReplay`` encapsulates the single-product daily logic that operates on
5
+ one product and its lifecycle state. The book/hedge-level engine constructs one
6
+ ``ProductReplay`` per product and delegates the per-product steps to it, while
7
+ keeping the futures-hedge and book-level accounting for itself.
8
+
9
+ This is a behavior-preserving extraction of methods that previously lived on
10
+ ``AutocallableBacktestEngine``; the engine passes in its own output lists as
11
+ ``*_sink`` arguments so recorded rows are unchanged.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from copy import deepcopy
17
+ from datetime import timedelta
18
+ from typing import Any, Optional
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+
23
+ from quantark.asset.equity.product.option.phoenix_option import PhoenixOption
24
+ from quantark.asset.equity.engine.base_engine import BaseEngine
25
+ from quantark.param import FlatRateCurve, FlatVolSurface, SpotQuote
26
+ from quantark.priceenv import PricingEnvironment
27
+
28
+ from .engine_factory import create_mc_event_stats_engine
29
+ from .market import (
30
+ ImpliedBasisYield,
31
+ SignedDividendYield,
32
+ derive_implied_dividend_yield,
33
+ )
34
+
35
+
36
+ class ProductReplay:
37
+ """
38
+ Per-product daily replay logic for a single autocallable product.
39
+
40
+ Holds the inputs the moved methods previously read off the engine/config,
41
+ a shared ``AutocallableLifecycleState`` (the same instance the engine uses,
42
+ so lifecycle mutations are visible to both), and output sink lists.
43
+
44
+ Two-phase initialisation note
45
+ ------------------------------
46
+ ``start_date`` is ``None`` at construction time. It MUST be populated
47
+ before any replay method is called. ``AutocallableBacktestEngine`` sets
48
+ ``self._replay.start_date`` to the first backtest date at the top of
49
+ ``run()``, before the daily loop begins. Any standalone caller is
50
+ responsible for the same assignment.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ product: Any,
57
+ product_quantity: float,
58
+ has_lifecycle: bool,
59
+ lifecycle: Any,
60
+ pricing_engine: BaseEngine,
61
+ surface_engine: BaseEngine,
62
+ event_stats_engine: BaseEngine,
63
+ engine_config: Any,
64
+ market_data: Any,
65
+ start_date: Optional[pd.Timestamp],
66
+ underlying: str,
67
+ actions_sink: list[dict[str, Any]],
68
+ event_prob_sink: list[dict[str, Any]],
69
+ daily_event_sink: list[dict[str, Any]],
70
+ surfaces_sink: list[dict[str, Any]],
71
+ fixed_dividend_yield: Optional[float] = None,
72
+ delta_bump_size: Optional[float] = None,
73
+ gamma_bump_size: Optional[float] = None,
74
+ surface_config: Any = None,
75
+ ) -> None:
76
+ self.product = product
77
+ self.product_quantity = product_quantity
78
+ self.has_lifecycle = has_lifecycle
79
+ self.lifecycle = lifecycle
80
+ self.pricing_engine = pricing_engine
81
+ self.surface_engine = surface_engine
82
+ self.event_stats_engine = event_stats_engine
83
+ self.engine_config = engine_config
84
+ self.market_data = market_data
85
+ self.start_date = start_date
86
+ self.underlying = underlying
87
+ self.fixed_dividend_yield = fixed_dividend_yield
88
+ self.delta_bump_size = delta_bump_size
89
+ self.gamma_bump_size = gamma_bump_size
90
+ self.surface_config = surface_config
91
+
92
+ self.actions_sink = actions_sink
93
+ self.event_prob_sink = event_prob_sink
94
+ self.daily_event_sink = daily_event_sink
95
+ self.surfaces_sink = surfaces_sink
96
+
97
+ def product_for_lifecycle(self):
98
+ product = deepcopy(self.product)
99
+ setattr(product, "_otc_lifecycle_knocked_in", self.lifecycle.knocked_in)
100
+ return product
101
+
102
+ def product_for_date(self, date: pd.Timestamp, pricing_env: PricingEnvironment):
103
+ product = deepcopy(self.product)
104
+ setattr(product, "_otc_lifecycle_knocked_in", self.lifecycle.knocked_in)
105
+ if (
106
+ getattr(product, "exercise_date", None) is None
107
+ and getattr(product, "maturity", None) is not None
108
+ and self.start_date is not None
109
+ ):
110
+ elapsed = max(0.0, (date - self.start_date).days / 365.0)
111
+ product.maturity = max(float(product.maturity) - elapsed, 1e-8)
112
+ elif self.start_date is not None:
113
+ elapsed = max(0.0, (date - self.start_date).days / 365.0)
114
+ else:
115
+ elapsed = 0.0
116
+ barrier_config = getattr(product, "barrier_config", None)
117
+ if barrier_config is not None and hasattr(barrier_config, "time_shift"):
118
+ shifted_config, dropped_all = barrier_config.time_shift(
119
+ elapsed,
120
+ date.to_pydatetime(),
121
+ pricing_env,
122
+ )
123
+ if shifted_config is not None and not dropped_all:
124
+ product.barrier_config = shifted_config
125
+ return product
126
+
127
+ def build_env(self, date: pd.Timestamp, market: dict[str, float], selected):
128
+ expiry = pd.Timestamp(selected["expiry_date"]).normalize()
129
+ futures_ttm = (expiry - date).days / 365.0
130
+ basis_yield, implied_q = derive_implied_dividend_yield(
131
+ rate=market["rate"],
132
+ spot=market["spot"],
133
+ futures_price=float(selected["futures_price"]),
134
+ time_to_maturity=futures_ttm,
135
+ )
136
+ pricing_q = self.pricing_dividend_yield(implied_q)
137
+ env = PricingEnvironment(
138
+ spot_quote=SpotQuote(spot=market["spot"], asset_name=self.underlying),
139
+ vol_surface=FlatVolSurface(volatility=market["volatility"]),
140
+ rate_curve=FlatRateCurve(rate=market["rate"]),
141
+ div_yield=SignedDividendYield(pricing_q),
142
+ basis_yield=ImpliedBasisYield(basis_yield),
143
+ valuation_date=date.to_pydatetime(),
144
+ )
145
+ return env, basis_yield, implied_q, futures_ttm
146
+
147
+ def pricing_dividend_yield(self, implied_q: float) -> float:
148
+ if self.fixed_dividend_yield is not None:
149
+ return float(self.fixed_dividend_yield)
150
+ return float(implied_q)
151
+
152
+ def calculate_greeks(
153
+ self, product: Any, env: PricingEnvironment, price: float
154
+ ) -> dict[str, float]:
155
+ params = getattr(self.pricing_engine, "params", None)
156
+ uses_base_greeks = (
157
+ isinstance(self.pricing_engine, BaseEngine)
158
+ and type(self.pricing_engine).calculate_greeks is BaseEngine.calculate_greeks
159
+ )
160
+ engine_bump = (
161
+ float(getattr(params, "bump_size", 1e-4)) if params is not None else 0.0
162
+ )
163
+ delta_bump = (
164
+ float(self.delta_bump_size)
165
+ if self.delta_bump_size is not None
166
+ else engine_bump
167
+ )
168
+ gamma_bump = (
169
+ float(self.gamma_bump_size)
170
+ if self.gamma_bump_size is not None
171
+ else engine_bump
172
+ )
173
+ if (
174
+ uses_base_greeks
175
+ and delta_bump > 0.0
176
+ and gamma_bump > 0.0
177
+ and np.isfinite(price)
178
+ ):
179
+ try:
180
+ spot = float(env.spot)
181
+ env_up = deepcopy(env)
182
+ env_up.spot_quote.spot *= 1.0 + delta_bump
183
+ delta_price_up = float(self.pricing_engine.price(product, env_up))
184
+
185
+ env_down = deepcopy(env)
186
+ env_down.spot_quote.spot *= 1.0 - delta_bump
187
+ delta_price_down = float(self.pricing_engine.price(product, env_down))
188
+
189
+ delta_spot_bump = spot * delta_bump
190
+ delta = (delta_price_up - delta_price_down) / (
191
+ 2.0 * delta_spot_bump
192
+ )
193
+
194
+ if np.isclose(delta_bump, gamma_bump):
195
+ gamma_price_up = delta_price_up
196
+ gamma_price_down = delta_price_down
197
+ else:
198
+ env_gamma_up = deepcopy(env)
199
+ env_gamma_up.spot_quote.spot *= 1.0 + gamma_bump
200
+ gamma_price_up = float(
201
+ self.pricing_engine.price(product, env_gamma_up)
202
+ )
203
+
204
+ env_gamma_down = deepcopy(env)
205
+ env_gamma_down.spot_quote.spot *= 1.0 - gamma_bump
206
+ gamma_price_down = float(
207
+ self.pricing_engine.price(product, env_gamma_down)
208
+ )
209
+
210
+ gamma_spot_bump = spot * gamma_bump
211
+ gamma = (gamma_price_up - 2.0 * float(price) + gamma_price_down) / (
212
+ gamma_spot_bump**2
213
+ )
214
+ return {"price": float(price), "delta": delta, "gamma": gamma}
215
+ except Exception:
216
+ pass
217
+ try:
218
+ greeks = dict(self.pricing_engine.calculate_greeks(product, env))
219
+ except Exception:
220
+ greeks = {"price": price, "delta": 0.0, "gamma": 0.0}
221
+ greeks.setdefault("price", price)
222
+ greeks.setdefault("delta", 0.0)
223
+ greeks.setdefault("gamma", 0.0)
224
+ return greeks
225
+
226
+ def record_surfaces(
227
+ self,
228
+ date: pd.Timestamp,
229
+ product: Any,
230
+ env: PricingEnvironment,
231
+ spot: float,
232
+ q_center: float,
233
+ ) -> None:
234
+ if self.surface_config is None:
235
+ return
236
+ spec = self.surface_config
237
+ spot_grid = np.linspace(
238
+ spot * (1.0 - spec.spot_width),
239
+ spot * (1.0 + spec.spot_width),
240
+ spec.spot_nodes,
241
+ )
242
+ if spec.q_nodes == 1:
243
+ q_grid = np.array([q_center], dtype=float)
244
+ else:
245
+ q_lower = max(0.0, q_center - spec.q_width)
246
+ q_upper = max(0.0, q_center + spec.q_width)
247
+ q_grid = np.linspace(
248
+ q_lower, q_upper, spec.q_nodes
249
+ )
250
+
251
+ for s in spot_grid:
252
+ for q in q_grid:
253
+ surf_env = deepcopy(env)
254
+ surf_env.spot_quote = SpotQuote(spot=float(s), asset_name=self.underlying)
255
+ surf_env.div_yield = SignedDividendYield(float(q))
256
+ try:
257
+ greeks = self.surface_engine.calculate_greeks(product, surf_env)
258
+ delta = float(greeks.get("delta", np.nan))
259
+ gamma = float(greeks.get("gamma", np.nan))
260
+ spot_node = float(s)
261
+ one_percent_node_move = spot_node * 0.01
262
+ row = {
263
+ "date": date,
264
+ "surface_type": "spot_q",
265
+ "spot_node": float(s),
266
+ "q_node": float(q),
267
+ "price": float(greeks.get("price", np.nan)),
268
+ "delta": delta,
269
+ "gamma": gamma,
270
+ "delta_cash_1pct": delta * one_percent_node_move,
271
+ "gamma_cash_1pct": gamma * spot_node**2 / 100.0,
272
+ }
273
+ except Exception:
274
+ row = {
275
+ "date": date,
276
+ "surface_type": "spot_q",
277
+ "spot_node": float(s),
278
+ "q_node": float(q),
279
+ "price": np.nan,
280
+ "delta": np.nan,
281
+ "gamma": np.nan,
282
+ "delta_cash_1pct": np.nan,
283
+ "gamma_cash_1pct": np.nan,
284
+ }
285
+ self.surfaces_sink.append(row)
286
+
287
+ def record_event_probabilities(
288
+ self, date: pd.Timestamp, product: Any, env: PricingEnvironment
289
+ ) -> None:
290
+ stats = self._calculate_event_stats(product, env)
291
+ if stats is None:
292
+ return
293
+
294
+ ko_probs = np.asarray(getattr(stats, "ko_probability", []), dtype=float)
295
+ ko_times = np.asarray(getattr(stats, "ko_times", []), dtype=float)
296
+ survival = np.asarray(getattr(stats, "survival_probability", []), dtype=float)
297
+ ed_ko_cf = np.asarray(
298
+ getattr(stats, "expected_discounted_ko_cashflow", []), dtype=float
299
+ )
300
+
301
+ next_ko_prob = float(ko_probs[0]) if ko_probs.size else 0.0
302
+ next_ko_date = (
303
+ self._date_from_time(date, float(ko_times[0])) if ko_times.size else pd.NaT
304
+ )
305
+ ki_prob_scalar = float(getattr(stats, "ki_probability", 0.0))
306
+ self.daily_event_sink.append(
307
+ {
308
+ "date": date,
309
+ "next_ko_date": next_ko_date,
310
+ "next_ko_probability": next_ko_prob,
311
+ "total_remaining_ko_probability": float(np.nansum(ko_probs)),
312
+ "ki_probability_to_maturity": ki_prob_scalar,
313
+ "survival_probability": float(survival[-1]) if survival.size else np.nan,
314
+ "expected_discounted_ko_cashflow": float(np.nansum(ed_ko_cf)),
315
+ "expected_discounted_maturity_cashflow": float(
316
+ getattr(stats, "expected_discounted_maturity_cashflow", np.nan)
317
+ ),
318
+ "pv": float(getattr(stats, "pv", np.nan)),
319
+ }
320
+ )
321
+
322
+ previous_survival = 1.0
323
+ for i, probability in enumerate(ko_probs):
324
+ conditional = probability / previous_survival if previous_survival > 0 else np.nan
325
+ event_date = self._date_from_time(date, float(ko_times[i]))
326
+ self.event_prob_sink.append(
327
+ {
328
+ "date": date,
329
+ "event_date": event_date,
330
+ "event_type": "KO",
331
+ "event_probability": float(probability),
332
+ "conditional_probability": float(conditional),
333
+ "survival_probability": float(survival[i]) if i < survival.size else np.nan,
334
+ "expected_discounted_cashflow": float(ed_ko_cf[i]) if i < ed_ko_cf.size else np.nan,
335
+ }
336
+ )
337
+ if i < survival.size:
338
+ previous_survival = float(survival[i])
339
+
340
+ ki_times = np.asarray(getattr(stats, "ki_times", []), dtype=float)
341
+ ki_event_prob = np.asarray(
342
+ getattr(stats, "ki_event_probability", []), dtype=float
343
+ )
344
+ ki_survival = np.asarray(
345
+ getattr(stats, "ki_survival_probability", []), dtype=float
346
+ )
347
+ if ki_times.size == 0 and ki_prob_scalar > 0:
348
+ ki_times = np.array([product.get_maturity(env)], dtype=float)
349
+ ki_event_prob = np.array([ki_prob_scalar], dtype=float)
350
+ ki_survival = np.array([np.nan], dtype=float)
351
+
352
+ for i, probability in enumerate(ki_event_prob):
353
+ event_date = self._date_from_time(date, float(ki_times[i]))
354
+ self.event_prob_sink.append(
355
+ {
356
+ "date": date,
357
+ "event_date": event_date,
358
+ "event_type": "KI",
359
+ "event_probability": float(probability),
360
+ "conditional_probability": np.nan,
361
+ "survival_probability": float(ki_survival[i]) if i < ki_survival.size else np.nan,
362
+ "expected_discounted_cashflow": np.nan,
363
+ }
364
+ )
365
+
366
+ def _calculate_event_stats(self, product: Any, env: PricingEnvironment):
367
+ try:
368
+ stats = self.event_stats_engine.calculate_event_stats(product, env)
369
+ if stats is not None:
370
+ return stats
371
+ except Exception:
372
+ pass
373
+ try:
374
+ fallback = create_mc_event_stats_engine(product, self.engine_config)
375
+ return fallback.calculate_event_stats(product, env)
376
+ except Exception:
377
+ return None
378
+
379
+ def _lifecycle_snapshot(self) -> dict[str, Any]:
380
+ return {
381
+ "alive": self.lifecycle.alive,
382
+ "knocked_in": self.lifecycle.knocked_in,
383
+ "knocked_out": self.lifecycle.knocked_out,
384
+ "matured": self.lifecycle.matured,
385
+ }
386
+
387
+ def _append_action(
388
+ self,
389
+ *,
390
+ before_state: dict[str, Any],
391
+ action_type: str,
392
+ date: pd.Timestamp,
393
+ observation_index: Optional[int],
394
+ spot: float,
395
+ barrier: Optional[float],
396
+ cashflow: float,
397
+ **extra: Any,
398
+ ) -> None:
399
+ after_state = self._lifecycle_snapshot()
400
+ row = {
401
+ "date": date,
402
+ "action_type": action_type,
403
+ "observation_index": observation_index,
404
+ "spot": spot,
405
+ "barrier": barrier,
406
+ "cashflow": cashflow,
407
+ "alive_before": before_state["alive"],
408
+ "knocked_in_before": before_state["knocked_in"],
409
+ "knocked_out_before": before_state["knocked_out"],
410
+ "matured_before": before_state["matured"],
411
+ "alive_after": after_state["alive"],
412
+ "knocked_in_after": after_state["knocked_in"],
413
+ "knocked_out_after": after_state["knocked_out"],
414
+ "matured_after": after_state["matured"],
415
+ }
416
+ row.update(extra)
417
+ self.actions_sink.append(row)
418
+
419
+ def _maturity_market_date(self, product: Any, env: PricingEnvironment) -> pd.Timestamp:
420
+ explicit = (
421
+ getattr(product, "maturity_date", None)
422
+ or getattr(product, "exercise_date", None)
423
+ )
424
+ if explicit is not None:
425
+ return self._next_available_market_date(pd.Timestamp(explicit).normalize())
426
+
427
+ maturity = float(product.get_maturity(env))
428
+ base_date = pd.Timestamp(getattr(product, "initial_date", None) or self.start_date)
429
+ maturity_date = (
430
+ base_date + timedelta(days=int(round(maturity * 365.0)))
431
+ ).normalize()
432
+ return self._next_available_market_date(maturity_date)
433
+
434
+ def settle_maturity_if_due(
435
+ self, date: pd.Timestamp, product: Any, env: PricingEnvironment, spot: float
436
+ ) -> None:
437
+ if not self.lifecycle.alive:
438
+ return
439
+ if date < self._maturity_market_date(product, env):
440
+ return
441
+
442
+ before = self._lifecycle_snapshot()
443
+ payoff = float(
444
+ product.get_payoff(
445
+ spot,
446
+ env,
447
+ knocked_in=self.lifecycle.knocked_in,
448
+ )
449
+ )
450
+ cashflow = self.product_quantity * payoff
451
+ if self.lifecycle.mark_maturity(date.to_pydatetime(), cashflow):
452
+ self._append_action(
453
+ before_state=before,
454
+ action_type="MATURITY",
455
+ date=date,
456
+ observation_index=None,
457
+ spot=spot,
458
+ barrier=None,
459
+ cashflow=cashflow,
460
+ payoff=payoff,
461
+ )
462
+
463
+ def apply_lifecycle_events(
464
+ self, date: pd.Timestamp, product: Any, env: PricingEnvironment, spot: float
465
+ ) -> None:
466
+ if not self.lifecycle.alive:
467
+ return
468
+ if not self.has_lifecycle:
469
+ return
470
+ ko_records = self._scheduled_records(product, env, "ko")
471
+ ko_disabled_after_ki = bool(
472
+ self.lifecycle.knocked_in
473
+ and getattr(product.barrier_config, "disable_ko_after_ki", False)
474
+ )
475
+ if not ko_disabled_after_ki:
476
+ for idx, rec in enumerate(ko_records):
477
+ if idx in self.lifecycle.observed_ko_indices:
478
+ continue
479
+ if date < rec["date"]:
480
+ continue
481
+ self.lifecycle.observed_ko_indices.add(idx)
482
+ if self._barrier_hit(spot, rec["barrier"], product.is_reverse, is_ko=True):
483
+ before = self._lifecycle_snapshot()
484
+ cashflow = self.product_quantity * float(rec.get("payoff", 0.0))
485
+ if self.lifecycle.mark_ko(date.to_pydatetime(), cashflow):
486
+ self._append_action(
487
+ before_state=before,
488
+ action_type="KO",
489
+ date=date,
490
+ observation_index=idx,
491
+ spot=spot,
492
+ barrier=rec["barrier"],
493
+ cashflow=cashflow,
494
+ payoff=float(rec.get("payoff", 0.0)),
495
+ )
496
+ return
497
+
498
+ ki_records = self._scheduled_records(product, env, "ki")
499
+ ki_observation_type = getattr(product.barrier_config, "ki_observation_type", None)
500
+ ki_continuous = getattr(product, "has_ki_barrier", False) and (
501
+ product.barrier_config.ki_continuous
502
+ or getattr(ki_observation_type, "name", None) == "CONTINUOUS"
503
+ )
504
+ if ki_continuous:
505
+ barrier = product.barrier_config.ki_barrier
506
+ if isinstance(barrier, list):
507
+ barrier = barrier[0]
508
+ if self._barrier_hit(spot, float(barrier), product.is_reverse, is_ko=False):
509
+ before = self._lifecycle_snapshot()
510
+ if self.lifecycle.mark_ki(date.to_pydatetime()):
511
+ self._append_action(
512
+ before_state=before,
513
+ action_type="KI",
514
+ date=date,
515
+ observation_index=None,
516
+ spot=spot,
517
+ barrier=float(barrier),
518
+ cashflow=0.0,
519
+ monitoring="daily_close",
520
+ )
521
+ else:
522
+ for idx, rec in enumerate(ki_records):
523
+ if idx in self.lifecycle.observed_ki_indices:
524
+ continue
525
+ if date < rec["date"]:
526
+ continue
527
+ self.lifecycle.observed_ki_indices.add(idx)
528
+ if self._barrier_hit(spot, rec["barrier"], product.is_reverse, is_ko=False):
529
+ before = self._lifecycle_snapshot()
530
+ if self.lifecycle.mark_ki(date.to_pydatetime()):
531
+ self._append_action(
532
+ before_state=before,
533
+ action_type="KI",
534
+ date=date,
535
+ observation_index=idx,
536
+ spot=spot,
537
+ barrier=rec["barrier"],
538
+ cashflow=0.0,
539
+ )
540
+
541
+ if isinstance(product, PhoenixOption):
542
+ for idx, rec in enumerate(ko_records):
543
+ if idx in self.lifecycle.observed_coupon_indices:
544
+ continue
545
+ if date < rec["date"]:
546
+ continue
547
+ self.lifecycle.observed_coupon_indices.add(idx)
548
+ if product.is_coupon_triggered(spot, idx):
549
+ before = self._lifecycle_snapshot()
550
+ coupon = self.product_quantity * product.get_coupon_payoff(idx)
551
+ self.lifecycle.add_cashflow(coupon)
552
+ self.lifecycle.coupon_memory_count = 0
553
+ self._append_action(
554
+ before_state=before,
555
+ action_type="COUPON",
556
+ date=date,
557
+ observation_index=idx,
558
+ spot=spot,
559
+ barrier=product.get_coupon_barrier_at(idx),
560
+ cashflow=coupon,
561
+ )
562
+ elif product.has_memory_coupon:
563
+ self.lifecycle.coupon_memory_count += 1
564
+
565
+ def _schedule_resolution_env(
566
+ self, product: Any, env: PricingEnvironment
567
+ ) -> PricingEnvironment:
568
+ """Resolve lifecycle schedules from the contract issue date."""
569
+ base_date = getattr(product, "initial_date", None) or self.start_date
570
+ if base_date is None:
571
+ return env
572
+
573
+ schedule_env = deepcopy(env)
574
+ schedule_env.valuation_date = pd.Timestamp(base_date).to_pydatetime()
575
+ return schedule_env
576
+
577
+ def _scheduled_records(
578
+ self, product: Any, env: PricingEnvironment, kind: str
579
+ ) -> list[dict[str, Any]]:
580
+ schedule_env = self._schedule_resolution_env(product, env)
581
+ if kind == "ko":
582
+ profile = product.get_ko_observation_profile(schedule_env)
583
+ schedule = getattr(product.barrier_config, "ko_observation_schedule", None)
584
+ else:
585
+ if not getattr(product, "has_ki_barrier", False):
586
+ return []
587
+ profile = product.get_ki_observation_profile(schedule_env)
588
+ schedule = getattr(product.barrier_config, "ki_observation_schedule", None)
589
+
590
+ times = list(profile.get("observation_times", []))
591
+ barriers = list(profile.get("barriers", []))
592
+ payoffs = list(profile.get("payoffs", [0.0] * len(times)))
593
+ schedule_dates = []
594
+ if schedule is not None:
595
+ for rec in schedule.records:
596
+ schedule_dates.append(getattr(rec, "observation_date", None))
597
+
598
+ records = []
599
+ base_date = pd.Timestamp(getattr(product, "initial_date", None) or self.start_date)
600
+ for idx, obs_time in enumerate(times):
601
+ if idx < len(schedule_dates) and schedule_dates[idx] is not None:
602
+ obs_date = pd.Timestamp(schedule_dates[idx]).normalize()
603
+ else:
604
+ obs_date = (base_date + timedelta(days=int(round(float(obs_time) * 365)))).normalize()
605
+ obs_date = self._next_available_market_date(obs_date)
606
+ records.append(
607
+ {
608
+ "date": obs_date,
609
+ "time": float(obs_time),
610
+ "barrier": float(barriers[idx]) if idx < len(barriers) and barriers[idx] is not None else None,
611
+ "payoff": float(payoffs[idx]) if idx < len(payoffs) and payoffs[idx] is not None else 0.0,
612
+ }
613
+ )
614
+ return records
615
+
616
+ def _next_available_market_date(self, date: pd.Timestamp) -> pd.Timestamp:
617
+ dates = self.market_data.dates
618
+ eligible = dates[dates >= pd.Timestamp(date).normalize()]
619
+ if len(eligible) == 0:
620
+ return pd.Timestamp(date).normalize()
621
+ return pd.Timestamp(eligible[0]).normalize()
622
+
623
+ @staticmethod
624
+ def _barrier_hit(
625
+ spot: float, barrier: Optional[float], is_reverse: bool, is_ko: bool
626
+ ) -> bool:
627
+ if barrier is None:
628
+ return False
629
+ if is_ko:
630
+ return spot <= barrier if is_reverse else spot >= barrier
631
+ return spot >= barrier if is_reverse else spot <= barrier
632
+
633
+ @staticmethod
634
+ def _date_from_time(date: pd.Timestamp, time_years: float) -> pd.Timestamp:
635
+ return pd.Timestamp(date).normalize() + timedelta(
636
+ days=int(round(float(time_years) * 365))
637
+ )