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,1260 @@
1
+ """
2
+ Adapter layer for building quadrature core inputs from product types.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass
10
+ from typing import Sequence, TYPE_CHECKING
11
+
12
+ import numpy as np
13
+
14
+ from quantark.asset.equity.engine.quad.quad_core import QuadCoreInputs
15
+ from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
16
+ from quantark.asset.equity.product.option import (
17
+ BarrierOption,
18
+ DoubleBarrierOption,
19
+ DoubleOneTouchOption,
20
+ EuropeanVanillaOption,
21
+ OneTouchOption,
22
+ )
23
+ from quantark.asset.equity.product.option.observation_schedule import (
24
+ ObservationSchedule,
25
+ ResolvedObservationRecord,
26
+ )
27
+ from quantark.priceenv import PricingEnvironment
28
+ from quantark.util.enum import ObservationAggregation, ObservationType, TouchType
29
+ from quantark.util.exceptions import PricingError, ValidationError
30
+ from quantark.util.numerical import (
31
+ Tolerance,
32
+ is_close,
33
+ is_zero,
34
+ validate_non_negative,
35
+ validate_positive,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from quantark.asset.equity.engine.quad.discrete_quad_engine import DiscreteQuadEngine
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class QuadPricingContext:
44
+ """Shared pricing context for discrete quadrature adapters."""
45
+
46
+ spot: float
47
+ maturity: float
48
+ rate: float
49
+ div: float
50
+ vol: float
51
+ contract_multiplier: float
52
+
53
+
54
+ class QuadInputAdapter(ABC):
55
+ """Interface for building quad core inputs from a product."""
56
+
57
+ @abstractmethod
58
+ def can_handle(self, product: BaseEquityProduct) -> bool:
59
+ """Return True if adapter can handle the product."""
60
+
61
+ @abstractmethod
62
+ def build_pricing_context(
63
+ self,
64
+ product: BaseEquityProduct,
65
+ pricing_env: PricingEnvironment,
66
+ engine: "DiscreteQuadEngine",
67
+ ) -> QuadPricingContext:
68
+ """Build shared pricing context for the product."""
69
+
70
+ @abstractmethod
71
+ def early_price(
72
+ self,
73
+ product: BaseEquityProduct,
74
+ pricing_env: PricingEnvironment,
75
+ context: QuadPricingContext,
76
+ engine: "DiscreteQuadEngine",
77
+ ) -> float | None:
78
+ """Return a price if no core evaluation is needed; otherwise None."""
79
+
80
+ @abstractmethod
81
+ def resolve_schedule(
82
+ self,
83
+ product: BaseEquityProduct,
84
+ pricing_env: PricingEnvironment,
85
+ context: QuadPricingContext,
86
+ ) -> Sequence[ResolvedObservationRecord]:
87
+ """Resolve observation schedule into concrete records."""
88
+
89
+ @abstractmethod
90
+ def build_inputs(
91
+ self,
92
+ product: BaseEquityProduct,
93
+ resolved: Sequence[ResolvedObservationRecord],
94
+ context: QuadPricingContext,
95
+ ) -> QuadCoreInputs:
96
+ """Construct quad core inputs for the product."""
97
+
98
+ @abstractmethod
99
+ def finalize_price(
100
+ self,
101
+ product: BaseEquityProduct,
102
+ pricing_env: PricingEnvironment,
103
+ context: QuadPricingContext,
104
+ core_price: float,
105
+ engine: "DiscreteQuadEngine",
106
+ ) -> float:
107
+ """Post-process the core output into a final product price."""
108
+
109
+
110
+ class BaseDiscreteQuadAdapter(QuadInputAdapter):
111
+ """Base adapter with shared schedule handling helpers."""
112
+
113
+ def resolve_schedule(
114
+ self,
115
+ product: BaseEquityProduct,
116
+ pricing_env: PricingEnvironment,
117
+ context: QuadPricingContext,
118
+ ) -> Sequence[ResolvedObservationRecord]:
119
+ maturity = context.maturity
120
+ default_payoff = self._default_payoff(product)
121
+
122
+ if product.observation_type == ObservationType.EXPIRY:
123
+ schedule = ObservationSchedule.from_legacy(
124
+ observation_dates=[maturity],
125
+ default_barrier=product.barrier,
126
+ default_payoff=default_payoff,
127
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
128
+ )
129
+ else:
130
+ schedule = product.observation_schedule
131
+ if schedule is None and product.observation_dates:
132
+ schedule = ObservationSchedule.from_legacy(
133
+ observation_dates=product.observation_dates,
134
+ default_barrier=product.barrier,
135
+ default_payoff=default_payoff,
136
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
137
+ )
138
+
139
+ if schedule is None or not schedule.records:
140
+ raise PricingError("Discrete monitoring requires ObservationSchedule.")
141
+ if schedule.aggregation_mode != ObservationAggregation.STOP_FIRST_HIT:
142
+ raise PricingError("DiscreteQuadEngine requires STOP_FIRST_HIT aggregation.")
143
+
144
+ resolved = schedule.resolve(
145
+ pricing_env,
146
+ default_barrier=product.barrier,
147
+ default_payoff=default_payoff,
148
+ require_single=True,
149
+ )
150
+ return resolved
151
+
152
+ def _default_payoff(self, product: BaseEquityProduct) -> float:
153
+ return float(getattr(product, "rebate", 0.0))
154
+
155
+ def _extract_observations(
156
+ self,
157
+ resolved: Sequence[ResolvedObservationRecord],
158
+ maturity: float,
159
+ is_up_barrier: bool,
160
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
161
+ obs_times = np.array([rec.observation_time for rec in resolved], dtype=float)
162
+ barriers = np.array([rec.barrier for rec in resolved], dtype=float)
163
+ payoffs = np.array([rec.payoff for rec in resolved], dtype=float)
164
+ settlement_times = np.array(
165
+ [
166
+ rec.settlement_time if rec.settlement_time is not None else rec.observation_time
167
+ for rec in resolved
168
+ ],
169
+ dtype=float,
170
+ )
171
+
172
+ if obs_times.size == 0:
173
+ raise ValidationError("Resolved observation schedule is empty.")
174
+
175
+ if maturity - obs_times[-1] > Tolerance.ZERO:
176
+ obs_times = np.append(obs_times, maturity)
177
+ barriers = np.append(barriers, math.inf if is_up_barrier else 0.0)
178
+ payoffs = np.append(payoffs, 0.0)
179
+ settlement_times = np.append(settlement_times, maturity)
180
+
181
+ return obs_times, barriers, payoffs, settlement_times
182
+
183
+ def _discount_rebates(
184
+ self,
185
+ payoffs: np.ndarray,
186
+ observation_times: np.ndarray,
187
+ settlement_times: np.ndarray,
188
+ maturity: float,
189
+ rate: float,
190
+ pay_at_hit: bool,
191
+ ) -> np.ndarray:
192
+ if payoffs.size == 0:
193
+ return payoffs
194
+ if pay_at_hit:
195
+ delays = np.maximum(settlement_times - observation_times, 0.0)
196
+ discount = np.exp(-rate * delays)
197
+ else:
198
+ discount = np.exp(-rate * (maturity - observation_times))
199
+ return payoffs * discount
200
+
201
+
202
+ class BarrierQuadInputAdapter(BaseDiscreteQuadAdapter):
203
+ """Adapter for barrier options in discrete quadrature."""
204
+
205
+ def can_handle(self, product: BaseEquityProduct) -> bool:
206
+ return isinstance(product, BarrierOption)
207
+
208
+ def build_pricing_context(
209
+ self,
210
+ product: BarrierOption,
211
+ pricing_env: PricingEnvironment,
212
+ engine: "DiscreteQuadEngine",
213
+ ) -> QuadPricingContext:
214
+ spot = pricing_env.spot
215
+ maturity = product.get_maturity(pricing_env)
216
+ rate = pricing_env.get_rate(maturity)
217
+ div = pricing_env.get_div_yield(maturity)
218
+ vol = pricing_env.get_vol(product.strike, maturity)
219
+ contract_multiplier = product.contract_multiplier
220
+
221
+ self._validate_inputs(
222
+ spot, product.strike, maturity, rate, div, vol, product.barrier, engine
223
+ )
224
+
225
+ return QuadPricingContext(
226
+ spot=spot,
227
+ maturity=maturity,
228
+ rate=rate,
229
+ div=div,
230
+ vol=vol,
231
+ contract_multiplier=contract_multiplier,
232
+ )
233
+
234
+ def early_price(
235
+ self,
236
+ product: BarrierOption,
237
+ pricing_env: PricingEnvironment,
238
+ context: QuadPricingContext,
239
+ engine: "DiscreteQuadEngine",
240
+ ) -> float | None:
241
+ if context.maturity < engine.MIN_MATURITY:
242
+ value = self._barrier_expired_price(
243
+ product, context.spot, context.rate, context.maturity
244
+ )
245
+ return value * context.contract_multiplier
246
+
247
+ if (
248
+ product.observation_type != ObservationType.EXPIRY
249
+ and product.is_barrier_hit(context.spot)
250
+ ):
251
+ if product.is_knock_out:
252
+ value = self._barrier_immediate_ko_price(
253
+ product, context.rate, context.maturity
254
+ )
255
+ return value * context.contract_multiplier
256
+ value = self._vanilla_price(product, pricing_env, engine)
257
+ return value * context.contract_multiplier
258
+
259
+ if product.observation_type == ObservationType.CONTINUOUS:
260
+ raise PricingError(
261
+ "DiscreteQuadEngine supports discrete monitoring only. "
262
+ "Use BarrierAnalyticalEngine for continuous barriers."
263
+ )
264
+
265
+ return None
266
+
267
+ def resolve_schedule(
268
+ self,
269
+ product: BarrierOption,
270
+ pricing_env: PricingEnvironment,
271
+ context: QuadPricingContext,
272
+ ) -> Sequence[ResolvedObservationRecord]:
273
+ return super().resolve_schedule(product, pricing_env, context)
274
+
275
+ def build_inputs(
276
+ self,
277
+ product: BarrierOption,
278
+ resolved: Sequence[ResolvedObservationRecord],
279
+ context: QuadPricingContext,
280
+ ) -> QuadCoreInputs:
281
+ obs_times, barriers, payoffs, settlement_times = self._extract_observations(
282
+ resolved, context.maturity, product.is_up_barrier
283
+ )
284
+
285
+ if product.is_up_barrier:
286
+ k_plus = np.array(barriers, dtype=float)
287
+ k_minus = np.zeros_like(k_plus)
288
+ else:
289
+ k_minus = np.array(barriers, dtype=float)
290
+ k_plus = np.full_like(k_minus, math.inf, dtype=float)
291
+
292
+ a_minus = np.zeros_like(k_minus, dtype=float)
293
+ b_minus = np.zeros_like(k_minus, dtype=float)
294
+ a_plus = np.zeros_like(k_plus, dtype=float)
295
+ b_plus = np.zeros_like(k_plus, dtype=float)
296
+
297
+ if product.rebate > 0.0:
298
+ rebate_values = self._discount_rebates(
299
+ payoffs,
300
+ obs_times,
301
+ settlement_times,
302
+ context.maturity,
303
+ context.rate,
304
+ product.pay_at_hit,
305
+ )
306
+ if product.is_up_barrier:
307
+ b_plus = rebate_values
308
+ else:
309
+ b_minus = rebate_values
310
+
311
+ maturity_barrier = self._resolve_maturity_barrier(
312
+ obs_times, barriers, context.maturity
313
+ )
314
+ a_terminal, b_terminal = self._apply_terminal_payoff_structure(
315
+ product,
316
+ maturity_barrier,
317
+ k_minus,
318
+ k_plus,
319
+ a_minus,
320
+ b_minus,
321
+ a_plus,
322
+ b_plus,
323
+ )
324
+
325
+ return QuadCoreInputs(
326
+ observation_times=obs_times,
327
+ k_minus=k_minus,
328
+ k_plus=k_plus,
329
+ a_minus=a_minus,
330
+ b_minus=b_minus,
331
+ a_plus=a_plus,
332
+ b_plus=b_plus,
333
+ a_terminal=a_terminal,
334
+ b_terminal=b_terminal,
335
+ )
336
+
337
+ def finalize_price(
338
+ self,
339
+ product: BarrierOption,
340
+ pricing_env: PricingEnvironment,
341
+ context: QuadPricingContext,
342
+ core_price: float,
343
+ engine: "DiscreteQuadEngine",
344
+ ) -> float:
345
+ if product.is_knock_in:
346
+ vanilla_price = self._vanilla_price(product, pricing_env, engine)
347
+ rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
348
+ value = vanilla_price + rebate_discount - core_price
349
+ return value * context.contract_multiplier
350
+ return core_price * context.contract_multiplier
351
+
352
+ def _validate_inputs(
353
+ self,
354
+ spot: float,
355
+ strike: float,
356
+ maturity: float,
357
+ rate: float,
358
+ div: float,
359
+ vol: float,
360
+ barrier: float,
361
+ engine: "DiscreteQuadEngine",
362
+ ) -> None:
363
+ if spot <= 0:
364
+ raise ValidationError(f"Spot price must be positive, got {spot}")
365
+ if strike <= 0:
366
+ raise ValidationError(f"Strike price must be positive, got {strike}")
367
+ if maturity < 0:
368
+ raise ValidationError(f"Time to maturity must be non-negative, got {maturity}")
369
+ if vol <= 0:
370
+ raise ValidationError(f"Volatility must be positive, got {vol}")
371
+ if barrier <= 0:
372
+ raise ValidationError(f"Barrier must be positive, got {barrier}")
373
+ if div < 0:
374
+ raise ValidationError(f"Dividend yield must be non-negative, got {div}")
375
+ if abs(rate) > 1.0:
376
+ raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
377
+ if vol > engine.MAX_VOL:
378
+ raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
379
+
380
+ def _resolve_maturity_barrier(
381
+ self, observation_times: np.ndarray, barriers: np.ndarray, maturity: float
382
+ ) -> float | None:
383
+ maturity_mask = np.isclose(
384
+ observation_times, maturity, atol=Tolerance.ZERO, rtol=0.0
385
+ )
386
+ if not np.any(maturity_mask):
387
+ return None
388
+ value = float(barriers[np.where(maturity_mask)[0][-1]])
389
+ if not math.isfinite(value) or value <= 0.0:
390
+ return None
391
+ return value
392
+
393
+ def _apply_terminal_payoff_structure(
394
+ self,
395
+ product: BarrierOption,
396
+ maturity_barrier: float | None,
397
+ k_minus: np.ndarray,
398
+ k_plus: np.ndarray,
399
+ a_minus: np.ndarray,
400
+ b_minus: np.ndarray,
401
+ a_plus: np.ndarray,
402
+ b_plus: np.ndarray,
403
+ ) -> tuple[float, float]:
404
+ participation = product.participation_rate
405
+ strike = product.strike
406
+ is_call = product.is_call()
407
+ a_terminal = 0.0
408
+ b_terminal = 0.0
409
+
410
+ if maturity_barrier is None:
411
+ if is_call:
412
+ k_minus[-1] = strike
413
+ a_terminal = participation
414
+ b_terminal = -participation * strike
415
+ else:
416
+ k_plus[-1] = strike
417
+ a_terminal = -participation
418
+ b_terminal = participation * strike
419
+ return a_terminal, b_terminal
420
+
421
+ barrier = maturity_barrier
422
+
423
+ if product.is_up_barrier:
424
+ k_plus[-1] = barrier
425
+ if is_call:
426
+ if barrier > strike:
427
+ k_minus[-1] = strike
428
+ a_terminal = participation
429
+ b_terminal = -participation * strike
430
+ else:
431
+ if barrier > strike:
432
+ k_minus[-1] = strike
433
+ a_minus[-1] = -participation
434
+ b_minus[-1] = participation * strike
435
+ else:
436
+ a_terminal = -participation
437
+ b_terminal = participation * strike
438
+ return a_terminal, b_terminal
439
+
440
+ k_minus[-1] = barrier
441
+ if is_call:
442
+ if barrier < strike:
443
+ k_plus[-1] = strike
444
+ a_plus[-1] = participation
445
+ b_plus[-1] = -participation * strike
446
+ else:
447
+ a_terminal = participation
448
+ b_terminal = -participation * strike
449
+ else:
450
+ if barrier < strike:
451
+ k_plus[-1] = strike
452
+ a_terminal = -participation
453
+ b_terminal = participation * strike
454
+
455
+ return a_terminal, b_terminal
456
+
457
+ def _barrier_expired_price(
458
+ self, product: BarrierOption, spot: float, rate: float, maturity: float
459
+ ) -> float:
460
+ hit = product.is_barrier_hit(spot)
461
+ if product.is_call():
462
+ intrinsic = max(spot - product.strike, 0.0)
463
+ else:
464
+ intrinsic = max(product.strike - spot, 0.0)
465
+ vanilla = intrinsic * product.participation_rate
466
+ if product.is_knock_out:
467
+ value = product.rebate if hit else vanilla
468
+ else:
469
+ value = vanilla if hit else product.rebate
470
+ if maturity <= 0.0:
471
+ return value
472
+ return value * math.exp(-rate * maturity)
473
+
474
+ def _barrier_immediate_ko_price(
475
+ self, product: BarrierOption, rate: float, maturity: float
476
+ ) -> float:
477
+ if product.pay_at_hit:
478
+ return product.rebate
479
+ return product.rebate * math.exp(-rate * maturity)
480
+
481
+ def _vanilla_price(
482
+ self,
483
+ product: BarrierOption,
484
+ pricing_env: PricingEnvironment,
485
+ engine: "DiscreteQuadEngine",
486
+ ) -> float:
487
+ vanilla = EuropeanVanillaOption(
488
+ strike=product.strike,
489
+ option_type=product.option_type,
490
+ maturity=product.maturity,
491
+ exercise_date=product.exercise_date,
492
+ settlement_date=product.settlement_date,
493
+ contract_multiplier=1.0,
494
+ )
495
+ price = engine.vanilla_engine.price(vanilla, pricing_env)
496
+ return price * product.participation_rate
497
+
498
+
499
+ class OneTouchQuadInputAdapter(BaseDiscreteQuadAdapter):
500
+ """Adapter for one-touch/no-touch options in discrete quadrature."""
501
+
502
+ def can_handle(self, product: BaseEquityProduct) -> bool:
503
+ return isinstance(product, OneTouchOption)
504
+
505
+ def build_pricing_context(
506
+ self,
507
+ product: OneTouchOption,
508
+ pricing_env: PricingEnvironment,
509
+ engine: "DiscreteQuadEngine",
510
+ ) -> QuadPricingContext:
511
+ spot = pricing_env.spot
512
+ maturity = product.get_maturity(pricing_env)
513
+ rate = pricing_env.get_rate(maturity)
514
+ div = pricing_env.get_div_yield(maturity)
515
+ vol = pricing_env.get_vol(product.barrier, maturity)
516
+ contract_multiplier = getattr(product, "contract_multiplier", 1.0)
517
+
518
+ self._validate_inputs(
519
+ spot, product.barrier, maturity, rate, div, vol, product.rebate, engine
520
+ )
521
+
522
+ return QuadPricingContext(
523
+ spot=spot,
524
+ maturity=maturity,
525
+ rate=rate,
526
+ div=div,
527
+ vol=vol,
528
+ contract_multiplier=contract_multiplier,
529
+ )
530
+
531
+ def early_price(
532
+ self,
533
+ product: OneTouchOption,
534
+ pricing_env: PricingEnvironment,
535
+ context: QuadPricingContext,
536
+ engine: "DiscreteQuadEngine",
537
+ ) -> float | None:
538
+ if is_zero(context.maturity, tol=engine.MIN_MATURITY):
539
+ pay_at_hit = product.payment_at_hit if product.is_one_touch else False
540
+ value = self._one_touch_instant_payoff(
541
+ product, context.spot, context.maturity, context.rate, pay_at_hit
542
+ )
543
+ return value * context.contract_multiplier
544
+
545
+ if (
546
+ product.observation_type != ObservationType.EXPIRY
547
+ and product.is_barrier_hit(context.spot)
548
+ ):
549
+ if product.is_one_touch:
550
+ pay_at_hit = product.payment_at_hit
551
+ value = product.rebate if pay_at_hit else product.rebate * math.exp(
552
+ -context.rate * context.maturity
553
+ )
554
+ return value * context.contract_multiplier
555
+ return 0.0
556
+
557
+ if product.observation_type == ObservationType.CONTINUOUS:
558
+ raise PricingError(
559
+ "DiscreteQuadEngine supports discrete or expiry monitoring only. "
560
+ "Use OneTouchAnalyticalEngine for continuous monitoring."
561
+ )
562
+
563
+ if product.rebate <= 0.0:
564
+ return 0.0
565
+
566
+ return None
567
+
568
+ def resolve_schedule(
569
+ self,
570
+ product: OneTouchOption,
571
+ pricing_env: PricingEnvironment,
572
+ context: QuadPricingContext,
573
+ ) -> Sequence[ResolvedObservationRecord]:
574
+ return super().resolve_schedule(product, pricing_env, context)
575
+
576
+ def build_inputs(
577
+ self,
578
+ product: OneTouchOption,
579
+ resolved: Sequence[ResolvedObservationRecord],
580
+ context: QuadPricingContext,
581
+ ) -> QuadCoreInputs:
582
+ obs_times, barriers, payoffs, settlement_times = self._extract_observations(
583
+ resolved, context.maturity, product.is_up_barrier
584
+ )
585
+
586
+ if product.is_up_barrier:
587
+ k_plus = np.array(barriers, dtype=float)
588
+ k_minus = np.zeros_like(k_plus)
589
+ else:
590
+ k_minus = np.array(barriers, dtype=float)
591
+ k_plus = np.full_like(k_minus, math.inf, dtype=float)
592
+
593
+ a_minus = np.zeros_like(k_minus, dtype=float)
594
+ b_minus = np.zeros_like(k_minus, dtype=float)
595
+ a_plus = np.zeros_like(k_plus, dtype=float)
596
+ b_plus = np.zeros_like(k_plus, dtype=float)
597
+
598
+ if product.rebate > 0.0:
599
+ rebate_values = self._discount_rebates(
600
+ payoffs,
601
+ obs_times,
602
+ settlement_times,
603
+ context.maturity,
604
+ context.rate,
605
+ product.payment_at_hit,
606
+ )
607
+ if product.is_up_barrier:
608
+ b_plus = rebate_values
609
+ else:
610
+ b_minus = rebate_values
611
+
612
+ return QuadCoreInputs(
613
+ observation_times=obs_times,
614
+ k_minus=k_minus,
615
+ k_plus=k_plus,
616
+ a_minus=a_minus,
617
+ b_minus=b_minus,
618
+ a_plus=a_plus,
619
+ b_plus=b_plus,
620
+ a_terminal=0.0,
621
+ b_terminal=0.0,
622
+ )
623
+
624
+ def finalize_price(
625
+ self,
626
+ product: OneTouchOption,
627
+ pricing_env: PricingEnvironment,
628
+ context: QuadPricingContext,
629
+ core_price: float,
630
+ engine: "DiscreteQuadEngine",
631
+ ) -> float:
632
+ if product.is_no_touch:
633
+ rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
634
+ value = rebate_discount - core_price
635
+ return max(0.0, value) * context.contract_multiplier
636
+ return core_price * context.contract_multiplier
637
+
638
+ def _validate_inputs(
639
+ self,
640
+ spot: float,
641
+ barrier: float,
642
+ maturity: float,
643
+ rate: float,
644
+ div: float,
645
+ vol: float,
646
+ rebate: float,
647
+ engine: "DiscreteQuadEngine",
648
+ ) -> None:
649
+ validate_positive(spot, "spot")
650
+ validate_positive(barrier, "barrier")
651
+ validate_positive(vol, "volatility")
652
+ validate_positive(maturity, "maturity", allow_zero=True)
653
+ validate_non_negative(rebate, "rebate")
654
+ validate_non_negative(div, "dividend_yield")
655
+
656
+ if abs(rate) > 1.0:
657
+ raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
658
+ if vol > engine.MAX_VOL:
659
+ raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
660
+
661
+ def _one_touch_instant_payoff(
662
+ self,
663
+ product: OneTouchOption,
664
+ spot: float,
665
+ maturity: float,
666
+ rate: float,
667
+ pay_at_hit: bool,
668
+ ) -> float:
669
+ touched = product.is_barrier_hit(spot)
670
+ discount = math.exp(-rate * maturity)
671
+ if product.is_one_touch:
672
+ if touched:
673
+ return product.rebate if pay_at_hit else product.rebate * discount
674
+ return 0.0
675
+ return product.rebate * discount if not touched else 0.0
676
+
677
+
678
+ class DoubleBarrierQuadInputAdapter(BaseDiscreteQuadAdapter):
679
+ """Adapter for double barrier options in discrete quadrature."""
680
+
681
+ def can_handle(self, product: BaseEquityProduct) -> bool:
682
+ return isinstance(product, DoubleBarrierOption)
683
+
684
+ def build_pricing_context(
685
+ self,
686
+ product: DoubleBarrierOption,
687
+ pricing_env: PricingEnvironment,
688
+ engine: "DiscreteQuadEngine",
689
+ ) -> QuadPricingContext:
690
+ spot = pricing_env.spot
691
+ maturity = product.get_maturity(pricing_env)
692
+ rate = pricing_env.get_rate(maturity)
693
+ div = pricing_env.get_div_yield(maturity)
694
+ vol = pricing_env.get_vol(product.strike, maturity)
695
+ contract_multiplier = product.contract_multiplier
696
+
697
+ self._validate_inputs(
698
+ spot,
699
+ product.strike,
700
+ maturity,
701
+ rate,
702
+ div,
703
+ vol,
704
+ product.lower_barrier,
705
+ product.upper_barrier,
706
+ engine,
707
+ )
708
+
709
+ return QuadPricingContext(
710
+ spot=spot,
711
+ maturity=maturity,
712
+ rate=rate,
713
+ div=div,
714
+ vol=vol,
715
+ contract_multiplier=contract_multiplier,
716
+ )
717
+
718
+ def early_price(
719
+ self,
720
+ product: DoubleBarrierOption,
721
+ pricing_env: PricingEnvironment,
722
+ context: QuadPricingContext,
723
+ engine: "DiscreteQuadEngine",
724
+ ) -> float | None:
725
+ if is_zero(context.maturity, tol=engine.MIN_MATURITY):
726
+ value = self._double_barrier_expired_price(
727
+ product, context.spot, context.rate, context.maturity
728
+ )
729
+ return value * context.contract_multiplier
730
+
731
+ if (
732
+ product.observation_type != ObservationType.EXPIRY
733
+ and product.is_barrier_hit(context.spot)
734
+ ):
735
+ if product.is_knock_out:
736
+ rebate_discount = math.exp(-context.rate * context.maturity)
737
+ return product.rebate * rebate_discount * context.contract_multiplier
738
+ vanilla_price = self._vanilla_price(product, pricing_env, engine)
739
+ return vanilla_price * context.contract_multiplier
740
+
741
+ if product.observation_type == ObservationType.CONTINUOUS:
742
+ raise PricingError(
743
+ "DiscreteQuadEngine supports discrete monitoring only. "
744
+ "Use a continuous barrier engine for continuous barriers."
745
+ )
746
+
747
+ return None
748
+
749
+ def resolve_schedule(
750
+ self,
751
+ product: DoubleBarrierOption,
752
+ pricing_env: PricingEnvironment,
753
+ context: QuadPricingContext,
754
+ ) -> Sequence[ResolvedObservationRecord]:
755
+ if product.observation_type == ObservationType.EXPIRY:
756
+ schedule = ObservationSchedule.from_legacy(
757
+ observation_dates=[context.maturity],
758
+ default_barrier=None,
759
+ default_payoff=product.rebate,
760
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
761
+ upper_barrier=product.upper_barrier,
762
+ lower_barrier=product.lower_barrier,
763
+ )
764
+ else:
765
+ schedule = product.observation_schedule
766
+ if schedule is None and product.observation_dates:
767
+ schedule = ObservationSchedule.from_legacy(
768
+ observation_dates=product.observation_dates,
769
+ default_barrier=None,
770
+ default_payoff=product.rebate,
771
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
772
+ upper_barrier=product.upper_barrier,
773
+ lower_barrier=product.lower_barrier,
774
+ )
775
+
776
+ if schedule is None or not schedule.records:
777
+ raise PricingError("Discrete monitoring requires ObservationSchedule.")
778
+ if schedule.aggregation_mode != ObservationAggregation.STOP_FIRST_HIT:
779
+ raise PricingError("DiscreteQuadEngine requires STOP_FIRST_HIT aggregation.")
780
+
781
+ return schedule.resolve(
782
+ pricing_env,
783
+ default_upper=product.upper_barrier,
784
+ default_lower=product.lower_barrier,
785
+ default_payoff=product.rebate,
786
+ require_double=True,
787
+ )
788
+
789
+ def build_inputs(
790
+ self,
791
+ product: DoubleBarrierOption,
792
+ resolved: Sequence[ResolvedObservationRecord],
793
+ context: QuadPricingContext,
794
+ ) -> QuadCoreInputs:
795
+ (
796
+ obs_times,
797
+ upper_barriers,
798
+ lower_barriers,
799
+ payoffs,
800
+ settlement_times,
801
+ ) = self._extract_double_observations(resolved, context.maturity)
802
+
803
+ k_plus = np.array(upper_barriers, dtype=float)
804
+ k_minus = np.array(lower_barriers, dtype=float)
805
+ a_minus = np.zeros_like(k_minus, dtype=float)
806
+ b_minus = np.zeros_like(k_minus, dtype=float)
807
+ a_plus = np.zeros_like(k_plus, dtype=float)
808
+ b_plus = np.zeros_like(k_plus, dtype=float)
809
+
810
+ if np.any(payoffs > 0.0):
811
+ pay_at_hit = self._pay_at_hit(settlement_times, context.maturity)
812
+ rebate_values = self._discount_rebates(
813
+ payoffs,
814
+ obs_times,
815
+ settlement_times,
816
+ context.maturity,
817
+ context.rate,
818
+ pay_at_hit,
819
+ )
820
+ b_minus = rebate_values
821
+ b_plus = rebate_values
822
+
823
+ a_terminal, b_terminal = self._terminal_payoff_coefficients(
824
+ product, k_minus, k_plus
825
+ )
826
+
827
+ return QuadCoreInputs(
828
+ observation_times=obs_times,
829
+ k_minus=k_minus,
830
+ k_plus=k_plus,
831
+ a_minus=a_minus,
832
+ b_minus=b_minus,
833
+ a_plus=a_plus,
834
+ b_plus=b_plus,
835
+ a_terminal=a_terminal,
836
+ b_terminal=b_terminal,
837
+ )
838
+
839
+ def finalize_price(
840
+ self,
841
+ product: DoubleBarrierOption,
842
+ pricing_env: PricingEnvironment,
843
+ context: QuadPricingContext,
844
+ core_price: float,
845
+ engine: "DiscreteQuadEngine",
846
+ ) -> float:
847
+ if product.is_knock_in:
848
+ vanilla_price = self._vanilla_price(product, pricing_env, engine)
849
+ rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
850
+ value = vanilla_price + rebate_discount - core_price
851
+ return value * context.contract_multiplier
852
+ return core_price * context.contract_multiplier
853
+
854
+ def _extract_double_observations(
855
+ self, resolved: Sequence[ResolvedObservationRecord], maturity: float
856
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
857
+ obs_times = np.array([rec.observation_time for rec in resolved], dtype=float)
858
+ upper = np.array([rec.upper_barrier for rec in resolved], dtype=float)
859
+ lower = np.array([rec.lower_barrier for rec in resolved], dtype=float)
860
+ payoffs = np.array([rec.payoff for rec in resolved], dtype=float)
861
+ settlement_times = np.array(
862
+ [
863
+ rec.settlement_time if rec.settlement_time is not None else rec.observation_time
864
+ for rec in resolved
865
+ ],
866
+ dtype=float,
867
+ )
868
+
869
+ if obs_times.size == 0:
870
+ raise ValidationError("Resolved observation schedule is empty.")
871
+
872
+ if maturity - obs_times[-1] > Tolerance.ZERO:
873
+ obs_times = np.append(obs_times, maturity)
874
+ upper = np.append(upper, math.inf)
875
+ lower = np.append(lower, 0.0)
876
+ payoffs = np.append(payoffs, 0.0)
877
+ settlement_times = np.append(settlement_times, maturity)
878
+
879
+ return obs_times, upper, lower, payoffs, settlement_times
880
+
881
+ def _terminal_payoff_coefficients(
882
+ self,
883
+ product: DoubleBarrierOption,
884
+ k_minus: np.ndarray,
885
+ k_plus: np.ndarray,
886
+ ) -> tuple[float, float]:
887
+ strike = product.strike
888
+ lower = float(k_minus[-1])
889
+ upper = float(k_plus[-1])
890
+
891
+ if product.is_call():
892
+ if strike <= lower:
893
+ return 1.0, -strike
894
+ if strike >= upper:
895
+ return 0.0, 0.0
896
+ k_minus[-1] = strike
897
+ return 1.0, -strike
898
+
899
+ if strike >= upper:
900
+ return -1.0, strike
901
+ if strike <= lower:
902
+ return 0.0, 0.0
903
+ k_plus[-1] = strike
904
+ return -1.0, strike
905
+
906
+ def _double_barrier_expired_price(
907
+ self,
908
+ product: DoubleBarrierOption,
909
+ spot: float,
910
+ rate: float,
911
+ maturity: float,
912
+ ) -> float:
913
+ hit = product.is_barrier_hit(spot)
914
+ if product.is_call():
915
+ intrinsic = max(spot - product.strike, 0.0)
916
+ else:
917
+ intrinsic = max(product.strike - spot, 0.0)
918
+ if product.is_knock_out:
919
+ value = product.rebate if hit else intrinsic
920
+ else:
921
+ value = intrinsic if hit else product.rebate
922
+ if maturity <= 0.0:
923
+ return value
924
+ return value * math.exp(-rate * maturity)
925
+
926
+ def _pay_at_hit(self, settlement_times: np.ndarray, maturity: float) -> bool:
927
+ if settlement_times.size == 0:
928
+ return False
929
+ return not np.all(
930
+ [is_close(t, maturity, abs_tol=Tolerance.PRECISION) for t in settlement_times]
931
+ )
932
+
933
+ def _vanilla_price(
934
+ self,
935
+ product: DoubleBarrierOption,
936
+ pricing_env: PricingEnvironment,
937
+ engine: "DiscreteQuadEngine",
938
+ ) -> float:
939
+ vanilla = EuropeanVanillaOption(
940
+ strike=product.strike,
941
+ option_type=product.option_type,
942
+ maturity=product.maturity,
943
+ exercise_date=product.exercise_date,
944
+ settlement_date=product.settlement_date,
945
+ contract_multiplier=1.0,
946
+ )
947
+ return engine.vanilla_engine.price(vanilla, pricing_env)
948
+
949
+ def _validate_inputs(
950
+ self,
951
+ spot: float,
952
+ strike: float,
953
+ maturity: float,
954
+ rate: float,
955
+ div: float,
956
+ vol: float,
957
+ lower: float,
958
+ upper: float,
959
+ engine: "DiscreteQuadEngine",
960
+ ) -> None:
961
+ validate_positive(spot, "spot")
962
+ validate_positive(strike, "strike")
963
+ validate_positive(vol, "volatility")
964
+ validate_positive(maturity, "maturity", allow_zero=True)
965
+ validate_positive(lower, "lower_barrier")
966
+ validate_positive(upper, "upper_barrier")
967
+ validate_non_negative(div, "dividend_yield")
968
+
969
+ if lower >= upper:
970
+ raise ValidationError(
971
+ f"lower_barrier ({lower}) must be less than upper_barrier ({upper})"
972
+ )
973
+ if abs(rate) > 1.0:
974
+ raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
975
+ if vol > engine.MAX_VOL:
976
+ raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
977
+
978
+
979
+ class DoubleOneTouchQuadInputAdapter(BaseDiscreteQuadAdapter):
980
+ """Adapter for double one-touch/no-touch options in discrete quadrature."""
981
+
982
+ def can_handle(self, product: BaseEquityProduct) -> bool:
983
+ return isinstance(product, DoubleOneTouchOption)
984
+
985
+ def build_pricing_context(
986
+ self,
987
+ product: DoubleOneTouchOption,
988
+ pricing_env: PricingEnvironment,
989
+ engine: "DiscreteQuadEngine",
990
+ ) -> QuadPricingContext:
991
+ spot = pricing_env.spot
992
+ maturity = product.get_maturity(pricing_env)
993
+ rate = pricing_env.get_rate(maturity)
994
+ div = pricing_env.get_div_yield(maturity)
995
+ barrier_ref = math.sqrt(product.upper_barrier * product.lower_barrier)
996
+ vol = pricing_env.get_vol(barrier_ref, maturity)
997
+ contract_multiplier = getattr(product, "contract_multiplier", 1.0)
998
+
999
+ self._validate_inputs(
1000
+ spot,
1001
+ product.lower_barrier,
1002
+ product.upper_barrier,
1003
+ maturity,
1004
+ rate,
1005
+ div,
1006
+ vol,
1007
+ product.rebate,
1008
+ engine,
1009
+ )
1010
+
1011
+ return QuadPricingContext(
1012
+ spot=spot,
1013
+ maturity=maturity,
1014
+ rate=rate,
1015
+ div=div,
1016
+ vol=vol,
1017
+ contract_multiplier=contract_multiplier,
1018
+ )
1019
+
1020
+ def early_price(
1021
+ self,
1022
+ product: DoubleOneTouchOption,
1023
+ pricing_env: PricingEnvironment,
1024
+ context: QuadPricingContext,
1025
+ engine: "DiscreteQuadEngine",
1026
+ ) -> float | None:
1027
+ if is_zero(context.maturity, tol=engine.MIN_MATURITY):
1028
+ value = self._double_touch_instant_payoff(
1029
+ product, context.spot, context.maturity, context.rate
1030
+ )
1031
+ return value * context.contract_multiplier
1032
+
1033
+ touched = (
1034
+ context.spot >= product.upper_barrier
1035
+ or context.spot <= product.lower_barrier
1036
+ )
1037
+ if product.observation_type != ObservationType.EXPIRY and touched:
1038
+ if product.touch_type == TouchType.DOUBLE_ONE_TOUCH:
1039
+ pay_at_hit = product.payment_at_hit
1040
+ value = product.rebate if pay_at_hit else product.rebate * math.exp(
1041
+ -context.rate * context.maturity
1042
+ )
1043
+ return value * context.contract_multiplier
1044
+ return 0.0
1045
+
1046
+ if product.observation_type == ObservationType.CONTINUOUS:
1047
+ raise PricingError(
1048
+ "DiscreteQuadEngine supports discrete monitoring only. "
1049
+ "Use a continuous engine for continuous barriers."
1050
+ )
1051
+
1052
+ if product.rebate <= 0.0:
1053
+ return 0.0
1054
+
1055
+ return None
1056
+
1057
+ def resolve_schedule(
1058
+ self,
1059
+ product: DoubleOneTouchOption,
1060
+ pricing_env: PricingEnvironment,
1061
+ context: QuadPricingContext,
1062
+ ) -> Sequence[ResolvedObservationRecord]:
1063
+ if product.observation_type == ObservationType.EXPIRY:
1064
+ schedule = ObservationSchedule.from_legacy(
1065
+ observation_dates=[context.maturity],
1066
+ default_barrier=None,
1067
+ default_payoff=product.rebate,
1068
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
1069
+ upper_barrier=product.upper_barrier,
1070
+ lower_barrier=product.lower_barrier,
1071
+ )
1072
+ else:
1073
+ schedule = product.observation_schedule
1074
+ if schedule is None and product.observation_dates:
1075
+ schedule = ObservationSchedule.from_legacy(
1076
+ observation_dates=product.observation_dates,
1077
+ default_barrier=None,
1078
+ default_payoff=product.rebate,
1079
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
1080
+ upper_barrier=product.upper_barrier,
1081
+ lower_barrier=product.lower_barrier,
1082
+ )
1083
+
1084
+ if schedule is None or not schedule.records:
1085
+ raise PricingError("Discrete monitoring requires ObservationSchedule.")
1086
+ if schedule.aggregation_mode != ObservationAggregation.STOP_FIRST_HIT:
1087
+ raise PricingError("DiscreteQuadEngine requires STOP_FIRST_HIT aggregation.")
1088
+
1089
+ return schedule.resolve(
1090
+ pricing_env,
1091
+ default_upper=product.upper_barrier,
1092
+ default_lower=product.lower_barrier,
1093
+ default_payoff=product.rebate,
1094
+ require_double=True,
1095
+ )
1096
+
1097
+ def build_inputs(
1098
+ self,
1099
+ product: DoubleOneTouchOption,
1100
+ resolved: Sequence[ResolvedObservationRecord],
1101
+ context: QuadPricingContext,
1102
+ ) -> QuadCoreInputs:
1103
+ (
1104
+ obs_times,
1105
+ upper_barriers,
1106
+ lower_barriers,
1107
+ payoffs,
1108
+ settlement_times,
1109
+ ) = self._extract_double_observations(resolved, context.maturity)
1110
+
1111
+ k_plus = np.array(upper_barriers, dtype=float)
1112
+ k_minus = np.array(lower_barriers, dtype=float)
1113
+ a_minus = np.zeros_like(k_minus, dtype=float)
1114
+ b_minus = np.zeros_like(k_minus, dtype=float)
1115
+ a_plus = np.zeros_like(k_plus, dtype=float)
1116
+ b_plus = np.zeros_like(k_plus, dtype=float)
1117
+
1118
+ if np.any(payoffs > 0.0):
1119
+ rebate_values = self._discount_rebates(
1120
+ payoffs,
1121
+ obs_times,
1122
+ settlement_times,
1123
+ context.maturity,
1124
+ context.rate,
1125
+ product.payment_at_hit,
1126
+ )
1127
+ b_minus = rebate_values
1128
+ b_plus = rebate_values
1129
+
1130
+ return QuadCoreInputs(
1131
+ observation_times=obs_times,
1132
+ k_minus=k_minus,
1133
+ k_plus=k_plus,
1134
+ a_minus=a_minus,
1135
+ b_minus=b_minus,
1136
+ a_plus=a_plus,
1137
+ b_plus=b_plus,
1138
+ a_terminal=0.0,
1139
+ b_terminal=0.0,
1140
+ )
1141
+
1142
+ def finalize_price(
1143
+ self,
1144
+ product: DoubleOneTouchOption,
1145
+ pricing_env: PricingEnvironment,
1146
+ context: QuadPricingContext,
1147
+ core_price: float,
1148
+ engine: "DiscreteQuadEngine",
1149
+ ) -> float:
1150
+ if product.touch_type == TouchType.DOUBLE_NO_TOUCH:
1151
+ rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
1152
+ value = rebate_discount - core_price
1153
+ return max(0.0, value) * context.contract_multiplier
1154
+ return core_price * context.contract_multiplier
1155
+
1156
+ def _extract_double_observations(
1157
+ self, resolved: Sequence[ResolvedObservationRecord], maturity: float
1158
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
1159
+ obs_times = np.array([rec.observation_time for rec in resolved], dtype=float)
1160
+ upper = np.array([rec.upper_barrier for rec in resolved], dtype=float)
1161
+ lower = np.array([rec.lower_barrier for rec in resolved], dtype=float)
1162
+ payoffs = np.array([rec.payoff for rec in resolved], dtype=float)
1163
+ settlement_times = np.array(
1164
+ [
1165
+ rec.settlement_time if rec.settlement_time is not None else rec.observation_time
1166
+ for rec in resolved
1167
+ ],
1168
+ dtype=float,
1169
+ )
1170
+
1171
+ if obs_times.size == 0:
1172
+ raise ValidationError("Resolved observation schedule is empty.")
1173
+
1174
+ if maturity - obs_times[-1] > Tolerance.ZERO:
1175
+ obs_times = np.append(obs_times, maturity)
1176
+ upper = np.append(upper, math.inf)
1177
+ lower = np.append(lower, 0.0)
1178
+ payoffs = np.append(payoffs, 0.0)
1179
+ settlement_times = np.append(settlement_times, maturity)
1180
+
1181
+ return obs_times, upper, lower, payoffs, settlement_times
1182
+
1183
+ def _double_touch_instant_payoff(
1184
+ self,
1185
+ product: DoubleOneTouchOption,
1186
+ spot: float,
1187
+ maturity: float,
1188
+ rate: float,
1189
+ ) -> float:
1190
+ touched = spot >= product.upper_barrier or spot <= product.lower_barrier
1191
+ discount = math.exp(-rate * maturity)
1192
+ if product.touch_type == TouchType.DOUBLE_ONE_TOUCH:
1193
+ if touched:
1194
+ return product.rebate if product.payment_at_hit else product.rebate * discount
1195
+ return 0.0
1196
+ return product.rebate * discount if not touched else 0.0
1197
+
1198
+ def _validate_inputs(
1199
+ self,
1200
+ spot: float,
1201
+ lower: float,
1202
+ upper: float,
1203
+ maturity: float,
1204
+ rate: float,
1205
+ div: float,
1206
+ vol: float,
1207
+ rebate: float,
1208
+ engine: "DiscreteQuadEngine",
1209
+ ) -> None:
1210
+ validate_positive(spot, "spot")
1211
+ validate_positive(lower, "lower_barrier")
1212
+ validate_positive(upper, "upper_barrier")
1213
+ validate_positive(vol, "volatility")
1214
+ validate_positive(maturity, "maturity", allow_zero=True)
1215
+ validate_non_negative(rebate, "rebate")
1216
+ validate_non_negative(div, "dividend_yield")
1217
+
1218
+ if lower >= upper:
1219
+ raise ValidationError(
1220
+ f"lower_barrier ({lower}) must be less than upper_barrier ({upper})"
1221
+ )
1222
+ if abs(rate) > 1.0:
1223
+ raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
1224
+ if vol > engine.MAX_VOL:
1225
+ raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
1226
+
1227
+
1228
+ class QuadInputAdapterRegistry:
1229
+ """Registry for quad input adapters."""
1230
+
1231
+ def __init__(self) -> None:
1232
+ self._adapters: list[QuadInputAdapter] = []
1233
+
1234
+ def register(self, adapter: QuadInputAdapter) -> None:
1235
+ self._adapters.append(adapter)
1236
+
1237
+ def resolve(self, product: BaseEquityProduct) -> QuadInputAdapter:
1238
+ for adapter in self._adapters:
1239
+ if adapter.can_handle(product):
1240
+ return adapter
1241
+ raise PricingError(
1242
+ f"DiscreteQuadEngine has no adapter for product type {type(product).__name__}"
1243
+ )
1244
+
1245
+
1246
+ _ADAPTER_REGISTRY = QuadInputAdapterRegistry()
1247
+ _ADAPTER_REGISTRY.register(BarrierQuadInputAdapter())
1248
+ _ADAPTER_REGISTRY.register(OneTouchQuadInputAdapter())
1249
+ _ADAPTER_REGISTRY.register(DoubleBarrierQuadInputAdapter())
1250
+ _ADAPTER_REGISTRY.register(DoubleOneTouchQuadInputAdapter())
1251
+
1252
+
1253
+ def register_quad_adapter(adapter: QuadInputAdapter) -> None:
1254
+ """Register a custom quad input adapter."""
1255
+ _ADAPTER_REGISTRY.register(adapter)
1256
+
1257
+
1258
+ def resolve_quad_adapter(product: BaseEquityProduct) -> QuadInputAdapter:
1259
+ """Resolve the quad input adapter for a product."""
1260
+ return _ADAPTER_REGISTRY.resolve(product)