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,1167 @@
1
+ """
2
+ Phoenix option implementation.
3
+
4
+ Phoenix options are autocallable structured products with periodic coupon payments
5
+ when spot exceeds a coupon barrier at observation dates. Unlike Snowball options
6
+ which only pay coupons on knock-out events, Phoenix options pay coupons at each
7
+ observation where the coupon barrier condition is met.
8
+ """
9
+
10
+ from dataclasses import dataclass, field, replace
11
+ from datetime import datetime
12
+ from typing import Dict, List, Optional, Union
13
+
14
+ from quantark.asset.equity.product.option.base_equity_option import BaseEquityOption
15
+ from quantark.util.calendar import calculate_year_fraction
16
+ from quantark.util.calendar.day_counter import DayCountConvention, calculate_day_count_fraction
17
+ from quantark.util.enum import (
18
+ BarrierType,
19
+ CouponPayType,
20
+ ExerciseType,
21
+ ObservationType,
22
+ OptionType,
23
+ ProtectionType,
24
+ TenorEnd,
25
+ )
26
+ from quantark.util.exceptions import ValidationError
27
+
28
+ from .observation_schedule import (
29
+ ObservationAggregation,
30
+ ObservationRecord,
31
+ ObservationSchedule,
32
+ ResolvedObservationRecord,
33
+ )
34
+ from .phoenix_config import CouponBarrierConfig
35
+ from .snowball_config import AccrualConfig, AirbagConfig, BarrierConfig, PayoffConfig
36
+
37
+
38
+ @dataclass
39
+ class PhoenixOption(BaseEquityOption):
40
+ """
41
+ Phoenix autocallable structured product with periodic coupon payments.
42
+
43
+ A Phoenix option is an autocallable product that:
44
+ - Pays coupons at each observation where spot >= coupon_barrier (or <= for reverse)
45
+ - Can accumulate missed coupons with memory coupon feature
46
+ - Has knock-out (KO) barrier that terminates the product early
47
+ - Has optional knock-in (KI) barrier that changes maturity payoff
48
+
49
+ Product Types:
50
+ Standard Phoenix (is_reverse=False):
51
+ - KO barrier: UP (above initial price)
52
+ - KI barrier: DOWN (below initial price)
53
+ - Coupon barrier: Pays when spot >= coupon_barrier
54
+ - Embedded option: PUT (investor is short put on KI)
55
+
56
+ Reverse Phoenix (is_reverse=True):
57
+ - KO barrier: DOWN (below initial price)
58
+ - KI barrier: UP (above initial price)
59
+ - Coupon barrier: Pays when spot <= coupon_barrier
60
+ - Embedded option: CALL (investor is short call on KI)
61
+
62
+ Payoff Scenarios:
63
+ 1. KO triggered: Principal + accumulated coupons + KO rate × accrued time
64
+ 2. At maturity, never KO and never KI (V0):
65
+ Principal + accumulated coupons + rebate
66
+ 3. At maturity, never KO but KI happened (V1):
67
+ Principal + participation × (Spot - strike), floored by protection
68
+
69
+ Key Difference from Snowball:
70
+ - Snowball: Coupons only paid on KO trigger
71
+ - Phoenix: Coupons paid at each observation where coupon barrier is hit
72
+
73
+ Core Attributes:
74
+ initial_price: Reference price for payoff calculations
75
+ strike: Strike for the embedded option (put for standard, call for reverse)
76
+ contract_multiplier: Underlying units represented by one contract
77
+ is_reverse: If True, reverse phoenix; if False (default), standard phoenix
78
+
79
+ Barrier Attributes (via barrier_config):
80
+ ko_barrier: Knock-out barrier level(s)
81
+ ko_rate: Knock-out return rate(s)
82
+ ki_barrier: Knock-in barrier level(s)
83
+
84
+ Coupon Attributes (via coupon_config):
85
+ coupon_barrier: Coupon barrier level(s)
86
+ coupon_rate: Per-period coupon rate
87
+ day_count_convention: Day count convention for year fraction calculation
88
+ memory_coupon: If True, missed coupons accumulate
89
+
90
+ Payoff Attributes (via payoff_config):
91
+ rebate_rate: Fixed rebate rate for V0 maturity payoff
92
+ participation_rate: Downside participation after KI
93
+ protection_type: NONE, PARTIAL, or FULL protection
94
+ """
95
+
96
+ # Core parameters
97
+ initial_price: float = 0.0
98
+ strike: float = 0.0
99
+ is_reverse: bool = False
100
+
101
+ # Option type parameters
102
+ option_type: OptionType = OptionType.PUT
103
+ exercise_type: ExerciseType = ExerciseType.EUROPEAN
104
+
105
+ # Date-based maturity
106
+ initial_date: Optional[datetime] = None
107
+ exercise_date: Optional[datetime] = None
108
+ settlement_date: Optional[datetime] = None
109
+ maturity_date: Optional[datetime] = None
110
+ tenor: Optional[float] = None
111
+ maturity: Optional[float] = None
112
+ tenor_end: TenorEnd = TenorEnd.EXERCISE
113
+ annualization_day_count: DayCountConvention = DayCountConvention.ACT_365
114
+
115
+ # Configuration objects
116
+ barrier_config: BarrierConfig = field(
117
+ default_factory=lambda: BarrierConfig(ko_barrier=0.0, ko_rate=0.0)
118
+ )
119
+ coupon_config: CouponBarrierConfig = field(
120
+ default_factory=lambda: CouponBarrierConfig(coupon_barrier=0.0)
121
+ )
122
+ payoff_config: PayoffConfig = field(default_factory=PayoffConfig)
123
+ accrual_config: AccrualConfig = field(default_factory=AccrualConfig)
124
+ airbag_config: AirbagConfig = field(default_factory=AirbagConfig)
125
+
126
+ def __init__(
127
+ self,
128
+ initial_price: float,
129
+ strike: float,
130
+ barrier_config: BarrierConfig,
131
+ coupon_config: CouponBarrierConfig,
132
+ payoff_config: Optional[PayoffConfig] = None,
133
+ accrual_config: Optional[AccrualConfig] = None,
134
+ airbag_config: Optional[AirbagConfig] = None,
135
+ contract_multiplier: float = 1.0,
136
+ is_reverse: bool = False,
137
+ maturity: Optional[float] = None,
138
+ tenor: Optional[float] = None,
139
+ initial_date: Optional[datetime] = None,
140
+ exercise_date: Optional[datetime] = None,
141
+ settlement_date: Optional[datetime] = None,
142
+ maturity_date: Optional[datetime] = None,
143
+ tenor_end: TenorEnd = TenorEnd.EXERCISE,
144
+ annualization_day_count: DayCountConvention = DayCountConvention.ACT_365,
145
+ ):
146
+ """
147
+ Initialize Phoenix option.
148
+
149
+ Args:
150
+ initial_price: Reference price for payoff calculations
151
+ strike: Strike for the embedded option
152
+ barrier_config: BarrierConfig with KO/KI barrier settings
153
+ coupon_config: CouponBarrierConfig with coupon barrier settings
154
+ payoff_config: PayoffConfig with rebate/protection settings
155
+ accrual_config: AccrualConfig with annualization flags
156
+ airbag_config: AirbagConfig with airbag barrier settings
157
+ contract_multiplier: Underlying units represented by one contract
158
+ is_reverse: If True, creates a reverse phoenix
159
+ maturity: Time to maturity from valuation (years)
160
+ tenor: Contract tenor in years
161
+ initial_date: Product start/issue date
162
+ exercise_date: Expiration date
163
+ settlement_date: Settlement date
164
+ maturity_date: Explicit maturity date
165
+ tenor_end: Tenor end-point selection
166
+ annualization_day_count: Day count basis for annualization
167
+
168
+ Raises:
169
+ ValidationError: If parameters are invalid
170
+ """
171
+ # Set configuration objects
172
+ self.barrier_config = barrier_config
173
+ self.coupon_config = coupon_config
174
+ self.payoff_config = (
175
+ payoff_config if payoff_config is not None else PayoffConfig()
176
+ )
177
+ self.accrual_config = (
178
+ accrual_config if accrual_config is not None else AccrualConfig()
179
+ )
180
+ self.airbag_config = (
181
+ airbag_config if airbag_config is not None else AirbagConfig()
182
+ )
183
+
184
+ # Set core attributes for local use before base init if needed
185
+ self.is_reverse = is_reverse
186
+ # Set option type based on standard vs reverse
187
+ self.option_type = OptionType.CALL if is_reverse else OptionType.PUT
188
+ self.exercise_type = ExerciseType.EUROPEAN
189
+
190
+ # Set base class attributes
191
+ super().__init__(
192
+ strike=strike,
193
+ option_type=self.option_type,
194
+ exercise_type=self.exercise_type,
195
+ maturity=maturity,
196
+ tenor=tenor,
197
+ initial_date=initial_date,
198
+ exercise_date=exercise_date,
199
+ settlement_date=settlement_date,
200
+ maturity_date=maturity_date,
201
+ tenor_end=tenor_end,
202
+ annualization_day_count=annualization_day_count,
203
+ initial_price=initial_price,
204
+ contract_multiplier=contract_multiplier,
205
+ )
206
+
207
+ self.initial_date = initial_date
208
+ self.exercise_date = exercise_date
209
+ self.settlement_date = settlement_date
210
+ self.maturity_date = maturity_date
211
+ self.tenor = tenor
212
+ self.maturity = maturity
213
+ self.tenor_end = tenor_end
214
+ self.annualization_day_count = annualization_day_count
215
+
216
+ # Set core attributes
217
+ self.initial_price = initial_price
218
+ self.strike = strike
219
+ self.contract_multiplier = contract_multiplier
220
+ # is_reverse already set above
221
+
222
+ # Configuration objects already set above
223
+
224
+ self.validate()
225
+
226
+ def validate(self) -> None:
227
+ """
228
+ Validate Phoenix option parameters.
229
+
230
+ Raises:
231
+ ValidationError: If parameters are invalid
232
+ """
233
+ self._validate_core_parameters()
234
+ self._validate_maturity_parameters()
235
+ super().validate()
236
+ self._validate_barrier_parameters()
237
+ self._validate_coupon_parameters()
238
+ self._validate_observation_parameters()
239
+ self._validate_payoff_parameters()
240
+ self._validate_accrual_parameters()
241
+ self._build_observation_schedules()
242
+
243
+ def _validate_core_parameters(self) -> None:
244
+ """Validate core product parameters."""
245
+ if self.initial_price <= 0:
246
+ raise ValidationError(
247
+ f"Initial price must be positive, got {self.initial_price}"
248
+ )
249
+ if self.strike <= 0:
250
+ raise ValidationError(f"Strike must be positive, got {self.strike}")
251
+
252
+ def _validate_maturity_parameters(self) -> None:
253
+ """Validate maturity, tenor, and date-related parameters."""
254
+ if self.maturity is None and self.exercise_date is None:
255
+ raise ValidationError("Either maturity or exercise_date must be provided")
256
+ if self.maturity is not None and self.maturity <= 0:
257
+ raise ValidationError(f"Maturity must be positive, got {self.maturity}")
258
+ if self.tenor is not None and self.tenor <= 0:
259
+ raise ValidationError(f"Tenor must be positive, got {self.tenor}")
260
+ if not isinstance(self.tenor_end, TenorEnd):
261
+ raise ValidationError(f"Invalid tenor_end: {self.tenor_end}")
262
+ if not isinstance(self.annualization_day_count, DayCountConvention):
263
+ raise ValidationError(
264
+ f"annualization_day_count must be DayCountConvention, "
265
+ f"got {self.annualization_day_count}"
266
+ )
267
+
268
+ def _validate_barrier_parameters(self) -> None:
269
+ """Validate barrier configurations."""
270
+ self._validate_barrier_array(self.barrier_config.ko_barrier, "KO barrier")
271
+ self._validate_rate_array(self.barrier_config.ko_rate, "KO rate")
272
+
273
+ if self.barrier_config.ki_barrier is not None:
274
+ self._validate_barrier_array(self.barrier_config.ki_barrier, "KI barrier")
275
+ # Validate continuous KI requires scalar barrier
276
+ if (
277
+ self.barrier_config.ki_continuous
278
+ or self.barrier_config.ki_observation_type == ObservationType.CONTINUOUS
279
+ ) and isinstance(self.barrier_config.ki_barrier, list):
280
+ raise ValidationError("Continuous KI requires scalar ki_barrier")
281
+
282
+ def _validate_coupon_parameters(self) -> None:
283
+ """Validate coupon barrier configuration."""
284
+ self._validate_barrier_array(
285
+ self.coupon_config.coupon_barrier, "Coupon barrier"
286
+ )
287
+ if self.coupon_config.coupon_rate < 0:
288
+ raise ValidationError(
289
+ f"Coupon rate must be non-negative, got {self.coupon_config.coupon_rate}"
290
+ )
291
+ if not isinstance(self.coupon_config.day_count_convention, DayCountConvention):
292
+ raise ValidationError(
293
+ f"day_count_convention must be DayCountConvention, "
294
+ f"got {self.coupon_config.day_count_convention}"
295
+ )
296
+ if not isinstance(self.coupon_config.coupon_pay_type, CouponPayType):
297
+ raise ValidationError(
298
+ f"coupon_pay_type must be CouponPayType, "
299
+ f"got {self.coupon_config.coupon_pay_type}"
300
+ )
301
+
302
+ def _validate_observation_parameters(self) -> None:
303
+ """Validate observation types and discrete observation requirements."""
304
+ if not isinstance(self.barrier_config.ko_observation_type, ObservationType):
305
+ raise ValidationError(
306
+ f"Invalid KO observation type: {self.barrier_config.ko_observation_type}"
307
+ )
308
+
309
+ if self.barrier_config.ko_observation_type == ObservationType.DISCRETE:
310
+ if (
311
+ self.barrier_config.ko_observation_schedule is None
312
+ and self.barrier_config.ko_observation_dates is None
313
+ ):
314
+ raise ValidationError(
315
+ "KO observation dates or schedule required for discrete monitoring"
316
+ )
317
+ self._validate_observation_dates(
318
+ self.barrier_config.ko_observation_dates, "KO"
319
+ )
320
+
321
+ # Validate KI observation type
322
+ if not isinstance(self.barrier_config.ki_observation_type, ObservationType):
323
+ raise ValidationError(
324
+ f"Invalid KI observation type: {self.barrier_config.ki_observation_type}"
325
+ )
326
+
327
+ # Validate discrete KI observations (if KI barrier provided)
328
+ if (
329
+ self.barrier_config.ki_barrier is not None
330
+ and self.barrier_config.ki_observation_type == ObservationType.DISCRETE
331
+ and not self.barrier_config.ki_continuous
332
+ ):
333
+ if (
334
+ self.barrier_config.ki_observation_schedule is None
335
+ and self.barrier_config.ki_observation_dates is None
336
+ ):
337
+ raise ValidationError(
338
+ "KI observation dates or schedule required for discrete monitoring"
339
+ )
340
+ self._validate_observation_dates(
341
+ self.barrier_config.ki_observation_dates, "KI"
342
+ )
343
+
344
+ def _validate_payoff_parameters(self) -> None:
345
+ """Validate payoff configuration."""
346
+ if not isinstance(self.accrual_config.coupon_pay_type, CouponPayType):
347
+ raise ValidationError(
348
+ f"Invalid coupon pay type: {self.accrual_config.coupon_pay_type}"
349
+ )
350
+
351
+ if self.payoff_config.call_rebate_enabled:
352
+ if self.payoff_config.call_strike is None:
353
+ raise ValidationError(
354
+ "Call strike required when call_rebate_enabled is True"
355
+ )
356
+ if self.payoff_config.call_strike <= 0:
357
+ raise ValidationError(
358
+ f"Call strike must be positive, got {self.payoff_config.call_strike}"
359
+ )
360
+
361
+ if not isinstance(self.payoff_config.protection_type, ProtectionType):
362
+ raise ValidationError(
363
+ f"Invalid protection type: {self.payoff_config.protection_type}"
364
+ )
365
+
366
+ if self.payoff_config.participation_rate <= 0:
367
+ raise ValidationError(
368
+ f"Participation rate must be positive, "
369
+ f"got {self.payoff_config.participation_rate}"
370
+ )
371
+
372
+ def _validate_accrual_parameters(self) -> None:
373
+ """Validate accrual configuration flags and external factors."""
374
+ for flag_name, flag_value in [
375
+ ("is_annualized", self.accrual_config.is_annualized),
376
+ ("is_annualized_ko", self.accrual_config.is_annualized_ko),
377
+ ("is_annualized_ki", self.accrual_config.is_annualized_ki),
378
+ ("is_annualized_rebate", self.accrual_config.is_annualized_rebate),
379
+ ]:
380
+ if flag_value is not None and not isinstance(flag_value, bool):
381
+ raise ValidationError(f"{flag_name} must be boolean, got {flag_value}")
382
+
383
+ accrual_factors = self.accrual_config.accrual_factors
384
+ if accrual_factors is not None:
385
+ ko_obs_len = self.num_ko_observations
386
+ if ko_obs_len and len(accrual_factors) != ko_obs_len:
387
+ raise ValidationError(
388
+ "accrual_factors length "
389
+ f"({len(accrual_factors)}) must match KO observation length "
390
+ f"({ko_obs_len})"
391
+ )
392
+
393
+ def _build_observation_schedules(self) -> None:
394
+ """Build ObservationSchedules from observation dates if needed (Legacy)."""
395
+ # Check if we need to build schedules
396
+ needs_ko_schedule = (
397
+ self.barrier_config.ko_observation_schedule is None
398
+ and self.barrier_config.ko_observation_dates is not None
399
+ )
400
+
401
+ if not needs_ko_schedule:
402
+ return
403
+
404
+ # Build KO schedule (simplified version matching Snowball pattern)
405
+ ko_barriers = (
406
+ self.barrier_config.ko_barrier
407
+ if isinstance(self.barrier_config.ko_barrier, list)
408
+ else None
409
+ )
410
+ ko_rates = (
411
+ self.barrier_config.ko_rate
412
+ if isinstance(self.barrier_config.ko_rate, list)
413
+ else None
414
+ )
415
+
416
+ records = []
417
+ for i, t in enumerate(self.barrier_config.ko_observation_dates):
418
+ barrier_val = (
419
+ ko_barriers[i] if ko_barriers else self.barrier_config.ko_barrier
420
+ )
421
+ rate_val = ko_rates[i] if ko_rates else self.barrier_config.ko_rate
422
+ records.append(
423
+ ObservationRecord(
424
+ observation_time=t,
425
+ barrier=barrier_val,
426
+ return_rate=rate_val,
427
+ is_rate_annualized=False,
428
+ )
429
+ )
430
+ ko_schedule = ObservationSchedule(
431
+ records=records,
432
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
433
+ )
434
+
435
+ # Update barrier_config with built schedule
436
+ self.barrier_config = replace(
437
+ self.barrier_config,
438
+ ko_observation_schedule=ko_schedule,
439
+ )
440
+
441
+ def time_shift(self, time_bump: float, bumped_date: datetime, pricing_env) -> bool:
442
+ """Shift barrier schedules for theta bumping."""
443
+ dropped_all = super().time_shift(time_bump, bumped_date, pricing_env)
444
+ if dropped_all:
445
+ return True
446
+
447
+ if self.barrier_config is not None:
448
+ new_config, dropped_all = self.barrier_config.time_shift(
449
+ time_bump, bumped_date, pricing_env
450
+ )
451
+ self.barrier_config = new_config
452
+
453
+ return dropped_all
454
+
455
+ def _validate_barrier_array(
456
+ self, barrier: Union[float, List[float]], name: str
457
+ ) -> None:
458
+ """Validate barrier level(s) are positive."""
459
+ if isinstance(barrier, list):
460
+ if not barrier:
461
+ raise ValidationError(f"{name} list cannot be empty")
462
+ for i, b in enumerate(barrier):
463
+ if b <= 0:
464
+ raise ValidationError(f"{name}[{i}] must be positive, got {b}")
465
+ else:
466
+ if barrier <= 0:
467
+ raise ValidationError(f"{name} must be positive, got {barrier}")
468
+
469
+ def _validate_rate_array(self, rate: Union[float, List[float]], name: str) -> None:
470
+ """Validate rate(s) - can be negative for some structures."""
471
+ if isinstance(rate, list):
472
+ for i, r in enumerate(rate):
473
+ if not isinstance(r, (int, float)):
474
+ raise ValidationError(f"{name}[{i}] must be numeric, got {r}")
475
+ else:
476
+ if not isinstance(rate, (int, float)):
477
+ raise ValidationError(f"{name} must be numeric, got {rate}")
478
+
479
+ def _validate_observation_dates(
480
+ self, dates: Optional[List[float]], name: str
481
+ ) -> None:
482
+ """Validate observation dates are non-negative and ordered."""
483
+ if dates is None:
484
+ return
485
+ for i, t in enumerate(dates):
486
+ if t < 0:
487
+ raise ValidationError(
488
+ f"{name} observation date[{i}] must be non-negative, got {t}"
489
+ )
490
+ # Check ordering
491
+ for i in range(1, len(dates)):
492
+ if dates[i] <= dates[i - 1]:
493
+ raise ValidationError(
494
+ f"{name} observation dates must be strictly increasing"
495
+ )
496
+
497
+ def resolve_ko_observations(self, pricing_env) -> List[ResolvedObservationRecord]:
498
+ """
499
+ Resolve KO observation schedule to concrete times, barriers, and settlement times.
500
+
501
+ KO payoffs exclude Phoenix coupon payments, which are handled by pricing engines.
502
+ """
503
+ if self.barrier_config.ko_observation_type != ObservationType.DISCRETE:
504
+ raise ValidationError(
505
+ "resolve_ko_observations currently supports discrete KO monitoring."
506
+ )
507
+
508
+ schedule = self.barrier_config.ko_observation_schedule
509
+ if schedule is None:
510
+ raise ValidationError(
511
+ "KO observation schedule is required to resolve KO observations."
512
+ )
513
+
514
+ default_barrier = (
515
+ None
516
+ if isinstance(self.barrier_config.ko_barrier, list)
517
+ else self.barrier_config.ko_barrier
518
+ )
519
+ resolved_schedule = schedule.resolve(
520
+ pricing_env=pricing_env,
521
+ default_barrier=default_barrier,
522
+ default_payoff=0.0,
523
+ require_single=True,
524
+ )
525
+
526
+ annualized_ko = self._effective_annualized_flag(
527
+ self.accrual_config.is_annualized_ko
528
+ )
529
+ principal_component = (
530
+ self.initial_price * self.contract_multiplier
531
+ if self.payoff_config.include_principal
532
+ else 0.0
533
+ )
534
+ maturity_time: Optional[float] = None
535
+ bus_days_in_year = pricing_env.bus_days_in_year if pricing_env else 252
536
+
537
+ ko_records: List[ResolvedObservationRecord] = []
538
+ accrual_factors = self.accrual_config.accrual_factors
539
+ for idx, rec in enumerate(resolved_schedule):
540
+ rate = schedule.records[idx].return_rate
541
+ if rate is None:
542
+ rate = self.get_ko_rate_at(idx)
543
+
544
+ if accrual_factors is not None:
545
+ accrual_factor = float(accrual_factors[idx])
546
+ elif annualized_ko:
547
+ schedule_record = schedule.records[idx]
548
+ accrual_start_date = self.initial_date
549
+ if schedule_record.observation_date is not None:
550
+ if accrual_start_date is None:
551
+ if pricing_env is None:
552
+ raise ValidationError(
553
+ "PricingEnvironment required to resolve KO accrual from observation_date."
554
+ )
555
+ accrual_start_date = pricing_env.valuation_date
556
+ accrual_factor = calculate_year_fraction(
557
+ accrual_start_date,
558
+ schedule_record.observation_date,
559
+ self.annualization_day_count,
560
+ bus_days_in_year,
561
+ calendar=getattr(pricing_env, "calendar", None),
562
+ )
563
+ else:
564
+ if accrual_start_date is None:
565
+ accrual_factor = rec.observation_time
566
+ else:
567
+ if pricing_env is None:
568
+ raise ValidationError(
569
+ "PricingEnvironment required to resolve KO accrual without observation_date."
570
+ )
571
+ if pricing_env.valuation_date < accrual_start_date:
572
+ raise ValidationError(
573
+ "valuation_date must be on or after initial_date to resolve KO accrual."
574
+ )
575
+ if pricing_env.valuation_date == accrual_start_date:
576
+ initial_to_valuation = 0.0
577
+ else:
578
+ initial_to_valuation = calculate_year_fraction(
579
+ accrual_start_date,
580
+ pricing_env.valuation_date,
581
+ self.annualization_day_count,
582
+ pricing_env.bus_days_in_year,
583
+ calendar=getattr(pricing_env, "calendar", None),
584
+ )
585
+ accrual_factor = initial_to_valuation + rec.observation_time
586
+ else:
587
+ accrual_factor = 1.0
588
+
589
+ ko_coupon = (
590
+ self.initial_price * self.contract_multiplier * rate * accrual_factor
591
+ )
592
+ payoff = principal_component + ko_coupon
593
+
594
+ settlement_time = rec.settlement_time
595
+ if self.accrual_config.coupon_pay_type == CouponPayType.EXPIRY:
596
+ maturity_time = (
597
+ maturity_time
598
+ if maturity_time is not None
599
+ else self.get_maturity(pricing_env)
600
+ )
601
+ settlement_time = maturity_time
602
+
603
+ ko_records.append(
604
+ ResolvedObservationRecord(
605
+ observation_time=rec.observation_time,
606
+ barrier=rec.barrier,
607
+ payoff=payoff,
608
+ settlement_time=settlement_time,
609
+ )
610
+ )
611
+ return ko_records
612
+
613
+ def resolve_ki_observations(self, pricing_env) -> List[ResolvedObservationRecord]:
614
+ """
615
+ Resolve KI observation schedule to times and barrier levels.
616
+ """
617
+ if self.barrier_config.ki_barrier is None:
618
+ raise ValidationError("KI barrier configuration is missing.")
619
+ if (
620
+ self.barrier_config.ki_observation_type != ObservationType.DISCRETE
621
+ or self.barrier_config.ki_continuous
622
+ ):
623
+ raise ValidationError(
624
+ "resolve_ki_observations currently supports discrete KI monitoring."
625
+ )
626
+
627
+ schedule = self.barrier_config.ki_observation_schedule
628
+ if schedule is None:
629
+ raise ValidationError(
630
+ "KI observation schedule is required to resolve KI observations."
631
+ )
632
+
633
+ default_barrier = (
634
+ None
635
+ if isinstance(self.barrier_config.ki_barrier, list)
636
+ else self.barrier_config.ki_barrier
637
+ )
638
+ resolved_schedule = schedule.resolve(
639
+ pricing_env=pricing_env,
640
+ default_barrier=default_barrier,
641
+ default_payoff=0.0,
642
+ require_single=True,
643
+ )
644
+
645
+ return [
646
+ ResolvedObservationRecord(
647
+ observation_time=rec.observation_time,
648
+ barrier=rec.barrier,
649
+ payoff=0.0,
650
+ settlement_time=rec.settlement_time,
651
+ )
652
+ for rec in resolved_schedule
653
+ ]
654
+
655
+ def get_ko_observation_profile(self, pricing_env) -> Dict[str, List[Optional[float]]]:
656
+ """Return KO observation attributes for engine consumption."""
657
+ records = self.resolve_ko_observations(pricing_env)
658
+ return {
659
+ "observation_times": [rec.observation_time for rec in records],
660
+ "barriers": [rec.barrier for rec in records],
661
+ "payoffs": [rec.payoff for rec in records],
662
+ "settlement_times": [rec.settlement_time for rec in records],
663
+ }
664
+
665
+ def get_ki_observation_profile(self, pricing_env) -> Dict[str, List[Optional[float]]]:
666
+ """Return KI observation attributes for engine consumption."""
667
+ records = self.resolve_ki_observations(pricing_env)
668
+ return {
669
+ "observation_times": [rec.observation_time for rec in records],
670
+ "barriers": [rec.barrier for rec in records],
671
+ }
672
+
673
+ # ==================== Coupon Barrier Methods ====================
674
+
675
+ def is_coupon_triggered(self, spot: float, observation_idx: int = 0) -> bool:
676
+ """
677
+ Check if coupon barrier is triggered at given spot.
678
+
679
+ For standard Phoenix: pays coupon when spot >= coupon_barrier
680
+ For reverse Phoenix: pays coupon when spot <= coupon_barrier
681
+
682
+ Args:
683
+ spot: Current spot price
684
+ observation_idx: Index of observation date (for time-varying barriers)
685
+
686
+ Returns:
687
+ True if coupon barrier is triggered
688
+ """
689
+ barrier = self._get_barrier_at(
690
+ self.coupon_config.coupon_barrier, observation_idx, "Coupon barrier"
691
+ )
692
+ if self.is_reverse:
693
+ return spot <= barrier
694
+ return spot >= barrier
695
+
696
+ def get_coupon_barrier_at(self, observation_idx: int) -> float:
697
+ """Get coupon barrier level at given observation index."""
698
+ return self._get_barrier_at(
699
+ self.coupon_config.coupon_barrier, observation_idx, "Coupon barrier"
700
+ )
701
+
702
+ def get_coupon_payoff(
703
+ self,
704
+ observation_idx: int,
705
+ start_date: Optional[datetime] = None,
706
+ end_date: Optional[datetime] = None,
707
+ year_fraction: Optional[float] = None,
708
+ ) -> float:
709
+ """
710
+ Calculate coupon payoff for a single observation period.
711
+
712
+ The coupon is calculated as:
713
+ coupon = initial_price × contract_multiplier × coupon_rate × year_fraction
714
+
715
+ Args:
716
+ observation_idx: Index of observation date
717
+ start_date: Start date for year fraction calculation
718
+ end_date: End date for year fraction calculation
719
+ year_fraction: Pre-calculated year fraction (overrides date calculation)
720
+
721
+ Returns:
722
+ Coupon payoff amount
723
+ """
724
+ if year_fraction is not None:
725
+ dcf = year_fraction
726
+ elif start_date is not None and end_date is not None:
727
+ dcf = calculate_day_count_fraction(
728
+ start_date, end_date, self.coupon_config.day_count_convention
729
+ )
730
+ else:
731
+ # Default to per-period rate without annualization
732
+ dcf = 1.0
733
+
734
+ principal = self.initial_price * self.contract_multiplier
735
+ return principal * self.coupon_config.coupon_rate * dcf
736
+
737
+ def get_coupon_year_fraction(
738
+ self, start_date: datetime, end_date: datetime
739
+ ) -> float:
740
+ """
741
+ Calculate year fraction for coupon accrual using configured day count convention.
742
+
743
+ Args:
744
+ start_date: Period start date
745
+ end_date: Period end date
746
+
747
+ Returns:
748
+ Year fraction according to day count convention
749
+ """
750
+ return calculate_day_count_fraction(
751
+ start_date, end_date, self.coupon_config.day_count_convention
752
+ )
753
+
754
+ def get_coupon_period_year_fractions(
755
+ self, observation_times: List[float]
756
+ ) -> List[float]:
757
+ """
758
+ Resolve coupon period accrual fractions for each observation.
759
+
760
+ If fixed_coupon_year_fraction is configured, use that value for every
761
+ coupon period (e.g., 1/12 for equal monthly coupons). Otherwise, derive
762
+ period fractions from successive observation times.
763
+ """
764
+ if not observation_times:
765
+ return []
766
+
767
+ accrual_factors = self.accrual_config.accrual_factors
768
+ if accrual_factors is not None:
769
+ if len(accrual_factors) != len(observation_times):
770
+ raise ValidationError(
771
+ "accrual_factors length "
772
+ f"({len(accrual_factors)}) must match observation_times length "
773
+ f"({len(observation_times)})"
774
+ )
775
+ return [float(factor) for factor in accrual_factors]
776
+
777
+ fixed = self.coupon_config.fixed_coupon_year_fraction
778
+ if fixed is not None:
779
+ return [float(fixed) for _ in observation_times]
780
+
781
+ yfs: List[float] = [float(observation_times[0])]
782
+ for idx in range(1, len(observation_times)):
783
+ yfs.append(float(observation_times[idx] - observation_times[idx - 1]))
784
+ return yfs
785
+
786
+ # ==================== KO/KI Barrier Methods ====================
787
+
788
+ def is_ko_triggered(self, spot: float, observation_idx: int = 0) -> bool:
789
+ """
790
+ Check if KO barrier is triggered at given spot.
791
+
792
+ For standard Phoenix: KO triggers when spot >= ko_barrier (up barrier)
793
+ For reverse Phoenix: KO triggers when spot <= ko_barrier (down barrier)
794
+
795
+ Args:
796
+ spot: Current spot price
797
+ observation_idx: Index of observation date (for time-varying barriers)
798
+
799
+ Returns:
800
+ True if KO barrier is triggered
801
+ """
802
+ barrier = self._get_barrier_at(
803
+ self.barrier_config.ko_barrier, observation_idx, "KO barrier"
804
+ )
805
+ if self.is_reverse:
806
+ return spot <= barrier
807
+ return spot >= barrier
808
+
809
+ def is_ki_triggered(self, spot: float, observation_idx: int = 0) -> bool:
810
+ """
811
+ Check if KI barrier is triggered at given spot.
812
+
813
+ For standard Phoenix: KI triggers when spot <= ki_barrier (down barrier)
814
+ For reverse Phoenix: KI triggers when spot >= ki_barrier (up barrier)
815
+
816
+ Args:
817
+ spot: Current spot price
818
+ observation_idx: Index of observation date (for time-varying barriers)
819
+
820
+ Returns:
821
+ True if KI barrier is triggered
822
+ """
823
+ if self.barrier_config.ki_barrier is None:
824
+ return False
825
+
826
+ barrier = self._get_barrier_at(
827
+ self.barrier_config.ki_barrier, observation_idx, "KI barrier"
828
+ )
829
+ if self.is_reverse:
830
+ return spot >= barrier
831
+ return spot <= barrier
832
+
833
+ def _get_barrier_at(
834
+ self, barrier_value: Union[float, List[float]], index: int, barrier_type: str
835
+ ) -> float:
836
+ """Extract barrier value at given observation index."""
837
+ if isinstance(barrier_value, list):
838
+ if index < 0 or index >= len(barrier_value):
839
+ raise ValidationError(
840
+ f"{barrier_type} observation index {index} out of range"
841
+ )
842
+ return barrier_value[index]
843
+ return barrier_value
844
+
845
+ def get_ko_barrier_at(self, observation_idx: int) -> float:
846
+ """Get KO barrier level at given observation index."""
847
+ return self._get_barrier_at(
848
+ self.barrier_config.ko_barrier, observation_idx, "KO barrier"
849
+ )
850
+
851
+ def get_ko_rate_at(self, observation_idx: int) -> float:
852
+ """Get KO rate at given observation index."""
853
+ return self._get_barrier_at(
854
+ self.barrier_config.ko_rate, observation_idx, "KO rate"
855
+ )
856
+
857
+ def get_ki_barrier_at(self, observation_idx: int) -> Optional[float]:
858
+ """Get KI barrier level at given observation index."""
859
+ if self.barrier_config.ki_barrier is None:
860
+ return None
861
+ return self._get_barrier_at(
862
+ self.barrier_config.ki_barrier, observation_idx, "KI barrier"
863
+ )
864
+
865
+ # ==================== Payoff Methods ====================
866
+
867
+ def get_ko_payoff(
868
+ self,
869
+ spot: float,
870
+ observation_idx: int,
871
+ accumulated_coupons: float = 0.0,
872
+ pricing_env=None,
873
+ ) -> float:
874
+ """
875
+ Calculate KO payoff including accumulated coupons.
876
+
877
+ Args:
878
+ spot: Current spot price
879
+ observation_idx: Index of KO observation
880
+ accumulated_coupons: Total accumulated coupon payments
881
+ pricing_env: PricingEnvironment for time calculations
882
+
883
+ Returns:
884
+ KO payoff = principal + ko_coupon + accumulated_coupons
885
+ """
886
+ principal = (
887
+ self.initial_price * self.contract_multiplier
888
+ if self.payoff_config.include_principal
889
+ else 0.0
890
+ )
891
+
892
+ # KO coupon based on ko_rate
893
+ ko_rate = self.get_ko_rate_at(observation_idx)
894
+
895
+ # Use specific flag or fall back to general is_annualized
896
+ annualized_ko = self._effective_annualized_flag(
897
+ self.accrual_config.is_annualized_ko
898
+ )
899
+
900
+ accrual_factors = self.accrual_config.accrual_factors
901
+ if accrual_factors is not None:
902
+ accrual_factor = float(accrual_factors[observation_idx])
903
+ elif annualized_ko:
904
+ # Calculate proper accrual from initial_date
905
+ if self.barrier_config.ko_observation_schedule is not None:
906
+ schedule = self.barrier_config.ko_observation_schedule
907
+ schedule_record = schedule.records[observation_idx]
908
+
909
+ if schedule_record.observation_date is not None:
910
+ # Use date-based calculation
911
+ accrual_start_date = self.initial_date
912
+ if accrual_start_date is None:
913
+ if pricing_env is None:
914
+ raise ValidationError(
915
+ "PricingEnvironment required to resolve KO accrual from observation_date."
916
+ )
917
+ accrual_start_date = pricing_env.valuation_date
918
+
919
+ bus_days_in_year = (
920
+ pricing_env.bus_days_in_year if pricing_env else 252
921
+ )
922
+ accrual_factor = calculate_year_fraction(
923
+ accrual_start_date,
924
+ schedule_record.observation_date,
925
+ self.annualization_day_count,
926
+ bus_days_in_year,
927
+ calendar=getattr(pricing_env, "calendar", None),
928
+ )
929
+ else:
930
+ # Fallback to observation_time
931
+ accrual_factor = schedule_record.observation_time
932
+ elif self.barrier_config.ko_observation_dates is not None:
933
+ # Legacy path: use year fraction directly
934
+ accrual_factor = self.barrier_config.ko_observation_dates[observation_idx]
935
+ else:
936
+ accrual_factor = 1.0
937
+ else:
938
+ accrual_factor = 1.0
939
+
940
+ ko_coupon = self.initial_price * self.contract_multiplier * ko_rate * accrual_factor
941
+
942
+ # Check if current period coupon is triggered
943
+ current_coupon = 0.0
944
+ if self.is_coupon_triggered(spot, observation_idx):
945
+ coupon_year_fraction = (
946
+ float(accrual_factors[observation_idx])
947
+ if accrual_factors is not None
948
+ else None
949
+ )
950
+ current_coupon = self.get_coupon_payoff(
951
+ observation_idx, year_fraction=coupon_year_fraction
952
+ )
953
+
954
+ return principal + ko_coupon + accumulated_coupons + current_coupon
955
+
956
+ def get_maturity_payoff_v0(
957
+ self,
958
+ spot: float,
959
+ accumulated_coupons: float = 0.0,
960
+ pricing_env=None,
961
+ ) -> float:
962
+ """
963
+ Calculate maturity payoff for V0 state (not knocked-in, no KO).
964
+
965
+ V0 payoff = principal + rebate + accumulated_coupons
966
+
967
+ Args:
968
+ spot: Spot price at maturity
969
+ accumulated_coupons: Total accumulated coupon payments
970
+ pricing_env: PricingEnvironment for calculations
971
+
972
+ Returns:
973
+ V0 maturity payoff
974
+ """
975
+ principal = (
976
+ self.initial_price * self.contract_multiplier
977
+ if self.payoff_config.include_principal
978
+ else 0.0
979
+ )
980
+ contract_tenor: Optional[float] = None
981
+
982
+ if (
983
+ self.payoff_config.call_rebate_enabled
984
+ and self.payoff_config.call_strike is not None
985
+ ):
986
+ # Call-style rebate
987
+ call_strike = self.payoff_config.call_strike
988
+ if self.is_reverse:
989
+ call_payoff = max(call_strike - spot, 0.0)
990
+ else:
991
+ call_payoff = max(spot - call_strike, 0.0)
992
+ rebate = (
993
+ self.payoff_config.call_participation_rate
994
+ * self.contract_multiplier
995
+ * call_payoff
996
+ )
997
+ if self.accrual_config.is_annualized_rebate:
998
+ contract_tenor = contract_tenor or self.get_contract_tenor(pricing_env)
999
+ rebate *= contract_tenor
1000
+ else:
1001
+ # Fixed rebate
1002
+ contract_tenor = contract_tenor or self.get_contract_tenor(pricing_env)
1003
+ if self.accrual_config.is_annualized_rebate:
1004
+ rebate = (
1005
+ self.payoff_config.rebate_rate
1006
+ * self.initial_price
1007
+ * self.contract_multiplier
1008
+ * contract_tenor
1009
+ )
1010
+ else:
1011
+ rebate = (
1012
+ self.payoff_config.rebate_rate
1013
+ * self.initial_price
1014
+ * self.contract_multiplier
1015
+ )
1016
+
1017
+ return principal + rebate + accumulated_coupons
1018
+
1019
+ def get_maturity_payoff_v1(self, spot: float, pricing_env=None) -> float:
1020
+ """
1021
+ Calculate maturity payoff for V1 state (knocked-in, no KO).
1022
+
1023
+ V1 payoff = principal + participation × downside (floored by protection)
1024
+
1025
+ Args:
1026
+ spot: Spot price at maturity
1027
+ pricing_env: PricingEnvironment for calculations
1028
+
1029
+ Returns:
1030
+ V1 maturity payoff
1031
+ """
1032
+ principal = (
1033
+ self.initial_price * self.contract_multiplier
1034
+ if self.payoff_config.include_principal
1035
+ else 0.0
1036
+ )
1037
+ participation_rate = self.payoff_config.participation_rate
1038
+ effective_strike = self.strike
1039
+
1040
+ # Check airbag
1041
+ airbag_barrier = self.airbag_config.airbag_barrier
1042
+ if airbag_barrier is not None:
1043
+ if self.is_reverse:
1044
+ is_unsafe = spot > airbag_barrier
1045
+ else:
1046
+ is_unsafe = spot < airbag_barrier
1047
+
1048
+ if is_unsafe:
1049
+ participation_rate = self.airbag_config.airbag_participation_rate
1050
+ if self.airbag_config.airbag_strike is not None:
1051
+ effective_strike = self.airbag_config.airbag_strike
1052
+
1053
+ # Downside calculation
1054
+ if self.is_reverse:
1055
+ raw_diff = effective_strike - spot
1056
+ else:
1057
+ raw_diff = spot - effective_strike
1058
+
1059
+ downside = participation_rate * min(raw_diff, 0.0) * self.contract_multiplier
1060
+
1061
+ # Apply protection floor
1062
+ if self.payoff_config.protection_type == ProtectionType.FULL:
1063
+ downside = max(downside, 0.0)
1064
+ elif self.payoff_config.protection_type == ProtectionType.PARTIAL:
1065
+ floor = (
1066
+ self.payoff_config.protection_rate
1067
+ * self.initial_price
1068
+ * self.contract_multiplier
1069
+ )
1070
+ downside = max(downside, -floor)
1071
+
1072
+ return principal + downside
1073
+
1074
+ def get_payoff(
1075
+ self,
1076
+ spot: float,
1077
+ knocked_in: bool = False,
1078
+ accumulated_coupons: float = 0.0,
1079
+ pricing_env=None,
1080
+ ) -> float:
1081
+ """
1082
+ Get maturity payoff based on knock-in state.
1083
+
1084
+ Args:
1085
+ spot: Spot price at maturity
1086
+ knocked_in: Whether KI barrier was triggered
1087
+ accumulated_coupons: Total accumulated coupon payments
1088
+ pricing_env: PricingEnvironment for calculations
1089
+
1090
+ Returns:
1091
+ Maturity payoff (V0 or V1)
1092
+ """
1093
+ if knocked_in:
1094
+ return self.get_maturity_payoff_v1(spot, pricing_env)
1095
+ return self.get_maturity_payoff_v0(spot, accumulated_coupons, pricing_env)
1096
+
1097
+ # ==================== Properties ====================
1098
+
1099
+ @property
1100
+ def has_ki_barrier(self) -> bool:
1101
+ """Check if product has a knock-in barrier."""
1102
+ return self.barrier_config.ki_barrier is not None
1103
+
1104
+ @property
1105
+ def has_memory_coupon(self) -> bool:
1106
+ """Check if memory coupon is enabled."""
1107
+ return self.coupon_config.memory_coupon
1108
+
1109
+ @property
1110
+ def num_ko_observations(self) -> int:
1111
+ """Get number of KO observation dates."""
1112
+ if self.barrier_config.ko_observation_schedule is not None:
1113
+ return len(self.barrier_config.ko_observation_schedule.records)
1114
+ if self.barrier_config.ko_observation_dates is not None:
1115
+ return len(self.barrier_config.ko_observation_dates)
1116
+ return 0
1117
+
1118
+ @property
1119
+ def is_standard(self) -> bool:
1120
+ """Check if this is a standard phoenix (not reverse)."""
1121
+ return not self.is_reverse
1122
+
1123
+ def get_ko_direction(self) -> BarrierType:
1124
+ """Get the direction of the KO barrier."""
1125
+ return BarrierType.DOWN_OUT if self.is_reverse else BarrierType.UP_OUT
1126
+
1127
+ def get_ki_direction(self) -> BarrierType:
1128
+ """Get the direction of the KI barrier."""
1129
+ return BarrierType.UP_IN if self.is_reverse else BarrierType.DOWN_IN
1130
+
1131
+ def _effective_annualized_flag(self, flag: Optional[bool]) -> bool:
1132
+ """Resolve specific annualized flag with product-level default."""
1133
+ if flag is None:
1134
+ return bool(self.accrual_config.is_annualized)
1135
+ return flag
1136
+
1137
+ def get_contract_tenor(self, pricing_env=None) -> float:
1138
+ """
1139
+ Get contract tenor in years.
1140
+
1141
+ Contract tenor is the time from initial date to exercise/maturity,
1142
+ used for annualized coupon and rebate calculations.
1143
+
1144
+ Args:
1145
+ pricing_env: Optional pricing environment for date-based tenor calculation
1146
+
1147
+ Returns:
1148
+ Contract tenor in years
1149
+ """
1150
+ # Use get_tenor from base class which handles various scenarios
1151
+ return self.get_tenor(pricing_env)
1152
+
1153
+ def intrinsic_value(self, spot: float) -> float:
1154
+ """
1155
+ Calculate intrinsic value of the embedded option.
1156
+
1157
+ For standard phoenix (PUT): max(strike - spot, 0)
1158
+ For reverse phoenix (CALL): max(spot - strike, 0)
1159
+ """
1160
+ if spot < 0:
1161
+ raise ValidationError(f"Spot must be non-negative, got {spot}")
1162
+
1163
+ if self.is_reverse:
1164
+ intrinsic = max(spot - self.strike, 0.0)
1165
+ else:
1166
+ intrinsic = max(self.strike - spot, 0.0)
1167
+ return intrinsic * self.contract_multiplier