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,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytical pricing engine for one-touch and no-touch options.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from scipy import stats
|
|
9
|
+
|
|
10
|
+
from quantark.asset.equity.engine.base_engine import BaseEngine
|
|
11
|
+
from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
|
|
12
|
+
from quantark.asset.equity.product.option import CashOrNothingDigitalOption, OneTouchOption
|
|
13
|
+
from quantark.asset.equity.param import EngineParams
|
|
14
|
+
from quantark.priceenv import PricingEnvironment
|
|
15
|
+
from quantark.util.barrier_shift import apply_barrier_shift
|
|
16
|
+
from quantark.util.enum import ObservationType, OptionType, TouchType
|
|
17
|
+
from quantark.util.enum.engine_enums import EngineType
|
|
18
|
+
from quantark.util.exceptions import PricingError, ValidationError
|
|
19
|
+
|
|
20
|
+
from .digital_option_engine import DigitalOptionAnalyticalEngine
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OneTouchAnalyticalEngine(BaseEngine):
|
|
24
|
+
"""
|
|
25
|
+
Closed-form pricing engine for one-touch and no-touch options.
|
|
26
|
+
|
|
27
|
+
Supports:
|
|
28
|
+
- Continuous monitoring
|
|
29
|
+
- Discrete monitoring via Broadie-Glasserman-Kou barrier shift (regular grids)
|
|
30
|
+
- Expiry-only monitoring via digital-option fallback
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
No tenor/365 scaling is applied at the engine level.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
engine_type = EngineType.ANALYTICAL
|
|
37
|
+
|
|
38
|
+
MIN_VOL = 0.001
|
|
39
|
+
MAX_VOL = 5.0
|
|
40
|
+
MIN_MATURITY = 1e-10
|
|
41
|
+
MAX_MATURITY = 50.0
|
|
42
|
+
|
|
43
|
+
def __init__(self, params: Optional[EngineParams] = None):
|
|
44
|
+
super().__init__(params)
|
|
45
|
+
self._digital_engine = DigitalOptionAnalyticalEngine(params)
|
|
46
|
+
|
|
47
|
+
def price(
|
|
48
|
+
self, product: BaseEquityProduct, pricing_env: PricingEnvironment
|
|
49
|
+
) -> float:
|
|
50
|
+
if not isinstance(product, OneTouchOption):
|
|
51
|
+
raise PricingError(
|
|
52
|
+
f"OneTouchAnalyticalEngine only supports OneTouchOption, "
|
|
53
|
+
f"got {type(product).__name__}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
spot = pricing_env.spot
|
|
57
|
+
maturity = product.get_maturity(pricing_env)
|
|
58
|
+
rate = pricing_env.get_rate(maturity)
|
|
59
|
+
div = pricing_env.get_div_yield(maturity)
|
|
60
|
+
vol = pricing_env.get_vol(product.barrier, maturity)
|
|
61
|
+
rebate = product.rebate
|
|
62
|
+
|
|
63
|
+
pay_at_hit = product.payment_at_hit if product.is_one_touch else False
|
|
64
|
+
|
|
65
|
+
self._validate_inputs(spot, product.barrier, maturity, vol, rebate)
|
|
66
|
+
|
|
67
|
+
# Immediate handling for near-expiry or already-hit barriers
|
|
68
|
+
if maturity < self.MIN_MATURITY:
|
|
69
|
+
return self._instantaneous_payoff(
|
|
70
|
+
product=product,
|
|
71
|
+
spot=spot,
|
|
72
|
+
maturity=maturity,
|
|
73
|
+
rate=rate,
|
|
74
|
+
pay_at_hit=pay_at_hit,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
obs_type = product.observation_type
|
|
78
|
+
|
|
79
|
+
if obs_type != ObservationType.EXPIRY and product.is_barrier_hit(spot):
|
|
80
|
+
if product.is_one_touch:
|
|
81
|
+
return rebate if pay_at_hit else rebate * math.exp(-rate * maturity)
|
|
82
|
+
return 0.0
|
|
83
|
+
|
|
84
|
+
if obs_type == ObservationType.EXPIRY:
|
|
85
|
+
return self._price_expiry(product, pricing_env)
|
|
86
|
+
|
|
87
|
+
if obs_type == ObservationType.DISCRETE:
|
|
88
|
+
schedule = product.observation_schedule
|
|
89
|
+
if schedule is None or not schedule.records:
|
|
90
|
+
raise PricingError(
|
|
91
|
+
"Discrete monitoring requires a populated ObservationSchedule."
|
|
92
|
+
)
|
|
93
|
+
schedule.assert_analytical_ready(default_payoff=rebate)
|
|
94
|
+
frequency = schedule.ensure_regular_frequency(schedule.times)
|
|
95
|
+
barrier = apply_barrier_shift(
|
|
96
|
+
barrier=product.barrier,
|
|
97
|
+
is_up_barrier=product.is_up_barrier,
|
|
98
|
+
volatility=vol,
|
|
99
|
+
observation_interval=frequency,
|
|
100
|
+
)
|
|
101
|
+
elif obs_type == ObservationType.CONTINUOUS:
|
|
102
|
+
barrier = product.barrier
|
|
103
|
+
else:
|
|
104
|
+
raise PricingError(f"Unsupported observation type: {obs_type}")
|
|
105
|
+
|
|
106
|
+
if product.is_one_touch:
|
|
107
|
+
return self._one_touch_price(
|
|
108
|
+
spot=spot,
|
|
109
|
+
barrier=barrier,
|
|
110
|
+
maturity=maturity,
|
|
111
|
+
rate=rate,
|
|
112
|
+
div=div,
|
|
113
|
+
vol=vol,
|
|
114
|
+
rebate=rebate,
|
|
115
|
+
pay_at_hit=pay_at_hit,
|
|
116
|
+
is_up=product.is_up_barrier,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# No-touch: pay only at expiry if not hit
|
|
120
|
+
prob_touch = self._touch_probability(
|
|
121
|
+
spot=spot,
|
|
122
|
+
barrier=barrier,
|
|
123
|
+
maturity=maturity,
|
|
124
|
+
rate=rate,
|
|
125
|
+
div=div,
|
|
126
|
+
vol=vol,
|
|
127
|
+
is_up=product.is_up_barrier,
|
|
128
|
+
)
|
|
129
|
+
prob_touch = min(max(prob_touch, 0.0), 1.0)
|
|
130
|
+
discount = math.exp(-rate * maturity)
|
|
131
|
+
return rebate * discount * max(0.0, 1.0 - prob_touch)
|
|
132
|
+
|
|
133
|
+
def _price_expiry(
|
|
134
|
+
self, product: OneTouchOption, pricing_env: PricingEnvironment
|
|
135
|
+
) -> float:
|
|
136
|
+
"""Price with expiry-only monitoring using digital option fallback."""
|
|
137
|
+
option_type = self._digital_direction(product)
|
|
138
|
+
digital = CashOrNothingDigitalOption(
|
|
139
|
+
strike=product.barrier,
|
|
140
|
+
payout=product.rebate,
|
|
141
|
+
option_type=option_type,
|
|
142
|
+
maturity=product.maturity,
|
|
143
|
+
exercise_date=product.exercise_date,
|
|
144
|
+
settlement_date=product.settlement_date,
|
|
145
|
+
)
|
|
146
|
+
return self._digital_engine.price(digital, pricing_env)
|
|
147
|
+
|
|
148
|
+
def _digital_direction(self, product: OneTouchOption) -> OptionType:
|
|
149
|
+
"""Map one-touch/no-touch direction to an equivalent digital payoff."""
|
|
150
|
+
if product.is_one_touch:
|
|
151
|
+
return OptionType.CALL if product.is_up_barrier else OptionType.PUT
|
|
152
|
+
# No-touch pays if terminal spot stays on the non-breach side
|
|
153
|
+
return OptionType.PUT if product.is_up_barrier else OptionType.CALL
|
|
154
|
+
|
|
155
|
+
def _one_touch_price(
|
|
156
|
+
self,
|
|
157
|
+
spot: float,
|
|
158
|
+
barrier: float,
|
|
159
|
+
maturity: float,
|
|
160
|
+
rate: float,
|
|
161
|
+
div: float,
|
|
162
|
+
vol: float,
|
|
163
|
+
rebate: float,
|
|
164
|
+
pay_at_hit: bool,
|
|
165
|
+
is_up: bool,
|
|
166
|
+
) -> float:
|
|
167
|
+
"""Closed-form one-touch price for continuous or shifted discrete barriers."""
|
|
168
|
+
if pay_at_hit:
|
|
169
|
+
return rebate * self._instant_touch_term(
|
|
170
|
+
spot=spot,
|
|
171
|
+
barrier=barrier,
|
|
172
|
+
maturity=maturity,
|
|
173
|
+
rate=rate,
|
|
174
|
+
div=div,
|
|
175
|
+
vol=vol,
|
|
176
|
+
is_up=is_up,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return rebate * math.exp(-rate * maturity) * self._expiry_touch_term(
|
|
180
|
+
spot=spot,
|
|
181
|
+
barrier=barrier,
|
|
182
|
+
maturity=maturity,
|
|
183
|
+
rate=rate,
|
|
184
|
+
div=div,
|
|
185
|
+
vol=vol,
|
|
186
|
+
is_up=is_up,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _touch_probability(
|
|
190
|
+
self,
|
|
191
|
+
spot: float,
|
|
192
|
+
barrier: float,
|
|
193
|
+
maturity: float,
|
|
194
|
+
rate: float,
|
|
195
|
+
div: float,
|
|
196
|
+
vol: float,
|
|
197
|
+
is_up: bool,
|
|
198
|
+
) -> float:
|
|
199
|
+
"""Probability of touching the barrier before expiry (used for no-touch)."""
|
|
200
|
+
return self._expiry_touch_term(
|
|
201
|
+
spot=spot,
|
|
202
|
+
barrier=barrier,
|
|
203
|
+
maturity=maturity,
|
|
204
|
+
rate=rate,
|
|
205
|
+
div=div,
|
|
206
|
+
vol=vol,
|
|
207
|
+
is_up=is_up,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _instant_touch_term(
|
|
211
|
+
self,
|
|
212
|
+
spot: float,
|
|
213
|
+
barrier: float,
|
|
214
|
+
maturity: float,
|
|
215
|
+
rate: float,
|
|
216
|
+
div: float,
|
|
217
|
+
vol: float,
|
|
218
|
+
is_up: bool,
|
|
219
|
+
) -> float:
|
|
220
|
+
b = rate - div
|
|
221
|
+
mu = (b - 0.5 * vol * vol) / (vol * vol)
|
|
222
|
+
lam = math.sqrt(mu * mu + 2.0 * rate / (vol * vol))
|
|
223
|
+
sqrt_t = math.sqrt(maturity)
|
|
224
|
+
z = math.log(barrier / spot) / (vol * sqrt_t) + lam * vol * sqrt_t
|
|
225
|
+
eta = -1.0 if is_up else 1.0
|
|
226
|
+
|
|
227
|
+
term1 = math.pow(barrier / spot, mu + lam) * stats.norm.cdf(eta * z)
|
|
228
|
+
term2 = math.pow(barrier / spot, mu - lam) * stats.norm.cdf(
|
|
229
|
+
eta * z - 2 * eta * lam * vol * sqrt_t
|
|
230
|
+
)
|
|
231
|
+
return term1 + term2
|
|
232
|
+
|
|
233
|
+
def _expiry_touch_term(
|
|
234
|
+
self,
|
|
235
|
+
spot: float,
|
|
236
|
+
barrier: float,
|
|
237
|
+
maturity: float,
|
|
238
|
+
rate: float,
|
|
239
|
+
div: float,
|
|
240
|
+
vol: float,
|
|
241
|
+
is_up: bool,
|
|
242
|
+
) -> float:
|
|
243
|
+
b = rate - div
|
|
244
|
+
mu = (b - 0.5 * vol * vol) / (vol * vol)
|
|
245
|
+
sqrt_t = math.sqrt(maturity)
|
|
246
|
+
log_s_b = math.log(spot / barrier)
|
|
247
|
+
x2 = log_s_b / (vol * sqrt_t) + (1 + mu) * vol * sqrt_t
|
|
248
|
+
y2 = -log_s_b / (vol * sqrt_t) + (1 + mu) * vol * sqrt_t
|
|
249
|
+
phi = 1.0 if is_up else -1.0
|
|
250
|
+
eta = -1.0 if is_up else 1.0
|
|
251
|
+
pow_term = math.pow(barrier / spot, 2 * mu)
|
|
252
|
+
return stats.norm.cdf(phi * x2 - phi * vol * sqrt_t) + pow_term * stats.norm.cdf(
|
|
253
|
+
eta * y2 - eta * vol * sqrt_t
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def _instantaneous_payoff(
|
|
257
|
+
self,
|
|
258
|
+
product: OneTouchOption,
|
|
259
|
+
spot: float,
|
|
260
|
+
maturity: float,
|
|
261
|
+
rate: float,
|
|
262
|
+
pay_at_hit: bool,
|
|
263
|
+
) -> float:
|
|
264
|
+
"""Handle payoffs when maturity is effectively zero."""
|
|
265
|
+
touched = product.is_barrier_hit(spot)
|
|
266
|
+
discount = math.exp(-rate * maturity)
|
|
267
|
+
if product.is_one_touch:
|
|
268
|
+
if touched:
|
|
269
|
+
return product.rebate if pay_at_hit else product.rebate * discount
|
|
270
|
+
return 0.0
|
|
271
|
+
return product.rebate * discount if not touched else 0.0
|
|
272
|
+
|
|
273
|
+
def _validate_inputs(
|
|
274
|
+
self,
|
|
275
|
+
spot: float,
|
|
276
|
+
barrier: float,
|
|
277
|
+
maturity: float,
|
|
278
|
+
vol: float,
|
|
279
|
+
rebate: float,
|
|
280
|
+
) -> None:
|
|
281
|
+
if spot <= 0:
|
|
282
|
+
raise ValidationError(f"Spot price must be positive, got {spot}")
|
|
283
|
+
if barrier <= 0:
|
|
284
|
+
raise ValidationError(f"Barrier must be positive, got {barrier}")
|
|
285
|
+
if maturity < 0:
|
|
286
|
+
raise ValidationError(f"Maturity must be non-negative, got {maturity}")
|
|
287
|
+
if vol <= 0:
|
|
288
|
+
raise ValidationError(f"Volatility must be positive, got {vol}")
|
|
289
|
+
if vol < self.MIN_VOL or vol > self.MAX_VOL:
|
|
290
|
+
raise ValidationError(
|
|
291
|
+
f"Volatility {vol} outside supported range [{self.MIN_VOL}, {self.MAX_VOL}]"
|
|
292
|
+
)
|
|
293
|
+
if maturity > self.MAX_MATURITY:
|
|
294
|
+
raise ValidationError(
|
|
295
|
+
f"Maturity too long for analytical one-touch pricing: {maturity}"
|
|
296
|
+
)
|
|
297
|
+
if rebate < 0:
|
|
298
|
+
raise ValidationError(f"Rebate must be non-negative, got {rebate}")
|
|
299
|
+
|
|
300
|
+
def __repr__(self):
|
|
301
|
+
return "OneTouchAnalyticalEngine()"
|
|
302
|
+
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytical pricing engine for Range Accrual options via digital option decomposition.
|
|
3
|
+
|
|
4
|
+
Under GBM, a Range Accrual decomposes into a portfolio of digital options by
|
|
5
|
+
linearity of expectation. Each observation contributes independently:
|
|
6
|
+
|
|
7
|
+
E[accrual_ratio] = (1/W) * sum_i w_i * P(L_i <= S(t_i) <= U_i)
|
|
8
|
+
|
|
9
|
+
where P(L <= S(t) <= U) = N(d2_L) - N(d2_U) with standard BSM d2 terms.
|
|
10
|
+
|
|
11
|
+
The price is:
|
|
12
|
+
Price = exp(-r*T) * S_0 * M * c * tau * E[accrual_ratio]
|
|
13
|
+
|
|
14
|
+
Historical observations are incorporated deterministically.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from scipy import stats
|
|
23
|
+
|
|
24
|
+
from quantark.asset.equity.engine.base_engine import BaseEngine
|
|
25
|
+
from quantark.asset.equity.param import EngineParams
|
|
26
|
+
from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
|
|
27
|
+
from quantark.asset.equity.product.option.range_accrual_option import RangeAccrualOption
|
|
28
|
+
from quantark.priceenv import PricingEnvironment
|
|
29
|
+
from quantark.util.enum.engine_enums import EngineType
|
|
30
|
+
from quantark.util.exceptions import PricingError, ValidationError
|
|
31
|
+
from quantark.util.numerical import is_zero, safe_log
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RangeAccrualAnalyticalResult:
|
|
36
|
+
"""Result container for Range Accrual analytical pricing."""
|
|
37
|
+
|
|
38
|
+
price: float
|
|
39
|
+
expected_accrual_ratio: float
|
|
40
|
+
per_observation_probs: List[float] # P_i for each future observation
|
|
41
|
+
past_in_range_weights: float
|
|
42
|
+
future_expected_in_range_weights: float
|
|
43
|
+
total_weights: float
|
|
44
|
+
num_past_observations: int
|
|
45
|
+
num_future_observations: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RangeAccrualAnalyticalEngine(BaseEngine):
|
|
49
|
+
"""
|
|
50
|
+
Closed-form pricing for Range Accrual options via digital decomposition.
|
|
51
|
+
|
|
52
|
+
Decomposes the Range Accrual payoff into a sum of digital option
|
|
53
|
+
probabilities under Black-Scholes-Merton. Each observation at time t_i
|
|
54
|
+
with barriers [L_i, U_i] contributes:
|
|
55
|
+
|
|
56
|
+
P_i = N(d2_L) - N(d2_U) (standard mode)
|
|
57
|
+
P_i = 1 - [N(d2_L) - N(d2_U)] (reverse mode)
|
|
58
|
+
|
|
59
|
+
where d2(K, t) = [ln(S/K) + (r - q - sigma^2/2)*t] / (sigma*sqrt(t))
|
|
60
|
+
|
|
61
|
+
Supports:
|
|
62
|
+
- Weighted observations (e.g., Friday=3 for weekend carry)
|
|
63
|
+
- Historical observations with known in-range outcomes
|
|
64
|
+
- Time-varying barriers (per-observation or scalar)
|
|
65
|
+
- Reverse mode (pay when outside range)
|
|
66
|
+
- Annualized or non-annualized accrual rates
|
|
67
|
+
- Per-observation vol term structure
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
engine_type = EngineType.ANALYTICAL
|
|
71
|
+
|
|
72
|
+
MIN_VOL = 0.001
|
|
73
|
+
MAX_VOL = 5.0
|
|
74
|
+
MIN_MATURITY = 1e-10
|
|
75
|
+
MAX_OBS_TIME = 30.0
|
|
76
|
+
|
|
77
|
+
def __init__(self, params: Optional[EngineParams] = None):
|
|
78
|
+
super().__init__(params)
|
|
79
|
+
self._last_result: Optional[RangeAccrualAnalyticalResult] = None
|
|
80
|
+
|
|
81
|
+
def price(
|
|
82
|
+
self, product: BaseEquityProduct, pricing_env: PricingEnvironment
|
|
83
|
+
) -> float:
|
|
84
|
+
"""
|
|
85
|
+
Price a Range Accrual option analytically.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
product: RangeAccrualOption to price
|
|
89
|
+
pricing_env: Pricing environment with market data
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Option price
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
PricingError: If product type is unsupported
|
|
96
|
+
ValidationError: If input parameters are invalid
|
|
97
|
+
NumericalError: If numerical computation fails
|
|
98
|
+
"""
|
|
99
|
+
if not isinstance(product, RangeAccrualOption):
|
|
100
|
+
raise PricingError(
|
|
101
|
+
f"RangeAccrualAnalyticalEngine only supports RangeAccrualOption, "
|
|
102
|
+
f"got {type(product).__name__}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if product.range_config is None:
|
|
106
|
+
raise ValidationError("range_config is required for Range Accrual option")
|
|
107
|
+
|
|
108
|
+
S = pricing_env.spot
|
|
109
|
+
T = product.get_maturity(pricing_env)
|
|
110
|
+
r = pricing_env.get_rate(T)
|
|
111
|
+
q = pricing_env.get_div_yield(T)
|
|
112
|
+
|
|
113
|
+
self._validate_inputs(S, T, r, q)
|
|
114
|
+
|
|
115
|
+
# Handle near-expiry: all observations are effectively past
|
|
116
|
+
if is_zero(T):
|
|
117
|
+
past_in_range, _ = product.get_past_accrual(pricing_env)
|
|
118
|
+
total_weights = product.get_total_weights()
|
|
119
|
+
return product.get_payoff(
|
|
120
|
+
S,
|
|
121
|
+
in_range_weights=past_in_range,
|
|
122
|
+
total_weights=total_weights,
|
|
123
|
+
pricing_env=pricing_env,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Resolve past vs future observations
|
|
127
|
+
past_obs, future_obs, total_weights = product.resolve_observations(pricing_env)
|
|
128
|
+
|
|
129
|
+
# Accumulate deterministic past contribution
|
|
130
|
+
past_in_range_weights = sum(w for w, in_range in past_obs if in_range)
|
|
131
|
+
|
|
132
|
+
# Compute expected future in-range weights analytically
|
|
133
|
+
future_expected, per_obs_probs = self._compute_future_expected_weights(
|
|
134
|
+
product, pricing_env, S, r, q, future_obs
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Expected accrual ratio
|
|
138
|
+
total_in_range = past_in_range_weights + future_expected
|
|
139
|
+
if is_zero(total_weights):
|
|
140
|
+
expected_ratio = 0.0
|
|
141
|
+
else:
|
|
142
|
+
expected_ratio = total_in_range / total_weights
|
|
143
|
+
|
|
144
|
+
# Compute price
|
|
145
|
+
year_fraction = product.get_year_fraction(pricing_env)
|
|
146
|
+
accrual_rate = product.range_config.accrual_rate
|
|
147
|
+
discount = math.exp(-r * T)
|
|
148
|
+
|
|
149
|
+
price = (
|
|
150
|
+
discount
|
|
151
|
+
* product.initial_price
|
|
152
|
+
* product.contract_multiplier
|
|
153
|
+
* accrual_rate
|
|
154
|
+
* expected_ratio
|
|
155
|
+
* year_fraction
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self._last_result = RangeAccrualAnalyticalResult(
|
|
159
|
+
price=price,
|
|
160
|
+
expected_accrual_ratio=expected_ratio,
|
|
161
|
+
per_observation_probs=per_obs_probs,
|
|
162
|
+
past_in_range_weights=past_in_range_weights,
|
|
163
|
+
future_expected_in_range_weights=future_expected,
|
|
164
|
+
total_weights=total_weights,
|
|
165
|
+
num_past_observations=len(past_obs),
|
|
166
|
+
num_future_observations=len(future_obs),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return price
|
|
170
|
+
|
|
171
|
+
def _compute_future_expected_weights(
|
|
172
|
+
self,
|
|
173
|
+
product: RangeAccrualOption,
|
|
174
|
+
pricing_env: PricingEnvironment,
|
|
175
|
+
S: float,
|
|
176
|
+
r: float,
|
|
177
|
+
q: float,
|
|
178
|
+
future_obs: List[Tuple[float, float, int]],
|
|
179
|
+
) -> Tuple[float, List[float]]:
|
|
180
|
+
"""
|
|
181
|
+
Compute expected in-range weights for future observations.
|
|
182
|
+
|
|
183
|
+
For each future observation, computes:
|
|
184
|
+
P_i = N(d2_L) - N(d2_U) (or 1 - that for reverse mode)
|
|
185
|
+
|
|
186
|
+
Uses vectorized NumPy for efficiency.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
product: Range Accrual option
|
|
190
|
+
pricing_env: Pricing environment
|
|
191
|
+
S: Current spot price
|
|
192
|
+
r: Risk-free rate
|
|
193
|
+
q: Dividend yield
|
|
194
|
+
future_obs: List of (weight, time, obs_idx) for future observations
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Tuple of (total_expected_in_range_weight, per_obs_probabilities)
|
|
198
|
+
"""
|
|
199
|
+
if len(future_obs) == 0:
|
|
200
|
+
return 0.0, []
|
|
201
|
+
|
|
202
|
+
config = product.range_config
|
|
203
|
+
assert config is not None # Validated by caller
|
|
204
|
+
|
|
205
|
+
n = len(future_obs)
|
|
206
|
+
weights = np.empty(n)
|
|
207
|
+
times = np.empty(n)
|
|
208
|
+
lowers = np.empty(n)
|
|
209
|
+
uppers = np.empty(n)
|
|
210
|
+
sigmas = np.empty(n)
|
|
211
|
+
|
|
212
|
+
for i, (w, t, obs_idx) in enumerate(future_obs):
|
|
213
|
+
weights[i] = w
|
|
214
|
+
times[i] = t
|
|
215
|
+
lowers[i] = config.get_lower_barrier(obs_idx)
|
|
216
|
+
uppers[i] = config.get_upper_barrier(obs_idx)
|
|
217
|
+
sigmas[i] = pricing_env.get_vol(product.initial_price, t)
|
|
218
|
+
|
|
219
|
+
# Handle observations with near-zero time or near-zero vol deterministically
|
|
220
|
+
probs = np.empty(n)
|
|
221
|
+
degenerate = (times < self.MIN_MATURITY) | (sigmas < self.MIN_VOL)
|
|
222
|
+
|
|
223
|
+
if np.any(degenerate):
|
|
224
|
+
deg_mask = degenerate
|
|
225
|
+
# For near-zero time: check current spot
|
|
226
|
+
# For near-zero vol: check forward spot
|
|
227
|
+
fwd = np.where(
|
|
228
|
+
times[deg_mask] < self.MIN_MATURITY,
|
|
229
|
+
S,
|
|
230
|
+
S * np.exp((r - q) * times[deg_mask]),
|
|
231
|
+
)
|
|
232
|
+
in_range = (fwd >= lowers[deg_mask]) & (fwd <= uppers[deg_mask])
|
|
233
|
+
probs[deg_mask] = in_range.astype(float)
|
|
234
|
+
|
|
235
|
+
# Process non-degenerate observations vectorized
|
|
236
|
+
normal = ~degenerate
|
|
237
|
+
if np.any(normal):
|
|
238
|
+
t_n = times[normal]
|
|
239
|
+
sig_n = sigmas[normal]
|
|
240
|
+
l_n = lowers[normal]
|
|
241
|
+
u_n = uppers[normal]
|
|
242
|
+
|
|
243
|
+
sqrt_t = np.sqrt(t_n)
|
|
244
|
+
drift = (r - q - 0.5 * sig_n * sig_n) * t_n
|
|
245
|
+
denom = sig_n * sqrt_t
|
|
246
|
+
|
|
247
|
+
# d2 for lower barrier: P(S(t) >= L)
|
|
248
|
+
d2_L = (np.log(S / l_n) + drift) / denom
|
|
249
|
+
# d2 for upper barrier: P(S(t) >= U)
|
|
250
|
+
d2_U = (np.log(S / u_n) + drift) / denom
|
|
251
|
+
|
|
252
|
+
probs[normal] = stats.norm.cdf(d2_L) - stats.norm.cdf(d2_U)
|
|
253
|
+
|
|
254
|
+
# Apply reverse mode
|
|
255
|
+
if config.is_reverse:
|
|
256
|
+
probs = 1.0 - probs
|
|
257
|
+
|
|
258
|
+
# Clamp probabilities to [0, 1] for numerical safety
|
|
259
|
+
np.clip(probs, 0.0, 1.0, out=probs)
|
|
260
|
+
|
|
261
|
+
# Expected in-range weight = sum(w_i * P_i)
|
|
262
|
+
expected_weight = float(np.dot(weights, probs))
|
|
263
|
+
per_obs_probs = probs.tolist()
|
|
264
|
+
|
|
265
|
+
return expected_weight, per_obs_probs
|
|
266
|
+
|
|
267
|
+
def _validate_inputs(
|
|
268
|
+
self, S: float, T: float, r: float, q: float
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Validate pricing inputs."""
|
|
271
|
+
if S <= 0:
|
|
272
|
+
raise ValidationError(f"Spot price must be positive, got {S}")
|
|
273
|
+
if T < 0:
|
|
274
|
+
raise ValidationError(f"Time to maturity must be non-negative, got {T}")
|
|
275
|
+
if q < 0:
|
|
276
|
+
raise ValidationError(f"Dividend yield must be non-negative, got {q}")
|
|
277
|
+
if abs(r) > 1.0:
|
|
278
|
+
raise ValidationError(
|
|
279
|
+
f"Risk-free rate outside reasonable bounds: {r}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def get_last_result(self) -> Optional[RangeAccrualAnalyticalResult]:
|
|
283
|
+
"""Get the full result from the last pricing run."""
|
|
284
|
+
return self._last_result
|
|
285
|
+
|
|
286
|
+
def calculate_greeks(
|
|
287
|
+
self, product: BaseEquityProduct, pricing_env: PricingEnvironment
|
|
288
|
+
) -> Dict[str, float]:
|
|
289
|
+
"""
|
|
290
|
+
Calculate Greeks using analytical formulas where possible.
|
|
291
|
+
|
|
292
|
+
Delta and gamma are computed analytically from the derivative of N(d2).
|
|
293
|
+
Falls back to base class bump-and-reprice for vega, theta, rho.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
product: Range Accrual option
|
|
297
|
+
pricing_env: Pricing environment
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Dictionary of Greeks: price, delta, gamma
|
|
301
|
+
"""
|
|
302
|
+
if not isinstance(product, RangeAccrualOption):
|
|
303
|
+
raise PricingError(
|
|
304
|
+
f"RangeAccrualAnalyticalEngine only supports RangeAccrualOption, "
|
|
305
|
+
f"got {type(product).__name__}"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
config = product.range_config
|
|
309
|
+
if config is None:
|
|
310
|
+
raise ValidationError("range_config is required")
|
|
311
|
+
|
|
312
|
+
S = pricing_env.spot
|
|
313
|
+
T = product.get_maturity(pricing_env)
|
|
314
|
+
r = pricing_env.get_rate(T)
|
|
315
|
+
q = pricing_env.get_div_yield(T)
|
|
316
|
+
|
|
317
|
+
base_price = self.price(product, pricing_env)
|
|
318
|
+
greeks: Dict[str, float] = {"price": base_price}
|
|
319
|
+
|
|
320
|
+
# Near-expiry: delta/gamma are zero (payoff is flat w.r.t. spot)
|
|
321
|
+
if is_zero(T):
|
|
322
|
+
greeks["delta"] = 0.0
|
|
323
|
+
greeks["gamma"] = 0.0
|
|
324
|
+
return greeks
|
|
325
|
+
|
|
326
|
+
_, future_obs, total_weights = product.resolve_observations(pricing_env)
|
|
327
|
+
|
|
328
|
+
if len(future_obs) == 0 or is_zero(total_weights):
|
|
329
|
+
greeks["delta"] = 0.0
|
|
330
|
+
greeks["gamma"] = 0.0
|
|
331
|
+
return greeks
|
|
332
|
+
|
|
333
|
+
# Analytical delta: dPrice/dS
|
|
334
|
+
# dP_i/dS = [n(d2_L) - n(d2_U)] / (S * sigma_i * sqrt(t_i))
|
|
335
|
+
# where n(.) is the standard normal PDF
|
|
336
|
+
delta_sum = 0.0
|
|
337
|
+
gamma_sum = 0.0
|
|
338
|
+
|
|
339
|
+
for w, t, obs_idx in future_obs:
|
|
340
|
+
if t < self.MIN_MATURITY:
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
sigma = pricing_env.get_vol(product.initial_price, t)
|
|
344
|
+
if sigma < self.MIN_VOL:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
lower = config.get_lower_barrier(obs_idx)
|
|
348
|
+
upper = config.get_upper_barrier(obs_idx)
|
|
349
|
+
|
|
350
|
+
sqrt_t = math.sqrt(t)
|
|
351
|
+
sig_sqrt_t = sigma * sqrt_t
|
|
352
|
+
drift = (r - q - 0.5 * sigma * sigma) * t
|
|
353
|
+
|
|
354
|
+
d2_L = (safe_log(S / lower) + drift) / sig_sqrt_t
|
|
355
|
+
d2_U = (safe_log(S / upper) + drift) / sig_sqrt_t
|
|
356
|
+
|
|
357
|
+
n_d2_L = float(stats.norm.pdf(d2_L))
|
|
358
|
+
n_d2_U = float(stats.norm.pdf(d2_U))
|
|
359
|
+
|
|
360
|
+
# dP_i/dS
|
|
361
|
+
dp_ds = (n_d2_L - n_d2_U) / (S * sig_sqrt_t)
|
|
362
|
+
|
|
363
|
+
# d^2P_i/dS^2 (second derivative of N(d2(K)) w.r.t. S)
|
|
364
|
+
# d/dS[n(d2)/(S*sig*sqrt_t)] =
|
|
365
|
+
# -d2*n(d2)/(S^2*sig^2*t) - n(d2)/(S^2*sig*sqrt_t)
|
|
366
|
+
s2 = S * S
|
|
367
|
+
sig2_t = sigma * sigma * t
|
|
368
|
+
d2p_ds2 = (
|
|
369
|
+
-(d2_L * n_d2_L - d2_U * n_d2_U) / (s2 * sig2_t)
|
|
370
|
+
- (n_d2_L - n_d2_U) / (s2 * sig_sqrt_t)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
sign = -1.0 if config.is_reverse else 1.0
|
|
374
|
+
delta_sum += w * dp_ds * sign
|
|
375
|
+
gamma_sum += w * d2p_ds2 * sign
|
|
376
|
+
|
|
377
|
+
# Scale by payoff parameters
|
|
378
|
+
year_fraction = product.get_year_fraction(pricing_env)
|
|
379
|
+
accrual_rate = config.accrual_rate
|
|
380
|
+
discount = math.exp(-r * T)
|
|
381
|
+
scale = (
|
|
382
|
+
discount
|
|
383
|
+
* product.initial_price
|
|
384
|
+
* product.contract_multiplier
|
|
385
|
+
* accrual_rate
|
|
386
|
+
* year_fraction
|
|
387
|
+
/ total_weights
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
greeks["delta"] = scale * delta_sum
|
|
391
|
+
greeks["gamma"] = scale * gamma_sum
|
|
392
|
+
|
|
393
|
+
return greeks
|
|
394
|
+
|
|
395
|
+
def __repr__(self):
|
|
396
|
+
return "RangeAccrualAnalyticalEngine()"
|