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,1204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Greeks calculation for equity derivatives.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import Dict, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
from scipy import stats
|
|
11
|
+
|
|
12
|
+
from quantark.asset.equity.engine.base_engine import BaseEngine
|
|
13
|
+
from quantark.asset.equity.param import EngineParams
|
|
14
|
+
from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
|
|
15
|
+
from quantark.asset.equity.product.option import EuropeanVanillaOption
|
|
16
|
+
from quantark.priceenv import PricingEnvironment
|
|
17
|
+
from quantark.util.calendar import calculate_year_fraction
|
|
18
|
+
from quantark.util.enum import CommonGreek, EquityGreek
|
|
19
|
+
from quantark.util.enum.engine_enums import EngineType, GreeksCalculationMode
|
|
20
|
+
from quantark.util.exceptions import ValidationError
|
|
21
|
+
from quantark.util.numerical import is_zero
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GreeksCalculator:
|
|
25
|
+
"""
|
|
26
|
+
Calculator for option Greeks using both analytical and numerical methods.
|
|
27
|
+
|
|
28
|
+
Supports:
|
|
29
|
+
- Analytical Greeks: Using closed-form Black-Scholes formulas
|
|
30
|
+
- Numerical Greeks: Using finite difference method (FDM)
|
|
31
|
+
|
|
32
|
+
The greeks_mode parameter controls delta/gamma calculation for engines that
|
|
33
|
+
implement their own calculate_greeks() method (e.g., PDE engines):
|
|
34
|
+
- GreeksCalculationMode.BUMP: Always use finite difference bump method
|
|
35
|
+
- GreeksCalculationMode.ENGINE: Use engine.calculate_greeks() when overridden
|
|
36
|
+
- GreeksCalculationMode.AUTO: Use engine method for PDE engines, bump otherwise
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
params: Optional[EngineParams] = None,
|
|
42
|
+
greeks_mode: GreeksCalculationMode = GreeksCalculationMode.BUMP,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize Greeks calculator.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
params: Engine parameters (for bump sizes in FDM)
|
|
49
|
+
greeks_mode: Mode for delta/gamma calculation when engine has
|
|
50
|
+
its own calculate_greeks() method (e.g., PDE engines)
|
|
51
|
+
"""
|
|
52
|
+
self.params = params if params is not None else EngineParams()
|
|
53
|
+
self._bump_config = self.params.get_effective_bump_config()
|
|
54
|
+
self.greeks_mode = greeks_mode
|
|
55
|
+
|
|
56
|
+
def calculate(
|
|
57
|
+
self,
|
|
58
|
+
product: BaseEquityProduct,
|
|
59
|
+
pricing_env: PricingEnvironment,
|
|
60
|
+
engine: BaseEngine,
|
|
61
|
+
method: str = "auto",
|
|
62
|
+
greeks: Optional[Sequence[object]] = None,
|
|
63
|
+
) -> Dict[str, float]:
|
|
64
|
+
"""Unified entry point for Greeks calculation."""
|
|
65
|
+
method = method.lower()
|
|
66
|
+
if method not in ("auto", "analytical", "numerical"):
|
|
67
|
+
raise ValidationError(f"Unknown greeks method: {method}")
|
|
68
|
+
|
|
69
|
+
requested = self._normalize_greeks(greeks)
|
|
70
|
+
analytical_supported = {
|
|
71
|
+
"price",
|
|
72
|
+
"delta",
|
|
73
|
+
"gamma",
|
|
74
|
+
"vega",
|
|
75
|
+
"theta",
|
|
76
|
+
"rho",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if method in ("auto", "analytical") and isinstance(
|
|
80
|
+
product, EuropeanVanillaOption
|
|
81
|
+
):
|
|
82
|
+
if requested is None or requested.issubset(analytical_supported):
|
|
83
|
+
greeks_out = self.calculate_analytical_greeks(product, pricing_env)
|
|
84
|
+
if requested is None:
|
|
85
|
+
return greeks_out
|
|
86
|
+
return {key: greeks_out[key] for key in greeks_out if key in requested}
|
|
87
|
+
if method == "analytical":
|
|
88
|
+
raise ValidationError(
|
|
89
|
+
"Analytical greeks do not support requested greeks: "
|
|
90
|
+
f"{sorted(requested - analytical_supported)}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return self.calculate_numerical_greeks(
|
|
94
|
+
product, pricing_env, engine, greeks=greeks
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _normalize_greeks(
|
|
98
|
+
self, greeks: Optional[Sequence[object]]
|
|
99
|
+
) -> Optional[set[str]]:
|
|
100
|
+
if greeks is None:
|
|
101
|
+
return None
|
|
102
|
+
if len(greeks) == 0:
|
|
103
|
+
return set()
|
|
104
|
+
aliases = {
|
|
105
|
+
"deltaq": "delta_q",
|
|
106
|
+
"deltadq": "delta_q",
|
|
107
|
+
"d_delta_d_q": "delta_q",
|
|
108
|
+
"d_delta_dq": "delta_q",
|
|
109
|
+
"rhoq": "dividend_rho",
|
|
110
|
+
"div_rho": "dividend_rho",
|
|
111
|
+
"dividendrho": "dividend_rho",
|
|
112
|
+
}
|
|
113
|
+
allowed = {
|
|
114
|
+
"price",
|
|
115
|
+
"delta",
|
|
116
|
+
"gamma",
|
|
117
|
+
"vega",
|
|
118
|
+
"theta",
|
|
119
|
+
"rho",
|
|
120
|
+
"dividend_rho",
|
|
121
|
+
"vanna",
|
|
122
|
+
"volga",
|
|
123
|
+
"delta_q",
|
|
124
|
+
"charm",
|
|
125
|
+
"color",
|
|
126
|
+
"convexity_theta",
|
|
127
|
+
"r_theta",
|
|
128
|
+
"q_theta",
|
|
129
|
+
}
|
|
130
|
+
normalized: set[str] = set()
|
|
131
|
+
for greek in greeks:
|
|
132
|
+
if isinstance(greek, (CommonGreek, EquityGreek)):
|
|
133
|
+
name = greek.value
|
|
134
|
+
elif isinstance(greek, str):
|
|
135
|
+
name = greek.strip().lower()
|
|
136
|
+
else:
|
|
137
|
+
raise ValidationError(
|
|
138
|
+
f"Unsupported greek identifier type: {type(greek).__name__}"
|
|
139
|
+
)
|
|
140
|
+
name = aliases.get(name, name)
|
|
141
|
+
if name not in allowed:
|
|
142
|
+
raise ValidationError(f"Unknown greek name: {name}")
|
|
143
|
+
normalized.add(name)
|
|
144
|
+
return normalized
|
|
145
|
+
|
|
146
|
+
def _has_custom_greeks(self, engine: BaseEngine) -> bool:
|
|
147
|
+
"""Return True if engine overrides calculate_greeks()."""
|
|
148
|
+
engine_calculate_greeks = getattr(engine.__class__, "calculate_greeks", None)
|
|
149
|
+
return (
|
|
150
|
+
engine_calculate_greeks is not None
|
|
151
|
+
and engine_calculate_greeks is not BaseEngine.calculate_greeks
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _should_use_engine_greeks(self, engine: BaseEngine) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check if engine's calculate_greeks() should be used for delta/gamma.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
engine: The pricing engine
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if engine.calculate_greeks() should be used
|
|
163
|
+
"""
|
|
164
|
+
if not self._has_custom_greeks(engine):
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
if self.greeks_mode == GreeksCalculationMode.BUMP:
|
|
168
|
+
return False
|
|
169
|
+
if self.greeks_mode == GreeksCalculationMode.ENGINE:
|
|
170
|
+
return True
|
|
171
|
+
# AUTO mode: use for PDE engines
|
|
172
|
+
return getattr(engine, "engine_type", None) == EngineType.PDE
|
|
173
|
+
|
|
174
|
+
def _ensure_base_price(
|
|
175
|
+
self,
|
|
176
|
+
product: BaseEquityProduct,
|
|
177
|
+
pricing_env: PricingEnvironment,
|
|
178
|
+
engine: BaseEngine,
|
|
179
|
+
base_price: Optional[float],
|
|
180
|
+
) -> float:
|
|
181
|
+
"""Return base price, computing it if needed."""
|
|
182
|
+
return (
|
|
183
|
+
base_price if base_price is not None else engine.price(product, pricing_env)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _calculate_sensitivity(
|
|
187
|
+
self,
|
|
188
|
+
base_price: float,
|
|
189
|
+
price_up: float,
|
|
190
|
+
price_down: Optional[float] = None,
|
|
191
|
+
bump: float = 1.0,
|
|
192
|
+
scale: float = 1.0,
|
|
193
|
+
mode: str = "central",
|
|
194
|
+
) -> float:
|
|
195
|
+
"""Generic finite-difference sensitivity helper."""
|
|
196
|
+
if mode == "central":
|
|
197
|
+
if price_down is None:
|
|
198
|
+
raise ValidationError("central mode requires price_down")
|
|
199
|
+
return (price_up - price_down) / (2.0 * scale * bump)
|
|
200
|
+
if mode == "second_order":
|
|
201
|
+
if price_down is None:
|
|
202
|
+
raise ValidationError("second_order mode requires price_down")
|
|
203
|
+
return (price_up - 2.0 * base_price + price_down) / (
|
|
204
|
+
scale * bump
|
|
205
|
+
) ** 2
|
|
206
|
+
if mode == "one_sided":
|
|
207
|
+
return price_up - base_price
|
|
208
|
+
raise ValidationError(f"Unknown sensitivity mode: {mode}")
|
|
209
|
+
|
|
210
|
+
def _get_delta_gamma(
|
|
211
|
+
self,
|
|
212
|
+
product: BaseEquityProduct,
|
|
213
|
+
pricing_env: PricingEnvironment,
|
|
214
|
+
engine: BaseEngine,
|
|
215
|
+
base_price: Optional[float],
|
|
216
|
+
) -> Tuple[float, float, float]:
|
|
217
|
+
"""Get base price, delta, and gamma via engine or bump method."""
|
|
218
|
+
if self._should_use_engine_greeks(engine):
|
|
219
|
+
engine_greeks = engine.calculate_greeks(product, pricing_env)
|
|
220
|
+
if base_price is None:
|
|
221
|
+
base_price = engine_greeks["price"]
|
|
222
|
+
return base_price, engine_greeks["delta"], engine_greeks["gamma"]
|
|
223
|
+
|
|
224
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
225
|
+
spot_prices = self._spot_bumped_prices(
|
|
226
|
+
product, pricing_env, engine, self._bump_config.spot_bump, base_price=base_price
|
|
227
|
+
)[1:]
|
|
228
|
+
|
|
229
|
+
delta = self.calculate_numerical_delta(
|
|
230
|
+
product,
|
|
231
|
+
pricing_env,
|
|
232
|
+
engine,
|
|
233
|
+
base_price=base_price,
|
|
234
|
+
spot_prices=spot_prices,
|
|
235
|
+
bump=self._bump_config.spot_bump,
|
|
236
|
+
)
|
|
237
|
+
gamma = self.calculate_numerical_gamma(
|
|
238
|
+
product,
|
|
239
|
+
pricing_env,
|
|
240
|
+
engine,
|
|
241
|
+
base_price=base_price,
|
|
242
|
+
spot_prices=spot_prices,
|
|
243
|
+
bump=self._bump_config.spot_bump,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return base_price, delta, gamma
|
|
247
|
+
|
|
248
|
+
def calculate_analytical_greeks(
|
|
249
|
+
self,
|
|
250
|
+
product: BaseEquityProduct,
|
|
251
|
+
pricing_env: PricingEnvironment,
|
|
252
|
+
price: Optional[float] = None,
|
|
253
|
+
) -> Dict[str, float]:
|
|
254
|
+
"""
|
|
255
|
+
Calculate Greeks using analytical Black-Scholes formulas.
|
|
256
|
+
|
|
257
|
+
Only works for European vanilla options under Black-Scholes model.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
product: European vanilla option
|
|
261
|
+
pricing_env: Pricing environment
|
|
262
|
+
price: Pre-calculated price (optional, will calculate if not provided)
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Dictionary of Greeks: delta, gamma, vega, theta, rho
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ValidationError: If product is not a European vanilla option
|
|
269
|
+
"""
|
|
270
|
+
if not isinstance(product, EuropeanVanillaOption):
|
|
271
|
+
raise ValidationError(
|
|
272
|
+
f"Analytical Greeks only support EuropeanVanillaOption, "
|
|
273
|
+
f"got {type(product).__name__}"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Extract parameters
|
|
277
|
+
S = pricing_env.spot
|
|
278
|
+
K = product.strike
|
|
279
|
+
T = product.get_maturity(pricing_env)
|
|
280
|
+
r = pricing_env.get_rate(T)
|
|
281
|
+
q = pricing_env.get_div_yield(T)
|
|
282
|
+
sigma = pricing_env.get_vol(K, T)
|
|
283
|
+
|
|
284
|
+
# Handle edge case: option at expiry
|
|
285
|
+
if is_zero(T):
|
|
286
|
+
return self._greeks_at_expiry(product, S)
|
|
287
|
+
|
|
288
|
+
# Calculate d1 and d2
|
|
289
|
+
sqrt_T = math.sqrt(T)
|
|
290
|
+
d1 = (math.log(S / K) + (r - q + 0.5 * sigma**2) * T) / (sigma * sqrt_T)
|
|
291
|
+
d2 = d1 - sigma * sqrt_T
|
|
292
|
+
|
|
293
|
+
# Calculate discount factors
|
|
294
|
+
discount_div = math.exp(-q * T)
|
|
295
|
+
discount_rf = math.exp(-r * T)
|
|
296
|
+
|
|
297
|
+
# Standard normal PDF and CDF
|
|
298
|
+
n_d1 = stats.norm.pdf(d1) # phi(d1)
|
|
299
|
+
N_d1 = stats.norm.cdf(d1) # Phi(d1)
|
|
300
|
+
N_d2 = stats.norm.cdf(d2) # Phi(d2)
|
|
301
|
+
|
|
302
|
+
greeks = {}
|
|
303
|
+
|
|
304
|
+
multiplier = product.contract_multiplier
|
|
305
|
+
|
|
306
|
+
# Calculate price if not provided (per-unit)
|
|
307
|
+
if price is None:
|
|
308
|
+
if product.is_call():
|
|
309
|
+
price = S * discount_div * N_d1 - K * discount_rf * N_d2
|
|
310
|
+
else:
|
|
311
|
+
price = K * discount_rf * stats.norm.cdf(
|
|
312
|
+
-d2
|
|
313
|
+
) - S * discount_div * stats.norm.cdf(-d1)
|
|
314
|
+
else:
|
|
315
|
+
price = price / multiplier
|
|
316
|
+
greeks["price"] = price
|
|
317
|
+
|
|
318
|
+
# Delta: ∂V/∂S
|
|
319
|
+
if product.is_call():
|
|
320
|
+
delta = discount_div * N_d1
|
|
321
|
+
else:
|
|
322
|
+
delta = -discount_div * stats.norm.cdf(-d1)
|
|
323
|
+
greeks["delta"] = delta
|
|
324
|
+
|
|
325
|
+
# Gamma: ∂²V/∂S²
|
|
326
|
+
gamma = discount_div * n_d1 / (S * sigma * sqrt_T)
|
|
327
|
+
greeks["gamma"] = gamma
|
|
328
|
+
|
|
329
|
+
# Vega: ∂V/∂σ (divided by 100 for 1% change)
|
|
330
|
+
vega = S * discount_div * n_d1 * sqrt_T / 100
|
|
331
|
+
greeks["vega"] = vega
|
|
332
|
+
|
|
333
|
+
# Theta: ∂V/∂t (per day, divided by 365)
|
|
334
|
+
# Decomposed into three components:
|
|
335
|
+
# convexity_theta: time decay from gamma/convexity erosion (always negative)
|
|
336
|
+
# r_theta: time decay from interest rate cost of carry
|
|
337
|
+
# q_theta: time decay from dividend yield
|
|
338
|
+
term1 = -S * discount_div * n_d1 * sigma / (2 * sqrt_T)
|
|
339
|
+
if product.is_call():
|
|
340
|
+
term2 = -r * K * discount_rf * N_d2
|
|
341
|
+
term3 = q * S * discount_div * N_d1
|
|
342
|
+
else:
|
|
343
|
+
term2 = r * K * discount_rf * stats.norm.cdf(-d2)
|
|
344
|
+
term3 = -q * S * discount_div * stats.norm.cdf(-d1)
|
|
345
|
+
|
|
346
|
+
# Store decomposed components (per day)
|
|
347
|
+
convexity_theta = term1 / 365
|
|
348
|
+
r_theta = term2 / 365
|
|
349
|
+
q_theta = term3 / 365
|
|
350
|
+
theta = convexity_theta + r_theta + q_theta
|
|
351
|
+
|
|
352
|
+
greeks["theta"] = theta
|
|
353
|
+
greeks["convexity_theta"] = convexity_theta
|
|
354
|
+
greeks["r_theta"] = r_theta
|
|
355
|
+
greeks["q_theta"] = q_theta
|
|
356
|
+
|
|
357
|
+
# Rho: ∂V/∂r (divided by 100 for 1% change)
|
|
358
|
+
if product.is_call():
|
|
359
|
+
rho = K * T * discount_rf * N_d2 / 100
|
|
360
|
+
else:
|
|
361
|
+
rho = -K * T * discount_rf * stats.norm.cdf(-d2) / 100
|
|
362
|
+
greeks["rho"] = rho
|
|
363
|
+
|
|
364
|
+
# Dividend Rho: ∂V/∂q (divided by 100 for 1% change)
|
|
365
|
+
if product.is_call():
|
|
366
|
+
dividend_rho = -S * T * discount_div * N_d1 / 100
|
|
367
|
+
else:
|
|
368
|
+
dividend_rho = S * T * discount_div * stats.norm.cdf(-d1) / 100
|
|
369
|
+
greeks["dividend_rho"] = dividend_rho
|
|
370
|
+
|
|
371
|
+
for key, value in greeks.items():
|
|
372
|
+
greeks[key] = value * multiplier
|
|
373
|
+
|
|
374
|
+
return greeks
|
|
375
|
+
|
|
376
|
+
def calculate_numerical_greeks(
|
|
377
|
+
self,
|
|
378
|
+
product: BaseEquityProduct,
|
|
379
|
+
pricing_env: PricingEnvironment,
|
|
380
|
+
engine: BaseEngine,
|
|
381
|
+
base_price: Optional[float] = None,
|
|
382
|
+
greeks: Optional[Sequence[object]] = None,
|
|
383
|
+
) -> Dict[str, float]:
|
|
384
|
+
"""
|
|
385
|
+
Calculate Greeks using finite difference method (FDM).
|
|
386
|
+
|
|
387
|
+
Uses central differences for better accuracy.
|
|
388
|
+
Works for any product and engine combination.
|
|
389
|
+
|
|
390
|
+
Bump sizes are configured via BumpConfig in EngineParams:
|
|
391
|
+
- Delta/Gamma: Relative spot bump (default: 1%)
|
|
392
|
+
- Vega: Absolute vol bump (default: 1 vol point)
|
|
393
|
+
- Theta: Time bump in days (default: 1 day)
|
|
394
|
+
- Rho: Absolute rate bump (default: 1bp), scaled to per 1% change
|
|
395
|
+
- Dividend Rho: Absolute div yield bump (default: 1bp), scaled to per 1% change
|
|
396
|
+
|
|
397
|
+
For delta and gamma, if greeks_mode is ENGINE or AUTO (with PDE engine),
|
|
398
|
+
the engine's own calculate_greeks() method is used instead of bumping.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
product: The derivative product
|
|
402
|
+
pricing_env: Pricing environment
|
|
403
|
+
engine: Pricing engine to use
|
|
404
|
+
base_price: Pre-calculated base price (optional)
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Dictionary of Greeks for the requested set (or defaults if None).
|
|
408
|
+
"""
|
|
409
|
+
requested = self._normalize_greeks(greeks)
|
|
410
|
+
if requested is None:
|
|
411
|
+
requested = {
|
|
412
|
+
"price",
|
|
413
|
+
"delta",
|
|
414
|
+
"gamma",
|
|
415
|
+
"vega",
|
|
416
|
+
"theta",
|
|
417
|
+
"rho",
|
|
418
|
+
"dividend_rho",
|
|
419
|
+
"convexity_theta",
|
|
420
|
+
"r_theta",
|
|
421
|
+
"q_theta",
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if product.is_linear:
|
|
425
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
426
|
+
greeks_out = self._greeks_for_linear(product, base_price)
|
|
427
|
+
for extra in requested:
|
|
428
|
+
greeks_out.setdefault(extra, 0.0)
|
|
429
|
+
return {key: greeks_out[key] for key in greeks_out if key in requested}
|
|
430
|
+
|
|
431
|
+
greeks_out: Dict[str, float] = {}
|
|
432
|
+
if "price" in requested and base_price is not None:
|
|
433
|
+
greeks_out["price"] = base_price
|
|
434
|
+
|
|
435
|
+
delta = None
|
|
436
|
+
gamma = None
|
|
437
|
+
if {"delta", "gamma", "delta_q", "vanna"} & requested:
|
|
438
|
+
base_price, delta, gamma = self._get_delta_gamma(
|
|
439
|
+
product, pricing_env, engine, base_price
|
|
440
|
+
)
|
|
441
|
+
if delta is not None and "delta" in requested:
|
|
442
|
+
greeks_out["delta"] = delta
|
|
443
|
+
if gamma is not None and "gamma" in requested:
|
|
444
|
+
greeks_out["gamma"] = gamma
|
|
445
|
+
|
|
446
|
+
if base_price is None:
|
|
447
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
448
|
+
if "price" in requested and "price" not in greeks_out:
|
|
449
|
+
greeks_out["price"] = base_price
|
|
450
|
+
|
|
451
|
+
# Other Greeks always use bump method
|
|
452
|
+
if "vega" in requested:
|
|
453
|
+
greeks_out["vega"] = self.calculate_numerical_vega(
|
|
454
|
+
product,
|
|
455
|
+
pricing_env,
|
|
456
|
+
engine,
|
|
457
|
+
base_price=base_price,
|
|
458
|
+
vol_bump=self._bump_config.vol_bump,
|
|
459
|
+
)
|
|
460
|
+
if "volga" in requested:
|
|
461
|
+
greeks_out["volga"] = self.calculate_numerical_volga(
|
|
462
|
+
product,
|
|
463
|
+
pricing_env,
|
|
464
|
+
engine,
|
|
465
|
+
base_price=base_price,
|
|
466
|
+
vol_bump=self._bump_config.vol_bump,
|
|
467
|
+
)
|
|
468
|
+
if "vanna" in requested:
|
|
469
|
+
greeks_out["vanna"] = self.calculate_numerical_vanna(
|
|
470
|
+
product,
|
|
471
|
+
pricing_env,
|
|
472
|
+
engine,
|
|
473
|
+
base_price=base_price,
|
|
474
|
+
vol_bump=self._bump_config.vol_bump,
|
|
475
|
+
)
|
|
476
|
+
if "delta_q" in requested:
|
|
477
|
+
greeks_out["delta_q"] = self.calculate_numerical_delta_q(
|
|
478
|
+
product,
|
|
479
|
+
pricing_env,
|
|
480
|
+
engine,
|
|
481
|
+
base_price=base_price,
|
|
482
|
+
div_bump=self._bump_config.div_bump,
|
|
483
|
+
base_delta=delta,
|
|
484
|
+
)
|
|
485
|
+
if "theta" in requested:
|
|
486
|
+
greeks_out["theta"] = self.calculate_numerical_theta(
|
|
487
|
+
product,
|
|
488
|
+
pricing_env,
|
|
489
|
+
engine,
|
|
490
|
+
base_price=base_price,
|
|
491
|
+
time_bump_days=self._bump_config.time_bump_days,
|
|
492
|
+
)
|
|
493
|
+
if "rho" in requested:
|
|
494
|
+
greeks_out["rho"] = self.calculate_numerical_rho(
|
|
495
|
+
product,
|
|
496
|
+
pricing_env,
|
|
497
|
+
engine,
|
|
498
|
+
base_price=base_price,
|
|
499
|
+
rate_bump=self._bump_config.rate_bump,
|
|
500
|
+
)
|
|
501
|
+
if "dividend_rho" in requested:
|
|
502
|
+
greeks_out["dividend_rho"] = self.calculate_numerical_dividend_rho(
|
|
503
|
+
product,
|
|
504
|
+
pricing_env,
|
|
505
|
+
engine,
|
|
506
|
+
base_price=base_price,
|
|
507
|
+
div_bump=self._bump_config.div_bump,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Estimate theta components using fast approximation from existing Greeks
|
|
511
|
+
if {"convexity_theta", "r_theta", "q_theta"} & requested:
|
|
512
|
+
if "theta" not in greeks_out:
|
|
513
|
+
greeks_out["theta"] = self.calculate_numerical_theta(
|
|
514
|
+
product,
|
|
515
|
+
pricing_env,
|
|
516
|
+
engine,
|
|
517
|
+
base_price=base_price,
|
|
518
|
+
time_bump_days=self._bump_config.time_bump_days,
|
|
519
|
+
)
|
|
520
|
+
if "rho" not in greeks_out:
|
|
521
|
+
greeks_out["rho"] = self.calculate_numerical_rho(
|
|
522
|
+
product,
|
|
523
|
+
pricing_env,
|
|
524
|
+
engine,
|
|
525
|
+
base_price=base_price,
|
|
526
|
+
rate_bump=self._bump_config.rate_bump,
|
|
527
|
+
)
|
|
528
|
+
if "dividend_rho" not in greeks_out:
|
|
529
|
+
greeks_out["dividend_rho"] = self.calculate_numerical_dividend_rho(
|
|
530
|
+
product,
|
|
531
|
+
pricing_env,
|
|
532
|
+
engine,
|
|
533
|
+
base_price=base_price,
|
|
534
|
+
div_bump=self._bump_config.div_bump,
|
|
535
|
+
)
|
|
536
|
+
T = product.get_maturity(pricing_env)
|
|
537
|
+
r = pricing_env.get_rate(T)
|
|
538
|
+
q = pricing_env.get_div_yield(T)
|
|
539
|
+
theta_components = self.estimate_theta_components(
|
|
540
|
+
theta=greeks_out["theta"],
|
|
541
|
+
rho=greeks_out["rho"],
|
|
542
|
+
dividend_rho=greeks_out["dividend_rho"],
|
|
543
|
+
r=r,
|
|
544
|
+
q=q,
|
|
545
|
+
T=T,
|
|
546
|
+
)
|
|
547
|
+
for key, value in theta_components.items():
|
|
548
|
+
if key in requested:
|
|
549
|
+
greeks_out[key] = value
|
|
550
|
+
|
|
551
|
+
return {key: greeks_out[key] for key in greeks_out if key in requested}
|
|
552
|
+
|
|
553
|
+
def calculate_numerical_delta(
|
|
554
|
+
self,
|
|
555
|
+
product: BaseEquityProduct,
|
|
556
|
+
pricing_env: PricingEnvironment,
|
|
557
|
+
engine: BaseEngine,
|
|
558
|
+
base_price: Optional[float] = None,
|
|
559
|
+
spot_prices: Optional[Tuple[float, float]] = None,
|
|
560
|
+
bump: Optional[float] = None,
|
|
561
|
+
) -> float:
|
|
562
|
+
"""Numerical delta using central spot bump."""
|
|
563
|
+
bump = bump if bump is not None else self.params.bump_size
|
|
564
|
+
base_price, price_up_spot, price_down_spot = self._spot_bumped_prices(
|
|
565
|
+
product,
|
|
566
|
+
pricing_env,
|
|
567
|
+
engine,
|
|
568
|
+
bump,
|
|
569
|
+
base_price=base_price,
|
|
570
|
+
reuse=spot_prices,
|
|
571
|
+
)
|
|
572
|
+
return self._calculate_sensitivity(
|
|
573
|
+
base_price,
|
|
574
|
+
price_up_spot,
|
|
575
|
+
price_down_spot,
|
|
576
|
+
bump=bump,
|
|
577
|
+
scale=pricing_env.spot,
|
|
578
|
+
mode="central",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def calculate_numerical_gamma(
|
|
582
|
+
self,
|
|
583
|
+
product: BaseEquityProduct,
|
|
584
|
+
pricing_env: PricingEnvironment,
|
|
585
|
+
engine: BaseEngine,
|
|
586
|
+
base_price: Optional[float] = None,
|
|
587
|
+
spot_prices: Optional[Tuple[float, float]] = None,
|
|
588
|
+
bump: Optional[float] = None,
|
|
589
|
+
) -> float:
|
|
590
|
+
"""Numerical gamma using central spot bump."""
|
|
591
|
+
bump = bump if bump is not None else self.params.bump_size
|
|
592
|
+
base_price, price_up_spot, price_down_spot = self._spot_bumped_prices(
|
|
593
|
+
product,
|
|
594
|
+
pricing_env,
|
|
595
|
+
engine,
|
|
596
|
+
bump,
|
|
597
|
+
base_price=base_price,
|
|
598
|
+
reuse=spot_prices,
|
|
599
|
+
)
|
|
600
|
+
return self._calculate_sensitivity(
|
|
601
|
+
base_price,
|
|
602
|
+
price_up_spot,
|
|
603
|
+
price_down_spot,
|
|
604
|
+
bump=bump,
|
|
605
|
+
scale=pricing_env.spot,
|
|
606
|
+
mode="second_order",
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
def calculate_numerical_vega(
|
|
610
|
+
self,
|
|
611
|
+
product: BaseEquityProduct,
|
|
612
|
+
pricing_env: PricingEnvironment,
|
|
613
|
+
engine: BaseEngine,
|
|
614
|
+
base_price: Optional[float] = None,
|
|
615
|
+
vol_bump: Optional[float] = None,
|
|
616
|
+
) -> float:
|
|
617
|
+
"""Numerical vega from a vol bump."""
|
|
618
|
+
vol_bump = vol_bump if vol_bump is not None else self._bump_config.vol_bump
|
|
619
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
620
|
+
T = product.get_maturity(pricing_env)
|
|
621
|
+
strike = getattr(product, "strike", pricing_env.spot)
|
|
622
|
+
current_vol = pricing_env.get_vol(strike, T)
|
|
623
|
+
env_up_vol = self._build_vol_bumped_env(
|
|
624
|
+
pricing_env, product, current_vol, vol_bump, direction=1.0
|
|
625
|
+
)
|
|
626
|
+
price_up_vol = engine.price(product, env_up_vol)
|
|
627
|
+
return self._calculate_sensitivity(
|
|
628
|
+
base_price, price_up_vol, bump=vol_bump, mode="one_sided"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def calculate_numerical_volga(
|
|
632
|
+
self,
|
|
633
|
+
product: BaseEquityProduct,
|
|
634
|
+
pricing_env: PricingEnvironment,
|
|
635
|
+
engine: BaseEngine,
|
|
636
|
+
base_price: Optional[float] = None,
|
|
637
|
+
vol_bump: Optional[float] = None,
|
|
638
|
+
) -> float:
|
|
639
|
+
"""Numerical volga (second derivative wrt vol) using vol bumps."""
|
|
640
|
+
vol_bump = vol_bump if vol_bump is not None else self._bump_config.vol_bump
|
|
641
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
642
|
+
T = product.get_maturity(pricing_env)
|
|
643
|
+
strike = getattr(product, "strike", pricing_env.spot)
|
|
644
|
+
current_vol = pricing_env.get_vol(strike, T)
|
|
645
|
+
|
|
646
|
+
if current_vol - vol_bump <= 0:
|
|
647
|
+
env_up = self._build_vol_bumped_env(
|
|
648
|
+
pricing_env, product, current_vol, vol_bump, direction=1.0
|
|
649
|
+
)
|
|
650
|
+
vega_base = self.calculate_numerical_vega(
|
|
651
|
+
product, pricing_env, engine, base_price=base_price, vol_bump=vol_bump
|
|
652
|
+
)
|
|
653
|
+
vega_up = self.calculate_numerical_vega(
|
|
654
|
+
product, env_up, engine, base_price=None, vol_bump=vol_bump
|
|
655
|
+
)
|
|
656
|
+
return (vega_up - vega_base) / vol_bump
|
|
657
|
+
|
|
658
|
+
env_up = self._build_vol_bumped_env(
|
|
659
|
+
pricing_env, product, current_vol, vol_bump, direction=1.0
|
|
660
|
+
)
|
|
661
|
+
env_down = self._build_vol_bumped_env(
|
|
662
|
+
pricing_env, product, current_vol, vol_bump, direction=-1.0
|
|
663
|
+
)
|
|
664
|
+
price_up = engine.price(product, env_up)
|
|
665
|
+
price_down = engine.price(product, env_down)
|
|
666
|
+
return self._calculate_sensitivity(
|
|
667
|
+
base_price, price_up, price_down, bump=vol_bump, mode="second_order"
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
def calculate_numerical_vanna(
|
|
671
|
+
self,
|
|
672
|
+
product: BaseEquityProduct,
|
|
673
|
+
pricing_env: PricingEnvironment,
|
|
674
|
+
engine: BaseEngine,
|
|
675
|
+
base_price: Optional[float] = None,
|
|
676
|
+
vol_bump: Optional[float] = None,
|
|
677
|
+
) -> float:
|
|
678
|
+
"""Numerical vanna (cross derivative wrt spot and vol)."""
|
|
679
|
+
vol_bump = vol_bump if vol_bump is not None else self._bump_config.vol_bump
|
|
680
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
681
|
+
T = product.get_maturity(pricing_env)
|
|
682
|
+
strike = getattr(product, "strike", pricing_env.spot)
|
|
683
|
+
current_vol = pricing_env.get_vol(strike, T)
|
|
684
|
+
|
|
685
|
+
env_up = self._build_vol_bumped_env(
|
|
686
|
+
pricing_env, product, current_vol, vol_bump, direction=1.0
|
|
687
|
+
)
|
|
688
|
+
env_down = self._build_vol_bumped_env(
|
|
689
|
+
pricing_env, product, current_vol, vol_bump, direction=-1.0
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
if current_vol - vol_bump <= 0:
|
|
693
|
+
base_delta = self.calculate_numerical_delta(
|
|
694
|
+
product,
|
|
695
|
+
pricing_env,
|
|
696
|
+
engine,
|
|
697
|
+
base_price=base_price,
|
|
698
|
+
bump=self._bump_config.spot_bump,
|
|
699
|
+
)
|
|
700
|
+
delta_up = self.calculate_numerical_delta(
|
|
701
|
+
product,
|
|
702
|
+
env_up,
|
|
703
|
+
engine,
|
|
704
|
+
base_price=base_price,
|
|
705
|
+
bump=self._bump_config.spot_bump,
|
|
706
|
+
)
|
|
707
|
+
return (delta_up - base_delta) / vol_bump
|
|
708
|
+
|
|
709
|
+
delta_up = self.calculate_numerical_delta(
|
|
710
|
+
product,
|
|
711
|
+
env_up,
|
|
712
|
+
engine,
|
|
713
|
+
base_price=base_price,
|
|
714
|
+
bump=self._bump_config.spot_bump,
|
|
715
|
+
)
|
|
716
|
+
delta_down = self.calculate_numerical_delta(
|
|
717
|
+
product,
|
|
718
|
+
env_down,
|
|
719
|
+
engine,
|
|
720
|
+
base_price=base_price,
|
|
721
|
+
bump=self._bump_config.spot_bump,
|
|
722
|
+
)
|
|
723
|
+
return (delta_up - delta_down) / (2.0 * vol_bump)
|
|
724
|
+
|
|
725
|
+
def calculate_numerical_theta(
|
|
726
|
+
self,
|
|
727
|
+
product: BaseEquityProduct,
|
|
728
|
+
pricing_env: PricingEnvironment,
|
|
729
|
+
engine: BaseEngine,
|
|
730
|
+
base_price: Optional[float] = None,
|
|
731
|
+
time_bump_days: Optional[int] = None,
|
|
732
|
+
) -> float:
|
|
733
|
+
"""Numerical theta via time bump with observation schedule handling."""
|
|
734
|
+
time_bump_days = (
|
|
735
|
+
time_bump_days
|
|
736
|
+
if time_bump_days is not None
|
|
737
|
+
else self._bump_config.time_bump_days
|
|
738
|
+
)
|
|
739
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
740
|
+
product_theta = deepcopy(product)
|
|
741
|
+
env_theta = deepcopy(pricing_env)
|
|
742
|
+
current_maturity = product.get_maturity(pricing_env)
|
|
743
|
+
|
|
744
|
+
bumped_date = pricing_env.valuation_date + timedelta(days=time_bump_days)
|
|
745
|
+
time_bump = calculate_year_fraction(
|
|
746
|
+
pricing_env.valuation_date,
|
|
747
|
+
bumped_date,
|
|
748
|
+
pricing_env.day_count_convention,
|
|
749
|
+
pricing_env.bus_days_in_year,
|
|
750
|
+
calendar=getattr(pricing_env, "calendar", None),
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
if current_maturity <= time_bump:
|
|
754
|
+
return 0.0
|
|
755
|
+
|
|
756
|
+
dropped_all_observations = product_theta.time_shift(
|
|
757
|
+
time_bump, bumped_date, env_theta
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
if dropped_all_observations:
|
|
761
|
+
return 0.0
|
|
762
|
+
|
|
763
|
+
price_theta = engine.price(product_theta, env_theta)
|
|
764
|
+
return price_theta - base_price
|
|
765
|
+
|
|
766
|
+
def calculate_numerical_rho(
|
|
767
|
+
self,
|
|
768
|
+
product: BaseEquityProduct,
|
|
769
|
+
pricing_env: PricingEnvironment,
|
|
770
|
+
engine: BaseEngine,
|
|
771
|
+
base_price: Optional[float] = None,
|
|
772
|
+
rate_bump: Optional[float] = None,
|
|
773
|
+
) -> float:
|
|
774
|
+
"""Numerical rho from a rate bump (per 1% rate change)."""
|
|
775
|
+
rate_bump = rate_bump if rate_bump is not None else self._bump_config.rate_bump
|
|
776
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
777
|
+
env_up_rate = deepcopy(pricing_env)
|
|
778
|
+
from quantark.param.rrf import FlatRateCurve
|
|
779
|
+
|
|
780
|
+
T = product.get_maturity(pricing_env)
|
|
781
|
+
current_rate = pricing_env.get_rate(T)
|
|
782
|
+
env_up_rate.rate_curve = FlatRateCurve(current_rate + rate_bump)
|
|
783
|
+
price_up_rate = engine.price(product, env_up_rate)
|
|
784
|
+
raw = self._calculate_sensitivity(
|
|
785
|
+
base_price, price_up_rate, bump=rate_bump, mode="one_sided"
|
|
786
|
+
)
|
|
787
|
+
return raw * (0.01 / rate_bump)
|
|
788
|
+
|
|
789
|
+
def calculate_numerical_dividend_rho(
|
|
790
|
+
self,
|
|
791
|
+
product: BaseEquityProduct,
|
|
792
|
+
pricing_env: PricingEnvironment,
|
|
793
|
+
engine: BaseEngine,
|
|
794
|
+
base_price: Optional[float] = None,
|
|
795
|
+
div_bump: Optional[float] = None,
|
|
796
|
+
) -> float:
|
|
797
|
+
"""
|
|
798
|
+
Numerical dividend_rho (psi) from dividend yield bump.
|
|
799
|
+
|
|
800
|
+
Measures price sensitivity to dividend yield changes:
|
|
801
|
+
dividend_rho = dV/dq
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
product: The derivative product
|
|
805
|
+
pricing_env: Pricing environment
|
|
806
|
+
engine: Pricing engine
|
|
807
|
+
base_price: Pre-calculated base price
|
|
808
|
+
div_bump: Absolute dividend yield bump (uses config if None)
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
Dividend rho value (price change per 1% div_yield change).
|
|
812
|
+
Negative for call options (higher div = lower call price).
|
|
813
|
+
Positive for put options (higher div = higher put price).
|
|
814
|
+
"""
|
|
815
|
+
div_bump = div_bump if div_bump is not None else self._bump_config.div_bump
|
|
816
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
817
|
+
T = product.get_maturity(pricing_env)
|
|
818
|
+
current_div = pricing_env.get_div_yield(T)
|
|
819
|
+
env_up_div = self._build_div_bumped_env(
|
|
820
|
+
pricing_env, product, current_div, div_bump, direction=1.0
|
|
821
|
+
)
|
|
822
|
+
price_up_div = engine.price(product, env_up_div)
|
|
823
|
+
raw = self._calculate_sensitivity(
|
|
824
|
+
base_price, price_up_div, bump=div_bump, mode="one_sided"
|
|
825
|
+
)
|
|
826
|
+
return raw * (0.01 / div_bump)
|
|
827
|
+
|
|
828
|
+
def calculate_numerical_delta_q(
|
|
829
|
+
self,
|
|
830
|
+
product: BaseEquityProduct,
|
|
831
|
+
pricing_env: PricingEnvironment,
|
|
832
|
+
engine: BaseEngine,
|
|
833
|
+
base_price: Optional[float] = None,
|
|
834
|
+
div_bump: Optional[float] = None,
|
|
835
|
+
base_delta: Optional[float] = None,
|
|
836
|
+
) -> float:
|
|
837
|
+
"""Numerical dDelta/dq via dividend yield bumps."""
|
|
838
|
+
div_bump = div_bump if div_bump is not None else self._bump_config.div_bump
|
|
839
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
840
|
+
T = product.get_maturity(pricing_env)
|
|
841
|
+
current_div = pricing_env.get_div_yield(T)
|
|
842
|
+
|
|
843
|
+
if base_delta is None:
|
|
844
|
+
base_delta = self.calculate_numerical_delta(
|
|
845
|
+
product,
|
|
846
|
+
pricing_env,
|
|
847
|
+
engine,
|
|
848
|
+
base_price=base_price,
|
|
849
|
+
bump=self._bump_config.spot_bump,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
if current_div - div_bump < 0:
|
|
853
|
+
env_up = self._build_div_bumped_env(
|
|
854
|
+
pricing_env, product, current_div, div_bump, direction=1.0
|
|
855
|
+
)
|
|
856
|
+
delta_up = self.calculate_numerical_delta(
|
|
857
|
+
product,
|
|
858
|
+
env_up,
|
|
859
|
+
engine,
|
|
860
|
+
base_price=base_price,
|
|
861
|
+
bump=self._bump_config.spot_bump,
|
|
862
|
+
)
|
|
863
|
+
return (delta_up - base_delta) / div_bump
|
|
864
|
+
|
|
865
|
+
env_up = self._build_div_bumped_env(
|
|
866
|
+
pricing_env, product, current_div, div_bump, direction=1.0
|
|
867
|
+
)
|
|
868
|
+
env_down = self._build_div_bumped_env(
|
|
869
|
+
pricing_env, product, current_div, div_bump, direction=-1.0
|
|
870
|
+
)
|
|
871
|
+
delta_up = self.calculate_numerical_delta(
|
|
872
|
+
product,
|
|
873
|
+
env_up,
|
|
874
|
+
engine,
|
|
875
|
+
base_price=base_price,
|
|
876
|
+
bump=self._bump_config.spot_bump,
|
|
877
|
+
)
|
|
878
|
+
delta_down = self.calculate_numerical_delta(
|
|
879
|
+
product,
|
|
880
|
+
env_down,
|
|
881
|
+
engine,
|
|
882
|
+
base_price=base_price,
|
|
883
|
+
bump=self._bump_config.spot_bump,
|
|
884
|
+
)
|
|
885
|
+
return (delta_up - delta_down) / (2.0 * div_bump)
|
|
886
|
+
|
|
887
|
+
def estimate_theta_components(
|
|
888
|
+
self,
|
|
889
|
+
theta: float,
|
|
890
|
+
rho: float,
|
|
891
|
+
dividend_rho: float,
|
|
892
|
+
r: float,
|
|
893
|
+
q: float,
|
|
894
|
+
T: float,
|
|
895
|
+
rate_bump: float = 0.01,
|
|
896
|
+
dividend_bump: float = 0.01,
|
|
897
|
+
) -> Dict[str, float]:
|
|
898
|
+
"""
|
|
899
|
+
Fast estimation of theta components from existing Greeks.
|
|
900
|
+
|
|
901
|
+
Uses the relationships between theta components and other Greeks:
|
|
902
|
+
r_theta ≈ -r/T * rho / rate_bump (corrected for scale and daily conversion)
|
|
903
|
+
q_theta ≈ -q/T * dividend_rho / dividend_bump (corrected for scale and daily conversion)
|
|
904
|
+
convexity_theta ≈ theta - r_theta - q_theta
|
|
905
|
+
|
|
906
|
+
This is an approximation that avoids repricing. For exact decomposition,
|
|
907
|
+
use _calculate_numerical_theta_components() instead.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
theta: Total theta (per day)
|
|
911
|
+
rho: Rho (sensitivity to rate, per 1% change)
|
|
912
|
+
dividend_rho: Dividend rho (sensitivity to dividend yield, per 1% change)
|
|
913
|
+
r: Interest rate (annual)
|
|
914
|
+
q: Dividend yield (annual)
|
|
915
|
+
T: Time to maturity in years
|
|
916
|
+
rate_bump: Rate scale of the rho input (default: 1% = 0.01)
|
|
917
|
+
dividend_bump: Dividend scale of the dividend_rho input (default: 1% = 0.01)
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
Dictionary with convexity_theta, r_theta, q_theta (all per day)
|
|
921
|
+
"""
|
|
922
|
+
if is_zero(T):
|
|
923
|
+
return {
|
|
924
|
+
"convexity_theta": 0.0,
|
|
925
|
+
"r_theta": 0.0,
|
|
926
|
+
"q_theta": 0.0,
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
# Rho/Dividend Rho are per rate_bump/dividend_bump size, so divide by scale.
|
|
930
|
+
# Divide by 365 to convert annual rate decay to daily theta equivalent.
|
|
931
|
+
# Divide by T to cancel out the T term in Rho (Rho = dV/dr = T * dV/d(rT) approx).
|
|
932
|
+
r_theta = -r / T * (rho / rate_bump) / 365
|
|
933
|
+
q_theta = -q / T * (dividend_rho / dividend_bump) / 365
|
|
934
|
+
convexity_theta = theta - r_theta - q_theta
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
"convexity_theta": convexity_theta,
|
|
938
|
+
"r_theta": r_theta,
|
|
939
|
+
"q_theta": q_theta,
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
def _calculate_numerical_theta_components(
|
|
943
|
+
self,
|
|
944
|
+
product: BaseEquityProduct,
|
|
945
|
+
pricing_env: PricingEnvironment,
|
|
946
|
+
engine: BaseEngine,
|
|
947
|
+
base_price: Optional[float] = None,
|
|
948
|
+
time_bump_days: Optional[int] = None,
|
|
949
|
+
) -> Dict[str, float]:
|
|
950
|
+
"""
|
|
951
|
+
Exact numerical theta decomposition via repricing with zeroed r/q.
|
|
952
|
+
|
|
953
|
+
This method computes exact theta components by repricing with different
|
|
954
|
+
rate and dividend yield combinations:
|
|
955
|
+
1. theta_no_rq = theta with r=0, q=0 → convexity_theta
|
|
956
|
+
2. theta_no_q = theta with q=0 → r_theta = theta_no_q - convexity_theta
|
|
957
|
+
3. theta_no_r = theta with r=0 → q_theta = theta_no_r - convexity_theta
|
|
958
|
+
|
|
959
|
+
Note: This is computationally expensive (3 extra pricings) and should
|
|
960
|
+
be treated as a slow path. For fast estimation, use
|
|
961
|
+
estimate_theta_components() instead.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
product: The derivative product
|
|
965
|
+
pricing_env: Pricing environment
|
|
966
|
+
engine: Pricing engine
|
|
967
|
+
base_price: Pre-calculated base price
|
|
968
|
+
time_bump_days: Time bump in days
|
|
969
|
+
|
|
970
|
+
Returns:
|
|
971
|
+
Dictionary with convexity_theta, r_theta, q_theta (all per day)
|
|
972
|
+
"""
|
|
973
|
+
from quantark.param.div import ContinuousDividendYield
|
|
974
|
+
from quantark.param.rrf import FlatRateCurve
|
|
975
|
+
|
|
976
|
+
time_bump_days = (
|
|
977
|
+
time_bump_days
|
|
978
|
+
if time_bump_days is not None
|
|
979
|
+
else self._bump_config.time_bump_days
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
T = product.get_maturity(pricing_env)
|
|
983
|
+
if is_zero(T):
|
|
984
|
+
return {
|
|
985
|
+
"convexity_theta": 0.0,
|
|
986
|
+
"r_theta": 0.0,
|
|
987
|
+
"q_theta": 0.0,
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
# Create environments with zeroed r and/or q
|
|
991
|
+
env_no_r = deepcopy(pricing_env)
|
|
992
|
+
env_no_r.rate_curve = FlatRateCurve(0.0)
|
|
993
|
+
|
|
994
|
+
env_no_q = deepcopy(pricing_env)
|
|
995
|
+
env_no_q.div_yield = ContinuousDividendYield(0.0)
|
|
996
|
+
|
|
997
|
+
env_no_rq = deepcopy(pricing_env)
|
|
998
|
+
env_no_rq.rate_curve = FlatRateCurve(0.0)
|
|
999
|
+
env_no_rq.div_yield = ContinuousDividendYield(0.0)
|
|
1000
|
+
|
|
1001
|
+
# Calculate theta in each environment
|
|
1002
|
+
theta_no_rq = self.calculate_numerical_theta(
|
|
1003
|
+
product, env_no_rq, engine, time_bump_days=time_bump_days
|
|
1004
|
+
)
|
|
1005
|
+
theta_no_q = self.calculate_numerical_theta(
|
|
1006
|
+
product, env_no_q, engine, time_bump_days=time_bump_days
|
|
1007
|
+
)
|
|
1008
|
+
theta_no_r = self.calculate_numerical_theta(
|
|
1009
|
+
product, env_no_r, engine, time_bump_days=time_bump_days
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
# Decompose
|
|
1013
|
+
convexity_theta = theta_no_rq
|
|
1014
|
+
r_theta = theta_no_q - convexity_theta
|
|
1015
|
+
q_theta = theta_no_r - convexity_theta
|
|
1016
|
+
|
|
1017
|
+
return {
|
|
1018
|
+
"convexity_theta": convexity_theta,
|
|
1019
|
+
"r_theta": r_theta,
|
|
1020
|
+
"q_theta": q_theta,
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
def _spot_bumped_prices(
|
|
1024
|
+
self,
|
|
1025
|
+
product: BaseEquityProduct,
|
|
1026
|
+
pricing_env: PricingEnvironment,
|
|
1027
|
+
engine: BaseEngine,
|
|
1028
|
+
bump: float,
|
|
1029
|
+
base_price: Optional[float] = None,
|
|
1030
|
+
reuse: Optional[Tuple[float, float]] = None,
|
|
1031
|
+
) -> Tuple[float, float, float]:
|
|
1032
|
+
"""
|
|
1033
|
+
Compute base, up, and down spot bump prices, optionally reusing bumps.
|
|
1034
|
+
"""
|
|
1035
|
+
base_price = self._ensure_base_price(product, pricing_env, engine, base_price)
|
|
1036
|
+
if reuse is not None:
|
|
1037
|
+
price_up_spot, price_down_spot = reuse
|
|
1038
|
+
else:
|
|
1039
|
+
env_up = deepcopy(pricing_env)
|
|
1040
|
+
env_up.spot_quote.spot *= 1 + bump
|
|
1041
|
+
price_up_spot = engine.price(product, env_up)
|
|
1042
|
+
|
|
1043
|
+
env_down = deepcopy(pricing_env)
|
|
1044
|
+
env_down.spot_quote.spot *= 1 - bump
|
|
1045
|
+
price_down_spot = engine.price(product, env_down)
|
|
1046
|
+
|
|
1047
|
+
return base_price, price_up_spot, price_down_spot
|
|
1048
|
+
|
|
1049
|
+
def _build_vol_bumped_env(
|
|
1050
|
+
self,
|
|
1051
|
+
pricing_env: PricingEnvironment,
|
|
1052
|
+
product: BaseEquityProduct,
|
|
1053
|
+
current_vol: float,
|
|
1054
|
+
vol_bump: float,
|
|
1055
|
+
*,
|
|
1056
|
+
direction: float,
|
|
1057
|
+
) -> PricingEnvironment:
|
|
1058
|
+
from quantark.param.vol import FlatVolSurface, TermStructureVolSurface
|
|
1059
|
+
|
|
1060
|
+
new_vol = current_vol + direction * vol_bump
|
|
1061
|
+
if new_vol <= 0:
|
|
1062
|
+
raise ValidationError(
|
|
1063
|
+
f"Stressed volatility must be positive, got {new_vol}"
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
env = deepcopy(pricing_env)
|
|
1067
|
+
if isinstance(pricing_env.vol_surface, TermStructureVolSurface):
|
|
1068
|
+
new_vols = [float(v) + direction * vol_bump for v in pricing_env.vol_surface.vols]
|
|
1069
|
+
if any(v <= 0 for v in new_vols):
|
|
1070
|
+
raise ValidationError("Stressed term-structure vol must be positive.")
|
|
1071
|
+
env.vol_surface = TermStructureVolSurface(
|
|
1072
|
+
times=list(pricing_env.vol_surface.times), vols=new_vols
|
|
1073
|
+
)
|
|
1074
|
+
else:
|
|
1075
|
+
env.vol_surface = FlatVolSurface(new_vol)
|
|
1076
|
+
return env
|
|
1077
|
+
|
|
1078
|
+
def _build_div_bumped_env(
|
|
1079
|
+
self,
|
|
1080
|
+
pricing_env: PricingEnvironment,
|
|
1081
|
+
product: BaseEquityProduct,
|
|
1082
|
+
current_div: float,
|
|
1083
|
+
div_bump: float,
|
|
1084
|
+
*,
|
|
1085
|
+
direction: float,
|
|
1086
|
+
) -> PricingEnvironment:
|
|
1087
|
+
from quantark.param.div import ContinuousDividendYield, TermStructureDividendYield
|
|
1088
|
+
|
|
1089
|
+
new_div = current_div + direction * div_bump
|
|
1090
|
+
if new_div < 0:
|
|
1091
|
+
raise ValidationError(
|
|
1092
|
+
f"Stressed dividend yield cannot be negative, got {new_div}"
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
env = deepcopy(pricing_env)
|
|
1096
|
+
if isinstance(pricing_env.div_yield, TermStructureDividendYield):
|
|
1097
|
+
new_yields = [
|
|
1098
|
+
float(y) + direction * div_bump for y in pricing_env.div_yield.yields
|
|
1099
|
+
]
|
|
1100
|
+
if any(y < 0 for y in new_yields):
|
|
1101
|
+
raise ValidationError(
|
|
1102
|
+
"Stressed term-structure dividend yield cannot be negative."
|
|
1103
|
+
)
|
|
1104
|
+
env.div_yield = TermStructureDividendYield(
|
|
1105
|
+
times=list(pricing_env.div_yield.times), yields=new_yields
|
|
1106
|
+
)
|
|
1107
|
+
else:
|
|
1108
|
+
env.div_yield = ContinuousDividendYield(new_div)
|
|
1109
|
+
return env
|
|
1110
|
+
|
|
1111
|
+
def _greeks_for_linear(
|
|
1112
|
+
self, product: BaseEquityProduct, price: float
|
|
1113
|
+
) -> Dict[str, float]:
|
|
1114
|
+
"""
|
|
1115
|
+
Calculate Greeks for linear (delta-one) products.
|
|
1116
|
+
|
|
1117
|
+
Delta-one products have trivial Greeks:
|
|
1118
|
+
- Delta = 1.0 (always)
|
|
1119
|
+
- Gamma, Vega, Theta, Rho, Dividend Rho = 0.0 (no optionality)
|
|
1120
|
+
|
|
1121
|
+
Args:
|
|
1122
|
+
product: Delta-one product
|
|
1123
|
+
price: Current price
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
Dictionary of Greeks
|
|
1127
|
+
"""
|
|
1128
|
+
return {
|
|
1129
|
+
"price": price,
|
|
1130
|
+
"delta": 1.0,
|
|
1131
|
+
"gamma": 0.0,
|
|
1132
|
+
"vega": 0.0,
|
|
1133
|
+
"theta": 0.0,
|
|
1134
|
+
"convexity_theta": 0.0,
|
|
1135
|
+
"r_theta": 0.0,
|
|
1136
|
+
"q_theta": 0.0,
|
|
1137
|
+
"rho": 0.0,
|
|
1138
|
+
"dividend_rho": 0.0,
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
def _greeks_at_expiry(
|
|
1142
|
+
self, product: EuropeanVanillaOption, spot: float
|
|
1143
|
+
) -> Dict[str, float]:
|
|
1144
|
+
"""
|
|
1145
|
+
Calculate Greeks at expiry.
|
|
1146
|
+
|
|
1147
|
+
At expiry:
|
|
1148
|
+
- Price = intrinsic value
|
|
1149
|
+
- Delta = 1 (ITM call), -1 (ITM put), 0 (OTM)
|
|
1150
|
+
- Gamma, Vega, Theta, Rho = 0
|
|
1151
|
+
|
|
1152
|
+
Args:
|
|
1153
|
+
product: European vanilla option
|
|
1154
|
+
spot: Current spot price
|
|
1155
|
+
|
|
1156
|
+
Returns:
|
|
1157
|
+
Dictionary of Greeks
|
|
1158
|
+
"""
|
|
1159
|
+
multiplier = product.contract_multiplier
|
|
1160
|
+
price = product.get_payoff(spot) / multiplier
|
|
1161
|
+
|
|
1162
|
+
# Delta at expiry
|
|
1163
|
+
if product.is_call():
|
|
1164
|
+
delta = 1.0 if spot > product.strike else 0.0
|
|
1165
|
+
else:
|
|
1166
|
+
delta = -1.0 if spot < product.strike else 0.0
|
|
1167
|
+
|
|
1168
|
+
return {
|
|
1169
|
+
"price": price * multiplier,
|
|
1170
|
+
"delta": delta * multiplier,
|
|
1171
|
+
"gamma": 0.0,
|
|
1172
|
+
"vega": 0.0,
|
|
1173
|
+
"theta": 0.0,
|
|
1174
|
+
"convexity_theta": 0.0,
|
|
1175
|
+
"r_theta": 0.0,
|
|
1176
|
+
"q_theta": 0.0,
|
|
1177
|
+
"rho": 0.0,
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
def compare_greeks(
|
|
1181
|
+
self, analytical: Dict[str, float], numerical: Dict[str, float]
|
|
1182
|
+
) -> Dict[str, Dict[str, float]]:
|
|
1183
|
+
"""
|
|
1184
|
+
Compare analytical and numerical Greeks.
|
|
1185
|
+
|
|
1186
|
+
Args:
|
|
1187
|
+
analytical: Analytical Greeks
|
|
1188
|
+
numerical: Numerical Greeks
|
|
1189
|
+
|
|
1190
|
+
Returns:
|
|
1191
|
+
Dictionary with 'analytical', 'numerical', and 'difference' sub-dictionaries
|
|
1192
|
+
"""
|
|
1193
|
+
difference = {}
|
|
1194
|
+
for key in analytical:
|
|
1195
|
+
if key in numerical:
|
|
1196
|
+
diff = analytical[key] - numerical[key]
|
|
1197
|
+
rel_diff = diff / analytical[key] if abs(analytical[key]) > 1e-10 else 0
|
|
1198
|
+
difference[key] = {"absolute": diff, "relative": rel_diff}
|
|
1199
|
+
|
|
1200
|
+
return {
|
|
1201
|
+
"analytical": analytical,
|
|
1202
|
+
"numerical": numerical,
|
|
1203
|
+
"difference": difference,
|
|
1204
|
+
}
|