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.
- quantark/__init__.py +3 -0
- quantark/_compat.py +150 -0
- quantark/asset/__init__.py +8 -0
- quantark/asset/bond/__init__.py +2 -0
- quantark/asset/bond/engine/__init__.py +44 -0
- quantark/asset/bond/engine/analytical/__init__.py +12 -0
- quantark/asset/bond/engine/analytical/black_engine.py +583 -0
- quantark/asset/bond/engine/analytical/bond_forward_engine.py +390 -0
- quantark/asset/bond/engine/analytical/bond_futures_engine.py +569 -0
- quantark/asset/bond/engine/convertible/__init__.py +12 -0
- quantark/asset/bond/engine/convertible/convertible_bond_engine.py +800 -0
- quantark/asset/bond/engine/discount/__init__.py +10 -0
- quantark/asset/bond/engine/discount/bond_discount_engine.py +517 -0
- quantark/asset/bond/engine/discount/frn_engine.py +913 -0
- quantark/asset/bond/engine/pde/__init__.py +14 -0
- quantark/asset/bond/engine/pde/convertible/__init__.py +21 -0
- quantark/asset/bond/engine/pde/convertible/jump_diffusion_engine.py +603 -0
- quantark/asset/bond/engine/pde/convertible/pde_params.py +59 -0
- quantark/asset/bond/engine/pde/convertible/tf_engine.py +546 -0
- quantark/asset/bond/engine/tree/__init__.py +14 -0
- quantark/asset/bond/engine/tree/convertible/__init__.py +21 -0
- quantark/asset/bond/engine/tree/convertible/binomial_engine.py +488 -0
- quantark/asset/bond/engine/tree/convertible/tree_params.py +72 -0
- quantark/asset/bond/engine/tree/convertible/trinomial_engine.py +1341 -0
- quantark/asset/bond/product/__init__.py +37 -0
- quantark/asset/bond/product/base_bond_product.py +114 -0
- quantark/asset/bond/product/convertible/__init__.py +16 -0
- quantark/asset/bond/product/convertible/convertible_bond.py +595 -0
- quantark/asset/bond/product/couponbond/__init__.py +12 -0
- quantark/asset/bond/product/couponbond/fixed_bond.py +285 -0
- quantark/asset/bond/product/couponbond/frn.py +538 -0
- quantark/asset/bond/product/forward/__init__.py +9 -0
- quantark/asset/bond/product/forward/base_bond_forward.py +92 -0
- quantark/asset/bond/product/forward/bond_forward.py +335 -0
- quantark/asset/bond/product/futures/__init__.py +8 -0
- quantark/asset/bond/product/futures/bond_futures.py +532 -0
- quantark/asset/bond/product/option/__init__.py +9 -0
- quantark/asset/bond/product/option/euro_short_term_bond_option.py +231 -0
- quantark/asset/bond/riskmeasures/__init__.py +13 -0
- quantark/asset/bond/riskmeasures/bond_greeks_calculator.py +484 -0
- quantark/asset/bond/schedule/__init__.py +21 -0
- quantark/asset/bond/schedule/cashflow.py +595 -0
- quantark/asset/equity/__init__.py +11 -0
- quantark/asset/equity/analysis/__init__.py +4 -0
- quantark/asset/equity/analysis/autocallable_path_analyzer.py +257 -0
- quantark/asset/equity/engine/__init__.py +84 -0
- quantark/asset/equity/engine/analytical/__init__.py +37 -0
- quantark/asset/equity/engine/analytical/american_option_engine.py +682 -0
- quantark/asset/equity/engine/analytical/asian_option_analytical_engine.py +1102 -0
- quantark/asset/equity/engine/analytical/barrier_analytical_engine.py +455 -0
- quantark/asset/equity/engine/analytical/black_scholes_engine.py +322 -0
- quantark/asset/equity/engine/analytical/deltaone_engine.py +340 -0
- quantark/asset/equity/engine/analytical/digital_option_engine.py +168 -0
- quantark/asset/equity/engine/analytical/double_barrier_option_engine.py +481 -0
- quantark/asset/equity/engine/analytical/double_sharkfin_option_analytical_engine.py +508 -0
- quantark/asset/equity/engine/analytical/one_touch_analytical_engine.py +302 -0
- quantark/asset/equity/engine/analytical/range_accrual_analytical_engine.py +396 -0
- quantark/asset/equity/engine/analytical/single_sharkfin_option_analytical_engine.py +229 -0
- quantark/asset/equity/engine/base_engine.py +137 -0
- quantark/asset/equity/engine/event_stats.py +85 -0
- quantark/asset/equity/engine/mc/__init__.py +31 -0
- quantark/asset/equity/engine/mc/american_option_mc_engine.py +485 -0
- quantark/asset/equity/engine/mc/asian_option_mc_engine.py +678 -0
- quantark/asset/equity/engine/mc/barrier_option_mc_engine.py +726 -0
- quantark/asset/equity/engine/mc/digital_option_mc_engine.py +419 -0
- quantark/asset/equity/engine/mc/double_sharkfin_option_mc_engine.py +676 -0
- quantark/asset/equity/engine/mc/euro_mc_engine.py +423 -0
- quantark/asset/equity/engine/mc/phoenix_mc_engine.py +1206 -0
- quantark/asset/equity/engine/mc/range_accrual_mc_engine.py +738 -0
- quantark/asset/equity/engine/mc/single_sharkfin_option_mc_engine.py +549 -0
- quantark/asset/equity/engine/mc/snowball_mc_engine.py +2250 -0
- quantark/asset/equity/engine/pde/__init__.py +36 -0
- quantark/asset/equity/engine/pde/american_pde_solver.py +211 -0
- quantark/asset/equity/engine/pde/barrier_pde_solver.py +692 -0
- quantark/asset/equity/engine/pde/base_pde_solver.py +994 -0
- quantark/asset/equity/engine/pde/double_barrier_pde_solver.py +510 -0
- quantark/asset/equity/engine/pde/double_one_touch_pde_solver.py +435 -0
- quantark/asset/equity/engine/pde/european_pde_solver.py +170 -0
- quantark/asset/equity/engine/pde/ko_reset_snowball_pde_solver.py +477 -0
- quantark/asset/equity/engine/pde/one_touch_pde_solver.py +439 -0
- quantark/asset/equity/engine/pde/phoenix_pde_solver.py +613 -0
- quantark/asset/equity/engine/pde/snowball_pde_solver.py +1810 -0
- quantark/asset/equity/engine/pde/spatial_grid.py +750 -0
- quantark/asset/equity/engine/pde/time_grid.py +308 -0
- quantark/asset/equity/engine/pde_engine.py +238 -0
- quantark/asset/equity/engine/quad/__init__.py +23 -0
- quantark/asset/equity/engine/quad/discrete_quad_engine.py +106 -0
- quantark/asset/equity/engine/quad/european_quad_engine.py +325 -0
- quantark/asset/equity/engine/quad/ko_reset_snowball_quad_engine.py +362 -0
- quantark/asset/equity/engine/quad/phoenix_quad_engine.py +614 -0
- quantark/asset/equity/engine/quad/quad_adapters.py +1260 -0
- quantark/asset/equity/engine/quad/quad_core.py +513 -0
- quantark/asset/equity/engine/quad/quad_math.py +219 -0
- quantark/asset/equity/engine/quad/snowball_quad_engine.py +1137 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_american_analytical.py +117 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_american_pde.py +114 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_asian_analytical.py +440 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_barrier_analytical.py +269 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_barrier_pde_solver.py +636 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_digital_option.py +256 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_snowball_pde_solver.py +807 -0
- quantark/asset/equity/engine/validation/script/boundary_check_american_analytical.py +290 -0
- quantark/asset/equity/engine/validation/script/boundary_check_american_pde.py +242 -0
- quantark/asset/equity/engine/validation/script/boundary_check_asian_analytical.py +612 -0
- quantark/asset/equity/engine/validation/script/boundary_check_barrier_analytical.py +434 -0
- quantark/asset/equity/engine/validation/script/boundary_check_barrier_pde_solver.py +748 -0
- quantark/asset/equity/engine/validation/script/boundary_check_digital_option.py +575 -0
- quantark/asset/equity/engine/validation/script/boundary_check_snowball_pde_solver.py +1101 -0
- quantark/asset/equity/engine/validation/script/greeks_check_digital_option.py +349 -0
- quantark/asset/equity/engine/validation/script/mc_comparison_barrier_pde.py +270 -0
- quantark/asset/equity/engine/validation/script/quick_mc_compare.py +51 -0
- quantark/asset/equity/engine/validation/script/validation_stepdown_improved.py +97 -0
- quantark/asset/equity/param/__init__.py +24 -0
- quantark/asset/equity/param/engine_param_profiles.py +325 -0
- quantark/asset/equity/param/engine_params.py +728 -0
- quantark/asset/equity/process/__init__.py +7 -0
- quantark/asset/equity/process/bsm/__init__.py +7 -0
- quantark/asset/equity/process/bsm/bsm_process.py +108 -0
- quantark/asset/equity/process/bsm/qmc_brownian_bridge.py +401 -0
- quantark/asset/equity/process/bsm/qmc_path_generator.py +694 -0
- quantark/asset/equity/process/bsm/qmc_rqmc_driver.py +163 -0
- quantark/asset/equity/process/bsm/qmc_sobol.py +195 -0
- quantark/asset/equity/process/bsm/qmc_variance_reduction.py +292 -0
- quantark/asset/equity/product/__init__.py +8 -0
- quantark/asset/equity/product/base_equity_product.py +72 -0
- quantark/asset/equity/product/deltaone/__init__.py +22 -0
- quantark/asset/equity/product/deltaone/base_deltaone_product.py +147 -0
- quantark/asset/equity/product/deltaone/futures.py +485 -0
- quantark/asset/equity/product/deltaone/spot_instrument.py +118 -0
- quantark/asset/equity/product/option/__init__.py +104 -0
- quantark/asset/equity/product/option/american_option.py +114 -0
- quantark/asset/equity/product/option/asian_option.py +531 -0
- quantark/asset/equity/product/option/barrier_option.py +289 -0
- quantark/asset/equity/product/option/base_equity_option.py +659 -0
- quantark/asset/equity/product/option/digital_option.py +102 -0
- quantark/asset/equity/product/option/double_barrier_option.py +286 -0
- quantark/asset/equity/product/option/double_one_touch_option.py +310 -0
- quantark/asset/equity/product/option/double_sharkfin_option.py +466 -0
- quantark/asset/equity/product/option/european_vanilla_option.py +103 -0
- quantark/asset/equity/product/option/ko_reset_snowball_option.py +563 -0
- quantark/asset/equity/product/option/observation_schedule.py +530 -0
- quantark/asset/equity/product/option/one_touch_option.py +287 -0
- quantark/asset/equity/product/option/phoenix_config.py +116 -0
- quantark/asset/equity/product/option/phoenix_helpers.py +576 -0
- quantark/asset/equity/product/option/phoenix_option.py +1167 -0
- quantark/asset/equity/product/option/range_accrual_config.py +288 -0
- quantark/asset/equity/product/option/range_accrual_helpers.py +608 -0
- quantark/asset/equity/product/option/range_accrual_option.py +526 -0
- quantark/asset/equity/product/option/single_sharkfin_option.py +420 -0
- quantark/asset/equity/product/option/snowball_config.py +261 -0
- quantark/asset/equity/product/option/snowball_helpers.py +977 -0
- quantark/asset/equity/product/option/snowball_option.py +1242 -0
- quantark/asset/equity/report/__init__.py +15 -0
- quantark/asset/equity/report/autocallable_risk_report.py +2118 -0
- quantark/asset/equity/report/plotting.py +87 -0
- quantark/asset/equity/report/snowball_risk_comparison_report.py +2230 -0
- quantark/asset/equity/report/surfaces.py +123 -0
- quantark/asset/equity/report/term_structure.py +126 -0
- quantark/asset/equity/riskmeasures/__init__.py +7 -0
- quantark/asset/equity/riskmeasures/greeks_calculator.py +1204 -0
- quantark/asset/rate/__init__.py +58 -0
- quantark/asset/rate/engine/__init__.py +25 -0
- quantark/asset/rate/engine/cap_floor_engine.py +514 -0
- quantark/asset/rate/engine/fra_engine.py +286 -0
- quantark/asset/rate/engine/irs_discount_engine.py +891 -0
- quantark/asset/rate/engine/swaption_engine.py +587 -0
- quantark/asset/rate/product/__init__.py +67 -0
- quantark/asset/rate/product/cap_floor.py +550 -0
- quantark/asset/rate/product/fra.py +219 -0
- quantark/asset/rate/product/irs.py +1223 -0
- quantark/asset/rate/product/swaption.py +372 -0
- quantark/backtest/__init__.py +153 -0
- quantark/backtest/base.py +263 -0
- quantark/backtest/dashboard.py +874 -0
- quantark/backtest/equity/__init__.py +35 -0
- quantark/backtest/equity/config.py +118 -0
- quantark/backtest/equity/engine.py +408 -0
- quantark/backtest/equity/hedge_executor.py +374 -0
- quantark/backtest/equity/metrics.py +396 -0
- quantark/backtest/equity/results.py +232 -0
- quantark/backtest/equity/state.py +252 -0
- quantark/backtest/examples/__init__.py +4 -0
- quantark/backtest/examples/advanced_backtest.py +345 -0
- quantark/backtest/examples/basic_delta_hedge.py +246 -0
- quantark/backtest/examples/fi_dv01_hedge.py +267 -0
- quantark/backtest/fi/__init__.py +30 -0
- quantark/backtest/fi/config.py +114 -0
- quantark/backtest/fi/engine.py +378 -0
- quantark/backtest/fi/hedge_executor.py +254 -0
- quantark/backtest/fi/metrics.py +308 -0
- quantark/backtest/fi/results.py +193 -0
- quantark/backtest/fi/state.py +212 -0
- quantark/backtest/logger.py +393 -0
- quantark/backtest/otc/__init__.py +74 -0
- quantark/backtest/otc/_replay.py +637 -0
- quantark/backtest/otc/book_engine.py +587 -0
- quantark/backtest/otc/config.py +175 -0
- quantark/backtest/otc/dashboard.py +1006 -0
- quantark/backtest/otc/engine.py +420 -0
- quantark/backtest/otc/engine_factory.py +138 -0
- quantark/backtest/otc/market.py +216 -0
- quantark/backtest/otc/results.py +107 -0
- quantark/backtest/otc/state.py +166 -0
- quantark/backtest/report_generator.py +608 -0
- quantark/backtest/strategy/__init__.py +28 -0
- quantark/backtest/strategy/base_strategy.py +235 -0
- quantark/backtest/strategy/convexity_neutral_strategy.py +247 -0
- quantark/backtest/strategy/delta_neutral_strategy.py +283 -0
- quantark/backtest/strategy/dv01_neutral_strategy.py +283 -0
- quantark/backtest/transaction_costs.py +485 -0
- quantark/backtest/visualizer.py +1019 -0
- quantark/cashleg/__init__.py +31 -0
- quantark/cashleg/accrual_leg.py +120 -0
- quantark/cashleg/base.py +48 -0
- quantark/cashleg/base_amount.py +60 -0
- quantark/cashleg/deterministic_leg.py +39 -0
- quantark/cashleg/event_distribution.py +262 -0
- quantark/cashleg/fixed_payoff_leg.py +92 -0
- quantark/cashleg/leg_schedule.py +95 -0
- quantark/cashleg/leg_valuator.py +40 -0
- quantark/dynamicscenario/__init__.py +97 -0
- quantark/dynamicscenario/base.py +297 -0
- quantark/dynamicscenario/config.py +122 -0
- quantark/dynamicscenario/engine.py +703 -0
- quantark/dynamicscenario/equity/__init__.py +14 -0
- quantark/dynamicscenario/fi/__init__.py +24 -0
- quantark/dynamicscenario/fi/config.py +149 -0
- quantark/dynamicscenario/fi/engine.py +500 -0
- quantark/dynamicscenario/fi/results.py +503 -0
- quantark/dynamicscenario/path/__init__.py +17 -0
- quantark/dynamicscenario/path/day_path.py +397 -0
- quantark/dynamicscenario/path/fi_path_library.py +488 -0
- quantark/dynamicscenario/path/path_builder.py +726 -0
- quantark/dynamicscenario/path/path_library.py +620 -0
- quantark/dynamicscenario/report/__init__.py +12 -0
- quantark/dynamicscenario/report/dynamic_report.py +1175 -0
- quantark/dynamicscenario/report/visualizer.py +1586 -0
- quantark/dynamicscenario/results/__init__.py +19 -0
- quantark/dynamicscenario/results/dynamic_results.py +579 -0
- quantark/dynamicscenario/results/result_exporter.py +438 -0
- quantark/param/__init__.py +75 -0
- quantark/param/basis/__init__.py +19 -0
- quantark/param/basis/basis_yield.py +301 -0
- quantark/param/div/__init__.py +16 -0
- quantark/param/div/dividend_yield.py +123 -0
- quantark/param/index/__init__.py +52 -0
- quantark/param/index/rate_index.py +568 -0
- quantark/param/quote/__init__.py +7 -0
- quantark/param/quote/spot_quote.py +35 -0
- quantark/param/rrf/__init__.py +22 -0
- quantark/param/rrf/rate_curve.py +436 -0
- quantark/param/vol/__init__.py +6 -0
- quantark/param/vol/vol_surface.py +118 -0
- quantark/portfolio/__init__.py +61 -0
- quantark/portfolio/base.py +203 -0
- quantark/portfolio/equity/__init__.py +17 -0
- quantark/portfolio/equity/portfolio.py +391 -0
- quantark/portfolio/equity/position.py +368 -0
- quantark/portfolio/fi/__init__.py +14 -0
- quantark/portfolio/fi/portfolio.py +424 -0
- quantark/portfolio/fi/position.py +272 -0
- quantark/portfolio/portfolio_snapshot.py +221 -0
- quantark/portfolio/portfolio_storage.py +414 -0
- quantark/priceenv/__init__.py +7 -0
- quantark/priceenv/pricing_environment.py +196 -0
- quantark/rfq/__init__.py +32 -0
- quantark/rfq/builders.py +102 -0
- quantark/rfq/models.py +214 -0
- quantark/rfq/registry.py +611 -0
- quantark/rfq/service.py +237 -0
- quantark/simm/__init__.py +155 -0
- quantark/simm/calibration/__init__.py +206 -0
- quantark/simm/calibration/accessors.py +439 -0
- quantark/simm/calibration/commodity.py +156 -0
- quantark/simm/calibration/credit_non_qualifying.py +79 -0
- quantark/simm/calibration/credit_qualifying.py +130 -0
- quantark/simm/calibration/cross_risk.py +39 -0
- quantark/simm/calibration/equity.py +125 -0
- quantark/simm/calibration/fx.py +92 -0
- quantark/simm/calibration/ir.py +152 -0
- quantark/simm/calibration/version.py +33 -0
- quantark/simm/config.py +186 -0
- quantark/simm/crif/__init__.py +35 -0
- quantark/simm/crif/models.py +230 -0
- quantark/simm/crif/parser.py +585 -0
- quantark/simm/engines/__init__.py +62 -0
- quantark/simm/engines/aggregation/__init__.py +67 -0
- quantark/simm/engines/aggregation/addon.py +141 -0
- quantark/simm/engines/aggregation/bucket_aggregator.py +298 -0
- quantark/simm/engines/aggregation/concentration.py +349 -0
- quantark/simm/engines/aggregation/product_class_aggregator.py +183 -0
- quantark/simm/engines/aggregation/risk_class_aggregator.py +403 -0
- quantark/simm/engines/aggregation/simm_calculator.py +430 -0
- quantark/simm/engines/aggregation/weighted_sensitivity.py +272 -0
- quantark/simm/engines/base.py +231 -0
- quantark/simm/engines/classification/__init__.py +10 -0
- quantark/simm/engines/classification/bucket_mapper.py +347 -0
- quantark/simm/engines/factory.py +137 -0
- quantark/simm/engines/portfolio_adapter.py +336 -0
- quantark/simm/engines/result.py +176 -0
- quantark/simm/engines/risk_class/__init__.py +18 -0
- quantark/simm/engines/risk_class/equity_engine.py +263 -0
- quantark/simm/engines/risk_class/ir_engine.py +264 -0
- quantark/simm/report/__init__.py +17 -0
- quantark/simm/report/crif_export.py +284 -0
- quantark/simm/report/excel_generator.py +401 -0
- quantark/simm/report/html_generator.py +840 -0
- quantark/simm/results/__init__.py +38 -0
- quantark/simm/results/attribution.py +313 -0
- quantark/simm/results/simm_result.py +339 -0
- quantark/simm/results/whatif.py +268 -0
- quantark/simm/sensitivity.py +533 -0
- quantark/simm/taxonomy.py +416 -0
- quantark/stresstest/__init__.py +67 -0
- quantark/stresstest/base.py +116 -0
- quantark/stresstest/config.py +5 -0
- quantark/stresstest/engine.py +5 -0
- quantark/stresstest/equity/__init__.py +17 -0
- quantark/stresstest/equity/config.py +69 -0
- quantark/stresstest/equity/engine.py +272 -0
- quantark/stresstest/equity/report/__init__.py +7 -0
- quantark/stresstest/equity/report/report_generator.py +423 -0
- quantark/stresstest/equity/report/visualizer.py +328 -0
- quantark/stresstest/equity/results.py +145 -0
- quantark/stresstest/fi/__init__.py +15 -0
- quantark/stresstest/fi/config.py +59 -0
- quantark/stresstest/fi/engine.py +213 -0
- quantark/stresstest/fi/metrics.py +60 -0
- quantark/stresstest/fi/results.py +64 -0
- quantark/stresstest/report/__init__.py +12 -0
- quantark/stresstest/report/report_generator.py +5 -0
- quantark/stresstest/report/visualizer.py +5 -0
- quantark/stresstest/results/__init__.py +16 -0
- quantark/stresstest/results/result_aggregator.py +325 -0
- quantark/stresstest/results/result_exporter.py +286 -0
- quantark/stresstest/results/stress_results.py +5 -0
- quantark/stresstest/scenario/__init__.py +13 -0
- quantark/stresstest/scenario/scenario.py +242 -0
- quantark/stresstest/scenario/scenario_builder.py +376 -0
- quantark/stresstest/scenario/scenario_library.py +435 -0
- quantark/stresstest/scenario/scenario_storage.py +224 -0
- quantark/stresstest/stress/__init__.py +13 -0
- quantark/stresstest/stress/stress_applicator.py +590 -0
- quantark/stresstest/stress/stress_types.py +142 -0
- quantark/util/__init__.py +23 -0
- quantark/util/barrier_shift.py +44 -0
- quantark/util/calendar/__init__.py +27 -0
- quantark/util/calendar/business_calendar.py +584 -0
- quantark/util/calendar/day_counter.py +517 -0
- quantark/util/calendar/holidayfile/china.csv +1920 -0
- quantark/util/calendar/holidayfile/china_sse.csv +1462 -0
- quantark/util/enum/__init__.py +81 -0
- quantark/util/enum/bond_enums.py +112 -0
- quantark/util/enum/deltaone_enums.py +16 -0
- quantark/util/enum/engine_enums.py +137 -0
- quantark/util/enum/greeks_enums.py +29 -0
- quantark/util/enum/option_enums.py +221 -0
- quantark/util/exceptions.py +66 -0
- quantark/util/marketdata/__init__.py +39 -0
- quantark/util/marketdata/adapter/base_adapter.py +203 -0
- quantark/util/marketdata/adapter/mock_adapter.py +265 -0
- quantark/util/marketdata/converter.py +289 -0
- quantark/util/marketdata/example_usage.py +314 -0
- quantark/util/marketdata/generator/__init__.py +7 -0
- quantark/util/marketdata/generator/mock_generator.py +466 -0
- quantark/util/marketdata/models.py +358 -0
- quantark/util/marketdata/storage/__init__.py +7 -0
- quantark/util/marketdata/storage/parquet_storage.py +340 -0
- quantark/util/numerical/__init__.py +98 -0
- quantark/util/numerical/comparison.py +219 -0
- quantark/util/numerical/constants.py +98 -0
- quantark/util/numerical/formatting.py +380 -0
- quantark/util/numerical/pnl.py +17 -0
- quantark/util/numerical/safe_math.py +238 -0
- quantark/util/numerical/validation.py +315 -0
- quantark/var/__init__.py +39 -0
- quantark/var/attribution.py +398 -0
- quantark/var/backtest/__init__.py +7 -0
- quantark/var/backtest/var_backtester.py +309 -0
- quantark/var/base.py +63 -0
- quantark/var/config.py +219 -0
- quantark/var/engines/__init__.py +13 -0
- quantark/var/engines/historical.py +925 -0
- quantark/var/engines/monte_carlo.py +870 -0
- quantark/var/engines/parametric.py +1199 -0
- quantark/var/results/__init__.py +16 -0
- quantark/var/results/incremental_var_result.py +131 -0
- quantark/var/results/var_report.py +346 -0
- quantark/var/results/var_result.py +134 -0
- quantark/var/risk_factors/__init__.py +22 -0
- quantark/var/risk_factors/base.py +41 -0
- quantark/var/risk_factors/equity_factors.py +158 -0
- quantark/var/risk_factors/fi_factors.py +99 -0
- quantark-0.1.0.dist-info/METADATA +351 -0
- quantark-0.1.0.dist-info/RECORD +399 -0
- quantark-0.1.0.dist-info/WHEEL +4 -0
- quantark-0.1.0.dist-info/licenses/LICENSE +202 -0
- quantark-0.1.0.dist-info/licenses/NOTICE +2 -0
- 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
|