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