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,302 @@
1
+ """
2
+ Analytical pricing engine for one-touch and no-touch options.
3
+ """
4
+
5
+ import math
6
+ from typing import Optional
7
+
8
+ from scipy import stats
9
+
10
+ from quantark.asset.equity.engine.base_engine import BaseEngine
11
+ from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
12
+ from quantark.asset.equity.product.option import CashOrNothingDigitalOption, OneTouchOption
13
+ from quantark.asset.equity.param import EngineParams
14
+ from quantark.priceenv import PricingEnvironment
15
+ from quantark.util.barrier_shift import apply_barrier_shift
16
+ from quantark.util.enum import ObservationType, OptionType, TouchType
17
+ from quantark.util.enum.engine_enums import EngineType
18
+ from quantark.util.exceptions import PricingError, ValidationError
19
+
20
+ from .digital_option_engine import DigitalOptionAnalyticalEngine
21
+
22
+
23
+ class OneTouchAnalyticalEngine(BaseEngine):
24
+ """
25
+ Closed-form pricing engine for one-touch and no-touch options.
26
+
27
+ Supports:
28
+ - Continuous monitoring
29
+ - Discrete monitoring via Broadie-Glasserman-Kou barrier shift (regular grids)
30
+ - Expiry-only monitoring via digital-option fallback
31
+
32
+ Note:
33
+ No tenor/365 scaling is applied at the engine level.
34
+ """
35
+
36
+ engine_type = EngineType.ANALYTICAL
37
+
38
+ MIN_VOL = 0.001
39
+ MAX_VOL = 5.0
40
+ MIN_MATURITY = 1e-10
41
+ MAX_MATURITY = 50.0
42
+
43
+ def __init__(self, params: Optional[EngineParams] = None):
44
+ super().__init__(params)
45
+ self._digital_engine = DigitalOptionAnalyticalEngine(params)
46
+
47
+ def price(
48
+ self, product: BaseEquityProduct, pricing_env: PricingEnvironment
49
+ ) -> float:
50
+ if not isinstance(product, OneTouchOption):
51
+ raise PricingError(
52
+ f"OneTouchAnalyticalEngine only supports OneTouchOption, "
53
+ f"got {type(product).__name__}"
54
+ )
55
+
56
+ spot = pricing_env.spot
57
+ maturity = product.get_maturity(pricing_env)
58
+ rate = pricing_env.get_rate(maturity)
59
+ div = pricing_env.get_div_yield(maturity)
60
+ vol = pricing_env.get_vol(product.barrier, maturity)
61
+ rebate = product.rebate
62
+
63
+ pay_at_hit = product.payment_at_hit if product.is_one_touch else False
64
+
65
+ self._validate_inputs(spot, product.barrier, maturity, vol, rebate)
66
+
67
+ # Immediate handling for near-expiry or already-hit barriers
68
+ if maturity < self.MIN_MATURITY:
69
+ return self._instantaneous_payoff(
70
+ product=product,
71
+ spot=spot,
72
+ maturity=maturity,
73
+ rate=rate,
74
+ pay_at_hit=pay_at_hit,
75
+ )
76
+
77
+ obs_type = product.observation_type
78
+
79
+ if obs_type != ObservationType.EXPIRY and product.is_barrier_hit(spot):
80
+ if product.is_one_touch:
81
+ return rebate if pay_at_hit else rebate * math.exp(-rate * maturity)
82
+ return 0.0
83
+
84
+ if obs_type == ObservationType.EXPIRY:
85
+ return self._price_expiry(product, pricing_env)
86
+
87
+ if obs_type == ObservationType.DISCRETE:
88
+ schedule = product.observation_schedule
89
+ if schedule is None or not schedule.records:
90
+ raise PricingError(
91
+ "Discrete monitoring requires a populated ObservationSchedule."
92
+ )
93
+ schedule.assert_analytical_ready(default_payoff=rebate)
94
+ frequency = schedule.ensure_regular_frequency(schedule.times)
95
+ barrier = apply_barrier_shift(
96
+ barrier=product.barrier,
97
+ is_up_barrier=product.is_up_barrier,
98
+ volatility=vol,
99
+ observation_interval=frequency,
100
+ )
101
+ elif obs_type == ObservationType.CONTINUOUS:
102
+ barrier = product.barrier
103
+ else:
104
+ raise PricingError(f"Unsupported observation type: {obs_type}")
105
+
106
+ if product.is_one_touch:
107
+ return self._one_touch_price(
108
+ spot=spot,
109
+ barrier=barrier,
110
+ maturity=maturity,
111
+ rate=rate,
112
+ div=div,
113
+ vol=vol,
114
+ rebate=rebate,
115
+ pay_at_hit=pay_at_hit,
116
+ is_up=product.is_up_barrier,
117
+ )
118
+
119
+ # No-touch: pay only at expiry if not hit
120
+ prob_touch = self._touch_probability(
121
+ spot=spot,
122
+ barrier=barrier,
123
+ maturity=maturity,
124
+ rate=rate,
125
+ div=div,
126
+ vol=vol,
127
+ is_up=product.is_up_barrier,
128
+ )
129
+ prob_touch = min(max(prob_touch, 0.0), 1.0)
130
+ discount = math.exp(-rate * maturity)
131
+ return rebate * discount * max(0.0, 1.0 - prob_touch)
132
+
133
+ def _price_expiry(
134
+ self, product: OneTouchOption, pricing_env: PricingEnvironment
135
+ ) -> float:
136
+ """Price with expiry-only monitoring using digital option fallback."""
137
+ option_type = self._digital_direction(product)
138
+ digital = CashOrNothingDigitalOption(
139
+ strike=product.barrier,
140
+ payout=product.rebate,
141
+ option_type=option_type,
142
+ maturity=product.maturity,
143
+ exercise_date=product.exercise_date,
144
+ settlement_date=product.settlement_date,
145
+ )
146
+ return self._digital_engine.price(digital, pricing_env)
147
+
148
+ def _digital_direction(self, product: OneTouchOption) -> OptionType:
149
+ """Map one-touch/no-touch direction to an equivalent digital payoff."""
150
+ if product.is_one_touch:
151
+ return OptionType.CALL if product.is_up_barrier else OptionType.PUT
152
+ # No-touch pays if terminal spot stays on the non-breach side
153
+ return OptionType.PUT if product.is_up_barrier else OptionType.CALL
154
+
155
+ def _one_touch_price(
156
+ self,
157
+ spot: float,
158
+ barrier: float,
159
+ maturity: float,
160
+ rate: float,
161
+ div: float,
162
+ vol: float,
163
+ rebate: float,
164
+ pay_at_hit: bool,
165
+ is_up: bool,
166
+ ) -> float:
167
+ """Closed-form one-touch price for continuous or shifted discrete barriers."""
168
+ if pay_at_hit:
169
+ return rebate * self._instant_touch_term(
170
+ spot=spot,
171
+ barrier=barrier,
172
+ maturity=maturity,
173
+ rate=rate,
174
+ div=div,
175
+ vol=vol,
176
+ is_up=is_up,
177
+ )
178
+
179
+ return rebate * math.exp(-rate * maturity) * self._expiry_touch_term(
180
+ spot=spot,
181
+ barrier=barrier,
182
+ maturity=maturity,
183
+ rate=rate,
184
+ div=div,
185
+ vol=vol,
186
+ is_up=is_up,
187
+ )
188
+
189
+ def _touch_probability(
190
+ self,
191
+ spot: float,
192
+ barrier: float,
193
+ maturity: float,
194
+ rate: float,
195
+ div: float,
196
+ vol: float,
197
+ is_up: bool,
198
+ ) -> float:
199
+ """Probability of touching the barrier before expiry (used for no-touch)."""
200
+ return self._expiry_touch_term(
201
+ spot=spot,
202
+ barrier=barrier,
203
+ maturity=maturity,
204
+ rate=rate,
205
+ div=div,
206
+ vol=vol,
207
+ is_up=is_up,
208
+ )
209
+
210
+ def _instant_touch_term(
211
+ self,
212
+ spot: float,
213
+ barrier: float,
214
+ maturity: float,
215
+ rate: float,
216
+ div: float,
217
+ vol: float,
218
+ is_up: bool,
219
+ ) -> float:
220
+ b = rate - div
221
+ mu = (b - 0.5 * vol * vol) / (vol * vol)
222
+ lam = math.sqrt(mu * mu + 2.0 * rate / (vol * vol))
223
+ sqrt_t = math.sqrt(maturity)
224
+ z = math.log(barrier / spot) / (vol * sqrt_t) + lam * vol * sqrt_t
225
+ eta = -1.0 if is_up else 1.0
226
+
227
+ term1 = math.pow(barrier / spot, mu + lam) * stats.norm.cdf(eta * z)
228
+ term2 = math.pow(barrier / spot, mu - lam) * stats.norm.cdf(
229
+ eta * z - 2 * eta * lam * vol * sqrt_t
230
+ )
231
+ return term1 + term2
232
+
233
+ def _expiry_touch_term(
234
+ self,
235
+ spot: float,
236
+ barrier: float,
237
+ maturity: float,
238
+ rate: float,
239
+ div: float,
240
+ vol: float,
241
+ is_up: bool,
242
+ ) -> float:
243
+ b = rate - div
244
+ mu = (b - 0.5 * vol * vol) / (vol * vol)
245
+ sqrt_t = math.sqrt(maturity)
246
+ log_s_b = math.log(spot / barrier)
247
+ x2 = log_s_b / (vol * sqrt_t) + (1 + mu) * vol * sqrt_t
248
+ y2 = -log_s_b / (vol * sqrt_t) + (1 + mu) * vol * sqrt_t
249
+ phi = 1.0 if is_up else -1.0
250
+ eta = -1.0 if is_up else 1.0
251
+ pow_term = math.pow(barrier / spot, 2 * mu)
252
+ return stats.norm.cdf(phi * x2 - phi * vol * sqrt_t) + pow_term * stats.norm.cdf(
253
+ eta * y2 - eta * vol * sqrt_t
254
+ )
255
+
256
+ def _instantaneous_payoff(
257
+ self,
258
+ product: OneTouchOption,
259
+ spot: float,
260
+ maturity: float,
261
+ rate: float,
262
+ pay_at_hit: bool,
263
+ ) -> float:
264
+ """Handle payoffs when maturity is effectively zero."""
265
+ touched = product.is_barrier_hit(spot)
266
+ discount = math.exp(-rate * maturity)
267
+ if product.is_one_touch:
268
+ if touched:
269
+ return product.rebate if pay_at_hit else product.rebate * discount
270
+ return 0.0
271
+ return product.rebate * discount if not touched else 0.0
272
+
273
+ def _validate_inputs(
274
+ self,
275
+ spot: float,
276
+ barrier: float,
277
+ maturity: float,
278
+ vol: float,
279
+ rebate: float,
280
+ ) -> None:
281
+ if spot <= 0:
282
+ raise ValidationError(f"Spot price must be positive, got {spot}")
283
+ if barrier <= 0:
284
+ raise ValidationError(f"Barrier must be positive, got {barrier}")
285
+ if maturity < 0:
286
+ raise ValidationError(f"Maturity must be non-negative, got {maturity}")
287
+ if vol <= 0:
288
+ raise ValidationError(f"Volatility must be positive, got {vol}")
289
+ if vol < self.MIN_VOL or vol > self.MAX_VOL:
290
+ raise ValidationError(
291
+ f"Volatility {vol} outside supported range [{self.MIN_VOL}, {self.MAX_VOL}]"
292
+ )
293
+ if maturity > self.MAX_MATURITY:
294
+ raise ValidationError(
295
+ f"Maturity too long for analytical one-touch pricing: {maturity}"
296
+ )
297
+ if rebate < 0:
298
+ raise ValidationError(f"Rebate must be non-negative, got {rebate}")
299
+
300
+ def __repr__(self):
301
+ return "OneTouchAnalyticalEngine()"
302
+
@@ -0,0 +1,396 @@
1
+ """
2
+ Analytical pricing engine for Range Accrual options via digital option decomposition.
3
+
4
+ Under GBM, a Range Accrual decomposes into a portfolio of digital options by
5
+ linearity of expectation. Each observation contributes independently:
6
+
7
+ E[accrual_ratio] = (1/W) * sum_i w_i * P(L_i <= S(t_i) <= U_i)
8
+
9
+ where P(L <= S(t) <= U) = N(d2_L) - N(d2_U) with standard BSM d2 terms.
10
+
11
+ The price is:
12
+ Price = exp(-r*T) * S_0 * M * c * tau * E[accrual_ratio]
13
+
14
+ Historical observations are incorporated deterministically.
15
+ """
16
+
17
+ import math
18
+ from dataclasses import dataclass
19
+ from typing import Dict, List, Optional, Tuple
20
+
21
+ import numpy as np
22
+ from scipy import stats
23
+
24
+ from quantark.asset.equity.engine.base_engine import BaseEngine
25
+ from quantark.asset.equity.param import EngineParams
26
+ from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
27
+ from quantark.asset.equity.product.option.range_accrual_option import RangeAccrualOption
28
+ from quantark.priceenv import PricingEnvironment
29
+ from quantark.util.enum.engine_enums import EngineType
30
+ from quantark.util.exceptions import PricingError, ValidationError
31
+ from quantark.util.numerical import is_zero, safe_log
32
+
33
+
34
+ @dataclass
35
+ class RangeAccrualAnalyticalResult:
36
+ """Result container for Range Accrual analytical pricing."""
37
+
38
+ price: float
39
+ expected_accrual_ratio: float
40
+ per_observation_probs: List[float] # P_i for each future observation
41
+ past_in_range_weights: float
42
+ future_expected_in_range_weights: float
43
+ total_weights: float
44
+ num_past_observations: int
45
+ num_future_observations: int
46
+
47
+
48
+ class RangeAccrualAnalyticalEngine(BaseEngine):
49
+ """
50
+ Closed-form pricing for Range Accrual options via digital decomposition.
51
+
52
+ Decomposes the Range Accrual payoff into a sum of digital option
53
+ probabilities under Black-Scholes-Merton. Each observation at time t_i
54
+ with barriers [L_i, U_i] contributes:
55
+
56
+ P_i = N(d2_L) - N(d2_U) (standard mode)
57
+ P_i = 1 - [N(d2_L) - N(d2_U)] (reverse mode)
58
+
59
+ where d2(K, t) = [ln(S/K) + (r - q - sigma^2/2)*t] / (sigma*sqrt(t))
60
+
61
+ Supports:
62
+ - Weighted observations (e.g., Friday=3 for weekend carry)
63
+ - Historical observations with known in-range outcomes
64
+ - Time-varying barriers (per-observation or scalar)
65
+ - Reverse mode (pay when outside range)
66
+ - Annualized or non-annualized accrual rates
67
+ - Per-observation vol term structure
68
+ """
69
+
70
+ engine_type = EngineType.ANALYTICAL
71
+
72
+ MIN_VOL = 0.001
73
+ MAX_VOL = 5.0
74
+ MIN_MATURITY = 1e-10
75
+ MAX_OBS_TIME = 30.0
76
+
77
+ def __init__(self, params: Optional[EngineParams] = None):
78
+ super().__init__(params)
79
+ self._last_result: Optional[RangeAccrualAnalyticalResult] = None
80
+
81
+ def price(
82
+ self, product: BaseEquityProduct, pricing_env: PricingEnvironment
83
+ ) -> float:
84
+ """
85
+ Price a Range Accrual option analytically.
86
+
87
+ Args:
88
+ product: RangeAccrualOption to price
89
+ pricing_env: Pricing environment with market data
90
+
91
+ Returns:
92
+ Option price
93
+
94
+ Raises:
95
+ PricingError: If product type is unsupported
96
+ ValidationError: If input parameters are invalid
97
+ NumericalError: If numerical computation fails
98
+ """
99
+ if not isinstance(product, RangeAccrualOption):
100
+ raise PricingError(
101
+ f"RangeAccrualAnalyticalEngine only supports RangeAccrualOption, "
102
+ f"got {type(product).__name__}"
103
+ )
104
+
105
+ if product.range_config is None:
106
+ raise ValidationError("range_config is required for Range Accrual option")
107
+
108
+ S = pricing_env.spot
109
+ T = product.get_maturity(pricing_env)
110
+ r = pricing_env.get_rate(T)
111
+ q = pricing_env.get_div_yield(T)
112
+
113
+ self._validate_inputs(S, T, r, q)
114
+
115
+ # Handle near-expiry: all observations are effectively past
116
+ if is_zero(T):
117
+ past_in_range, _ = product.get_past_accrual(pricing_env)
118
+ total_weights = product.get_total_weights()
119
+ return product.get_payoff(
120
+ S,
121
+ in_range_weights=past_in_range,
122
+ total_weights=total_weights,
123
+ pricing_env=pricing_env,
124
+ )
125
+
126
+ # Resolve past vs future observations
127
+ past_obs, future_obs, total_weights = product.resolve_observations(pricing_env)
128
+
129
+ # Accumulate deterministic past contribution
130
+ past_in_range_weights = sum(w for w, in_range in past_obs if in_range)
131
+
132
+ # Compute expected future in-range weights analytically
133
+ future_expected, per_obs_probs = self._compute_future_expected_weights(
134
+ product, pricing_env, S, r, q, future_obs
135
+ )
136
+
137
+ # Expected accrual ratio
138
+ total_in_range = past_in_range_weights + future_expected
139
+ if is_zero(total_weights):
140
+ expected_ratio = 0.0
141
+ else:
142
+ expected_ratio = total_in_range / total_weights
143
+
144
+ # Compute price
145
+ year_fraction = product.get_year_fraction(pricing_env)
146
+ accrual_rate = product.range_config.accrual_rate
147
+ discount = math.exp(-r * T)
148
+
149
+ price = (
150
+ discount
151
+ * product.initial_price
152
+ * product.contract_multiplier
153
+ * accrual_rate
154
+ * expected_ratio
155
+ * year_fraction
156
+ )
157
+
158
+ self._last_result = RangeAccrualAnalyticalResult(
159
+ price=price,
160
+ expected_accrual_ratio=expected_ratio,
161
+ per_observation_probs=per_obs_probs,
162
+ past_in_range_weights=past_in_range_weights,
163
+ future_expected_in_range_weights=future_expected,
164
+ total_weights=total_weights,
165
+ num_past_observations=len(past_obs),
166
+ num_future_observations=len(future_obs),
167
+ )
168
+
169
+ return price
170
+
171
+ def _compute_future_expected_weights(
172
+ self,
173
+ product: RangeAccrualOption,
174
+ pricing_env: PricingEnvironment,
175
+ S: float,
176
+ r: float,
177
+ q: float,
178
+ future_obs: List[Tuple[float, float, int]],
179
+ ) -> Tuple[float, List[float]]:
180
+ """
181
+ Compute expected in-range weights for future observations.
182
+
183
+ For each future observation, computes:
184
+ P_i = N(d2_L) - N(d2_U) (or 1 - that for reverse mode)
185
+
186
+ Uses vectorized NumPy for efficiency.
187
+
188
+ Args:
189
+ product: Range Accrual option
190
+ pricing_env: Pricing environment
191
+ S: Current spot price
192
+ r: Risk-free rate
193
+ q: Dividend yield
194
+ future_obs: List of (weight, time, obs_idx) for future observations
195
+
196
+ Returns:
197
+ Tuple of (total_expected_in_range_weight, per_obs_probabilities)
198
+ """
199
+ if len(future_obs) == 0:
200
+ return 0.0, []
201
+
202
+ config = product.range_config
203
+ assert config is not None # Validated by caller
204
+
205
+ n = len(future_obs)
206
+ weights = np.empty(n)
207
+ times = np.empty(n)
208
+ lowers = np.empty(n)
209
+ uppers = np.empty(n)
210
+ sigmas = np.empty(n)
211
+
212
+ for i, (w, t, obs_idx) in enumerate(future_obs):
213
+ weights[i] = w
214
+ times[i] = t
215
+ lowers[i] = config.get_lower_barrier(obs_idx)
216
+ uppers[i] = config.get_upper_barrier(obs_idx)
217
+ sigmas[i] = pricing_env.get_vol(product.initial_price, t)
218
+
219
+ # Handle observations with near-zero time or near-zero vol deterministically
220
+ probs = np.empty(n)
221
+ degenerate = (times < self.MIN_MATURITY) | (sigmas < self.MIN_VOL)
222
+
223
+ if np.any(degenerate):
224
+ deg_mask = degenerate
225
+ # For near-zero time: check current spot
226
+ # For near-zero vol: check forward spot
227
+ fwd = np.where(
228
+ times[deg_mask] < self.MIN_MATURITY,
229
+ S,
230
+ S * np.exp((r - q) * times[deg_mask]),
231
+ )
232
+ in_range = (fwd >= lowers[deg_mask]) & (fwd <= uppers[deg_mask])
233
+ probs[deg_mask] = in_range.astype(float)
234
+
235
+ # Process non-degenerate observations vectorized
236
+ normal = ~degenerate
237
+ if np.any(normal):
238
+ t_n = times[normal]
239
+ sig_n = sigmas[normal]
240
+ l_n = lowers[normal]
241
+ u_n = uppers[normal]
242
+
243
+ sqrt_t = np.sqrt(t_n)
244
+ drift = (r - q - 0.5 * sig_n * sig_n) * t_n
245
+ denom = sig_n * sqrt_t
246
+
247
+ # d2 for lower barrier: P(S(t) >= L)
248
+ d2_L = (np.log(S / l_n) + drift) / denom
249
+ # d2 for upper barrier: P(S(t) >= U)
250
+ d2_U = (np.log(S / u_n) + drift) / denom
251
+
252
+ probs[normal] = stats.norm.cdf(d2_L) - stats.norm.cdf(d2_U)
253
+
254
+ # Apply reverse mode
255
+ if config.is_reverse:
256
+ probs = 1.0 - probs
257
+
258
+ # Clamp probabilities to [0, 1] for numerical safety
259
+ np.clip(probs, 0.0, 1.0, out=probs)
260
+
261
+ # Expected in-range weight = sum(w_i * P_i)
262
+ expected_weight = float(np.dot(weights, probs))
263
+ per_obs_probs = probs.tolist()
264
+
265
+ return expected_weight, per_obs_probs
266
+
267
+ def _validate_inputs(
268
+ self, S: float, T: float, r: float, q: float
269
+ ) -> None:
270
+ """Validate pricing inputs."""
271
+ if S <= 0:
272
+ raise ValidationError(f"Spot price must be positive, got {S}")
273
+ if T < 0:
274
+ raise ValidationError(f"Time to maturity must be non-negative, got {T}")
275
+ if q < 0:
276
+ raise ValidationError(f"Dividend yield must be non-negative, got {q}")
277
+ if abs(r) > 1.0:
278
+ raise ValidationError(
279
+ f"Risk-free rate outside reasonable bounds: {r}"
280
+ )
281
+
282
+ def get_last_result(self) -> Optional[RangeAccrualAnalyticalResult]:
283
+ """Get the full result from the last pricing run."""
284
+ return self._last_result
285
+
286
+ def calculate_greeks(
287
+ self, product: BaseEquityProduct, pricing_env: PricingEnvironment
288
+ ) -> Dict[str, float]:
289
+ """
290
+ Calculate Greeks using analytical formulas where possible.
291
+
292
+ Delta and gamma are computed analytically from the derivative of N(d2).
293
+ Falls back to base class bump-and-reprice for vega, theta, rho.
294
+
295
+ Args:
296
+ product: Range Accrual option
297
+ pricing_env: Pricing environment
298
+
299
+ Returns:
300
+ Dictionary of Greeks: price, delta, gamma
301
+ """
302
+ if not isinstance(product, RangeAccrualOption):
303
+ raise PricingError(
304
+ f"RangeAccrualAnalyticalEngine only supports RangeAccrualOption, "
305
+ f"got {type(product).__name__}"
306
+ )
307
+
308
+ config = product.range_config
309
+ if config is None:
310
+ raise ValidationError("range_config is required")
311
+
312
+ S = pricing_env.spot
313
+ T = product.get_maturity(pricing_env)
314
+ r = pricing_env.get_rate(T)
315
+ q = pricing_env.get_div_yield(T)
316
+
317
+ base_price = self.price(product, pricing_env)
318
+ greeks: Dict[str, float] = {"price": base_price}
319
+
320
+ # Near-expiry: delta/gamma are zero (payoff is flat w.r.t. spot)
321
+ if is_zero(T):
322
+ greeks["delta"] = 0.0
323
+ greeks["gamma"] = 0.0
324
+ return greeks
325
+
326
+ _, future_obs, total_weights = product.resolve_observations(pricing_env)
327
+
328
+ if len(future_obs) == 0 or is_zero(total_weights):
329
+ greeks["delta"] = 0.0
330
+ greeks["gamma"] = 0.0
331
+ return greeks
332
+
333
+ # Analytical delta: dPrice/dS
334
+ # dP_i/dS = [n(d2_L) - n(d2_U)] / (S * sigma_i * sqrt(t_i))
335
+ # where n(.) is the standard normal PDF
336
+ delta_sum = 0.0
337
+ gamma_sum = 0.0
338
+
339
+ for w, t, obs_idx in future_obs:
340
+ if t < self.MIN_MATURITY:
341
+ continue
342
+
343
+ sigma = pricing_env.get_vol(product.initial_price, t)
344
+ if sigma < self.MIN_VOL:
345
+ continue
346
+
347
+ lower = config.get_lower_barrier(obs_idx)
348
+ upper = config.get_upper_barrier(obs_idx)
349
+
350
+ sqrt_t = math.sqrt(t)
351
+ sig_sqrt_t = sigma * sqrt_t
352
+ drift = (r - q - 0.5 * sigma * sigma) * t
353
+
354
+ d2_L = (safe_log(S / lower) + drift) / sig_sqrt_t
355
+ d2_U = (safe_log(S / upper) + drift) / sig_sqrt_t
356
+
357
+ n_d2_L = float(stats.norm.pdf(d2_L))
358
+ n_d2_U = float(stats.norm.pdf(d2_U))
359
+
360
+ # dP_i/dS
361
+ dp_ds = (n_d2_L - n_d2_U) / (S * sig_sqrt_t)
362
+
363
+ # d^2P_i/dS^2 (second derivative of N(d2(K)) w.r.t. S)
364
+ # d/dS[n(d2)/(S*sig*sqrt_t)] =
365
+ # -d2*n(d2)/(S^2*sig^2*t) - n(d2)/(S^2*sig*sqrt_t)
366
+ s2 = S * S
367
+ sig2_t = sigma * sigma * t
368
+ d2p_ds2 = (
369
+ -(d2_L * n_d2_L - d2_U * n_d2_U) / (s2 * sig2_t)
370
+ - (n_d2_L - n_d2_U) / (s2 * sig_sqrt_t)
371
+ )
372
+
373
+ sign = -1.0 if config.is_reverse else 1.0
374
+ delta_sum += w * dp_ds * sign
375
+ gamma_sum += w * d2p_ds2 * sign
376
+
377
+ # Scale by payoff parameters
378
+ year_fraction = product.get_year_fraction(pricing_env)
379
+ accrual_rate = config.accrual_rate
380
+ discount = math.exp(-r * T)
381
+ scale = (
382
+ discount
383
+ * product.initial_price
384
+ * product.contract_multiplier
385
+ * accrual_rate
386
+ * year_fraction
387
+ / total_weights
388
+ )
389
+
390
+ greeks["delta"] = scale * delta_sum
391
+ greeks["gamma"] = scale * gamma_sum
392
+
393
+ return greeks
394
+
395
+ def __repr__(self):
396
+ return "RangeAccrualAnalyticalEngine()"