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,420 @@
1
+ """
2
+ Single sharkfin option product definition.
3
+
4
+ Single sharkfin options are capped single-barrier structures. A call sharkfin
5
+ uses an upper knock-out barrier; a put sharkfin uses a lower knock-out barrier.
6
+ If the barrier is hit, the product pays a fixed knock-out rebate. Otherwise it
7
+ pays a capped vanilla-style payoff plus any no-hit rebate.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ from math import ceil
13
+ from typing import List, Optional, Sequence
14
+
15
+ from quantark.util.enum import (
16
+ ExerciseType,
17
+ ObservationAggregation,
18
+ ObservationFrequency,
19
+ ObservationType,
20
+ OptionType,
21
+ )
22
+ from quantark.util.exceptions import ValidationError
23
+ from quantark.util.numerical import is_close
24
+
25
+ from .base_equity_option import BaseEquityOption
26
+ from .observation_schedule import ObservationRecord, ObservationSchedule
27
+
28
+
29
+ @dataclass
30
+ class SingleSharkfinOption(BaseEquityOption):
31
+ """
32
+ Single sharkfin option with one knock-out barrier.
33
+
34
+ Payoff states:
35
+ 1. Barrier hit: knock_out_rebate
36
+ 2. Barrier not hit:
37
+ - CALL: no_hit_rebate + participation_rate * max(min(S, B) - K, 0)
38
+ - PUT: no_hit_rebate + participation_rate * max(K - max(S, B), 0)
39
+
40
+ Monitoring styles:
41
+ - EXPIRY: barrier checked only at terminal spot
42
+ - DISCRETE: barrier checked on observation dates, including daily schedules
43
+ - CONTINUOUS: barrier checked continuously by the pricing engine/path logic
44
+
45
+ Attributes:
46
+ strike: Strike price
47
+ option_type: CALL for upper-barrier sharkfin, PUT for lower-barrier sharkfin
48
+ barrier: Knock-out barrier level
49
+ maturity: Time to maturity in years
50
+ participation_rate: Participation in the capped vanilla payoff
51
+ knock_out_rebate: Fixed payoff if the barrier is hit
52
+ no_hit_rebate: Fixed payoff added when the barrier is not hit
53
+ pay_at_hit: If True, pay knock-out rebate immediately on hit;
54
+ otherwise pay it at expiry
55
+ observation_type: EXPIRY, DISCRETE, or CONTINUOUS monitoring
56
+ observation_dates: Legacy discrete observation times in years
57
+ observation_schedule: Preferred discrete observation schedule
58
+ observation_frequency: Frequency used when generating a regular schedule
59
+ contract_multiplier: Underlying units represented by one contract
60
+ """
61
+
62
+ barrier: float = 0.0
63
+ participation_rate: float = 1.0
64
+ knock_out_rebate: float = 0.0
65
+ no_hit_rebate: float = 0.0
66
+ pay_at_hit: bool = False
67
+ observation_type: ObservationType = ObservationType.CONTINUOUS
68
+ observation_dates: Optional[List[float]] = None
69
+ observation_schedule: Optional[ObservationSchedule] = None
70
+ observation_frequency: ObservationFrequency = ObservationFrequency.CUSTOM
71
+ use_business_days_for_frequency: bool = True
72
+ business_days_in_year: float = 252.0
73
+
74
+ def __init__(
75
+ self,
76
+ strike: float,
77
+ option_type: OptionType,
78
+ barrier: float,
79
+ maturity: Optional[float] = None,
80
+ exercise_date: Optional[datetime] = None,
81
+ settlement_date: Optional[datetime] = None,
82
+ participation_rate: float = 1.0,
83
+ knock_out_rebate: float = 0.0,
84
+ no_hit_rebate: float = 0.0,
85
+ pay_at_hit: bool = False,
86
+ observation_type: ObservationType = ObservationType.CONTINUOUS,
87
+ observation_dates: Optional[List[float]] = None,
88
+ observation_schedule: Optional[ObservationSchedule] = None,
89
+ observation_frequency: ObservationFrequency = ObservationFrequency.CUSTOM,
90
+ use_business_days_for_frequency: bool = True,
91
+ business_days_in_year: float = 252.0,
92
+ contract_multiplier: float = 1.0,
93
+ ):
94
+ """
95
+ Initialize a single sharkfin option.
96
+
97
+ Args:
98
+ strike: Strike price.
99
+ option_type: CALL for upper sharkfin or PUT for lower sharkfin.
100
+ barrier: Knock-out barrier level.
101
+ maturity: Time to maturity in years (optional if exercise_date provided).
102
+ exercise_date: Expiration date (optional if maturity provided).
103
+ settlement_date: Settlement date.
104
+ participation_rate: Participation in the capped no-hit payoff.
105
+ knock_out_rebate: Fixed payoff if barrier is hit.
106
+ no_hit_rebate: Fixed payoff added if barrier is not hit.
107
+ pay_at_hit: If True, knock-out rebate is paid immediately on hit.
108
+ If False, it is paid at expiry.
109
+ observation_type: EXPIRY, DISCRETE, or CONTINUOUS monitoring.
110
+ observation_dates: Discrete observation times in year fractions.
111
+ observation_schedule: Preferred discrete observation schedule.
112
+ observation_frequency: Frequency for generated discrete schedules.
113
+ use_business_days_for_frequency: Use business-day spacing for frequency.
114
+ business_days_in_year: Business-day count for generated schedules.
115
+ contract_multiplier: Underlying units represented by one contract.
116
+
117
+ Raises:
118
+ ValidationError: If parameters are invalid.
119
+ """
120
+ if maturity is None and exercise_date is None:
121
+ maturity = 0.0
122
+ elif maturity is None:
123
+ maturity = 0.0
124
+
125
+ self.barrier = barrier
126
+ self.participation_rate = participation_rate
127
+ self.knock_out_rebate = knock_out_rebate
128
+ self.no_hit_rebate = no_hit_rebate
129
+ self.pay_at_hit = pay_at_hit
130
+ self.observation_type = observation_type
131
+ self.observation_dates = observation_dates
132
+ self.observation_schedule = observation_schedule
133
+ self.observation_frequency = observation_frequency
134
+ self.use_business_days_for_frequency = use_business_days_for_frequency
135
+ self.business_days_in_year = business_days_in_year
136
+
137
+ super().__init__(
138
+ strike=strike,
139
+ option_type=option_type,
140
+ exercise_type=ExerciseType.EUROPEAN,
141
+ maturity=maturity,
142
+ exercise_date=exercise_date,
143
+ settlement_date=settlement_date,
144
+ contract_multiplier=contract_multiplier,
145
+ )
146
+
147
+ def validate(self) -> None:
148
+ """
149
+ Validate single sharkfin option parameters.
150
+
151
+ Raises:
152
+ ValidationError: If any parameter is invalid.
153
+ """
154
+ super().validate()
155
+
156
+ if self.barrier <= 0:
157
+ raise ValidationError(f"Barrier must be positive, got {self.barrier}")
158
+ if self.participation_rate < 0:
159
+ raise ValidationError(
160
+ f"Participation rate must be non-negative, got {self.participation_rate}"
161
+ )
162
+ if self.knock_out_rebate < 0:
163
+ raise ValidationError(
164
+ f"Knock-out rebate must be non-negative, got {self.knock_out_rebate}"
165
+ )
166
+ if self.no_hit_rebate < 0:
167
+ raise ValidationError(
168
+ f"No-hit rebate must be non-negative, got {self.no_hit_rebate}"
169
+ )
170
+ if not isinstance(self.pay_at_hit, bool):
171
+ raise ValidationError(f"pay_at_hit must be boolean, got {self.pay_at_hit}")
172
+ if not isinstance(self.observation_type, ObservationType):
173
+ raise ValidationError(f"Invalid observation type: {self.observation_type}")
174
+ if not isinstance(self.observation_frequency, ObservationFrequency):
175
+ raise ValidationError(
176
+ f"Invalid observation frequency: {self.observation_frequency}"
177
+ )
178
+ if self.business_days_in_year <= 0:
179
+ raise ValidationError(
180
+ f"business_days_in_year must be positive, got {self.business_days_in_year}"
181
+ )
182
+
183
+ self._validate_barrier_orientation()
184
+ self._normalize_observation_schedule()
185
+
186
+ def _validate_barrier_orientation(self) -> None:
187
+ """Validate the barrier is on the sharkfin side of the strike."""
188
+ if self.is_call() and self.barrier <= self.strike:
189
+ raise ValidationError(
190
+ "Call sharkfin requires an upper barrier above strike, "
191
+ f"got barrier={self.barrier}, strike={self.strike}"
192
+ )
193
+ if self.is_put() and self.barrier >= self.strike:
194
+ raise ValidationError(
195
+ "Put sharkfin requires a lower barrier below strike, "
196
+ f"got barrier={self.barrier}, strike={self.strike}"
197
+ )
198
+
199
+ def _normalize_observation_schedule(self) -> None:
200
+ """Normalize discrete monitoring inputs into ObservationSchedule."""
201
+ if self.observation_type in (ObservationType.CONTINUOUS, ObservationType.EXPIRY):
202
+ if self.observation_schedule is not None:
203
+ raise ValidationError(
204
+ "ObservationSchedule requires DISCRETE observation_type."
205
+ )
206
+ self.observation_dates = self.observation_dates or []
207
+ return
208
+
209
+ if self.observation_schedule is not None:
210
+ normalized_schedule = ObservationSchedule(
211
+ records=[
212
+ ObservationRecord(
213
+ observation_time=rec.observation_time,
214
+ observation_date=rec.observation_date,
215
+ barrier=rec.barrier if rec.barrier is not None else self.barrier,
216
+ payoff=(
217
+ rec.payoff
218
+ if rec.payoff is not None
219
+ else self.knock_out_rebate
220
+ ),
221
+ return_rate=rec.return_rate,
222
+ is_rate_annualized=rec.is_rate_annualized,
223
+ initial_date=rec.initial_date,
224
+ settlement_date=rec.settlement_date,
225
+ maturity_date=rec.maturity_date,
226
+ day_count_convention=rec.day_count_convention,
227
+ tenor_end=rec.tenor_end,
228
+ day_count_fraction=rec.day_count_fraction,
229
+ )
230
+ for rec in self.observation_schedule.records
231
+ ],
232
+ aggregation_mode=self.observation_schedule.aggregation_mode,
233
+ frequency=self.observation_schedule.frequency,
234
+ )
235
+ self._validate_observation_times(normalized_schedule.times)
236
+ normalized_schedule.validate(require_single=True)
237
+ self.observation_schedule = normalized_schedule
238
+ if self.observation_schedule.times:
239
+ self.observation_dates = self.observation_schedule.times
240
+ return
241
+
242
+ if self.observation_dates is None or len(self.observation_dates) == 0:
243
+ self.observation_dates = self._generate_observation_dates()
244
+ self._validate_observation_times(self.observation_dates)
245
+
246
+ self.observation_schedule = ObservationSchedule.from_legacy(
247
+ observation_dates=self.observation_dates,
248
+ default_barrier=self.barrier,
249
+ default_payoff=self.knock_out_rebate,
250
+ aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
251
+ frequency=self.observation_frequency,
252
+ )
253
+ self.observation_dates = self.observation_schedule.times
254
+
255
+ def _validate_observation_times(self, times: List[float]) -> None:
256
+ """Validate numeric observation times when present."""
257
+ if any(t < 0 for t in times):
258
+ raise ValidationError("Observation dates must be non-negative.")
259
+ if times != sorted(times):
260
+ raise ValidationError("Observation dates must be sorted in ascending order.")
261
+
262
+ def _generate_observation_dates(self) -> List[float]:
263
+ """
264
+ Generate regular discrete observation times from observation_frequency.
265
+
266
+ Returns:
267
+ Observation times in years.
268
+
269
+ Raises:
270
+ ValidationError: If a schedule cannot be generated.
271
+ """
272
+ if self.observation_frequency == ObservationFrequency.CUSTOM:
273
+ raise ValidationError(
274
+ "Observation dates required for discrete sharkfin monitoring "
275
+ "when observation_frequency is CUSTOM."
276
+ )
277
+ if self.maturity is None or self.maturity <= 0:
278
+ raise ValidationError(
279
+ "Positive maturity is required to generate discrete observations."
280
+ )
281
+
282
+ days_in_year = (
283
+ self.business_days_in_year
284
+ if self.use_business_days_for_frequency
285
+ else 365.0
286
+ )
287
+ dt = self.observation_frequency.to_year_fraction(
288
+ use_business_days=self.use_business_days_for_frequency,
289
+ days_in_year=days_in_year,
290
+ )
291
+ if dt <= 0:
292
+ raise ValidationError(f"Observation frequency produced invalid dt={dt}")
293
+
294
+ count = max(1, int(ceil(self.maturity / dt)))
295
+ times = [min((idx + 1) * dt, self.maturity) for idx in range(count)]
296
+ if not is_close(times[-1], self.maturity):
297
+ times.append(self.maturity)
298
+ return times
299
+
300
+ def is_barrier_hit(self, spot: float) -> bool:
301
+ """
302
+ Check whether a single observed spot hits the sharkfin barrier.
303
+
304
+ Args:
305
+ spot: Observed underlying price.
306
+
307
+ Returns:
308
+ True if the barrier is hit.
309
+ """
310
+ if spot < 0:
311
+ raise ValidationError(f"Spot price must be non-negative, got {spot}")
312
+
313
+ if self.is_call():
314
+ return spot >= self.barrier
315
+ return spot <= self.barrier
316
+
317
+ def has_barrier_hit(self, path: Sequence[float]) -> bool:
318
+ """
319
+ Check whether any observed spot in a path hits the barrier.
320
+
321
+ Args:
322
+ path: Sequence of observed underlying prices.
323
+
324
+ Returns:
325
+ True if any observation hits the barrier.
326
+ """
327
+ if len(path) == 0:
328
+ raise ValidationError("Path must contain at least one observed spot.")
329
+ return any(self.is_barrier_hit(spot) for spot in path)
330
+
331
+ def get_no_hit_payoff(self, spot: float) -> float:
332
+ """
333
+ Calculate the capped no-hit payoff.
334
+
335
+ Args:
336
+ spot: Terminal underlying price.
337
+
338
+ Returns:
339
+ Payoff assuming the barrier was not hit.
340
+ """
341
+ if spot < 0:
342
+ raise ValidationError(f"Spot price must be non-negative, got {spot}")
343
+
344
+ if self.is_call():
345
+ capped_spot = min(spot, self.barrier)
346
+ intrinsic = max(capped_spot - self.strike, 0.0)
347
+ else:
348
+ capped_spot = max(spot, self.barrier)
349
+ intrinsic = max(self.strike - capped_spot, 0.0)
350
+
351
+ payoff = self.no_hit_rebate + self.participation_rate * intrinsic
352
+ return payoff * self.contract_multiplier
353
+
354
+ def get_barrier_payoff(self) -> float:
355
+ """
356
+ Calculate payoff when the sharkfin barrier has been hit.
357
+
358
+ Returns:
359
+ Knock-out rebate scaled by contract multiplier.
360
+ """
361
+ return self.knock_out_rebate * self.contract_multiplier
362
+
363
+ def get_payoff(self, spot: float, barrier_hit: Optional[bool] = None) -> float:
364
+ """
365
+ Calculate the terminal sharkfin payoff.
366
+
367
+ Args:
368
+ spot: Terminal underlying price.
369
+ barrier_hit: Optional observed barrier state. When omitted, terminal
370
+ spot is used to infer a hit. Pricing engines for DISCRETE or
371
+ CONTINUOUS monitoring should pass the path-derived barrier state.
372
+
373
+ Returns:
374
+ Sharkfin payoff scaled by contract multiplier.
375
+ """
376
+ if spot < 0:
377
+ raise ValidationError(f"Spot price must be non-negative, got {spot}")
378
+
379
+ if barrier_hit is None:
380
+ barrier_hit = self.is_barrier_hit(spot)
381
+
382
+ if barrier_hit:
383
+ return self.get_barrier_payoff()
384
+ return self.get_no_hit_payoff(spot)
385
+
386
+ def get_observation_times(self) -> List[float]:
387
+ """
388
+ Get discrete observation times.
389
+
390
+ Returns:
391
+ Observation times in years. Empty for EXPIRY or CONTINUOUS monitoring.
392
+ """
393
+ if self.observation_schedule is not None:
394
+ return self.observation_schedule.times
395
+ return self.observation_dates or []
396
+
397
+ def time_shift(self, time_bump: float, bumped_date: datetime, pricing_env) -> bool:
398
+ """Shift observation schedule and maturity for theta bumping."""
399
+ schedule = getattr(self, "observation_schedule", None)
400
+ if schedule is not None:
401
+ if schedule.uses_dates():
402
+ pricing_env.valuation_date = bumped_date
403
+ bumped_schedule = schedule.time_shift(time_bump, bumped_date)
404
+ if bumped_schedule is None:
405
+ return True
406
+ self.observation_schedule = bumped_schedule
407
+ if bumped_schedule.uses_times():
408
+ self.observation_dates = bumped_schedule.times
409
+
410
+ if getattr(self, "exercise_date", None) is None and self.maturity is not None:
411
+ self.maturity -= time_bump
412
+
413
+ return False
414
+
415
+ def __repr__(self) -> str:
416
+ return (
417
+ "SingleSharkfinOption("
418
+ f"{self.option_type}, K={self.strike:.2f}, "
419
+ f"B={self.barrier:.2f}, T={self.maturity:.4f})"
420
+ )
@@ -0,0 +1,261 @@
1
+ """
2
+ Configuration classes for Snowball (autocallable) options.
3
+
4
+ These classes group related parameters to simplify the SnowballOption API.
5
+ """
6
+
7
+ from dataclasses import dataclass, replace
8
+ from datetime import datetime
9
+ from typing import List, Optional, Union
10
+
11
+ from quantark.util.enum import (
12
+ ObservationType,
13
+ CouponPayType,
14
+ ProtectionType,
15
+ )
16
+ from .observation_schedule import ObservationSchedule
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class BarrierConfig:
21
+ """
22
+ Configuration for knock-out (KO) and knock-in (KI) barriers.
23
+
24
+ Attributes:
25
+ ko_barrier: Knock-out barrier level(s), up barrier by default
26
+ ko_rate: Knock-out return rate(s)
27
+ ko_observation_type: DISCRETE or CONTINUOUS monitoring for KO
28
+ ko_observation_dates: Year fractions for KO observations (legacy)
29
+ ko_observation_schedule: ObservationSchedule for KO (preferred)
30
+ ki_barrier: Optional knock-in barrier level(s), down barrier by default
31
+ ki_observation_type: DISCRETE or CONTINUOUS monitoring for KI
32
+ ki_observation_dates: Year fractions for KI observations (legacy)
33
+ ki_observation_schedule: ObservationSchedule for KI (preferred)
34
+ ki_continuous: If True, KI monitored continuously
35
+ disable_ko_after_ki: If True, disable KO after KI is triggered
36
+ """
37
+
38
+ # Knock-out barrier (required)
39
+ ko_barrier: Union[float, List[float]]
40
+ ko_rate: Union[float, List[float]]
41
+ ko_observation_type: ObservationType = ObservationType.DISCRETE
42
+ ko_observation_dates: Optional[List[float]] = None
43
+ ko_observation_schedule: Optional[ObservationSchedule] = None
44
+
45
+ # Knock-in barrier (optional)
46
+ ki_barrier: Optional[Union[float, List[float]]] = None
47
+ ki_observation_type: ObservationType = ObservationType.DISCRETE
48
+ ki_observation_dates: Optional[List[float]] = None
49
+ ki_observation_schedule: Optional[ObservationSchedule] = None
50
+ ki_continuous: bool = False
51
+
52
+ # Interaction
53
+ disable_ko_after_ki: bool = False
54
+
55
+ def __post_init__(self):
56
+ """Validate configuration after initialization."""
57
+ # Validate ko_barrier is positive
58
+ self._validate_barrier_positive(self.ko_barrier, "ko_barrier")
59
+
60
+ # Validate ki_barrier is positive if provided
61
+ if self.ki_barrier is not None:
62
+ self._validate_barrier_positive(self.ki_barrier, "ki_barrier")
63
+
64
+ # Validate observation types are enums
65
+ if not isinstance(self.ko_observation_type, ObservationType):
66
+ raise ValueError(f"ko_observation_type must be ObservationType, got {type(self.ko_observation_type)}")
67
+ if not isinstance(self.ki_observation_type, ObservationType):
68
+ raise ValueError(f"ki_observation_type must be ObservationType, got {type(self.ki_observation_type)}")
69
+
70
+ def time_shift(self, time_bump: float, bumped_date, pricing_env) -> tuple["BarrierConfig", bool]:
71
+ """
72
+ Shift KO/KI observation schedules and legacy dates for theta bumps.
73
+
74
+ Returns a new BarrierConfig and a flag indicating if all observations were dropped.
75
+ """
76
+ dropped_all = False
77
+
78
+ ko_schedule = self.ko_observation_schedule
79
+ if ko_schedule is not None:
80
+ if ko_schedule.uses_dates():
81
+ pricing_env.valuation_date = bumped_date
82
+ ko_schedule = ko_schedule.time_shift(time_bump, bumped_date)
83
+ if ko_schedule is None:
84
+ dropped_all = True
85
+
86
+ ki_schedule = self.ki_observation_schedule
87
+ if ki_schedule is not None:
88
+ if ki_schedule.uses_dates():
89
+ pricing_env.valuation_date = bumped_date
90
+ ki_schedule = ki_schedule.time_shift(time_bump, bumped_date)
91
+ if ki_schedule is None:
92
+ dropped_all = True
93
+
94
+ ko_dates = self.ko_observation_dates
95
+ if ko_schedule is not None and ko_schedule.uses_times():
96
+ ko_dates = ko_schedule.times
97
+ elif ko_schedule is None and ko_dates:
98
+ ko_dates = [t - time_bump for t in ko_dates if t - time_bump > 0]
99
+ if not ko_dates:
100
+ dropped_all = True
101
+
102
+ ki_dates = self.ki_observation_dates
103
+ if ki_schedule is not None and ki_schedule.uses_times():
104
+ ki_dates = ki_schedule.times
105
+ elif ki_schedule is None and ki_dates:
106
+ ki_dates = [t - time_bump for t in ki_dates if t - time_bump > 0]
107
+ if not ki_dates:
108
+ dropped_all = True
109
+
110
+ return (
111
+ replace(
112
+ self,
113
+ ko_observation_schedule=ko_schedule,
114
+ ki_observation_schedule=ki_schedule,
115
+ ko_observation_dates=ko_dates,
116
+ ki_observation_dates=ki_dates,
117
+ ),
118
+ dropped_all,
119
+ )
120
+
121
+ @staticmethod
122
+ def _validate_barrier_positive(barrier: Union[float, List[float]], name: str) -> None:
123
+ """Validate that barrier level(s) are positive."""
124
+ if isinstance(barrier, list):
125
+ if not barrier:
126
+ raise ValueError(f"{name} list cannot be empty")
127
+ for i, b in enumerate(barrier):
128
+ if not isinstance(b, (int, float)) or b <= 0:
129
+ raise ValueError(f"{name}[{i}] must be positive number, got {b}")
130
+ else:
131
+ if not isinstance(barrier, (int, float)) or barrier <= 0:
132
+ raise ValueError(f"{name} must be positive number, got {barrier}")
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class PayoffConfig:
137
+ """
138
+ Configuration for payoff features (rebate, protection, participation).
139
+
140
+ Attributes:
141
+ rebate_rate: Fixed rebate rate for V0 maturity payoff
142
+ call_rebate_enabled: If True, use call-style rebate instead of fixed
143
+ call_strike: Strike for call rebate
144
+ call_participation_rate: Participation rate for call rebate
145
+ include_principal: Whether principal is part of payouts
146
+ participation_rate: Downside participation rate after KI
147
+ protection_type: NONE, PARTIAL, or FULL protection
148
+ protection_rate: Rate for partial protection floor
149
+ """
150
+
151
+ # Rebate (V0 maturity payoff)
152
+ rebate_rate: float = 0.0
153
+ call_rebate_enabled: bool = False
154
+ call_strike: Optional[float] = None
155
+ call_participation_rate: float = 1.0
156
+
157
+ # Participation and protection (V1 maturity payoff)
158
+ include_principal: bool = True
159
+ participation_rate: float = 1.0
160
+ protection_type: ProtectionType = ProtectionType.NONE
161
+ protection_rate: float = 0.0
162
+
163
+ def __post_init__(self):
164
+ """Validate configuration after initialization."""
165
+ # Validate participation rate is positive
166
+ if self.participation_rate <= 0:
167
+ raise ValueError(f"participation_rate must be positive, got {self.participation_rate}")
168
+
169
+ # Validate call rebate parameters if enabled
170
+ if self.call_rebate_enabled:
171
+ if self.call_strike is None:
172
+ raise ValueError("call_strike required when call_rebate_enabled is True")
173
+ if self.call_strike <= 0:
174
+ raise ValueError(f"call_strike must be positive, got {self.call_strike}")
175
+ if self.call_participation_rate <= 0:
176
+ raise ValueError(f"call_participation_rate must be positive, got {self.call_participation_rate}")
177
+
178
+ # Validate protection type is enum
179
+ if not isinstance(self.protection_type, ProtectionType):
180
+ raise ValueError(f"protection_type must be ProtectionType, got {type(self.protection_type)}")
181
+
182
+ # Validate protection rate for partial protection
183
+ if self.protection_type == ProtectionType.PARTIAL:
184
+ if not 0 <= self.protection_rate <= 1:
185
+ raise ValueError(f"protection_rate must be in [0, 1], got {self.protection_rate}")
186
+
187
+
188
+ @dataclass(frozen=True)
189
+ class AccrualConfig:
190
+ """
191
+ Configuration for accrual and coupon payment settings.
192
+
193
+ Attributes:
194
+ coupon_pay_type: INSTANT (at KO date) or EXPIRY (discounted to maturity)
195
+ is_annualized: Backward-compatible flag for annualized accruals (default for all)
196
+ is_annualized_ko: If True, KO return accrues with year fraction
197
+ is_annualized_ki: If True, KI return accrues with year fraction
198
+ is_annualized_rebate: If True, rebate accrues with year fraction
199
+ accrual_dates: Calendar dates for annualized coupon calculation
200
+ accrual_factors: Optional externally supplied accrual factors by
201
+ KO/coupon observation
202
+ """
203
+
204
+ coupon_pay_type: CouponPayType = CouponPayType.INSTANT
205
+ is_annualized: bool = True
206
+ is_annualized_ko: Optional[bool] = None
207
+ is_annualized_ki: Optional[bool] = None
208
+ is_annualized_rebate: Optional[bool] = None
209
+ accrual_dates: Optional[List[datetime]] = None
210
+ accrual_factors: Optional[List[float]] = None
211
+
212
+ def __post_init__(self):
213
+ """Validate configuration after initialization."""
214
+ # Validate coupon_pay_type is enum
215
+ if not isinstance(self.coupon_pay_type, CouponPayType):
216
+ raise ValueError(f"coupon_pay_type must be CouponPayType, got {type(self.coupon_pay_type)}")
217
+ if self.accrual_factors is not None:
218
+ if not isinstance(self.accrual_factors, list):
219
+ raise ValueError(
220
+ f"accrual_factors must be a list, got {type(self.accrual_factors)}"
221
+ )
222
+ for i, factor in enumerate(self.accrual_factors):
223
+ if isinstance(factor, bool) or not isinstance(factor, (int, float)):
224
+ raise ValueError(
225
+ f"accrual_factors[{i}] must be numeric, got {factor}"
226
+ )
227
+ if factor < 0:
228
+ raise ValueError(
229
+ f"accrual_factors[{i}] must be non-negative, got {factor}"
230
+ )
231
+
232
+ @dataclass(frozen=True)
233
+ class AirbagConfig:
234
+ """
235
+ Configuration for airbag features.
236
+
237
+ Attributes:
238
+ airbag_barrier: Barrier level for airbag protection.
239
+ If spot > airbag_barrier, principal is fully protected (subject to other terms).
240
+ If spot < airbag_barrier, investor participates in downside.
241
+ airbag_participation_rate: Participation rate when spot < airbag_barrier (default: 1.0).
242
+ airbag_strike: Strike price for airbag payoff calculation (optional).
243
+ If None, defaults to the product's strike.
244
+ """
245
+
246
+ airbag_barrier: Optional[float] = None
247
+ airbag_participation_rate: float = 1.0
248
+ airbag_strike: Optional[float] = None
249
+
250
+ def __post_init__(self):
251
+ """Validate configuration after initialization."""
252
+ if self.airbag_barrier is not None:
253
+ if not isinstance(self.airbag_barrier, (int, float)) or self.airbag_barrier <= 0:
254
+ raise ValueError(f"airbag_barrier must be a positive number, got {self.airbag_barrier}")
255
+
256
+ if self.airbag_participation_rate <= 0:
257
+ raise ValueError(f"airbag_participation_rate must be positive, got {self.airbag_participation_rate}")
258
+
259
+ if self.airbag_strike is not None:
260
+ if not isinstance(self.airbag_strike, (int, float)) or self.airbag_strike <= 0:
261
+ raise ValueError(f"airbag_strike must be a positive number, got {self.airbag_strike}")