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,1206 @@
1
+ """
2
+ Monte Carlo pricing engine for Phoenix (autocallable) options.
3
+
4
+ This engine prices Phoenix options using Monte Carlo simulation with support for:
5
+ - Standard and reverse Phoenix structures
6
+ - Discrete KO observations with time-varying barriers and rates
7
+ - Discrete or continuous KI monitoring
8
+ - Periodic coupon payments with optional memory feature
9
+ - INSTANT or EXPIRY coupon payment timing
10
+ - Optional Dask parallelization for batch processing
11
+ """
12
+
13
+ import math
14
+ import warnings
15
+ from dataclasses import dataclass
16
+ from typing import Dict, Optional, Tuple, Union
17
+
18
+ import numpy as np
19
+
20
+ from quantark.asset.equity.engine.base_engine import BaseEngine
21
+ from quantark.asset.equity.engine.event_stats import PhoenixEventStats
22
+ from quantark.asset.equity.param import MCParams
23
+ from quantark.asset.equity.process.bsm.qmc_path_generator import GBMPathGenerator
24
+ from quantark.asset.equity.process.bsm.qmc_rqmc_driver import run_rqmc
25
+ from quantark.asset.equity.process.bsm.qmc_sobol import (
26
+ PseudoRandomNormalGenerator,
27
+ SobolNormalGenerator,
28
+ )
29
+ from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
30
+ from quantark.asset.equity.product.option.phoenix_option import PhoenixOption
31
+ from quantark.priceenv import PricingEnvironment
32
+ from quantark.util.enum import CouponPayType, ObservationType
33
+ from quantark.util.enum.engine_enums import EngineType, MonteCarloMethod
34
+ from quantark.util.exceptions import PricingError, ValidationError
35
+ from quantark.util.numerical import is_zero, safe_log
36
+
37
+ try:
38
+ from dask import delayed
39
+ from dask.compute import compute
40
+
41
+ DASK_AVAILABLE = True
42
+ except ImportError:
43
+ DASK_AVAILABLE = False
44
+
45
+
46
+ @dataclass
47
+ class PhoenixMCResult:
48
+ """Result container for Phoenix MC pricing."""
49
+
50
+ price: float
51
+ std_error: float
52
+ num_paths: int
53
+ ko_probability: float
54
+ v0_probability: float
55
+ v1_probability: float
56
+ avg_ko_time: Optional[float] = None
57
+ batches_used: Optional[int] = None
58
+ coupon_probabilities: Optional[np.ndarray] = None
59
+ expected_discounted_coupon_cashflow: Optional[np.ndarray] = None
60
+
61
+
62
+ class PhoenixMCEngine(BaseEngine):
63
+ """
64
+ Monte Carlo pricing engine for Phoenix options.
65
+
66
+ Supports three Monte Carlo methods:
67
+ - PSEUDO: Standard Monte Carlo with pseudorandom numbers
68
+ - QUASI: Quasi-Monte Carlo with Sobol sequences
69
+ - RANDOMIZED_QUASI: Randomized QMC with adaptive batching
70
+ """
71
+
72
+ DEFAULT_METHOD = MonteCarloMethod.PSEUDO
73
+
74
+ def __init__(
75
+ self,
76
+ params: Optional[MCParams] = None,
77
+ method: Union[str, MonteCarloMethod, tuple, None] = None,
78
+ use_dask: bool = False,
79
+ num_batches: int = 4,
80
+ ):
81
+ if params is None:
82
+ params = MCParams()
83
+
84
+ if not isinstance(params, MCParams):
85
+ raise ValidationError(
86
+ f"params must be MCParams instance, got {type(params).__name__}"
87
+ )
88
+
89
+ super().__init__(params)
90
+
91
+ if method is None:
92
+ self.method = self.DEFAULT_METHOD
93
+ elif isinstance(method, tuple):
94
+ engine_type, mc_method = method
95
+ if engine_type != EngineType.MONTE_CARLO:
96
+ raise ValidationError(
97
+ f"Expected EngineType.MONTE_CARLO, got {engine_type}"
98
+ )
99
+ if not isinstance(mc_method, MonteCarloMethod):
100
+ raise ValidationError(
101
+ f"Expected MonteCarloMethod, got {type(mc_method).__name__}"
102
+ )
103
+ self.method = mc_method
104
+ elif isinstance(method, MonteCarloMethod):
105
+ self.method = method
106
+ elif isinstance(method, str):
107
+ try:
108
+ self.method = MonteCarloMethod[method.upper()]
109
+ except KeyError:
110
+ valid_methods = [m.name for m in MonteCarloMethod]
111
+ raise ValidationError(
112
+ f"Invalid method string '{method}'. Valid methods: {valid_methods}"
113
+ )
114
+ else:
115
+ raise ValidationError(
116
+ f"Invalid method type {type(method).__name__}. "
117
+ "Expected MonteCarloMethod, tuple, str, or None"
118
+ )
119
+
120
+ self.use_dask = use_dask and DASK_AVAILABLE
121
+ if use_dask and not DASK_AVAILABLE:
122
+ warnings.warn(
123
+ "Dask requested but not installed. Falling back to single-threaded NumPy.",
124
+ UserWarning,
125
+ )
126
+ self.num_batches = num_batches
127
+
128
+ self._last_result: Optional[PhoenixMCResult] = None
129
+
130
+ def price(
131
+ self, product: BaseEquityProduct, pricing_env: PricingEnvironment
132
+ ) -> float:
133
+ if not isinstance(product, PhoenixOption):
134
+ raise PricingError(
135
+ f"PhoenixMCEngine only supports PhoenixOption, got {type(product).__name__}"
136
+ )
137
+
138
+ S = pricing_env.spot
139
+ T = product.get_maturity(pricing_env)
140
+ r = pricing_env.get_rate(T)
141
+ q = pricing_env.get_div_yield(T)
142
+ sigma = pricing_env.get_vol(product.strike, T)
143
+
144
+ self._validate_inputs(S, T, r, q, sigma, product)
145
+
146
+ if T < 1e-10:
147
+ return product.get_payoff(S, pricing_env=pricing_env)
148
+
149
+ if self.method == MonteCarloMethod.RANDOMIZED_QUASI:
150
+ result = self._price_rqmc(product, pricing_env, S, T, r, q, sigma)
151
+ elif self.use_dask and self.num_batches > 1:
152
+ result = self._price_parallel(product, pricing_env, S, T, r, q, sigma)
153
+ else:
154
+ result = self._price_mc_or_qmc(product, pricing_env, S, T, r, q, sigma)
155
+
156
+ self._last_result = result
157
+
158
+ if result.price < 0 and product.payoff_config.include_principal:
159
+ raise PricingError(f"Negative price computed: {result.price}")
160
+
161
+ return result.price
162
+
163
+ def calculate_event_stats(
164
+ self, product: BaseEquityProduct, pricing_env: PricingEnvironment
165
+ ) -> Optional[PhoenixEventStats]:
166
+ if not isinstance(product, PhoenixOption):
167
+ return None
168
+
169
+ S = pricing_env.spot
170
+ T = product.get_maturity(pricing_env)
171
+ r = pricing_env.get_rate(T)
172
+ q = pricing_env.get_div_yield(T)
173
+ sigma = pricing_env.get_vol(product.strike, T)
174
+ self._validate_inputs(S, T, r, q, sigma, product)
175
+
176
+ all_times, dt_array, ko_indices, ki_indices = self._build_time_grid(
177
+ product, pricing_env, T
178
+ )
179
+ ki_continuous = product.has_ki_barrier and (
180
+ product.barrier_config.ki_observation_type == ObservationType.CONTINUOUS
181
+ or product.barrier_config.ki_continuous
182
+ )
183
+ ki_event_times = np.array([], dtype=float)
184
+ if product.has_ki_barrier:
185
+ if ki_continuous:
186
+ ki_event_times = np.array(all_times, dtype=float)
187
+ else:
188
+ ki_profile = product.get_ki_observation_profile(pricing_env)
189
+ ki_event_times = np.array(
190
+ ki_profile["observation_times"], dtype=float
191
+ )
192
+ generator = self._create_path_generator(S, r, q, sigma, T, dt_array)
193
+ paths, _ = generator.generate_paths(return_aux=False)
194
+
195
+ (
196
+ payoffs,
197
+ settlement_times,
198
+ stats,
199
+ coupon_probabilities,
200
+ coupon_cashflows,
201
+ instant_coupon_discounted,
202
+ ko_times,
203
+ ko_payoffs,
204
+ ko_settlement_times,
205
+ ) = self._compute_payoffs(
206
+ product,
207
+ pricing_env,
208
+ paths,
209
+ all_times,
210
+ ko_indices,
211
+ ki_indices,
212
+ r,
213
+ T,
214
+ sigma,
215
+ rng_seed=int(self.params.seed) + 1337,
216
+ collect_coupon_stats=True,
217
+ )
218
+
219
+ discount_factors = np.exp(-r * settlement_times)
220
+ discounted_payoffs = payoffs * discount_factors + instant_coupon_discounted
221
+ pv = float(discounted_payoffs.mean())
222
+
223
+ ko_probability = np.zeros(len(ko_times), dtype=float)
224
+ expected_discounted_ko_cashflow = np.zeros(len(ko_times), dtype=float)
225
+ for i in range(len(ko_times)):
226
+ hit_i = stats["is_ko"] & (stats["first_ko_idx"] == i)
227
+ p_i = float(np.mean(hit_i))
228
+ ko_probability[i] = p_i
229
+ if p_i > 0.0:
230
+ df = math.exp(-r * float(ko_settlement_times[i]))
231
+ expected_discounted_ko_cashflow[i] = p_i * float(ko_payoffs[i]) * df
232
+
233
+ survival_probability = np.ones(len(ko_times), dtype=float)
234
+ cumulative_ko = 0.0
235
+ for i in range(len(ko_times)):
236
+ cumulative_ko += ko_probability[i]
237
+ survival_probability[i] = max(0.0, 1.0 - cumulative_ko)
238
+
239
+ maturity_df = math.exp(-r * float(T))
240
+ maturity_payoff_all = np.zeros(len(paths), dtype=float)
241
+ is_ko = stats["is_ko"]
242
+ is_v0 = stats["is_v0"]
243
+ is_v1 = stats["is_v1"]
244
+ if product.coupon_config.coupon_pay_type == CouponPayType.EXPIRY:
245
+ maturity_payoff_all[~is_ko] = payoffs[~is_ko]
246
+ else:
247
+ if is_v0.any():
248
+ maturity_payoff_all[is_v0] = np.array(
249
+ [
250
+ product.get_maturity_payoff_v0(
251
+ float(s),
252
+ pricing_env=pricing_env,
253
+ )
254
+ for s in paths[is_v0, -1]
255
+ ],
256
+ dtype=float,
257
+ )
258
+ if is_v1.any():
259
+ maturity_payoff_all[is_v1] = np.array(
260
+ [
261
+ product.get_maturity_payoff_v1(
262
+ float(s),
263
+ pricing_env=pricing_env,
264
+ )
265
+ for s in paths[is_v1, -1]
266
+ ],
267
+ dtype=float,
268
+ )
269
+ expected_discounted_maturity_cashflow = float(
270
+ np.mean(maturity_payoff_all * maturity_df)
271
+ )
272
+
273
+ coupon_cf_total = float(np.sum(coupon_cashflows))
274
+ if product.coupon_config.coupon_pay_type == CouponPayType.EXPIRY:
275
+ coupon_cf_total = 0.0
276
+ pv_cashflows = float(
277
+ np.sum(expected_discounted_ko_cashflow)
278
+ + expected_discounted_maturity_cashflow
279
+ + coupon_cf_total
280
+ )
281
+ reconciliation_error = pv - pv_cashflows
282
+ ki_event_probability = np.array([], dtype=float)
283
+ ki_survival_probability = np.array([], dtype=float)
284
+ if product.has_ki_barrier:
285
+ if bool(getattr(product, "_otc_lifecycle_knocked_in", False)):
286
+ ki_event_times = np.array([0.0], dtype=float)
287
+ ki_event_probability = np.array([1.0], dtype=float)
288
+ ki_survival_probability = np.array([0.0], dtype=float)
289
+ elif ki_event_times.size and "first_ki_idx" in stats:
290
+ first_ki_idx = stats["first_ki_idx"]
291
+ ki_triggered = stats["ki_triggered"]
292
+ ki_event_probability = np.zeros(len(ki_event_times), dtype=float)
293
+ for i in range(len(ki_event_times)):
294
+ ki_event_probability[i] = float(
295
+ np.mean(ki_triggered & (first_ki_idx == i))
296
+ )
297
+ ki_survival_probability = np.maximum(
298
+ 0.0, 1.0 - np.cumsum(ki_event_probability)
299
+ )
300
+
301
+ return PhoenixEventStats(
302
+ pv=pv,
303
+ ko_times=np.array(ko_times, dtype=float),
304
+ ko_probability=ko_probability,
305
+ survival_probability=survival_probability,
306
+ expected_discounted_ko_cashflow=expected_discounted_ko_cashflow,
307
+ ki_probability=float(np.mean(stats["ki_triggered"]))
308
+ if product.has_ki_barrier
309
+ else 0.0,
310
+ expected_discounted_maturity_cashflow=expected_discounted_maturity_cashflow,
311
+ reconciliation_error=float(reconciliation_error),
312
+ ki_times=ki_event_times,
313
+ ki_event_probability=ki_event_probability,
314
+ ki_survival_probability=ki_survival_probability,
315
+ coupon_probability=coupon_probabilities,
316
+ expected_discounted_coupon_cashflow=coupon_cashflows,
317
+ )
318
+
319
+ def _validate_inputs(
320
+ self,
321
+ S: float,
322
+ T: float,
323
+ r: float,
324
+ q: float,
325
+ sigma: float,
326
+ product: PhoenixOption,
327
+ ) -> None:
328
+ if S <= 0:
329
+ raise ValidationError(f"Spot price must be positive, got {S}")
330
+ if T < 0:
331
+ raise ValidationError(f"Time to maturity must be non-negative, got {T}")
332
+ if sigma <= 0:
333
+ raise ValidationError(f"Volatility must be positive, got {sigma}")
334
+ if not np.isfinite(q):
335
+ raise ValidationError(f"Dividend yield must be finite, got {q}")
336
+
337
+ if product.barrier_config.ko_observation_type == ObservationType.DISCRETE:
338
+ if (
339
+ product.barrier_config.ko_observation_schedule is None
340
+ and product.barrier_config.ko_observation_dates is None
341
+ ):
342
+ raise ValidationError(
343
+ "KO observation schedule or dates required for discrete monitoring"
344
+ )
345
+
346
+ def _build_time_grid(
347
+ self, product: PhoenixOption, pricing_env: PricingEnvironment, T: float
348
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
349
+ ko_profile = product.get_ko_observation_profile(pricing_env)
350
+ ko_times = np.array(ko_profile["observation_times"], dtype=float)
351
+
352
+ ki_times = np.array([], dtype=float)
353
+ ki_continuous = (
354
+ product.barrier_config.ki_observation_type == ObservationType.CONTINUOUS
355
+ or product.barrier_config.ki_continuous
356
+ )
357
+
358
+ if product.has_ki_barrier:
359
+ if ki_continuous:
360
+ num_ki_steps = int(pricing_env.bus_days_in_year * T) + 1
361
+ ki_times = np.linspace(0, T, num_ki_steps + 1)[1:]
362
+ else:
363
+ ki_profile = product.get_ki_observation_profile(pricing_env)
364
+ ki_times = np.array(ki_profile["observation_times"], dtype=float)
365
+
366
+ ko_grid_times = [t for t in ko_times if t > 0 and not is_zero(t)]
367
+ ki_grid_times = [t for t in ki_times if t > 0 and not is_zero(t)]
368
+
369
+ all_times_set = set(ko_grid_times) | set(ki_grid_times) | {T}
370
+ all_times = np.array(sorted(all_times_set), dtype=float)
371
+
372
+ times_with_zero = np.concatenate([[0.0], all_times])
373
+ dt_array = np.diff(times_with_zero)
374
+
375
+ def path_index_for_time(time_val: float) -> int:
376
+ if time_val <= 0.0 or is_zero(time_val):
377
+ return 0
378
+ return int(np.searchsorted(all_times, time_val)) + 1
379
+
380
+ ko_indices = np.array([path_index_for_time(t) for t in ko_times], dtype=int)
381
+ if not ki_continuous and ki_times.size > 0:
382
+ ki_indices = np.array(
383
+ [path_index_for_time(t) for t in ki_times], dtype=int
384
+ )
385
+ else:
386
+ ki_indices = np.array([], dtype=int)
387
+
388
+ return all_times, dt_array, ko_indices, ki_indices
389
+
390
+ def _create_path_generator(
391
+ self,
392
+ S: float,
393
+ r: float,
394
+ q: float,
395
+ sigma: float,
396
+ T: float,
397
+ dt_array: np.ndarray,
398
+ batch_id: Optional[int] = None,
399
+ num_paths: Optional[int] = None,
400
+ ) -> GBMPathGenerator:
401
+ params = self.params
402
+ effective_num_paths = params.num_paths if num_paths is None else int(num_paths)
403
+ if effective_num_paths <= 0:
404
+ raise ValidationError(
405
+ f"num_paths must be positive, got {effective_num_paths}"
406
+ )
407
+
408
+ if self.method == MonteCarloMethod.PSEUDO:
409
+ seed = params.seed + (batch_id or 0) * 1000
410
+ random_stream = PseudoRandomNormalGenerator(seed=seed)
411
+ is_qmc = False
412
+ elif self.method in (MonteCarloMethod.QUASI, MonteCarloMethod.RANDOMIZED_QUASI):
413
+ random_stream = SobolNormalGenerator(base_seed=params.seed)
414
+ is_qmc = True
415
+ else:
416
+ raise ValidationError(f"Unknown Monte Carlo method: {self.method}")
417
+
418
+ generator = GBMPathGenerator(
419
+ initial_value=S,
420
+ vol=sigma,
421
+ rrf=r,
422
+ div=q,
423
+ maturity=T,
424
+ time_steps=len(dt_array),
425
+ num_paths=effective_num_paths,
426
+ model="bsm",
427
+ random_stream=random_stream,
428
+ use_brownian_bridge=is_qmc,
429
+ vr_config=None,
430
+ is_qmc=is_qmc,
431
+ dt_array=dt_array,
432
+ )
433
+
434
+ return generator
435
+
436
+ def _check_ko_barriers(
437
+ self,
438
+ paths: np.ndarray,
439
+ ko_indices: np.ndarray,
440
+ ko_barriers: np.ndarray,
441
+ is_reverse: bool,
442
+ ) -> Tuple[np.ndarray, np.ndarray]:
443
+ ko_prices = paths[:, ko_indices]
444
+
445
+ if is_reverse:
446
+ ko_hit = ko_prices <= ko_barriers
447
+ else:
448
+ ko_hit = ko_prices >= ko_barriers
449
+
450
+ ko_triggered = ko_hit.any(axis=1)
451
+ first_ko_idx = np.full(len(paths), -1, dtype=int)
452
+
453
+ if ko_triggered.any():
454
+ first_ko_idx[ko_triggered] = np.argmax(ko_hit[ko_triggered], axis=1)
455
+
456
+ return ko_triggered, first_ko_idx
457
+
458
+ def _check_ki_barriers(
459
+ self,
460
+ paths: np.ndarray,
461
+ ki_indices: np.ndarray,
462
+ ki_barriers: Union[float, np.ndarray],
463
+ is_reverse: bool,
464
+ ) -> Tuple[np.ndarray, np.ndarray]:
465
+ if len(ki_indices) == 0:
466
+ return np.zeros(len(paths), dtype=bool), np.full(len(paths), -1, dtype=int)
467
+
468
+ ki_prices = paths[:, ki_indices]
469
+ num_ki_obs_times = ki_prices.shape[1]
470
+
471
+ ki_barriers_effective = np.array(ki_barriers)
472
+
473
+ if ki_barriers_effective.shape == () or ki_barriers_effective.shape == (1,):
474
+ ki_barriers_aligned = np.full(
475
+ num_ki_obs_times, ki_barriers_effective.item()
476
+ )
477
+ else:
478
+ if ki_barriers_effective.shape[0] != num_ki_obs_times:
479
+ raise ValidationError(
480
+ f"ki_barriers array (shape {ki_barriers_effective.shape[0]}) "
481
+ f"does not match number of KI observation times ({num_ki_obs_times})"
482
+ )
483
+ ki_barriers_aligned = ki_barriers_effective
484
+
485
+ if is_reverse:
486
+ ki_hit = ki_prices >= ki_barriers_aligned
487
+ else:
488
+ ki_hit = ki_prices <= ki_barriers_aligned
489
+
490
+ ki_triggered = ki_hit.any(axis=1)
491
+ first_ki_idx = np.full(len(paths), -1, dtype=int)
492
+
493
+ if ki_triggered.any():
494
+ first_ki_idx[ki_triggered] = np.argmax(ki_hit[ki_triggered], axis=1)
495
+
496
+ return ki_triggered, first_ki_idx
497
+
498
+ def _check_ki_barriers_continuous_with_bridge(
499
+ self,
500
+ paths: np.ndarray,
501
+ all_times: np.ndarray,
502
+ ki_barrier: float,
503
+ sigma: float,
504
+ is_reverse: bool,
505
+ rng_seed: int,
506
+ ) -> Tuple[np.ndarray, np.ndarray]:
507
+ if ki_barrier <= 0:
508
+ raise ValidationError(f"ki_barrier must be positive, got {ki_barrier}")
509
+ if sigma <= 0:
510
+ raise ValidationError(f"volatility must be positive, got {sigma}")
511
+
512
+ n_paths = len(paths)
513
+ if n_paths == 0:
514
+ return np.zeros(0, dtype=bool), np.zeros(0, dtype=int)
515
+
516
+ ki_triggered = np.zeros(n_paths, dtype=bool)
517
+ first_ki_idx = np.full(n_paths, -1, dtype=int)
518
+
519
+ spot0 = paths[:, 0]
520
+ if is_reverse:
521
+ already_breached = spot0 >= ki_barrier
522
+ else:
523
+ already_breached = spot0 <= ki_barrier
524
+ if already_breached.any():
525
+ ki_triggered[already_breached] = True
526
+ first_ki_idx[already_breached] = 0
527
+
528
+ all_times = np.asarray(all_times, dtype=float)
529
+ if all_times.ndim != 1:
530
+ raise ValidationError("all_times must be a 1D array of time points")
531
+
532
+ n_steps = paths.shape[1] - 1
533
+ if all_times.shape[0] != n_steps:
534
+ raise ValidationError(
535
+ f"all_times length ({all_times.shape[0]}) must match number of steps ({n_steps})"
536
+ )
537
+
538
+ dt = np.empty(n_steps, dtype=float)
539
+ dt[0] = float(all_times[0])
540
+ if n_steps > 1:
541
+ dt[1:] = np.diff(all_times)
542
+ if np.any(dt <= 0.0):
543
+ raise ValidationError("all_times must be strictly increasing and > 0")
544
+
545
+ rng = np.random.default_rng(int(rng_seed))
546
+
547
+ for k in range(n_steps):
548
+ active = ~ki_triggered
549
+ if not active.any():
550
+ break
551
+
552
+ s1 = paths[:, k + 1]
553
+ if is_reverse:
554
+ breached_at_endpoint = s1 >= ki_barrier
555
+ else:
556
+ breached_at_endpoint = s1 <= ki_barrier
557
+
558
+ new_hit = active & breached_at_endpoint
559
+ if new_hit.any():
560
+ ki_triggered[new_hit] = True
561
+ first_ki_idx[new_hit] = k
562
+
563
+ active = ~ki_triggered
564
+ if not active.any():
565
+ break
566
+
567
+ s0 = paths[:, k]
568
+ s1 = paths[:, k + 1]
569
+
570
+ if is_reverse:
571
+ non_breached = (s0 < ki_barrier) & (s1 < ki_barrier)
572
+ else:
573
+ non_breached = (s0 > ki_barrier) & (s1 > ki_barrier)
574
+
575
+ bridge_candidates = active & non_breached
576
+ if not bridge_candidates.any():
577
+ continue
578
+
579
+ idx = np.flatnonzero(bridge_candidates)
580
+ dt_k = float(dt[k])
581
+ h2 = float(sigma * sigma) * dt_k
582
+
583
+ log_term = safe_log(s0[idx] / ki_barrier) * safe_log(s1[idx] / ki_barrier)
584
+ exponent = -2.0 * log_term / h2
585
+ exponent = np.clip(exponent, -745.0, 0.0)
586
+ p = np.exp(exponent)
587
+
588
+ u = rng.random(idx.size)
589
+ hit = u < p
590
+ if hit.any():
591
+ hit_paths = idx[hit]
592
+ ki_triggered[hit_paths] = True
593
+ first_ki_idx[hit_paths] = k
594
+
595
+ return ki_triggered, first_ki_idx
596
+
597
+ @staticmethod
598
+ def _coupon_trigger_mask(
599
+ product: PhoenixOption, spots: np.ndarray, obs_idx: int
600
+ ) -> np.ndarray:
601
+ """Vectorized coupon trigger check using product logic."""
602
+ return np.array(
603
+ [product.is_coupon_triggered(float(s), obs_idx) for s in spots],
604
+ dtype=bool,
605
+ )
606
+
607
+ def _compute_payoffs(
608
+ self,
609
+ product: PhoenixOption,
610
+ pricing_env: PricingEnvironment,
611
+ paths: np.ndarray,
612
+ all_times: np.ndarray,
613
+ ko_indices: np.ndarray,
614
+ ki_indices: np.ndarray,
615
+ r: float,
616
+ T: float,
617
+ sigma: float,
618
+ rng_seed: int,
619
+ collect_coupon_stats: bool = False,
620
+ ) -> Tuple[
621
+ np.ndarray,
622
+ np.ndarray,
623
+ Dict[str, np.ndarray],
624
+ np.ndarray,
625
+ np.ndarray,
626
+ np.ndarray,
627
+ np.ndarray,
628
+ np.ndarray,
629
+ np.ndarray,
630
+ ]:
631
+ num_paths = len(paths)
632
+
633
+ ko_profile = product.get_ko_observation_profile(pricing_env)
634
+ ko_times = np.array(ko_profile["observation_times"], dtype=float)
635
+ ko_barriers = np.array(ko_profile["barriers"], dtype=float)
636
+ ko_payoffs_schedule = np.array(ko_profile["payoffs"], dtype=float)
637
+ ko_settlement_times = np.array(ko_profile["settlement_times"], dtype=float)
638
+
639
+ num_obs = len(ko_times)
640
+ coupon_barrier = product.coupon_config.coupon_barrier
641
+ if isinstance(coupon_barrier, list):
642
+ if len(coupon_barrier) != num_obs:
643
+ raise ValidationError(
644
+ "Coupon barrier schedule length does not match KO observations."
645
+ )
646
+ coupon_barriers = np.array(coupon_barrier, dtype=float)
647
+ else:
648
+ coupon_barriers = np.full(num_obs, float(coupon_barrier))
649
+
650
+ period_year_fractions = np.array(
651
+ product.get_coupon_period_year_fractions(ko_times.tolist()),
652
+ dtype=float,
653
+ )
654
+ coupon_amounts = np.array(
655
+ [
656
+ product.get_coupon_payoff(i, year_fraction=period_year_fractions[i])
657
+ for i in range(num_obs)
658
+ ],
659
+ dtype=float,
660
+ )
661
+
662
+ ko_triggered, first_ko_idx = self._check_ko_barriers(
663
+ paths, ko_indices, ko_barriers, product.is_reverse
664
+ )
665
+
666
+ ki_triggered = np.zeros(num_paths, dtype=bool)
667
+ first_ki_idx = np.full(num_paths, -1, dtype=int)
668
+ ki_times = np.array([], dtype=float)
669
+ if product.has_ki_barrier:
670
+ ki_continuous = (
671
+ product.barrier_config.ki_observation_type == ObservationType.CONTINUOUS
672
+ or product.barrier_config.ki_continuous
673
+ )
674
+ if ki_continuous:
675
+ ki_barrier_val = product.barrier_config.ki_barrier
676
+ if isinstance(ki_barrier_val, list):
677
+ raise ValidationError(
678
+ "Continuous KI monitoring requires a scalar ki_barrier."
679
+ )
680
+ ki_barrier_scalar = float(ki_barrier_val)
681
+ ki_triggered, first_ki_idx = (
682
+ self._check_ki_barriers_continuous_with_bridge(
683
+ paths=paths,
684
+ all_times=all_times,
685
+ ki_barrier=ki_barrier_scalar,
686
+ sigma=float(sigma),
687
+ is_reverse=product.is_reverse,
688
+ rng_seed=int(rng_seed),
689
+ )
690
+ )
691
+ else:
692
+ ki_profile = product.get_ki_observation_profile(pricing_env)
693
+ ki_times = np.array(ki_profile["observation_times"], dtype=float)
694
+ ki_barriers_val = np.array(ki_profile["barriers"], dtype=float)
695
+ ki_triggered, first_ki_idx = self._check_ki_barriers(
696
+ paths, ki_indices, ki_barriers_val, product.is_reverse
697
+ )
698
+
699
+ already_knocked_in = bool(getattr(product, "_otc_lifecycle_knocked_in", False))
700
+ if already_knocked_in and product.has_ki_barrier:
701
+ ki_triggered[:] = True
702
+ first_ki_idx[:] = 0
703
+
704
+ if product.barrier_config.disable_ko_after_ki and product.has_ki_barrier:
705
+ if already_knocked_in:
706
+ ko_valid = np.zeros_like(ko_triggered)
707
+ else:
708
+ ko_trigger_times = np.where(
709
+ first_ko_idx >= 0, ko_times[first_ko_idx], np.inf
710
+ )
711
+ if product.has_ki_barrier:
712
+ if ki_continuous:
713
+ ki_obs_times = all_times
714
+ else:
715
+ ki_obs_times = ki_times
716
+ if ki_obs_times.size > 0:
717
+ ki_trigger_times = np.where(
718
+ first_ki_idx >= 0, ki_obs_times[first_ki_idx], np.inf
719
+ )
720
+ else:
721
+ ki_trigger_times = np.full(num_paths, np.inf, dtype=float)
722
+ else:
723
+ ki_trigger_times = np.full(num_paths, np.inf, dtype=float)
724
+ ko_valid = ko_triggered & (ko_trigger_times < ki_trigger_times)
725
+ else:
726
+ ko_valid = ko_triggered
727
+
728
+ is_ko = ko_valid
729
+ is_v0 = ~is_ko & ~ki_triggered
730
+ is_v1 = ~is_ko & ki_triggered
731
+
732
+ payoffs = np.zeros(num_paths, dtype=float)
733
+ settlement_times = np.full(num_paths, T, dtype=float)
734
+ instant_coupon_discounted = np.zeros(num_paths, dtype=float)
735
+
736
+ coupon_probabilities = np.zeros(num_obs, dtype=float)
737
+ coupon_cashflows = np.zeros(num_obs, dtype=float)
738
+
739
+ accrued = np.zeros(num_paths, dtype=float)
740
+ expiry_coupon = np.zeros(num_paths, dtype=float)
741
+
742
+ for obs_idx in range(num_obs):
743
+ spot_obs = paths[:, ko_indices[obs_idx]]
744
+ ko_at_obs = is_ko & (first_ko_idx == obs_idx)
745
+ alive_before = (~is_ko) | (first_ko_idx >= obs_idx)
746
+
747
+ coupon_hit = self._coupon_trigger_mask(product, spot_obs, obs_idx)
748
+ coupon_hit = coupon_hit & alive_before
749
+
750
+ current_coupon = coupon_amounts[obs_idx]
751
+ if product.coupon_config.memory_coupon:
752
+ coupon_to_pay = current_coupon + accrued
753
+ else:
754
+ coupon_to_pay = np.full(num_paths, current_coupon, dtype=float)
755
+
756
+ if collect_coupon_stats:
757
+ coupon_probabilities[obs_idx] = float(np.mean(coupon_hit))
758
+ if product.coupon_config.coupon_pay_type == CouponPayType.INSTANT:
759
+ df_obs = math.exp(-r * float(ko_times[obs_idx]))
760
+ coupon_cashflows[obs_idx] = float(
761
+ np.mean(coupon_to_pay * coupon_hit) * df_obs
762
+ )
763
+ else:
764
+ df_T = math.exp(-r * float(T))
765
+ coupon_cashflows[obs_idx] = float(
766
+ np.mean(coupon_to_pay * coupon_hit) * df_T
767
+ )
768
+
769
+ non_ko_coupon = coupon_hit & ~ko_at_obs
770
+ if non_ko_coupon.any():
771
+ if product.coupon_config.coupon_pay_type == CouponPayType.INSTANT:
772
+ df_obs = math.exp(-r * float(ko_times[obs_idx]))
773
+ instant_coupon_discounted[non_ko_coupon] += (
774
+ coupon_to_pay[non_ko_coupon] * df_obs
775
+ )
776
+ else:
777
+ expiry_coupon[non_ko_coupon] += coupon_to_pay[non_ko_coupon]
778
+
779
+ if product.coupon_config.memory_coupon:
780
+ accrued[non_ko_coupon] = 0.0
781
+
782
+ if product.coupon_config.memory_coupon:
783
+ missed = alive_before & ~coupon_hit & ~ko_at_obs
784
+ if missed.any():
785
+ accrued[missed] += current_coupon
786
+
787
+ if ko_at_obs.any():
788
+ ko_coupon_hit = self._coupon_trigger_mask(product, spot_obs, obs_idx)
789
+
790
+ current_coupon_pay = current_coupon
791
+ if product.coupon_config.memory_coupon:
792
+ current_coupon_pay = current_coupon + accrued
793
+ ko_coupon = np.where(ko_coupon_hit, current_coupon_pay, 0.0)
794
+
795
+ payoffs[ko_at_obs] = ko_payoffs_schedule[obs_idx] + ko_coupon[ko_at_obs]
796
+
797
+ if product.coupon_config.coupon_pay_type == CouponPayType.INSTANT:
798
+ settlement_times[ko_at_obs] = ko_settlement_times[obs_idx]
799
+
800
+ if product.coupon_config.memory_coupon:
801
+ accrued[ko_at_obs] = 0.0
802
+
803
+ if (~is_ko).any():
804
+ maturity_spots = paths[~is_ko, -1]
805
+ maturity_payoff = np.zeros(maturity_spots.size, dtype=float)
806
+ if is_v0.any():
807
+ maturity_payoff[is_v0[~is_ko]] = np.array(
808
+ [
809
+ product.get_maturity_payoff_v0(
810
+ float(s),
811
+ pricing_env=pricing_env,
812
+ )
813
+ for s in maturity_spots[is_v0[~is_ko]]
814
+ ],
815
+ dtype=float,
816
+ )
817
+ if is_v1.any():
818
+ maturity_payoff[is_v1[~is_ko]] = np.array(
819
+ [
820
+ product.get_maturity_payoff_v1(
821
+ float(s),
822
+ pricing_env=pricing_env,
823
+ )
824
+ for s in maturity_spots[is_v1[~is_ko]]
825
+ ],
826
+ dtype=float,
827
+ )
828
+ if product.coupon_config.coupon_pay_type == CouponPayType.EXPIRY:
829
+ maturity_payoff += expiry_coupon[~is_ko]
830
+ payoffs[~is_ko] += maturity_payoff
831
+
832
+ stats = {
833
+ "ko_probability": float(is_ko.mean()),
834
+ "v0_probability": float(is_v0.mean()),
835
+ "v1_probability": float(is_v1.mean()),
836
+ "ko_count": int(is_ko.sum()),
837
+ "v0_count": int(is_v0.sum()),
838
+ "v1_count": int(is_v1.sum()),
839
+ "avg_ko_time": None,
840
+ "ko_time_sum": 0.0,
841
+ "ko_time_count": 0,
842
+ "is_ko": is_ko,
843
+ "is_v0": is_v0,
844
+ "is_v1": is_v1,
845
+ "first_ko_idx": first_ko_idx,
846
+ "ki_triggered": ki_triggered,
847
+ "first_ki_idx": first_ki_idx,
848
+ }
849
+
850
+ if is_ko.any():
851
+ ko_times_hit = ko_times[first_ko_idx[is_ko]]
852
+ stats["avg_ko_time"] = float(ko_times_hit.mean())
853
+ stats["ko_time_sum"] = float(ko_times_hit.sum())
854
+ stats["ko_time_count"] = int(is_ko.sum())
855
+
856
+ return (
857
+ payoffs,
858
+ settlement_times,
859
+ stats,
860
+ coupon_probabilities,
861
+ coupon_cashflows,
862
+ instant_coupon_discounted,
863
+ ko_times,
864
+ ko_payoffs_schedule,
865
+ ko_settlement_times,
866
+ )
867
+
868
+ def _price_mc_or_qmc(
869
+ self,
870
+ product: PhoenixOption,
871
+ pricing_env: PricingEnvironment,
872
+ S: float,
873
+ T: float,
874
+ r: float,
875
+ q: float,
876
+ sigma: float,
877
+ ) -> PhoenixMCResult:
878
+ all_times, dt_array, ko_indices, ki_indices = self._build_time_grid(
879
+ product, pricing_env, T
880
+ )
881
+ generator = self._create_path_generator(S, r, q, sigma, T, dt_array)
882
+ paths, _ = generator.generate_paths(return_aux=False)
883
+
884
+ (
885
+ payoffs,
886
+ settlement_times,
887
+ stats,
888
+ coupon_probabilities,
889
+ coupon_cashflows,
890
+ instant_coupon_discounted,
891
+ _,
892
+ _,
893
+ _,
894
+ ) = self._compute_payoffs(
895
+ product,
896
+ pricing_env,
897
+ paths,
898
+ all_times,
899
+ ko_indices,
900
+ ki_indices,
901
+ r,
902
+ T,
903
+ sigma,
904
+ rng_seed=int(self.params.seed) + 1337,
905
+ collect_coupon_stats=True,
906
+ )
907
+
908
+ discount_factors = np.exp(-r * settlement_times)
909
+ discounted_payoffs = payoffs * discount_factors + instant_coupon_discounted
910
+
911
+ price = float(discounted_payoffs.mean())
912
+ std_payoff = float(discounted_payoffs.std(ddof=1))
913
+ std_error = std_payoff / math.sqrt(len(payoffs))
914
+
915
+ return PhoenixMCResult(
916
+ price=price,
917
+ std_error=std_error,
918
+ num_paths=len(paths),
919
+ ko_probability=stats["ko_probability"],
920
+ v0_probability=stats["v0_probability"],
921
+ v1_probability=stats["v1_probability"],
922
+ avg_ko_time=stats.get("avg_ko_time"),
923
+ coupon_probabilities=coupon_probabilities,
924
+ expected_discounted_coupon_cashflow=coupon_cashflows,
925
+ )
926
+
927
+ def _price_single_batch(
928
+ self,
929
+ batch_id: int,
930
+ batch_num_paths: int,
931
+ product: PhoenixOption,
932
+ pricing_env: PricingEnvironment,
933
+ S: float,
934
+ T: float,
935
+ r: float,
936
+ q: float,
937
+ sigma: float,
938
+ all_times: np.ndarray,
939
+ dt_array: np.ndarray,
940
+ ko_indices: np.ndarray,
941
+ ki_indices: np.ndarray,
942
+ ) -> Dict[str, float]:
943
+ generator = self._create_path_generator(
944
+ S, r, q, sigma, T, dt_array, batch_id=batch_id, num_paths=batch_num_paths
945
+ )
946
+
947
+ paths, _ = generator.generate_paths(return_aux=False, batch_id=batch_id)
948
+
949
+ (
950
+ payoffs,
951
+ settlement_times,
952
+ stats,
953
+ _,
954
+ _,
955
+ instant_coupon_discounted,
956
+ _,
957
+ _,
958
+ _,
959
+ ) = self._compute_payoffs(
960
+ product,
961
+ pricing_env,
962
+ paths,
963
+ all_times,
964
+ ko_indices,
965
+ ki_indices,
966
+ r,
967
+ T,
968
+ sigma,
969
+ rng_seed=int(self.params.seed) + 1337 + int(batch_id) * 1000,
970
+ )
971
+
972
+ discount_factors = np.exp(-r * settlement_times)
973
+ discounted_payoffs = payoffs * discount_factors + instant_coupon_discounted
974
+
975
+ discounted_payoffs = np.asarray(discounted_payoffs, dtype=float)
976
+ n = int(discounted_payoffs.size)
977
+ sum_x = float(discounted_payoffs.sum())
978
+ sum_x2 = float(np.square(discounted_payoffs).sum())
979
+
980
+ return {
981
+ "n": n,
982
+ "sum_x": sum_x,
983
+ "sum_x2": sum_x2,
984
+ "ko_count": int(stats.get("ko_count", 0)),
985
+ "v0_count": int(stats.get("v0_count", 0)),
986
+ "v1_count": int(stats.get("v1_count", 0)),
987
+ "ko_time_sum": float(stats.get("ko_time_sum", 0.0)),
988
+ "ko_time_count": int(stats.get("ko_time_count", 0)),
989
+ }
990
+
991
+ def _price_parallel(
992
+ self,
993
+ product: PhoenixOption,
994
+ pricing_env: PricingEnvironment,
995
+ S: float,
996
+ T: float,
997
+ r: float,
998
+ q: float,
999
+ sigma: float,
1000
+ ) -> PhoenixMCResult:
1001
+ all_times, dt_array, ko_indices, ki_indices = self._build_time_grid(
1002
+ product, pricing_env, T
1003
+ )
1004
+
1005
+ if self.num_batches <= 0:
1006
+ raise ValidationError(
1007
+ f"num_batches must be positive, got {self.num_batches}"
1008
+ )
1009
+
1010
+ total_paths_target = int(self.params.num_paths)
1011
+ base = total_paths_target // self.num_batches
1012
+ remainder = total_paths_target % self.num_batches
1013
+ batch_sizes = [
1014
+ (base + 1 if i < remainder else base) for i in range(self.num_batches)
1015
+ ]
1016
+
1017
+ batch_results = []
1018
+ batches_used = 0
1019
+ for batch_id, batch_num_paths in enumerate(batch_sizes):
1020
+ if batch_num_paths <= 0:
1021
+ continue
1022
+ batches_used += 1
1023
+ batch_results.append(
1024
+ delayed(self._price_single_batch)(
1025
+ batch_id=batch_id,
1026
+ batch_num_paths=batch_num_paths,
1027
+ product=product,
1028
+ pricing_env=pricing_env,
1029
+ S=S,
1030
+ T=T,
1031
+ r=r,
1032
+ q=q,
1033
+ sigma=sigma,
1034
+ all_times=all_times,
1035
+ dt_array=dt_array,
1036
+ ko_indices=ko_indices,
1037
+ ki_indices=ki_indices,
1038
+ )
1039
+ )
1040
+
1041
+ results = compute(*batch_results)
1042
+
1043
+ total_n = 0
1044
+ total_sum_x = 0.0
1045
+ total_sum_x2 = 0.0
1046
+ total_ko_count = 0
1047
+ total_v0_count = 0
1048
+ total_v1_count = 0
1049
+ total_ko_time_sum = 0.0
1050
+ total_ko_time_count = 0
1051
+
1052
+ for res in results:
1053
+ total_n += int(res["n"])
1054
+ total_sum_x += float(res["sum_x"])
1055
+ total_sum_x2 += float(res["sum_x2"])
1056
+ total_ko_count += int(res.get("ko_count", 0))
1057
+ total_v0_count += int(res.get("v0_count", 0))
1058
+ total_v1_count += int(res.get("v1_count", 0))
1059
+ total_ko_time_sum += float(res.get("ko_time_sum", 0.0))
1060
+ total_ko_time_count += int(res.get("ko_time_count", 0))
1061
+
1062
+ if total_n <= 0:
1063
+ raise PricingError("Dask parallel pricing produced zero simulated paths")
1064
+
1065
+ price = total_sum_x / total_n
1066
+
1067
+ if total_n > 1:
1068
+ sample_var = (total_sum_x2 - (total_sum_x * total_sum_x) / total_n) / (
1069
+ total_n - 1
1070
+ )
1071
+ sample_var = max(sample_var, 0.0)
1072
+ std_error = math.sqrt(sample_var) / math.sqrt(total_n)
1073
+ else:
1074
+ std_error = 0.0
1075
+
1076
+ ko_probability = float(total_ko_count / total_n)
1077
+ v0_probability = float(total_v0_count / total_n)
1078
+ v1_probability = float(total_v1_count / total_n)
1079
+
1080
+ if total_ko_time_count > 0:
1081
+ avg_ko_time = float(total_ko_time_sum / total_ko_time_count)
1082
+ else:
1083
+ avg_ko_time = None
1084
+
1085
+ return PhoenixMCResult(
1086
+ price=float(price),
1087
+ std_error=float(std_error),
1088
+ num_paths=int(total_n),
1089
+ ko_probability=ko_probability,
1090
+ v0_probability=v0_probability,
1091
+ v1_probability=v1_probability,
1092
+ avg_ko_time=avg_ko_time,
1093
+ batches_used=batches_used,
1094
+ )
1095
+
1096
+ def _price_rqmc(
1097
+ self,
1098
+ product: PhoenixOption,
1099
+ pricing_env: PricingEnvironment,
1100
+ S: float,
1101
+ T: float,
1102
+ r: float,
1103
+ q: float,
1104
+ sigma: float,
1105
+ ) -> PhoenixMCResult:
1106
+ all_times, dt_array, ko_indices, ki_indices = self._build_time_grid(
1107
+ product, pricing_env, T
1108
+ )
1109
+ generator = self._create_path_generator(S, r, q, sigma, T, dt_array)
1110
+
1111
+ def pricer_fn(paths, aux):
1112
+ batch_id = 0
1113
+ if aux is not None and "batch_id" in aux:
1114
+ batch_id = int(aux["batch_id"])
1115
+ (
1116
+ payoffs,
1117
+ settlement_times,
1118
+ _,
1119
+ _,
1120
+ _,
1121
+ instant_coupon_discounted,
1122
+ _,
1123
+ _,
1124
+ _,
1125
+ ) = self._compute_payoffs(
1126
+ product,
1127
+ pricing_env,
1128
+ paths,
1129
+ all_times,
1130
+ ko_indices,
1131
+ ki_indices,
1132
+ r,
1133
+ T,
1134
+ sigma,
1135
+ rng_seed=int(self.params.seed) + 1337 + batch_id * 1000,
1136
+ )
1137
+ discount_factors = np.exp(-r * settlement_times)
1138
+ return payoffs * discount_factors + instant_coupon_discounted
1139
+
1140
+ params = self.params
1141
+ max_batches = getattr(
1142
+ params, "rqmc_max_batches", getattr(params, "max_batches", 32)
1143
+ )
1144
+ min_batches = getattr(
1145
+ params, "rqmc_min_batches", getattr(params, "min_batches", 4)
1146
+ )
1147
+ if hasattr(params, "resolve_rqmc_target_std"):
1148
+ target_std = params.resolve_rqmc_target_std(
1149
+ product=product, pricing_env=pricing_env
1150
+ )
1151
+ else:
1152
+ target_std = getattr(params, "target_std", 1e-4)
1153
+ if hasattr(params, "resolve_rqmc_paths_per_batch"):
1154
+ per_batch_paths = params.resolve_rqmc_paths_per_batch(
1155
+ max_batches=max_batches
1156
+ )
1157
+ else:
1158
+ per_batch_paths = params.num_paths
1159
+
1160
+ generator = self._create_path_generator(
1161
+ S, r, q, sigma, T, dt_array, num_paths=per_batch_paths
1162
+ )
1163
+
1164
+ result = run_rqmc(
1165
+ pricer_fn=pricer_fn,
1166
+ path_generator=generator,
1167
+ max_batches=max_batches,
1168
+ target_std=target_std,
1169
+ min_batches=min_batches,
1170
+ )
1171
+
1172
+ paths, _ = generator.generate_paths(return_aux=False, batch_id=0)
1173
+ _, _, stats, _, _, _, _, _, _ = self._compute_payoffs(
1174
+ product,
1175
+ pricing_env,
1176
+ paths,
1177
+ all_times,
1178
+ ko_indices,
1179
+ ki_indices,
1180
+ r,
1181
+ T,
1182
+ sigma,
1183
+ rng_seed=int(self.params.seed) + 1337,
1184
+ )
1185
+
1186
+ return PhoenixMCResult(
1187
+ price=result.price,
1188
+ std_error=result.std_error,
1189
+ num_paths=result.total_paths,
1190
+ ko_probability=stats["ko_probability"],
1191
+ v0_probability=stats["v0_probability"],
1192
+ v1_probability=stats["v1_probability"],
1193
+ batches_used=result.batches_used,
1194
+ )
1195
+
1196
+ def get_last_result(self) -> Optional[PhoenixMCResult]:
1197
+ return self._last_result
1198
+
1199
+ def get_last_std_error(self) -> Optional[float]:
1200
+ if self._last_result is None:
1201
+ return None
1202
+ return self._last_result.std_error
1203
+
1204
+ def __repr__(self):
1205
+ dask_str = f", use_dask={self.use_dask}" if self.use_dask else ""
1206
+ return f"PhoenixMCEngine(method={self.method.name}{dask_str})"