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,1199 @@
1
+ """
2
+ Parametric VaR engine using variance-covariance approach.
3
+ """
4
+
5
+ import time
6
+ from datetime import datetime
7
+ from typing import Dict, List, Optional, Union
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from scipy import stats
12
+
13
+ from quantark.portfolio.equity.portfolio import EquityPortfolio
14
+ from quantark.portfolio.fi.portfolio import FIPortfolio
15
+ from quantark.util.exceptions import ValidationError, MarketDataError
16
+ from quantark.var.results import IncrementalVaRResult, VaRResult
17
+ from quantark.var.config import VaRConfig, VaRMethod, EquityRiskFactorConfig
18
+ from quantark.var.risk_factors import (
19
+ SpotReturnFactor,
20
+ VolChangeFactor,
21
+ RateShiftFactor,
22
+ DivYieldShiftFactor,
23
+ )
24
+
25
+
26
+ class ParametricVaREngine:
27
+ """
28
+ Parametric Value-at-Risk engine using variance-covariance approach.
29
+
30
+ The Parametric VaR engine calculates VaR using portfolio sensitivities
31
+ (Greeks for equity, DV01 for fixed income) and the historical covariance
32
+ matrix of risk factors. This is also known as the variance-covariance method
33
+ or the sensitivity-based method.
34
+
35
+ Key Features:
36
+ - Uses portfolio sensitivities (delta, gamma, vega, rho, DV01)
37
+ - Leverages historical covariance matrix of risk factors
38
+ - Fastest calculation method (closed-form solutions)
39
+ - Supports both DataFrame and MarketDataSet inputs
40
+ - Works with equity and fixed income portfolios
41
+ - Supports Component, Marginal, Factor, Incremental, and Stressed VaR
42
+ - Supports Fixed Income risk factors (parallel shift, key rates)
43
+
44
+ Mathematical Foundation:
45
+ VaR = z_score * sqrt(s^T * Σ * s)
46
+ where:
47
+ - s = sensitivity vector (Greeks/DV01)
48
+ - Σ = covariance matrix of risk factors
49
+ - z_score = inverse CDF of normal distribution at confidence level
50
+
51
+ Advantages:
52
+ - Fastest calculation (scalable to very large portfolios)
53
+ - Closed-form Greeks support
54
+ - Well-suited for linear portfolios
55
+ - Real-time risk monitoring
56
+ - Efficient for backtesting
57
+ - Industry standard for equity and FI trading
58
+
59
+ Disadvantages:
60
+ - Assumes linear relationship (or approximations for non-linear)
61
+ - Distributional assumptions (normally distributed returns)
62
+ - Limited accuracy for options and derivatives
63
+ - Requires reliable Greeks calculations
64
+ - May not capture fat tails
65
+
66
+ Performance:
67
+ - Calculation time: O(f^3) for covariance matrix inversion, O(p*f) for sensitivities
68
+ - Memory usage: O(f^2) for covariance matrix storage
69
+ - Suitable for portfolios with 100,000+ positions
70
+ - Excellent for real-time risk monitoring
71
+
72
+ Use Cases:
73
+ - Large equity portfolios (delta, gamma, vega monitoring)
74
+ - Fixed income portfolios (DV01, convexity monitoring)
75
+ - Real-time P&L attribution
76
+ - Stress testing with sensitivity shocks
77
+ - Regulatory reporting (sensitivity-based)
78
+ - Risk decomposition and attribution
79
+
80
+ Examples:
81
+ Basic parametric VaR:
82
+ >>> from var import VaRConfig, ParametricVaREngine
83
+ >>> config = VaRConfig(confidence_level=0.99)
84
+ >>> engine = ParametricVaREngine(config=config)
85
+ >>> result = engine.calculate_var(portfolio, risk_factors)
86
+
87
+ Equity with options:
88
+ >>> from var.config import EquityRiskFactorConfig
89
+ >>> config = VaRConfig(
90
+ ... confidence_level=0.99,
91
+ ... equity_factors=EquityRiskFactorConfig(
92
+ ... include_spot=True,
93
+ ... include_vol=True,
94
+ ... include_rate=True
95
+ ... )
96
+ ... )
97
+ >>> engine = ParametricVaREngine(config=config)
98
+ >>> result = engine.calculate_var(equity_portfolio, data)
99
+
100
+ Fixed Income with key rates:
101
+ >>> from var.config import FIRiskFactorConfig
102
+ >>> config = VaRConfig(
103
+ ... confidence_level=0.99,
104
+ ... fi_factors=FIRiskFactorConfig(
105
+ ... include_parallel_shift=True,
106
+ ... include_key_rates=True,
107
+ ... key_rate_tenors=[2.0, 5.0, 10.0, 30.0]
108
+ ... )
109
+ ... )
110
+ >>> engine = ParametricVaREngine(config=config)
111
+ >>> result = engine.calculate_var(fi_portfolio, fi_data)
112
+
113
+ With attribution:
114
+ >>> config = VaRConfig(
115
+ ... confidence_level=0.99,
116
+ ... calculate_component_var=True,
117
+ ... calculate_marginal_var=True,
118
+ ... calculate_incremental_var=True
119
+ ... )
120
+ >>> engine = ParametricVaREngine(config=config)
121
+ >>> result = engine.calculate_var(portfolio, data)
122
+
123
+ References:
124
+ - RiskMetrics Group. "RiskMetrics™ Technical Document"
125
+ - Basel Committee. "The Internal Ratings-Based Approach"
126
+ - Jorion, P. "Value at Risk: The New Benchmark for Managing Financial Risk"
127
+ """
128
+
129
+ def __init__(self, config: Optional[VaRConfig] = None):
130
+ """
131
+ Initialize parametric VaR engine.
132
+
133
+ Args:
134
+ config: VaR configuration (defaults to VaRConfig())
135
+ """
136
+ self.config = config if config is not None else VaRConfig()
137
+
138
+ if self.config.var_method != VaRMethod.PARAMETRIC:
139
+ self.config.var_method = VaRMethod.PARAMETRIC
140
+
141
+ def supports_portfolio(self, portfolio: any) -> bool:
142
+ """Check if engine supports the portfolio type."""
143
+ return isinstance(portfolio, (EquityPortfolio, FIPortfolio))
144
+
145
+ def calculate_var(
146
+ self,
147
+ portfolio: Union[EquityPortfolio, FIPortfolio],
148
+ historical_data: Union[any, pd.DataFrame],
149
+ ) -> VaRResult:
150
+ """
151
+ Calculate parametric VaR for the portfolio.
152
+
153
+ Args:
154
+ portfolio: Portfolio object
155
+ historical_data: Historical market data (MarketDataSet or DataFrame)
156
+
157
+ Returns:
158
+ VaRResult with VaR metrics
159
+
160
+ Raises:
161
+ ValidationError: If portfolio is empty or inputs invalid
162
+ MarketDataError: If historical data insufficient
163
+ """
164
+ start_time = time.time()
165
+
166
+ if len(portfolio.positions) == 0:
167
+ raise ValidationError("Cannot calculate VaR for empty portfolio")
168
+
169
+ if isinstance(portfolio, EquityPortfolio):
170
+ result = self._calculate_equity_var(portfolio, historical_data)
171
+ elif isinstance(portfolio, FIPortfolio):
172
+ result = self._calculate_fi_var(portfolio, historical_data)
173
+ else:
174
+ raise ValidationError(
175
+ f"Unsupported portfolio type: {type(portfolio).__name__}"
176
+ )
177
+
178
+ result.execution_time_seconds = time.time() - start_time
179
+ result.config_summary = {
180
+ "confidence_level": self.config.confidence_level,
181
+ "holding_period": self.config.holding_period,
182
+ "lookback_days": self.config.lookback_days,
183
+ "method": str(self.config.var_method),
184
+ }
185
+
186
+ return result
187
+
188
+ def _calculate_equity_var(
189
+ self,
190
+ portfolio: EquityPortfolio,
191
+ historical_data: Union[any, pd.DataFrame],
192
+ ) -> VaRResult:
193
+ """Calculate parametric VaR for equity portfolio."""
194
+ if isinstance(historical_data, pd.DataFrame):
195
+ risk_factors_df = self._extract_risk_factors_from_dataframe(
196
+ historical_data, is_equity=True
197
+ )
198
+ else:
199
+ risk_factors_df = self._extract_risk_factors_from_market_data(
200
+ historical_data, is_equity=True
201
+ )
202
+
203
+ if len(risk_factors_df) < self.config.lookback_days:
204
+ raise MarketDataError(
205
+ f"Insufficient historical data: {len(risk_factors_df)} days "
206
+ f"available, {self.config.lookback_days} required"
207
+ )
208
+
209
+ risk_factors_df = risk_factors_df.tail(self.config.lookback_days)
210
+
211
+ sensitivities = self._compute_equity_sensitivities(portfolio)
212
+
213
+ cov_matrix = risk_factors_df.cov().values
214
+
215
+ sensitivity_vector = np.array(list(sensitivities.values()))
216
+
217
+ portfolio_variance = sensitivity_vector @ cov_matrix @ sensitivity_vector
218
+ portfolio_std = np.sqrt(portfolio_variance)
219
+
220
+ if self.config.holding_period > 1:
221
+ if self.config.scaling_method == "sqrt_t":
222
+ portfolio_std *= np.sqrt(self.config.holding_period)
223
+
224
+ z_score = stats.norm.ppf(self.config.confidence_level)
225
+ var = z_score * portfolio_std
226
+
227
+ cvar = (
228
+ portfolio_std * stats.norm.pdf(z_score) / (1 - self.config.confidence_level)
229
+ )
230
+
231
+ portfolio_value = portfolio.get_portfolio_value()
232
+
233
+ result = VaRResult(
234
+ var=abs(var),
235
+ cvar=abs(cvar),
236
+ confidence_level=self.config.confidence_level,
237
+ holding_period=self.config.holding_period,
238
+ method=VaRMethod.PARAMETRIC,
239
+ portfolio_value=portfolio_value,
240
+ var_as_pct=abs(var) / portfolio_value if portfolio_value != 0 else 0.0,
241
+ )
242
+
243
+ if self.config.calculate_factor_var:
244
+ result.factor_var = self._compute_factor_var(
245
+ sensitivity_vector,
246
+ cov_matrix,
247
+ list(sensitivities.keys()),
248
+ portfolio_std,
249
+ )
250
+
251
+ # Calculate component VaR if enabled
252
+ if self.config.calculate_component_var:
253
+ result.component_var = self._calculate_component_var(
254
+ portfolio, risk_factors_df, sensitivities, cov_matrix
255
+ )
256
+
257
+ # Calculate marginal VaR if enabled
258
+ if self.config.calculate_marginal_var:
259
+ result.marginal_var = self._calculate_marginal_var(
260
+ portfolio, risk_factors_df, sensitivities, cov_matrix
261
+ )
262
+
263
+ # Calculate Stressed VaR if enabled
264
+ if self.config.calculate_stressed_var:
265
+ # For parametric VaR, we use stressed volatility multipliers
266
+ # Simplified: apply stress factor to covariance matrix
267
+ stress_multiplier = 1.5 # 50% increase in volatility as stress
268
+
269
+ stressed_cov_matrix = cov_matrix * stress_multiplier
270
+
271
+ # Recalculate portfolio variance with stressed covariance
272
+ stressed_variance = (
273
+ sensitivity_vector @ stressed_cov_matrix @ sensitivity_vector
274
+ )
275
+ stressed_std = np.sqrt(stressed_variance)
276
+
277
+ if self.config.holding_period > 1:
278
+ if self.config.scaling_method == "sqrt_t":
279
+ stressed_std *= np.sqrt(self.config.holding_period)
280
+
281
+ z_score = stats.norm.ppf(self.config.confidence_level)
282
+ stressed_var = z_score * stressed_std
283
+ stressed_cvar = (
284
+ stressed_std
285
+ * stats.norm.pdf(z_score)
286
+ / (1 - self.config.confidence_level)
287
+ )
288
+
289
+ # Store stressed VaR
290
+ result.stressed_var = abs(stressed_var)
291
+ result.stressed_cvar = abs(stressed_cvar)
292
+
293
+ # Store stressed period (using entire lookback as stressed)
294
+ result.stressed_period = {
295
+ "start_date": risk_factors_df.index.min(),
296
+ "end_date": risk_factors_df.index.max(),
297
+ }
298
+
299
+ # Calculate Incremental VaR if enabled
300
+ if self.config.calculate_incremental_var:
301
+ result.incremental_var = self._calculate_incremental_var(
302
+ portfolio, risk_factors_df, cov_matrix, portfolio_std
303
+ )
304
+
305
+ return result
306
+
307
+ def _calculate_fi_var(
308
+ self,
309
+ portfolio: FIPortfolio,
310
+ historical_data: Union[any, pd.DataFrame],
311
+ ) -> VaRResult:
312
+ """Calculate parametric VaR for FI portfolio."""
313
+ if isinstance(historical_data, pd.DataFrame):
314
+ risk_factors_df = self._extract_risk_factors_from_dataframe(
315
+ historical_data, is_equity=False
316
+ )
317
+ else:
318
+ risk_factors_df = self._extract_risk_factors_from_market_data(
319
+ historical_data, is_equity=False
320
+ )
321
+
322
+ if len(risk_factors_df) < self.config.lookback_days:
323
+ raise MarketDataError(
324
+ f"Insufficient historical data: {len(risk_factors_df)} days "
325
+ f"available, {self.config.lookback_days} required"
326
+ )
327
+
328
+ risk_factors_df = risk_factors_df.tail(self.config.lookback_days)
329
+
330
+ sensitivities = self._compute_fi_sensitivities(portfolio)
331
+
332
+ cov_matrix = risk_factors_df.cov().values
333
+
334
+ sensitivity_vector = np.array(list(sensitivities.values()))
335
+
336
+ portfolio_variance = sensitivity_vector @ cov_matrix @ sensitivity_vector
337
+ portfolio_std = np.sqrt(portfolio_variance)
338
+
339
+ if self.config.holding_period > 1:
340
+ if self.config.scaling_method == "sqrt_t":
341
+ portfolio_std *= np.sqrt(self.config.holding_period)
342
+
343
+ z_score = stats.norm.ppf(self.config.confidence_level)
344
+ var = z_score * portfolio_std
345
+
346
+ cvar = (
347
+ portfolio_std * stats.norm.pdf(z_score) / (1 - self.config.confidence_level)
348
+ )
349
+
350
+ portfolio_value = portfolio.get_portfolio_value()
351
+
352
+ result = VaRResult(
353
+ var=abs(var),
354
+ cvar=abs(cvar),
355
+ confidence_level=self.config.confidence_level,
356
+ holding_period=self.config.holding_period,
357
+ method=VaRMethod.PARAMETRIC,
358
+ portfolio_value=portfolio_value,
359
+ var_as_pct=abs(var) / portfolio_value if portfolio_value != 0 else 0.0,
360
+ )
361
+
362
+ if self.config.calculate_factor_var:
363
+ result.factor_var = self._compute_factor_var(
364
+ sensitivity_vector,
365
+ cov_matrix,
366
+ list(sensitivities.keys()),
367
+ portfolio_std,
368
+ )
369
+
370
+ # Calculate component VaR if enabled
371
+ if self.config.calculate_component_var:
372
+ result.component_var = self._calculate_component_var(
373
+ portfolio, risk_factors_df, sensitivities, cov_matrix
374
+ )
375
+
376
+ # Calculate marginal VaR if enabled
377
+ if self.config.calculate_marginal_var:
378
+ result.marginal_var = self._calculate_marginal_var(
379
+ portfolio, risk_factors_df, sensitivities, cov_matrix
380
+ )
381
+
382
+ # Calculate Stressed VaR if enabled
383
+ if self.config.calculate_stressed_var:
384
+ # For parametric VaR, we use stressed volatility multipliers
385
+ # Simplified: apply stress factor to covariance matrix
386
+ stress_multiplier = 1.5 # 50% increase in volatility as stress
387
+
388
+ stressed_cov_matrix = cov_matrix * stress_multiplier
389
+
390
+ # Recalculate portfolio variance with stressed covariance
391
+ stressed_variance = (
392
+ sensitivity_vector @ stressed_cov_matrix @ sensitivity_vector
393
+ )
394
+ stressed_std = np.sqrt(stressed_variance)
395
+
396
+ if self.config.holding_period > 1:
397
+ if self.config.scaling_method == "sqrt_t":
398
+ stressed_std *= np.sqrt(self.config.holding_period)
399
+
400
+ z_score = stats.norm.ppf(self.config.confidence_level)
401
+ stressed_var = z_score * stressed_std
402
+ stressed_cvar = (
403
+ stressed_std
404
+ * stats.norm.pdf(z_score)
405
+ / (1 - self.config.confidence_level)
406
+ )
407
+
408
+ # Store stressed VaR
409
+ result.stressed_var = abs(stressed_var)
410
+ result.stressed_cvar = abs(stressed_cvar)
411
+
412
+ # Store stressed period (using entire lookback as stressed)
413
+ result.stressed_period = {
414
+ "start_date": risk_factors_df.index.min(),
415
+ "end_date": risk_factors_df.index.max(),
416
+ }
417
+
418
+ # Calculate Incremental VaR if enabled
419
+ if self.config.calculate_incremental_var:
420
+ result.incremental_var = self._calculate_incremental_var(
421
+ portfolio, risk_factors_df, cov_matrix, portfolio_std
422
+ )
423
+
424
+ return result
425
+
426
+ def _extract_risk_factors_from_dataframe(
427
+ self, df: pd.DataFrame, is_equity: bool
428
+ ) -> pd.DataFrame:
429
+ """Extract risk factors from DataFrame."""
430
+ if is_equity:
431
+ factors_config = self.config.equity_factors or EquityRiskFactorConfig()
432
+
433
+ risk_factors = {}
434
+
435
+ if factors_config.include_spot:
436
+ factor = SpotReturnFactor()
437
+ risk_factors["spot_return"] = factor.extract_from_dataframe(df)
438
+
439
+ if factors_config.include_vol:
440
+ factor = VolChangeFactor()
441
+ risk_factors["vol_change"] = factor.extract_from_dataframe(df)
442
+
443
+ if factors_config.include_rate:
444
+ factor = RateShiftFactor()
445
+ risk_factors["rate_shift"] = factor.extract_from_dataframe(df)
446
+
447
+ if factors_config.include_div_yield:
448
+ factor = DivYieldShiftFactor()
449
+ risk_factors["div_yield_shift"] = factor.extract_from_dataframe(df)
450
+
451
+ return pd.DataFrame(risk_factors)
452
+ else:
453
+ # Fixed Income risk factors
454
+ from quantark.var.config import FIRiskFactorConfig
455
+ from quantark.var.risk_factors.fi_factors import (
456
+ ParallelShiftFactor,
457
+ KeyRateShiftFactor,
458
+ )
459
+
460
+ factors_config = self.config.fi_factors or FIRiskFactorConfig()
461
+
462
+ risk_factors = {}
463
+
464
+ # Parallel shift factor (most important for FI)
465
+ if factors_config.include_parallel_shift:
466
+ factor = ParallelShiftFactor()
467
+ try:
468
+ risk_factors["parallel_shift"] = factor.extract_from_dataframe(df)
469
+ except ValueError as e:
470
+ # If no parallel_shift column, try rate column
471
+ if "rate" in df.columns:
472
+ risk_factors["parallel_shift"] = df["rate"].diff().dropna()
473
+
474
+ # Key rate factors (optional, more sophisticated)
475
+ if factors_config.include_key_rates:
476
+ key_rate_factor = KeyRateShiftFactor(
477
+ tenors=factors_config.key_rate_tenors
478
+ )
479
+ try:
480
+ key_rate_shifts = key_rate_factor.extract_from_dataframe(df)
481
+ # Add key rate shifts to risk factors
482
+ for col in key_rate_shifts.columns:
483
+ risk_factors[col] = key_rate_shifts[col]
484
+ except ValueError as e:
485
+ # If key rate columns don't exist, skip
486
+ pass
487
+
488
+ # Return risk factors DataFrame
489
+ if not risk_factors:
490
+ raise ValueError(
491
+ "No valid FI risk factors found. Check that DataFrame contains "
492
+ "required columns: 'parallel_shift' or 'rate' for parallel shifts, "
493
+ "and 'rate_Xy' for key rate shifts."
494
+ )
495
+
496
+ return pd.DataFrame(risk_factors)
497
+
498
+ def _extract_risk_factors_from_market_data(
499
+ self, market_data: any, is_equity: bool
500
+ ) -> pd.DataFrame:
501
+ """
502
+ Extract risk factors from MarketDataSet.
503
+
504
+ Converts MarketDataSet containing spot, vol, rate, and dividend time series
505
+ into a DataFrame with returns and changes suitable for covariance calculation
506
+ in parametric VaR.
507
+
508
+ Args:
509
+ market_data: MarketDataSet with historical time series
510
+ is_equity: Whether this is for equity portfolio (vs fixed income)
511
+
512
+ Returns:
513
+ DataFrame with columns: spot_return, vol_change, rate_shift, div_yield_shift
514
+
515
+ Raises:
516
+ MarketDataError: If market data is invalid or insufficient
517
+ """
518
+ from quantark.util.exceptions import MarketDataError
519
+
520
+ # Align all time series to common date range
521
+ aligned_data = market_data.align_dates()
522
+
523
+ # Convert to DataFrames
524
+ spot_df = aligned_data.spot_data.to_dataframe()
525
+ vol_df = aligned_data.vol_data.to_dataframe()
526
+ rate_df = aligned_data.rate_data.to_dataframe()
527
+
528
+ # Calculate spot returns (percentage change)
529
+ spot_returns = spot_df["spot"].pct_change().dropna()
530
+
531
+ # Calculate volatility changes (absolute change)
532
+ vol_changes = vol_df["volatility"].diff().dropna()
533
+
534
+ # Calculate rate shifts (absolute change)
535
+ rate_shifts = rate_df["rate"].diff().dropna()
536
+
537
+ # Calculate dividend yield shifts if available
538
+ div_yield_shifts = pd.Series(dtype=float, index=spot_returns.index)
539
+ if aligned_data.div_yield_data is not None:
540
+ div_df = aligned_data.div_yield_data.to_dataframe()
541
+ div_yield_shifts = div_df["div_yield"].diff().dropna()
542
+
543
+ # Align all series to common index
544
+ common_index = spot_returns.index.intersection(vol_changes.index)
545
+ common_index = common_index.intersection(rate_shifts.index)
546
+ if len(div_yield_shifts) > 0:
547
+ common_index = common_index.intersection(div_yield_shifts.index)
548
+
549
+ if len(common_index) == 0:
550
+ raise MarketDataError("No common dates across all risk factor series")
551
+
552
+ # Create risk factors DataFrame
553
+ risk_factors = pd.DataFrame(index=common_index)
554
+ risk_factors["spot_return"] = spot_returns[common_index]
555
+ risk_factors["vol_change"] = vol_changes[common_index]
556
+ risk_factors["rate_shift"] = rate_shifts[common_index]
557
+
558
+ if len(div_yield_shifts) > 0:
559
+ risk_factors["div_yield_shift"] = div_yield_shifts[common_index]
560
+ else:
561
+ risk_factors["div_yield_shift"] = 0.0
562
+
563
+ # For parametric VaR, we need sufficient data points
564
+ # Filter to only include lookback_days
565
+ risk_factors = risk_factors.tail(self.config.lookback_days)
566
+
567
+ # Drop any remaining NaN values
568
+ risk_factors = risk_factors.dropna()
569
+
570
+ if len(risk_factors) == 0:
571
+ raise MarketDataError(
572
+ "No valid risk factors after processing. Check data quality."
573
+ )
574
+
575
+ if len(risk_factors) < 30:
576
+ raise MarketDataError(
577
+ f"Insufficient data for parametric VaR: {len(risk_factors)} days, "
578
+ f"minimum 30 days required for stable covariance estimation"
579
+ )
580
+
581
+ return risk_factors
582
+
583
+ def _compute_equity_sensitivities(
584
+ self, portfolio: EquityPortfolio
585
+ ) -> Dict[str, float]:
586
+ """Compute portfolio-level sensitivities for equity."""
587
+ from quantark.asset.equity.riskmeasures import GreeksCalculator
588
+
589
+ calculator = GreeksCalculator()
590
+ factors_config = self.config.equity_factors or EquityRiskFactorConfig()
591
+
592
+ sensitivities = {}
593
+
594
+ if factors_config.include_spot:
595
+ total_delta = 0.0
596
+ for position in portfolio.positions.values():
597
+ pricing_env = portfolio.pricing_environments[position.underlying]
598
+ greeks = calculator.calculate_analytical_greeks(
599
+ position.product, pricing_env
600
+ )
601
+ total_delta += greeks["delta"] * position.quantity * pricing_env.spot
602
+ sensitivities["spot_return"] = total_delta
603
+
604
+ if factors_config.include_vol:
605
+ total_vega = 0.0
606
+ for position in portfolio.positions.values():
607
+ pricing_env = portfolio.pricing_environments[position.underlying]
608
+ greeks = calculator.calculate_analytical_greeks(
609
+ position.product, pricing_env
610
+ )
611
+ total_vega += greeks["vega"] * position.quantity
612
+ sensitivities["vol_change"] = total_vega
613
+
614
+ if factors_config.include_rate:
615
+ total_rho = 0.0
616
+ for position in portfolio.positions.values():
617
+ pricing_env = portfolio.pricing_environments[position.underlying]
618
+ greeks = calculator.calculate_analytical_greeks(
619
+ position.product, pricing_env
620
+ )
621
+ total_rho += greeks["rho"] * position.quantity
622
+ sensitivities["rate_shift"] = total_rho
623
+
624
+ if factors_config.include_div_yield:
625
+ total_psi = 0.0
626
+ for position in portfolio.positions.values():
627
+ pricing_env = portfolio.pricing_environments[position.underlying]
628
+ psi = self._calculate_div_yield_sensitivity(
629
+ position.product, pricing_env
630
+ )
631
+ total_psi += psi * position.quantity
632
+ sensitivities["div_yield_shift"] = total_psi
633
+
634
+ return sensitivities
635
+
636
+ def _calculate_div_yield_sensitivity(
637
+ self, product: any, pricing_env: any, bump_size: float = 0.0001
638
+ ) -> float:
639
+ """
640
+ Calculate dividend yield sensitivity (psi) using finite difference.
641
+
642
+ Args:
643
+ product: Option product
644
+ pricing_env: Pricing environment
645
+ bump_size: Bump size for dividend yield (default: 1 bp = 0.0001)
646
+
647
+ Returns:
648
+ Psi: $ change per 1bp change in dividend yield
649
+ """
650
+ from quantark.asset.equity.engine.analytical import BlackScholesEngine
651
+ from quantark.param.div_yield import ContinuousDividendYield
652
+ from quantark.priceenv.pricing_environment import PricingEnvironment
653
+
654
+ engine = BlackScholesEngine()
655
+ base_price = engine.price(product, pricing_env)
656
+
657
+ # Bump dividend yield up
658
+ original_div = pricing_env.div_yield
659
+ T = product.get_maturity(pricing_env)
660
+ base_div_yield = pricing_env.get_div_yield(T)
661
+
662
+ bumped_div_yield = ContinuousDividendYield(base_div_yield + bump_size)
663
+ bumped_env = PricingEnvironment(
664
+ rate_curve=pricing_env.rate_curve,
665
+ valuation_date=pricing_env.valuation_date,
666
+ spot_quote=pricing_env.spot_quote,
667
+ vol_surface=pricing_env.vol_surface,
668
+ div_yield=bumped_div_yield,
669
+ )
670
+
671
+ bumped_price = engine.price(product, bumped_env)
672
+
673
+ # Sensitivity per bump_size change
674
+ psi = (bumped_price - base_price) / bump_size
675
+
676
+ return psi
677
+
678
+ def _compute_fi_sensitivities(self, portfolio: FIPortfolio) -> Dict[str, float]:
679
+ """Compute portfolio-level sensitivities for FI."""
680
+ sensitivities = {}
681
+
682
+ total_dv01 = portfolio.get_portfolio_dv01()
683
+ sensitivities["parallel_shift"] = total_dv01
684
+
685
+ return sensitivities
686
+
687
+ def _compute_factor_var(
688
+ self,
689
+ sensitivity_vector: np.ndarray,
690
+ cov_matrix: np.ndarray,
691
+ factor_names: List[str],
692
+ portfolio_std: float,
693
+ ) -> Dict[str, float]:
694
+ """Compute VaR attribution by risk factor using correlation-based approach."""
695
+ # Factor VaR = |Correlation(Factor, Portfolio)| × Portfolio VaR
696
+ # This uses the actual covariance to calculate meaningful factor contributions
697
+
698
+ z_score = stats.norm.ppf(self.config.confidence_level)
699
+ portfolio_var_result = z_score * portfolio_std
700
+
701
+ factor_var = {}
702
+
703
+ # Calculate portfolio P&L as weighted sum of factor returns
704
+ # For each factor, calculate its standalone variance and correlation with portfolio
705
+ for i, factor_name in enumerate(factor_names):
706
+ # Factor variance
707
+ factor_var_i = cov_matrix[i, i]
708
+
709
+ # Factor's standalone VaR (in return units)
710
+ factor_std = np.sqrt(factor_var_i)
711
+ factor_var_return = z_score * factor_std
712
+
713
+ # Portfolio return from this factor only: sensitivity × factor_return
714
+ # Correlation between this factor and portfolio:
715
+ # corr = Cov(factor_return, portfolio_return) / (std(factor) × std(portfolio))
716
+ # Cov(factor_return, portfolio_return) = sensitivity_factor × factor_var
717
+ # std(portfolio) = portfolio_std (already calculated)
718
+
719
+ if factor_std > 0 and portfolio_std > 0 and sensitivity_vector[i] != 0:
720
+ # Calculate correlation between factor and portfolio
721
+ covariance_factor_portfolio = sensitivity_vector[i] * factor_var_i
722
+ correlation = covariance_factor_portfolio / (factor_std * portfolio_std)
723
+ correlation = abs(correlation) # VaR is always positive
724
+ else:
725
+ correlation = 0
726
+
727
+ # Factor VaR contribution
728
+ factor_var_result = correlation * portfolio_var_result
729
+ factor_var[factor_name] = factor_var_result
730
+
731
+ return factor_var
732
+
733
+ def _calculate_component_var(
734
+ self,
735
+ portfolio: Union[EquityPortfolio, FIPortfolio],
736
+ risk_factors_df: pd.DataFrame,
737
+ factor_sensitivities: Dict[str, float],
738
+ cov_matrix: np.ndarray,
739
+ ) -> Dict[str, float]:
740
+ """
741
+ Calculate component VaR for each position using Euler decomposition.
742
+
743
+ Uses the formula: Component VaR_i = Cov(P&L_i, P&L_portfolio) / Var(P&L_portfolio) * Portfolio VaR
744
+
745
+ This implementation converts dollar sensitivities to return sensitivities
746
+ to match the covariance matrix units (returns).
747
+
748
+ Args:
749
+ portfolio: Portfolio to analyze
750
+ risk_factors_df: Historical risk factor data
751
+ factor_sensitivities: Portfolio-level factor sensitivities (dollar units)
752
+ cov_matrix: Covariance matrix of risk factors (return units)
753
+
754
+ Returns:
755
+ Dictionary mapping position ID to component VaR
756
+ """
757
+ from scipy import stats
758
+
759
+ # Calculate portfolio value to convert dollar sensitivities to return sensitivities
760
+ portfolio_value = portfolio.get_portfolio_value()
761
+
762
+ # Convert dollar sensitivities to return sensitivities by dividing by portfolio value
763
+ factor_names = list(factor_sensitivities.keys())
764
+ return_sensitivities = {
765
+ factor: factor_sensitivities[factor] / portfolio_value
766
+ for factor in factor_names
767
+ }
768
+
769
+ # Calculate portfolio P&L as return × portfolio value
770
+ factor_returns = risk_factors_df[factor_names].values
771
+ return_vector = np.array(
772
+ [return_sensitivities[factor] for factor in factor_names]
773
+ )
774
+ portfolio_return = factor_returns @ return_vector
775
+ portfolio_pnl = portfolio_return * portfolio_value
776
+
777
+ # Calculate position P&L for each scenario
778
+ position_pnls = {}
779
+ if isinstance(portfolio, EquityPortfolio):
780
+ from quantark.asset.equity.riskmeasures import GreeksCalculator
781
+
782
+ calculator = GreeksCalculator()
783
+
784
+ for pos_id, position in portfolio.positions.items():
785
+ pricing_env = portfolio.pricing_environments[position.underlying]
786
+ # Get position market value
787
+ option_price = position.engine.price(position.product, pricing_env)
788
+ pos_value = option_price * position.quantity
789
+
790
+ # Get position return sensitivity (delta)
791
+ greeks = calculator.calculate_analytical_greeks(
792
+ position.product, pricing_env
793
+ )
794
+ pos_return_sensitivity = greeks["delta"]
795
+
796
+ # Calculate P&L for each scenario
797
+ pos_return = (
798
+ factor_returns[:, 0] * pos_return_sensitivity
799
+ ) # Use spot_return
800
+ pos_pnl = pos_return * pos_value
801
+ position_pnls[pos_id] = pos_pnl
802
+
803
+ # Calculate portfolio variance and VaR
804
+ portfolio_var = np.var(portfolio_pnl, ddof=1)
805
+ portfolio_std = np.sqrt(portfolio_var)
806
+ z_score = stats.norm.ppf(self.config.confidence_level)
807
+ portfolio_var_result = z_score * portfolio_std
808
+
809
+ # Calculate Component VaR using Euler decomposition
810
+ # For parametric VaR: Component VaR_i = (sensitivity_i / total_sensitivity) × Portfolio VaR
811
+ # where sensitivity_i is the dollar sensitivity (delta × spot × quantity)
812
+ component_var = {}
813
+ if portfolio_var > 0:
814
+ # Get total portfolio sensitivity (dollar units)
815
+ total_sensitivity = sum(
816
+ [factor_sensitivities[factor] for factor in factor_names]
817
+ )
818
+
819
+ # Calculate position-level dollar sensitivities
820
+ position_dollar_sensitivities = {}
821
+ if isinstance(portfolio, EquityPortfolio):
822
+ from quantark.asset.equity.riskmeasures import GreeksCalculator
823
+
824
+ calculator = GreeksCalculator()
825
+
826
+ for pos_id, position in portfolio.positions.items():
827
+ pricing_env = portfolio.pricing_environments[position.underlying]
828
+ greeks = calculator.calculate_analytical_greeks(
829
+ position.product, pricing_env
830
+ )
831
+ # Position dollar sensitivity (delta × spot × quantity)
832
+ pos_dollar_sensitivity = (
833
+ greeks["delta"] * pricing_env.spot * position.quantity
834
+ )
835
+ position_dollar_sensitivities[pos_id] = pos_dollar_sensitivity
836
+
837
+ # Calculate Component VaR
838
+ if abs(total_sensitivity) > 0:
839
+ for pos_id in position_pnls.keys():
840
+ pos_sensitivity = position_dollar_sensitivities.get(pos_id, 0.0)
841
+ # Component VaR = (sensitivity_i / total_sensitivity) × Portfolio VaR
842
+ # This gives signed Component VaR (can be negative for short positions)
843
+ component_var_result = (
844
+ pos_sensitivity / total_sensitivity
845
+ ) * portfolio_var_result
846
+ component_var[pos_id] = component_var_result
847
+ else:
848
+ # No sensitivity, distribute equally
849
+ num_positions = len(position_pnls)
850
+ for pos_id in position_pnls.keys():
851
+ component_var[pos_id] = portfolio_var_result / num_positions
852
+ else:
853
+ # No variance, assign zero
854
+ for pos_id in position_pnls.keys():
855
+ component_var[pos_id] = 0.0
856
+
857
+ return component_var
858
+
859
+ def _calculate_marginal_var(
860
+ self,
861
+ portfolio: Union[EquityPortfolio, FIPortfolio],
862
+ risk_factors_df: pd.DataFrame,
863
+ sensitivities: Dict[str, float],
864
+ cov_matrix: np.ndarray,
865
+ ) -> Dict[str, float]:
866
+ """
867
+ Calculate marginal VaR for each position.
868
+
869
+ Args:
870
+ portfolio: Portfolio to analyze
871
+ risk_factors_df: Historical risk factor data
872
+ sensitivities: Portfolio-level sensitivities
873
+ cov_matrix: Covariance matrix of risk factors
874
+
875
+ Returns:
876
+ Dictionary mapping position ID to marginal VaR
877
+ """
878
+ # Calculate portfolio volatility
879
+ sensitivity_vector = np.array(list(sensitivities.values()))
880
+ portfolio_variance = sensitivity_vector @ cov_matrix @ sensitivity_vector
881
+ portfolio_std = np.sqrt(portfolio_variance)
882
+
883
+ # Calculate marginal VaR for each position
884
+ marginal_var = {}
885
+ for pos_id, position in portfolio.positions.items():
886
+ pricing_env = portfolio.pricing_environments[position.underlying]
887
+
888
+ # Get position market value
889
+ if hasattr(position.product, "strike"):
890
+ # Option: use pricing engine
891
+ option_price = position.engine.price(position.product, pricing_env)
892
+ pos_value = option_price * position.quantity
893
+ else:
894
+ # Stock: spot × quantity
895
+ pos_value = pricing_env.spot * position.quantity
896
+
897
+ # Get position-level sensitivity
898
+ from quantark.asset.equity.riskmeasures import GreeksCalculator
899
+
900
+ calculator = GreeksCalculator()
901
+ greeks = calculator.calculate_analytical_greeks(
902
+ position.product, pricing_env
903
+ )
904
+ pos_sensitivity = greeks["delta"] * pricing_env.spot
905
+
906
+ # Marginal VaR ≈ Component VaR for parametric method
907
+ # Use the same formula as Component VaR
908
+ total_sensitivity = sum(sensitivities.values())
909
+ if abs(total_sensitivity) > 0:
910
+ marg_var = abs(pos_sensitivity / total_sensitivity) * (
911
+ portfolio_std * stats.norm.ppf(self.config.confidence_level)
912
+ )
913
+ else:
914
+ marg_var = 0.0
915
+
916
+ marginal_var[pos_id] = marg_var
917
+
918
+ return marginal_var
919
+
920
+ def _detect_stressed_period(
921
+ self, scenarios: pd.DataFrame, window_size: int = 252
922
+ ) -> Dict[str, datetime]:
923
+ """
924
+ Detect the highest volatility period in the scenarios.
925
+
926
+ Uses rolling volatility to identify the most stressful 12-month
927
+ (or specified window) period in the historical data.
928
+
929
+ Args:
930
+ scenarios: DataFrame of scenarios with risk factor returns
931
+ window_size: Size of rolling window in days (default 252)
932
+
933
+ Returns:
934
+ Dictionary with 'start_date' and 'end_date' of stressed period
935
+ """
936
+ if len(scenarios) < window_size:
937
+ # Not enough data, return entire period
938
+ return {
939
+ "start_date": scenarios.index.min(),
940
+ "end_date": scenarios.index.max(),
941
+ }
942
+
943
+ # Calculate portfolio-level volatility
944
+ # For single column, use that column directly
945
+ # For multiple columns, use equal-weighted combination
946
+ if len(scenarios.columns) == 1:
947
+ # Single risk factor - use it directly
948
+ scenario_volatility = scenarios.iloc[:, 0]
949
+ else:
950
+ # Multiple risk factors - use equal-weighted std
951
+ # Calculate std across columns for each row, then average
952
+ scenario_volatility = scenarios.std(axis=1)
953
+
954
+ # Calculate rolling volatility
955
+ rolling_vol = scenario_volatility.rolling(window=window_size).std()
956
+
957
+ # Drop NaN values before finding max
958
+ rolling_vol_clean = rolling_vol.dropna()
959
+
960
+ if len(rolling_vol_clean) == 0:
961
+ # All values are NaN, return entire period
962
+ return {
963
+ "start_date": scenarios.index.min(),
964
+ "end_date": scenarios.index.max(),
965
+ }
966
+
967
+ # Find the window with maximum volatility
968
+ max_vol_idx = rolling_vol_clean.idxmax()
969
+ max_vol_date = pd.Timestamp(max_vol_idx)
970
+
971
+ # Calculate start and end dates of the stressed period
972
+ end_date = max_vol_date
973
+ start_date = end_date - pd.Timedelta(days=window_size - 1)
974
+
975
+ # Ensure dates are within the scenario range
976
+ min_date = scenarios.index.min()
977
+ max_date = scenarios.index.max()
978
+
979
+ if start_date < min_date:
980
+ start_date = min_date
981
+ if end_date > max_date:
982
+ end_date = max_date
983
+
984
+ return {"start_date": start_date, "end_date": end_date}
985
+
986
+ def _calculate_incremental_var(
987
+ self,
988
+ portfolio: Union[EquityPortfolio, FIPortfolio],
989
+ risk_factors_df: pd.DataFrame,
990
+ cov_matrix: np.ndarray,
991
+ portfolio_std: float,
992
+ ) -> Dict[str, float]:
993
+ """
994
+ Calculate Incremental VaR for each position using parametric approach.
995
+
996
+ For parametric VaR, Incremental VaR is calculated using the formula:
997
+ IVaR_i = (∂VaR/∂x_i) = (cov_matrix @ sensitivity_vector / portfolio_std)[i] * z_score
998
+
999
+ This is derived from the Euler decomposition of the risk measure.
1000
+
1001
+ Args:
1002
+ portfolio: Portfolio to analyze
1003
+ risk_factors_df: Historical risk factor data
1004
+ cov_matrix: Covariance matrix of risk factors
1005
+ portfolio_std: Portfolio standard deviation
1006
+
1007
+ Returns:
1008
+ Dictionary mapping position ID to Incremental VaR
1009
+ """
1010
+ position_ivar = {}
1011
+
1012
+ # Calculate marginal contributions for each position
1013
+ z_score = stats.norm.ppf(self.config.confidence_level)
1014
+
1015
+ # Calculate full portfolio sensitivity vector
1016
+ factor_sensitivities = self._compute_equity_sensitivities(portfolio)
1017
+ sensitivity_vector = np.array(list(factor_sensitivities.values()))
1018
+
1019
+ # Pad sensitivity vector to match covariance matrix dimensions
1020
+ if len(sensitivity_vector) < cov_matrix.shape[0]:
1021
+ # Pad with zeros for uncalculated factors
1022
+ padded_vector = np.zeros(cov_matrix.shape[0])
1023
+ padded_vector[: len(sensitivity_vector)] = sensitivity_vector
1024
+ sensitivity_vector = padded_vector
1025
+
1026
+ # Calculate marginal VaR components
1027
+ if portfolio_std > 0:
1028
+ marginal_contrib = (
1029
+ (cov_matrix @ sensitivity_vector) / portfolio_std * z_score
1030
+ )
1031
+ else:
1032
+ marginal_contrib = np.zeros_like(sensitivity_vector)
1033
+
1034
+ # For parametric VaR, Incremental VaR ≈ Marginal VaR
1035
+ # Distribute based on position weights
1036
+ total_position_value = sum(
1037
+ [
1038
+ abs(
1039
+ position.get_portfolio_value()
1040
+ if hasattr(position, "get_portfolio_value")
1041
+ else position.quantity * 100
1042
+ ) # Fallback to quantity × spot
1043
+ for position in portfolio.positions.values()
1044
+ ]
1045
+ )
1046
+
1047
+ for i, pos_id in enumerate(portfolio.positions.keys()):
1048
+ position = portfolio.positions[pos_id]
1049
+ pos_value = abs(
1050
+ position.get_portfolio_value()
1051
+ if hasattr(position, "get_portfolio_value")
1052
+ else position.quantity * 100
1053
+ )
1054
+ pos_weight = (
1055
+ pos_value / total_position_value if total_position_value > 0 else 0
1056
+ )
1057
+
1058
+ # Allocate portfolio-level marginal contribution to this position
1059
+ position_ivar[pos_id] = abs(sum(marginal_contrib) * pos_weight)
1060
+
1061
+ return position_ivar
1062
+
1063
+ def calculate_incremental_var(
1064
+ self,
1065
+ portfolio: Union[EquityPortfolio, FIPortfolio],
1066
+ historical_data: Union[any, pd.DataFrame],
1067
+ ) -> "IncrementalVaRResult":
1068
+ """
1069
+ Calculate Incremental VaR for the portfolio using parametric approach.
1070
+
1071
+ Args:
1072
+ portfolio: Portfolio object
1073
+ historical_data: Historical market data
1074
+
1075
+ Returns:
1076
+ IncrementalVaRResult with position-level IVaR analysis
1077
+
1078
+ Raises:
1079
+ ValidationError: If portfolio is empty
1080
+ MarketDataError: If insufficient historical data
1081
+ """
1082
+ from quantark.var.results.incremental_var_result import IncrementalVaRResult
1083
+
1084
+ # Calculate full portfolio VaR first
1085
+ full_var_result = self.calculate_var(portfolio, historical_data)
1086
+ full_var = full_var_result.var
1087
+
1088
+ # Get risk factors
1089
+ if isinstance(historical_data, pd.DataFrame):
1090
+ if isinstance(portfolio, EquityPortfolio):
1091
+ risk_factors_df = self._extract_risk_factors_from_dataframe(
1092
+ historical_data, is_equity=True
1093
+ )
1094
+ else:
1095
+ risk_factors_df = self._extract_risk_factors_from_dataframe(
1096
+ historical_data, is_equity=False
1097
+ )
1098
+ else:
1099
+ if isinstance(portfolio, EquityPortfolio):
1100
+ risk_factors_df = self._extract_risk_factors_from_market_data(
1101
+ historical_data, is_equity=True
1102
+ )
1103
+ else:
1104
+ risk_factors_df = self._extract_risk_factors_from_market_data(
1105
+ historical_data, is_equity=False
1106
+ )
1107
+
1108
+ risk_factors_df = risk_factors_df.tail(self.config.lookback_days)
1109
+ cov_matrix = risk_factors_df.cov().values
1110
+
1111
+ # Calculate portfolio standard deviation
1112
+ if isinstance(portfolio, EquityPortfolio):
1113
+ sensitivities = self._compute_equity_sensitivities(portfolio)
1114
+ else:
1115
+ sensitivities = self._compute_fi_sensitivities(portfolio)
1116
+
1117
+ sensitivity_vector = np.array(list(sensitivities.values()))
1118
+ portfolio_variance = sensitivity_vector @ cov_matrix @ sensitivity_vector
1119
+ portfolio_std = np.sqrt(portfolio_variance)
1120
+
1121
+ # Calculate Incremental VaR for each position
1122
+ position_ivar = self._calculate_incremental_var(
1123
+ portfolio, risk_factors_df, cov_matrix, portfolio_std
1124
+ )
1125
+
1126
+ # Calculate VaR without each position for reporting
1127
+ var_without_dict = {}
1128
+ for pos_id in portfolio.positions.keys():
1129
+ # Create portfolio without this position
1130
+ portfolio_without = self._create_portfolio_without_position(
1131
+ portfolio, pos_id
1132
+ )
1133
+
1134
+ if len(portfolio_without.positions) > 0:
1135
+ # Calculate VaR without this position
1136
+ if isinstance(portfolio_without, EquityPortfolio):
1137
+ sensitivities_without = self._compute_equity_sensitivities(
1138
+ portfolio_without
1139
+ )
1140
+ else:
1141
+ sensitivities_without = self._compute_fi_sensitivities(
1142
+ portfolio_without
1143
+ )
1144
+
1145
+ if len(sensitivities_without) > 0:
1146
+ sensitivity_vector_without = np.array(
1147
+ list(sensitivities_without.values())
1148
+ )
1149
+ variance_without = (
1150
+ sensitivity_vector_without
1151
+ @ cov_matrix
1152
+ @ sensitivity_vector_without
1153
+ )
1154
+ std_without = np.sqrt(variance_without)
1155
+
1156
+ if self.config.holding_period > 1:
1157
+ if self.config.scaling_method == "sqrt_t":
1158
+ std_without *= np.sqrt(self.config.holding_period)
1159
+
1160
+ z_score = stats.norm.ppf(self.config.confidence_level)
1161
+ var_without = z_score * std_without
1162
+ var_without_dict[pos_id] = abs(var_without)
1163
+
1164
+ # Calculate diversification benefit
1165
+ total_individual_var = sum(position_ivar.values())
1166
+ diversification_benefit = total_individual_var - full_var
1167
+
1168
+ # Create result
1169
+ result = IncrementalVaRResult(
1170
+ portfolio_var=full_var,
1171
+ position_ivari=position_ivar,
1172
+ diversification_benefit=diversification_benefit,
1173
+ portfolio_var_without_position=var_without_dict,
1174
+ ivari_method="Parametric",
1175
+ config=self.config.__dict__,
1176
+ )
1177
+
1178
+ return result
1179
+
1180
+ def _create_portfolio_without_position(
1181
+ self, portfolio: Union[EquityPortfolio, FIPortfolio], exclude_pos_id: str
1182
+ ) -> Union[EquityPortfolio, FIPortfolio]:
1183
+ """
1184
+ Create a copy of portfolio without a specific position.
1185
+
1186
+ Args:
1187
+ portfolio: Original portfolio
1188
+ exclude_pos_id: Position ID to exclude
1189
+
1190
+ Returns:
1191
+ New portfolio without the position
1192
+ """
1193
+ import copy
1194
+
1195
+ new_portfolio = copy.deepcopy(portfolio)
1196
+ if exclude_pos_id in new_portfolio.positions:
1197
+ del new_portfolio.positions[exclude_pos_id]
1198
+
1199
+ return new_portfolio