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,1101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Boundary Check Script for Snowball PDE Solver
|
|
3
|
+
Engine: asset/equity/engine/pde/snowball_pde_solver.py
|
|
4
|
+
Generated: 2025-12-29
|
|
5
|
+
|
|
6
|
+
This script validates the SnowballPDESolver against theoretical boundary
|
|
7
|
+
conditions and extreme market scenarios for the Two-Surface PDE method.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, ".")
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
|
|
17
|
+
from quantark.asset.equity.product.option.snowball_option import SnowballOption
|
|
18
|
+
from quantark.asset.equity.product.option.snowball_config import (
|
|
19
|
+
BarrierConfig,
|
|
20
|
+
PayoffConfig,
|
|
21
|
+
AccrualConfig,
|
|
22
|
+
AirbagConfig,
|
|
23
|
+
)
|
|
24
|
+
from quantark.asset.equity.product.option.snowball_helpers import (
|
|
25
|
+
create_standard_snowball as create_standard_helper,
|
|
26
|
+
create_stepdown_snowball as create_stepdown_helper,
|
|
27
|
+
create_european_ki_snowball as create_european_ki_helper,
|
|
28
|
+
create_parachute_snowball as create_parachute_helper,
|
|
29
|
+
create_airbag_snowball as create_airbag_helper,
|
|
30
|
+
)
|
|
31
|
+
from quantark.asset.equity.engine.pde.snowball_pde_solver import SnowballPDESolver
|
|
32
|
+
from quantark.asset.equity.param import PDEParams
|
|
33
|
+
from quantark.priceenv import PricingEnvironment
|
|
34
|
+
from quantark.param import SpotQuote, FlatVolSurface, FlatRateCurve, ContinuousDividendYield
|
|
35
|
+
from quantark.util.enum import ObservationType
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BoundaryCheckResults:
|
|
39
|
+
"""Collect and report boundary check results."""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.passed = []
|
|
43
|
+
self.failed = []
|
|
44
|
+
self.warnings = []
|
|
45
|
+
|
|
46
|
+
def add_result(self, test_name: str, passed: bool, message: str):
|
|
47
|
+
if passed:
|
|
48
|
+
self.passed.append((test_name, message))
|
|
49
|
+
else:
|
|
50
|
+
self.failed.append((test_name, message))
|
|
51
|
+
|
|
52
|
+
def add_warning(self, test_name: str, message: str):
|
|
53
|
+
self.warnings.append((test_name, message))
|
|
54
|
+
|
|
55
|
+
def summary(self):
|
|
56
|
+
total = len(self.passed) + len(self.failed)
|
|
57
|
+
print(f"\n{'='*70}")
|
|
58
|
+
print(f"BOUNDARY CHECK SUMMARY: Snowball PDE Solver")
|
|
59
|
+
print(f"{'='*70}")
|
|
60
|
+
print(f"Total Tests: {total}")
|
|
61
|
+
print(f"Passed: {len(self.passed)} ({100*len(self.passed)/max(total,1):.1f}%)")
|
|
62
|
+
print(f"Failed: {len(self.failed)} ({100*len(self.failed)/max(total,1):.1f}%)")
|
|
63
|
+
print(f"Warnings: {len(self.warnings)}")
|
|
64
|
+
|
|
65
|
+
if self.passed:
|
|
66
|
+
print(f"\n{'─'*70}")
|
|
67
|
+
print("PASSED TESTS:")
|
|
68
|
+
for name, msg in self.passed:
|
|
69
|
+
print(f" ✓ {name}: {msg}")
|
|
70
|
+
|
|
71
|
+
if self.failed:
|
|
72
|
+
print(f"\n{'─'*70}")
|
|
73
|
+
print("FAILED TESTS:")
|
|
74
|
+
for name, msg in self.failed:
|
|
75
|
+
print(f" ✗ {name}: {msg}")
|
|
76
|
+
|
|
77
|
+
if self.warnings:
|
|
78
|
+
print(f"\n{'─'*70}")
|
|
79
|
+
print("WARNINGS:")
|
|
80
|
+
for name, msg in self.warnings:
|
|
81
|
+
print(f" ⚠ {name}: {msg}")
|
|
82
|
+
|
|
83
|
+
return len(self.failed) == 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create_pricing_env(spot: float, rate: float = 0.03, vol: float = 0.20, div: float = 0.0):
|
|
87
|
+
"""Create a pricing environment with given parameters."""
|
|
88
|
+
val_date = datetime(2024, 1, 15)
|
|
89
|
+
return PricingEnvironment(
|
|
90
|
+
spot_quote=SpotQuote(spot=spot),
|
|
91
|
+
rate_curve=FlatRateCurve(rate=rate),
|
|
92
|
+
vol_surface=FlatVolSurface(volatility=vol),
|
|
93
|
+
div_yield=ContinuousDividendYield(div_yield=div),
|
|
94
|
+
valuation_date=val_date,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_standard_snowball(
|
|
99
|
+
initial_price: float = 100.0,
|
|
100
|
+
ko_barrier: float = 103.0,
|
|
101
|
+
ki_barrier: float = 75.0,
|
|
102
|
+
ko_rate: float = 0.15,
|
|
103
|
+
maturity: float = 1.0,
|
|
104
|
+
contract_multiplier: float = 10_000.0,
|
|
105
|
+
num_ko_obs: int = 12,
|
|
106
|
+
ki_continuous: bool = True,
|
|
107
|
+
) -> SnowballOption:
|
|
108
|
+
"""Create a standard snowball option for testing."""
|
|
109
|
+
ko_obs_dates = [maturity * (i + 1) / num_ko_obs for i in range(num_ko_obs)]
|
|
110
|
+
|
|
111
|
+
barrier_config = BarrierConfig(
|
|
112
|
+
ko_barrier=ko_barrier,
|
|
113
|
+
ko_rate=ko_rate,
|
|
114
|
+
ko_observation_dates=ko_obs_dates,
|
|
115
|
+
ki_barrier=ki_barrier,
|
|
116
|
+
ki_continuous=ki_continuous,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return SnowballOption(
|
|
120
|
+
initial_price=initial_price,
|
|
121
|
+
strike=initial_price,
|
|
122
|
+
barrier_config=barrier_config,
|
|
123
|
+
contract_multiplier=contract_multiplier,
|
|
124
|
+
maturity=maturity,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_solver(grid_size: int = 200, time_steps: int = 200) -> SnowballPDESolver:
|
|
129
|
+
"""Create a PDE solver with specified grid size."""
|
|
130
|
+
params = PDEParams(
|
|
131
|
+
grid_size=grid_size,
|
|
132
|
+
time_steps=time_steps,
|
|
133
|
+
theta=0.5, # Crank-Nicolson
|
|
134
|
+
use_rannacher=True,
|
|
135
|
+
rannacher_steps=2,
|
|
136
|
+
)
|
|
137
|
+
return SnowballPDESolver(params=params)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ============================================================
|
|
141
|
+
# EXTREME MARKET CASE TESTS
|
|
142
|
+
# ============================================================
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_low_volatility(results: BoundaryCheckResults):
|
|
146
|
+
"""
|
|
147
|
+
Test: Low volatility should produce a value close to expected deterministic outcome.
|
|
148
|
+
|
|
149
|
+
For a snowball with KO barrier above spot, low vol means:
|
|
150
|
+
- High probability of KO at first observation
|
|
151
|
+
- Price should be close to discounted KO payoff
|
|
152
|
+
"""
|
|
153
|
+
spot = 100.0
|
|
154
|
+
vol = 0.01 # Very low volatility
|
|
155
|
+
rate = 0.03
|
|
156
|
+
maturity = 1.0
|
|
157
|
+
|
|
158
|
+
env = create_pricing_env(spot=spot, rate=rate, vol=vol)
|
|
159
|
+
snowball = create_standard_snowball(
|
|
160
|
+
initial_price=100.0,
|
|
161
|
+
ko_barrier=105.0, # Above spot
|
|
162
|
+
ki_barrier=75.0,
|
|
163
|
+
maturity=maturity,
|
|
164
|
+
)
|
|
165
|
+
solver = create_solver()
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
price = solver.price(snowball, env)
|
|
169
|
+
# With very low vol and KO barrier above spot, price should be positive
|
|
170
|
+
# (principal + rebate if survives, KO payoff if triggered)
|
|
171
|
+
passed = price > 0 and price <= snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
172
|
+
results.add_result(
|
|
173
|
+
"Low Volatility",
|
|
174
|
+
passed,
|
|
175
|
+
f"Price = {price:,.2f} (expected positive, bounded)"
|
|
176
|
+
)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
results.add_result("Low Volatility", False, f"Error: {e}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_high_volatility(results: BoundaryCheckResults):
|
|
182
|
+
"""
|
|
183
|
+
Test: High volatility increases option value uncertainty.
|
|
184
|
+
|
|
185
|
+
With high vol, more KI scenarios become possible.
|
|
186
|
+
"""
|
|
187
|
+
spot = 100.0
|
|
188
|
+
env_low = create_pricing_env(spot=spot, vol=0.10)
|
|
189
|
+
env_high = create_pricing_env(spot=spot, vol=0.50)
|
|
190
|
+
|
|
191
|
+
snowball = create_standard_snowball(
|
|
192
|
+
initial_price=100.0,
|
|
193
|
+
ko_barrier=103.0,
|
|
194
|
+
ki_barrier=75.0,
|
|
195
|
+
)
|
|
196
|
+
solver = create_solver()
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
price_low = solver.price(snowball, env_low)
|
|
200
|
+
price_high = solver.price(snowball, env_high)
|
|
201
|
+
|
|
202
|
+
# Higher vol generally decreases snowball value (more KI risk)
|
|
203
|
+
# But can also increase KO probability
|
|
204
|
+
# Just check both prices are reasonable
|
|
205
|
+
passed = price_low > 0 and price_high > 0
|
|
206
|
+
results.add_result(
|
|
207
|
+
"High Volatility",
|
|
208
|
+
passed,
|
|
209
|
+
f"Low vol: {price_low:,.2f}, High vol: {price_high:,.2f}"
|
|
210
|
+
)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
results.add_result("High Volatility", False, f"Error: {e}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_near_expiry(results: BoundaryCheckResults):
|
|
216
|
+
"""
|
|
217
|
+
Test: Near expiry option value approaches terminal payoff.
|
|
218
|
+
"""
|
|
219
|
+
spot = 100.0
|
|
220
|
+
env = create_pricing_env(spot=spot)
|
|
221
|
+
|
|
222
|
+
# Very short maturity
|
|
223
|
+
snowball = create_standard_snowball(
|
|
224
|
+
initial_price=100.0,
|
|
225
|
+
ko_barrier=103.0,
|
|
226
|
+
ki_barrier=75.0,
|
|
227
|
+
maturity=0.01, # ~3.5 days
|
|
228
|
+
num_ko_obs=1,
|
|
229
|
+
)
|
|
230
|
+
solver = create_solver()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
price = solver.price(snowball, env)
|
|
234
|
+
# Near expiry, price should be close to terminal V0 payoff
|
|
235
|
+
# (spot is between KI and KO barriers)
|
|
236
|
+
terminal_v0 = snowball.get_maturity_payoff_v0(spot, env)
|
|
237
|
+
passed = abs(price - terminal_v0) < snowball.initial_price * snowball.contract_multiplier * 0.05
|
|
238
|
+
results.add_result(
|
|
239
|
+
"Near Expiry",
|
|
240
|
+
passed,
|
|
241
|
+
f"Price = {price:,.2f}, Terminal V0 = {terminal_v0:,.2f}"
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
results.add_result("Near Expiry", False, f"Error: {e}")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_deep_in_ko_region(results: BoundaryCheckResults):
|
|
248
|
+
"""
|
|
249
|
+
Test: Spot deep in KO region should produce KO payoff.
|
|
250
|
+
"""
|
|
251
|
+
spot = 110.0 # Above KO barrier
|
|
252
|
+
env = create_pricing_env(spot=spot)
|
|
253
|
+
|
|
254
|
+
snowball = create_standard_snowball(
|
|
255
|
+
initial_price=100.0,
|
|
256
|
+
ko_barrier=103.0, # Spot is above this
|
|
257
|
+
ki_barrier=75.0,
|
|
258
|
+
)
|
|
259
|
+
solver = create_solver()
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
price = solver.price(snowball, env)
|
|
263
|
+
# If spot starts above KO barrier and first observation is at t=0,
|
|
264
|
+
# price should be immediate KO payoff
|
|
265
|
+
# Otherwise, interpolated from V0 surface at high spot
|
|
266
|
+
passed = price > 0 and price <= snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
267
|
+
results.add_result(
|
|
268
|
+
"Deep in KO Region",
|
|
269
|
+
passed,
|
|
270
|
+
f"Price = {price:,.2f} (spot {spot} > KO barrier 103)"
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
results.add_result("Deep in KO Region", False, f"Error: {e}")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_deep_in_ki_region(results: BoundaryCheckResults):
|
|
277
|
+
"""
|
|
278
|
+
Test: Spot deep in KI region should use V1 surface.
|
|
279
|
+
"""
|
|
280
|
+
spot = 70.0 # Below KI barrier
|
|
281
|
+
env = create_pricing_env(spot=spot)
|
|
282
|
+
|
|
283
|
+
snowball = create_standard_snowball(
|
|
284
|
+
initial_price=100.0,
|
|
285
|
+
ko_barrier=103.0,
|
|
286
|
+
ki_barrier=75.0, # Spot is below this
|
|
287
|
+
ki_continuous=True,
|
|
288
|
+
)
|
|
289
|
+
solver = create_solver()
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
price = solver.price(snowball, env)
|
|
293
|
+
# Spot below KI barrier with continuous monitoring means already knocked-in
|
|
294
|
+
# Should use V1 surface (lower value due to downside exposure)
|
|
295
|
+
terminal_v1 = snowball.get_maturity_payoff_v1(spot, env)
|
|
296
|
+
# V1 price should be less than principal due to downside
|
|
297
|
+
passed = price < snowball.initial_price * snowball.contract_multiplier
|
|
298
|
+
results.add_result(
|
|
299
|
+
"Deep in KI Region",
|
|
300
|
+
passed,
|
|
301
|
+
f"Price = {price:,.2f} (spot {spot} < KI barrier 75, V1 state)"
|
|
302
|
+
)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
results.add_result("Deep in KI Region", False, f"Error: {e}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_zero_rate(results: BoundaryCheckResults):
|
|
308
|
+
"""
|
|
309
|
+
Test: Zero interest rate removes discounting effects.
|
|
310
|
+
"""
|
|
311
|
+
spot = 100.0
|
|
312
|
+
env_zero = create_pricing_env(spot=spot, rate=0.0)
|
|
313
|
+
env_positive = create_pricing_env(spot=spot, rate=0.05)
|
|
314
|
+
|
|
315
|
+
snowball = create_standard_snowball()
|
|
316
|
+
solver = create_solver()
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
price_zero = solver.price(snowball, env_zero)
|
|
320
|
+
price_positive = solver.price(snowball, env_positive)
|
|
321
|
+
|
|
322
|
+
# Both should be positive and reasonable
|
|
323
|
+
passed = price_zero > 0 and price_positive > 0
|
|
324
|
+
results.add_result(
|
|
325
|
+
"Zero Interest Rate",
|
|
326
|
+
passed,
|
|
327
|
+
f"r=0: {price_zero:,.2f}, r=5%: {price_positive:,.2f}"
|
|
328
|
+
)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
results.add_result("Zero Interest Rate", False, f"Error: {e}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ============================================================
|
|
334
|
+
# THEORETICAL RELATIONSHIP TESTS
|
|
335
|
+
# ============================================================
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_v0_v1_relationship(results: BoundaryCheckResults):
|
|
339
|
+
"""
|
|
340
|
+
Test: V0 surface should be >= V1 surface for standard snowball.
|
|
341
|
+
|
|
342
|
+
V0 (never KI) receives rebate at maturity.
|
|
343
|
+
V1 (knocked-in) has downside exposure.
|
|
344
|
+
"""
|
|
345
|
+
spot = 100.0
|
|
346
|
+
env = create_pricing_env(spot=spot)
|
|
347
|
+
|
|
348
|
+
# Use continuous KI for simpler test
|
|
349
|
+
snowball = create_standard_snowball(
|
|
350
|
+
ki_continuous=True,
|
|
351
|
+
)
|
|
352
|
+
solver = create_solver()
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Price when never knocked-in (V0)
|
|
356
|
+
price_v0 = solver.price(snowball, env)
|
|
357
|
+
|
|
358
|
+
# For V1 comparison, we'd need to set up a knocked-in state
|
|
359
|
+
# For now, check that V0 price is reasonable
|
|
360
|
+
passed = price_v0 > 0 and price_v0 <= snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
361
|
+
results.add_result(
|
|
362
|
+
"V0-V1 Relationship",
|
|
363
|
+
passed,
|
|
364
|
+
f"V0 price = {price_v0:,.2f} (should be >= V1 due to rebate vs downside)"
|
|
365
|
+
)
|
|
366
|
+
except Exception as e:
|
|
367
|
+
results.add_result("V0-V1 Relationship", False, f"Error: {e}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_ko_barrier_effect(results: BoundaryCheckResults):
|
|
371
|
+
"""
|
|
372
|
+
Test: Higher KO barrier should decrease snowball value.
|
|
373
|
+
|
|
374
|
+
Higher barrier = harder to knock out = more KI risk.
|
|
375
|
+
"""
|
|
376
|
+
spot = 100.0
|
|
377
|
+
env = create_pricing_env(spot=spot)
|
|
378
|
+
|
|
379
|
+
snowball_low_ko = create_standard_snowball(ko_barrier=102.0)
|
|
380
|
+
snowball_high_ko = create_standard_snowball(ko_barrier=110.0)
|
|
381
|
+
solver = create_solver()
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
price_low = solver.price(snowball_low_ko, env)
|
|
385
|
+
price_high = solver.price(snowball_high_ko, env)
|
|
386
|
+
|
|
387
|
+
# Higher KO barrier = harder to KO = more risk = lower price
|
|
388
|
+
passed = price_low >= price_high * 0.95 # Allow some tolerance
|
|
389
|
+
results.add_result(
|
|
390
|
+
"KO Barrier Effect",
|
|
391
|
+
passed,
|
|
392
|
+
f"KO@102: {price_low:,.2f}, KO@110: {price_high:,.2f}"
|
|
393
|
+
)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
results.add_result("KO Barrier Effect", False, f"Error: {e}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_ki_barrier_effect(results: BoundaryCheckResults):
|
|
399
|
+
"""
|
|
400
|
+
Test: Lower KI barrier should increase snowball value.
|
|
401
|
+
|
|
402
|
+
Lower barrier = harder to knock in = less downside risk.
|
|
403
|
+
"""
|
|
404
|
+
spot = 100.0
|
|
405
|
+
env = create_pricing_env(spot=spot)
|
|
406
|
+
|
|
407
|
+
snowball_high_ki = create_standard_snowball(ki_barrier=80.0)
|
|
408
|
+
snowball_low_ki = create_standard_snowball(ki_barrier=60.0)
|
|
409
|
+
solver = create_solver()
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
price_high = solver.price(snowball_high_ki, env)
|
|
413
|
+
price_low = solver.price(snowball_low_ki, env)
|
|
414
|
+
|
|
415
|
+
# Lower KI barrier = harder to KI = less risk = higher price
|
|
416
|
+
passed = price_low >= price_high * 0.95 # Allow some tolerance
|
|
417
|
+
results.add_result(
|
|
418
|
+
"KI Barrier Effect",
|
|
419
|
+
passed,
|
|
420
|
+
f"KI@80: {price_high:,.2f}, KI@60: {price_low:,.2f}"
|
|
421
|
+
)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
results.add_result("KI Barrier Effect", False, f"Error: {e}")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_maturity_effect(results: BoundaryCheckResults):
|
|
427
|
+
"""
|
|
428
|
+
Test: Longer maturity effect on snowball value.
|
|
429
|
+
|
|
430
|
+
Longer maturity = more time for KO but also more KI risk.
|
|
431
|
+
Effect depends on barrier configuration.
|
|
432
|
+
"""
|
|
433
|
+
spot = 100.0
|
|
434
|
+
env = create_pricing_env(spot=spot)
|
|
435
|
+
|
|
436
|
+
snowball_short = create_standard_snowball(maturity=0.5, num_ko_obs=6)
|
|
437
|
+
snowball_long = create_standard_snowball(maturity=2.0, num_ko_obs=24)
|
|
438
|
+
solver = create_solver()
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
price_short = solver.price(snowball_short, env)
|
|
442
|
+
price_long = solver.price(snowball_long, env)
|
|
443
|
+
|
|
444
|
+
# Both should be positive and reasonable
|
|
445
|
+
passed = price_short > 0 and price_long > 0
|
|
446
|
+
results.add_result(
|
|
447
|
+
"Maturity Effect",
|
|
448
|
+
passed,
|
|
449
|
+
f"T=0.5Y: {price_short:,.2f}, T=2Y: {price_long:,.2f}"
|
|
450
|
+
)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
results.add_result("Maturity Effect", False, f"Error: {e}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_ko_rate_effect(results: BoundaryCheckResults):
|
|
456
|
+
"""
|
|
457
|
+
Test: Higher KO coupon rate should increase snowball value.
|
|
458
|
+
"""
|
|
459
|
+
spot = 100.0
|
|
460
|
+
env = create_pricing_env(spot=spot)
|
|
461
|
+
|
|
462
|
+
snowball_low_rate = create_standard_snowball(ko_rate=0.10)
|
|
463
|
+
snowball_high_rate = create_standard_snowball(ko_rate=0.25)
|
|
464
|
+
solver = create_solver()
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
price_low = solver.price(snowball_low_rate, env)
|
|
468
|
+
price_high = solver.price(snowball_high_rate, env)
|
|
469
|
+
|
|
470
|
+
# Higher KO rate = larger coupon on KO = higher value
|
|
471
|
+
passed = price_high >= price_low * 0.98 # Allow small tolerance
|
|
472
|
+
results.add_result(
|
|
473
|
+
"KO Rate Effect",
|
|
474
|
+
passed,
|
|
475
|
+
f"Rate=10%: {price_low:,.2f}, Rate=25%: {price_high:,.2f}"
|
|
476
|
+
)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
results.add_result("KO Rate Effect", False, f"Error: {e}")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def test_principal_bounds(results: BoundaryCheckResults):
|
|
482
|
+
"""
|
|
483
|
+
Test: Price should be bounded by reasonable limits.
|
|
484
|
+
|
|
485
|
+
Lower bound: 0 (worst case KI with full loss)
|
|
486
|
+
Upper bound: Principal + max coupons
|
|
487
|
+
"""
|
|
488
|
+
spot = 100.0
|
|
489
|
+
env = create_pricing_env(spot=spot)
|
|
490
|
+
|
|
491
|
+
snowball = create_standard_snowball()
|
|
492
|
+
solver = create_solver()
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
price = solver.price(snowball, env)
|
|
496
|
+
|
|
497
|
+
# Price should be positive and bounded
|
|
498
|
+
lower_bound = 0
|
|
499
|
+
upper_bound = snowball.initial_price * snowball.contract_multiplier * 1.5 # Principal + generous coupon estimate
|
|
500
|
+
passed = lower_bound < price < upper_bound
|
|
501
|
+
results.add_result(
|
|
502
|
+
"Principal Bounds",
|
|
503
|
+
passed,
|
|
504
|
+
f"Price = {price:,.2f}, bounds = [{lower_bound:,.0f}, {upper_bound:,.0f}]"
|
|
505
|
+
)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
results.add_result("Principal Bounds", False, f"Error: {e}")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def test_continuous_vs_discrete_ki(results: BoundaryCheckResults):
|
|
511
|
+
"""
|
|
512
|
+
Test: Continuous KI should result in lower price than discrete KI.
|
|
513
|
+
|
|
514
|
+
Continuous monitoring = more chances to knock in = higher KI probability.
|
|
515
|
+
"""
|
|
516
|
+
spot = 100.0
|
|
517
|
+
env = create_pricing_env(spot=spot)
|
|
518
|
+
|
|
519
|
+
# For discrete KI, need to provide ki_observation_dates
|
|
520
|
+
maturity = 1.0
|
|
521
|
+
num_obs = 12
|
|
522
|
+
ko_obs_dates = [maturity * (i + 1) / num_obs for i in range(num_obs)]
|
|
523
|
+
|
|
524
|
+
barrier_config_discrete = BarrierConfig(
|
|
525
|
+
ko_barrier=103.0,
|
|
526
|
+
ko_rate=0.15,
|
|
527
|
+
ko_observation_dates=ko_obs_dates,
|
|
528
|
+
ki_barrier=75.0,
|
|
529
|
+
ki_continuous=False,
|
|
530
|
+
ki_observation_dates=ko_obs_dates, # Monthly discrete KI
|
|
531
|
+
)
|
|
532
|
+
snowball_discrete = SnowballOption(
|
|
533
|
+
initial_price=100.0,
|
|
534
|
+
strike=100.0,
|
|
535
|
+
barrier_config=barrier_config_discrete,
|
|
536
|
+
contract_multiplier=10_000.0,
|
|
537
|
+
maturity=maturity,
|
|
538
|
+
)
|
|
539
|
+
snowball_continuous = create_standard_snowball(ki_continuous=True)
|
|
540
|
+
solver = create_solver()
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
price_discrete = solver.price(snowball_discrete, env)
|
|
544
|
+
price_continuous = solver.price(snowball_continuous, env)
|
|
545
|
+
|
|
546
|
+
# Continuous KI = more KI risk = lower price
|
|
547
|
+
passed = price_discrete >= price_continuous * 0.95
|
|
548
|
+
results.add_result(
|
|
549
|
+
"Continuous vs Discrete KI",
|
|
550
|
+
passed,
|
|
551
|
+
f"Discrete: {price_discrete:,.2f}, Continuous: {price_continuous:,.2f}"
|
|
552
|
+
)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
results.add_result("Continuous vs Discrete KI", False, f"Error: {e}")
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ============================================================
|
|
558
|
+
# NUMERICAL STABILITY TESTS
|
|
559
|
+
# ============================================================
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def test_grid_convergence(results: BoundaryCheckResults):
|
|
563
|
+
"""
|
|
564
|
+
Test: Price should converge as grid resolution increases.
|
|
565
|
+
"""
|
|
566
|
+
spot = 100.0
|
|
567
|
+
env = create_pricing_env(spot=spot)
|
|
568
|
+
snowball = create_standard_snowball()
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
# Coarse grid
|
|
572
|
+
solver_coarse = create_solver(grid_size=100, time_steps=100)
|
|
573
|
+
price_coarse = solver_coarse.price(snowball, env)
|
|
574
|
+
|
|
575
|
+
# Fine grid
|
|
576
|
+
solver_fine = create_solver(grid_size=300, time_steps=300)
|
|
577
|
+
price_fine = solver_fine.price(snowball, env)
|
|
578
|
+
|
|
579
|
+
# Prices should be close (convergence)
|
|
580
|
+
rel_diff = abs(price_fine - price_coarse) / price_fine
|
|
581
|
+
passed = rel_diff < 0.05 # 5% tolerance
|
|
582
|
+
results.add_result(
|
|
583
|
+
"Grid Convergence",
|
|
584
|
+
passed,
|
|
585
|
+
f"Coarse: {price_coarse:,.2f}, Fine: {price_fine:,.2f}, diff: {rel_diff*100:.2f}%"
|
|
586
|
+
)
|
|
587
|
+
except Exception as e:
|
|
588
|
+
results.add_result("Grid Convergence", False, f"Error: {e}")
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_spot_sensitivity(results: BoundaryCheckResults):
|
|
592
|
+
"""
|
|
593
|
+
Test: Price should change smoothly with spot.
|
|
594
|
+
"""
|
|
595
|
+
env_base = create_pricing_env(spot=100.0)
|
|
596
|
+
env_up = create_pricing_env(spot=101.0)
|
|
597
|
+
env_down = create_pricing_env(spot=99.0)
|
|
598
|
+
|
|
599
|
+
snowball = create_standard_snowball()
|
|
600
|
+
solver = create_solver()
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
price_base = solver.price(snowball, env_base)
|
|
604
|
+
price_up = solver.price(snowball, env_up)
|
|
605
|
+
price_down = solver.price(snowball, env_down)
|
|
606
|
+
|
|
607
|
+
# Delta should be reasonable (not too extreme)
|
|
608
|
+
delta_up = (price_up - price_base) / 1.0
|
|
609
|
+
delta_down = (price_base - price_down) / 1.0
|
|
610
|
+
|
|
611
|
+
# Deltas should have same sign and similar magnitude
|
|
612
|
+
passed = abs(delta_up - delta_down) < snowball.initial_price * snowball.contract_multiplier * 0.1
|
|
613
|
+
results.add_result(
|
|
614
|
+
"Spot Sensitivity",
|
|
615
|
+
passed,
|
|
616
|
+
f"ΔUp: {delta_up:,.0f}, ΔDown: {delta_down:,.0f}"
|
|
617
|
+
)
|
|
618
|
+
except Exception as e:
|
|
619
|
+
results.add_result("Spot Sensitivity", False, f"Error: {e}")
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ============================================================
|
|
623
|
+
# SNOWBALL VARIANT TESTS
|
|
624
|
+
# ============================================================
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def test_stepdown_snowball_basics(results: BoundaryCheckResults):
|
|
628
|
+
"""
|
|
629
|
+
Test: Stepdown snowball should have valid pricing.
|
|
630
|
+
|
|
631
|
+
Stepdown structure has decreasing KO barriers over time,
|
|
632
|
+
making knock-out progressively easier.
|
|
633
|
+
"""
|
|
634
|
+
spot = 100.0
|
|
635
|
+
env = create_pricing_env(spot=spot)
|
|
636
|
+
|
|
637
|
+
snowball = create_stepdown_helper(
|
|
638
|
+
initial_price=100.0,
|
|
639
|
+
strike=100.0,
|
|
640
|
+
maturity=1.0,
|
|
641
|
+
contract_multiplier=10_000.0,
|
|
642
|
+
initial_ko_barrier=103.0,
|
|
643
|
+
stepdown_rate=0.005, # 0.5% per period
|
|
644
|
+
ki_barrier=75.0,
|
|
645
|
+
)
|
|
646
|
+
solver = create_solver()
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
price = solver.price(snowball, env)
|
|
650
|
+
# Price should be positive and bounded
|
|
651
|
+
passed = 0 < price < snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
652
|
+
results.add_result(
|
|
653
|
+
"Stepdown Snowball Basics",
|
|
654
|
+
passed,
|
|
655
|
+
f"Price = {price:,.2f} (stepdown KO barriers)"
|
|
656
|
+
)
|
|
657
|
+
except Exception as e:
|
|
658
|
+
results.add_result("Stepdown Snowball Basics", False, f"Error: {e}")
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def test_stepdown_vs_flat_ko(results: BoundaryCheckResults):
|
|
662
|
+
"""
|
|
663
|
+
Test: Stepdown snowball should be worth more than flat KO snowball.
|
|
664
|
+
|
|
665
|
+
Decreasing barriers = easier to knock out = less KI risk = higher value.
|
|
666
|
+
"""
|
|
667
|
+
spot = 100.0
|
|
668
|
+
env = create_pricing_env(spot=spot)
|
|
669
|
+
|
|
670
|
+
# Stepdown: starts at 103%, steps down by 0.5% each month
|
|
671
|
+
snowball_stepdown = create_stepdown_helper(
|
|
672
|
+
initial_price=100.0,
|
|
673
|
+
strike=100.0,
|
|
674
|
+
maturity=1.0,
|
|
675
|
+
contract_multiplier=10_000.0,
|
|
676
|
+
initial_ko_barrier=103.0,
|
|
677
|
+
stepdown_rate=0.005,
|
|
678
|
+
ki_barrier=75.0,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Flat KO at 103%
|
|
682
|
+
snowball_flat = create_standard_helper(
|
|
683
|
+
initial_price=100.0,
|
|
684
|
+
strike=100.0,
|
|
685
|
+
maturity=1.0,
|
|
686
|
+
contract_multiplier=10_000.0,
|
|
687
|
+
ko_barrier=103.0,
|
|
688
|
+
ki_barrier=75.0,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
solver = create_solver()
|
|
692
|
+
|
|
693
|
+
try:
|
|
694
|
+
price_stepdown = solver.price(snowball_stepdown, env)
|
|
695
|
+
price_flat = solver.price(snowball_flat, env)
|
|
696
|
+
|
|
697
|
+
# Stepdown should be worth more (easier KO = less risk)
|
|
698
|
+
passed = price_stepdown >= price_flat * 0.98
|
|
699
|
+
results.add_result(
|
|
700
|
+
"Stepdown vs Flat KO",
|
|
701
|
+
passed,
|
|
702
|
+
f"Stepdown: {price_stepdown:,.2f}, Flat: {price_flat:,.2f}"
|
|
703
|
+
)
|
|
704
|
+
except Exception as e:
|
|
705
|
+
results.add_result("Stepdown vs Flat KO", False, f"Error: {e}")
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def test_european_ki_snowball_basics(results: BoundaryCheckResults):
|
|
709
|
+
"""
|
|
710
|
+
Test: European KI snowball should have valid pricing.
|
|
711
|
+
|
|
712
|
+
KI only observed at maturity (European-style),
|
|
713
|
+
which increases probability of V0 outcome.
|
|
714
|
+
"""
|
|
715
|
+
spot = 100.0
|
|
716
|
+
env = create_pricing_env(spot=spot)
|
|
717
|
+
|
|
718
|
+
snowball = create_european_ki_helper(
|
|
719
|
+
initial_price=100.0,
|
|
720
|
+
strike=100.0,
|
|
721
|
+
maturity=1.0,
|
|
722
|
+
contract_multiplier=10_000.0,
|
|
723
|
+
ko_barrier=103.0,
|
|
724
|
+
ki_barrier=75.0,
|
|
725
|
+
)
|
|
726
|
+
solver = create_solver()
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
price = solver.price(snowball, env)
|
|
730
|
+
# Price should be positive and bounded
|
|
731
|
+
passed = 0 < price < snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
732
|
+
results.add_result(
|
|
733
|
+
"European KI Snowball Basics",
|
|
734
|
+
passed,
|
|
735
|
+
f"Price = {price:,.2f} (KI only at maturity)"
|
|
736
|
+
)
|
|
737
|
+
except Exception as e:
|
|
738
|
+
results.add_result("European KI Snowball Basics", False, f"Error: {e}")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def test_european_ki_vs_continuous_ki(results: BoundaryCheckResults):
|
|
742
|
+
"""
|
|
743
|
+
Test: European KI should be worth more than continuous KI.
|
|
744
|
+
|
|
745
|
+
European KI = less monitoring = lower KI probability = higher value.
|
|
746
|
+
"""
|
|
747
|
+
spot = 100.0
|
|
748
|
+
env = create_pricing_env(spot=spot)
|
|
749
|
+
|
|
750
|
+
# European KI (only at maturity)
|
|
751
|
+
snowball_european = create_european_ki_helper(
|
|
752
|
+
initial_price=100.0,
|
|
753
|
+
strike=100.0,
|
|
754
|
+
maturity=1.0,
|
|
755
|
+
contract_multiplier=10_000.0,
|
|
756
|
+
ko_barrier=103.0,
|
|
757
|
+
ki_barrier=75.0,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# Continuous KI
|
|
761
|
+
snowball_continuous = create_standard_helper(
|
|
762
|
+
initial_price=100.0,
|
|
763
|
+
strike=100.0,
|
|
764
|
+
maturity=1.0,
|
|
765
|
+
contract_multiplier=10_000.0,
|
|
766
|
+
ko_barrier=103.0,
|
|
767
|
+
ki_barrier=75.0,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
solver = create_solver()
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
price_european = solver.price(snowball_european, env)
|
|
774
|
+
price_continuous = solver.price(snowball_continuous, env)
|
|
775
|
+
|
|
776
|
+
# European KI should be worth more (less KI risk)
|
|
777
|
+
passed = price_european >= price_continuous * 0.98
|
|
778
|
+
results.add_result(
|
|
779
|
+
"European KI vs Continuous KI",
|
|
780
|
+
passed,
|
|
781
|
+
f"European: {price_european:,.2f}, Continuous: {price_continuous:,.2f}"
|
|
782
|
+
)
|
|
783
|
+
except Exception as e:
|
|
784
|
+
results.add_result("European KI vs Continuous KI", False, f"Error: {e}")
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def test_parachute_snowball_basics(results: BoundaryCheckResults):
|
|
788
|
+
"""
|
|
789
|
+
Test: Parachute snowball should have valid pricing.
|
|
790
|
+
|
|
791
|
+
Last KO barrier drops to KI level, guaranteeing an exit
|
|
792
|
+
at maturity if no prior KI.
|
|
793
|
+
"""
|
|
794
|
+
spot = 100.0
|
|
795
|
+
env = create_pricing_env(spot=spot)
|
|
796
|
+
|
|
797
|
+
snowball = create_parachute_helper(
|
|
798
|
+
initial_price=100.0,
|
|
799
|
+
strike=100.0,
|
|
800
|
+
maturity=1.0,
|
|
801
|
+
contract_multiplier=10_000.0,
|
|
802
|
+
ko_barrier=103.0,
|
|
803
|
+
ki_barrier=75.0,
|
|
804
|
+
)
|
|
805
|
+
solver = create_solver()
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
price = solver.price(snowball, env)
|
|
809
|
+
# Price should be positive and bounded
|
|
810
|
+
passed = 0 < price < snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
811
|
+
results.add_result(
|
|
812
|
+
"Parachute Snowball Basics",
|
|
813
|
+
passed,
|
|
814
|
+
f"Price = {price:,.2f} (last KO = KI barrier)"
|
|
815
|
+
)
|
|
816
|
+
except Exception as e:
|
|
817
|
+
results.add_result("Parachute Snowball Basics", False, f"Error: {e}")
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def test_parachute_vs_standard(results: BoundaryCheckResults):
|
|
821
|
+
"""
|
|
822
|
+
Test: Parachute snowball should be worth more than standard.
|
|
823
|
+
|
|
824
|
+
Parachute guarantees KO at maturity if not KI'ed,
|
|
825
|
+
which reduces the probability of ending in V0/V1 at maturity.
|
|
826
|
+
"""
|
|
827
|
+
spot = 100.0
|
|
828
|
+
env = create_pricing_env(spot=spot)
|
|
829
|
+
|
|
830
|
+
# Parachute: last KO barrier = KI barrier
|
|
831
|
+
snowball_parachute = create_parachute_helper(
|
|
832
|
+
initial_price=100.0,
|
|
833
|
+
strike=100.0,
|
|
834
|
+
maturity=1.0,
|
|
835
|
+
contract_multiplier=10_000.0,
|
|
836
|
+
ko_barrier=103.0,
|
|
837
|
+
ki_barrier=75.0,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# Standard: flat KO at 103%
|
|
841
|
+
snowball_standard = create_standard_helper(
|
|
842
|
+
initial_price=100.0,
|
|
843
|
+
strike=100.0,
|
|
844
|
+
maturity=1.0,
|
|
845
|
+
contract_multiplier=10_000.0,
|
|
846
|
+
ko_barrier=103.0,
|
|
847
|
+
ki_barrier=75.0,
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
solver = create_solver()
|
|
851
|
+
|
|
852
|
+
try:
|
|
853
|
+
price_parachute = solver.price(snowball_parachute, env)
|
|
854
|
+
price_standard = solver.price(snowball_standard, env)
|
|
855
|
+
|
|
856
|
+
# Parachute should be worth more (guaranteed exit if not KI'ed)
|
|
857
|
+
passed = price_parachute >= price_standard * 0.98
|
|
858
|
+
results.add_result(
|
|
859
|
+
"Parachute vs Standard",
|
|
860
|
+
passed,
|
|
861
|
+
f"Parachute: {price_parachute:,.2f}, Standard: {price_standard:,.2f}"
|
|
862
|
+
)
|
|
863
|
+
except Exception as e:
|
|
864
|
+
results.add_result("Parachute vs Standard", False, f"Error: {e}")
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def test_airbag_snowball_basics(results: BoundaryCheckResults):
|
|
868
|
+
"""
|
|
869
|
+
Test: Airbag snowball should have valid pricing.
|
|
870
|
+
|
|
871
|
+
Reduced participation below airbag barrier limits extreme losses.
|
|
872
|
+
"""
|
|
873
|
+
spot = 100.0
|
|
874
|
+
env = create_pricing_env(spot=spot)
|
|
875
|
+
|
|
876
|
+
snowball = create_airbag_helper(
|
|
877
|
+
initial_price=100.0,
|
|
878
|
+
strike=100.0,
|
|
879
|
+
maturity=1.0,
|
|
880
|
+
contract_multiplier=10_000.0,
|
|
881
|
+
ko_barrier=103.0,
|
|
882
|
+
ki_barrier=75.0,
|
|
883
|
+
airbag_barrier=60.0,
|
|
884
|
+
participation_rate=1.0,
|
|
885
|
+
airbag_participation_rate=0.5, # 50% participation below airbag
|
|
886
|
+
)
|
|
887
|
+
solver = create_solver()
|
|
888
|
+
|
|
889
|
+
try:
|
|
890
|
+
price = solver.price(snowball, env)
|
|
891
|
+
# Price should be positive and bounded
|
|
892
|
+
passed = 0 < price < snowball.initial_price * snowball.contract_multiplier * 1.5
|
|
893
|
+
results.add_result(
|
|
894
|
+
"Airbag Snowball Basics",
|
|
895
|
+
passed,
|
|
896
|
+
f"Price = {price:,.2f} (50% participation below airbag)"
|
|
897
|
+
)
|
|
898
|
+
except Exception as e:
|
|
899
|
+
results.add_result("Airbag Snowball Basics", False, f"Error: {e}")
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def test_airbag_vs_standard(results: BoundaryCheckResults):
|
|
903
|
+
"""
|
|
904
|
+
Test: Airbag snowball should be worth more than standard.
|
|
905
|
+
|
|
906
|
+
Airbag limits downside exposure, so value should be higher.
|
|
907
|
+
"""
|
|
908
|
+
spot = 100.0
|
|
909
|
+
env = create_pricing_env(spot=spot)
|
|
910
|
+
|
|
911
|
+
# Airbag: 50% participation below 60%
|
|
912
|
+
snowball_airbag = create_airbag_helper(
|
|
913
|
+
initial_price=100.0,
|
|
914
|
+
strike=100.0,
|
|
915
|
+
maturity=1.0,
|
|
916
|
+
contract_multiplier=10_000.0,
|
|
917
|
+
ko_barrier=103.0,
|
|
918
|
+
ki_barrier=75.0,
|
|
919
|
+
airbag_barrier=60.0,
|
|
920
|
+
participation_rate=1.0,
|
|
921
|
+
airbag_participation_rate=0.5,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# Standard: full participation on downside
|
|
925
|
+
snowball_standard = create_standard_helper(
|
|
926
|
+
initial_price=100.0,
|
|
927
|
+
strike=100.0,
|
|
928
|
+
maturity=1.0,
|
|
929
|
+
contract_multiplier=10_000.0,
|
|
930
|
+
ko_barrier=103.0,
|
|
931
|
+
ki_barrier=75.0,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
solver = create_solver()
|
|
935
|
+
|
|
936
|
+
try:
|
|
937
|
+
price_airbag = solver.price(snowball_airbag, env)
|
|
938
|
+
price_standard = solver.price(snowball_standard, env)
|
|
939
|
+
|
|
940
|
+
# Airbag should be worth more (limited downside)
|
|
941
|
+
passed = price_airbag >= price_standard * 0.98
|
|
942
|
+
results.add_result(
|
|
943
|
+
"Airbag vs Standard",
|
|
944
|
+
passed,
|
|
945
|
+
f"Airbag: {price_airbag:,.2f}, Standard: {price_standard:,.2f}"
|
|
946
|
+
)
|
|
947
|
+
except Exception as e:
|
|
948
|
+
results.add_result("Airbag vs Standard", False, f"Error: {e}")
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def test_airbag_barrier_effect(results: BoundaryCheckResults):
|
|
952
|
+
"""
|
|
953
|
+
Test: Higher airbag barrier should increase snowball value.
|
|
954
|
+
|
|
955
|
+
Higher airbag barrier = more protection = higher value.
|
|
956
|
+
"""
|
|
957
|
+
spot = 100.0
|
|
958
|
+
env = create_pricing_env(spot=spot)
|
|
959
|
+
|
|
960
|
+
# Low airbag barrier (less protection)
|
|
961
|
+
snowball_low = create_airbag_helper(
|
|
962
|
+
initial_price=100.0,
|
|
963
|
+
strike=100.0,
|
|
964
|
+
maturity=1.0,
|
|
965
|
+
contract_multiplier=10_000.0,
|
|
966
|
+
ko_barrier=103.0,
|
|
967
|
+
ki_barrier=75.0,
|
|
968
|
+
airbag_barrier=50.0, # Low
|
|
969
|
+
airbag_participation_rate=0.5,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
# High airbag barrier (more protection)
|
|
973
|
+
snowball_high = create_airbag_helper(
|
|
974
|
+
initial_price=100.0,
|
|
975
|
+
strike=100.0,
|
|
976
|
+
maturity=1.0,
|
|
977
|
+
contract_multiplier=10_000.0,
|
|
978
|
+
ko_barrier=103.0,
|
|
979
|
+
ki_barrier=75.0,
|
|
980
|
+
airbag_barrier=70.0, # High (closer to KI)
|
|
981
|
+
airbag_participation_rate=0.5,
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
solver = create_solver()
|
|
985
|
+
|
|
986
|
+
try:
|
|
987
|
+
price_low = solver.price(snowball_low, env)
|
|
988
|
+
price_high = solver.price(snowball_high, env)
|
|
989
|
+
|
|
990
|
+
# Higher airbag barrier = more protection = higher value
|
|
991
|
+
passed = price_high >= price_low * 0.98
|
|
992
|
+
results.add_result(
|
|
993
|
+
"Airbag Barrier Effect",
|
|
994
|
+
passed,
|
|
995
|
+
f"Airbag@50: {price_low:,.2f}, Airbag@70: {price_high:,.2f}"
|
|
996
|
+
)
|
|
997
|
+
except Exception as e:
|
|
998
|
+
results.add_result("Airbag Barrier Effect", False, f"Error: {e}")
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def test_variant_volatility_sensitivity(results: BoundaryCheckResults):
|
|
1002
|
+
"""
|
|
1003
|
+
Test: All variants should respond reasonably to volatility changes.
|
|
1004
|
+
"""
|
|
1005
|
+
spot = 100.0
|
|
1006
|
+
env_low = create_pricing_env(spot=spot, vol=0.15)
|
|
1007
|
+
env_high = create_pricing_env(spot=spot, vol=0.35)
|
|
1008
|
+
|
|
1009
|
+
variants = [
|
|
1010
|
+
("Standard", create_standard_helper(
|
|
1011
|
+
initial_price=100.0, strike=100.0, maturity=1.0, contract_multiplier=10_000.0
|
|
1012
|
+
)),
|
|
1013
|
+
("Stepdown", create_stepdown_helper(
|
|
1014
|
+
initial_price=100.0, strike=100.0, maturity=1.0, contract_multiplier=10_000.0
|
|
1015
|
+
)),
|
|
1016
|
+
("European KI", create_european_ki_helper(
|
|
1017
|
+
initial_price=100.0, strike=100.0, maturity=1.0, contract_multiplier=10_000.0
|
|
1018
|
+
)),
|
|
1019
|
+
("Parachute", create_parachute_helper(
|
|
1020
|
+
initial_price=100.0, strike=100.0, maturity=1.0, contract_multiplier=10_000.0
|
|
1021
|
+
)),
|
|
1022
|
+
("Airbag", create_airbag_helper(
|
|
1023
|
+
initial_price=100.0, strike=100.0, maturity=1.0, contract_multiplier=10_000.0,
|
|
1024
|
+
airbag_barrier=60.0, airbag_participation_rate=0.5
|
|
1025
|
+
)),
|
|
1026
|
+
]
|
|
1027
|
+
|
|
1028
|
+
solver = create_solver()
|
|
1029
|
+
all_passed = True
|
|
1030
|
+
|
|
1031
|
+
for name, snowball in variants:
|
|
1032
|
+
try:
|
|
1033
|
+
price_low = solver.price(snowball, env_low)
|
|
1034
|
+
price_high = solver.price(snowball, env_high)
|
|
1035
|
+
|
|
1036
|
+
# Both should be positive and bounded
|
|
1037
|
+
passed = price_low > 0 and price_high > 0
|
|
1038
|
+
if not passed:
|
|
1039
|
+
all_passed = False
|
|
1040
|
+
except Exception as e:
|
|
1041
|
+
all_passed = False
|
|
1042
|
+
|
|
1043
|
+
results.add_result(
|
|
1044
|
+
"Variant Volatility Sensitivity",
|
|
1045
|
+
all_passed,
|
|
1046
|
+
"All variants respond reasonably to volatility changes"
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
# ============================================================
|
|
1051
|
+
# MAIN
|
|
1052
|
+
# ============================================================
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
if __name__ == "__main__":
|
|
1056
|
+
results = BoundaryCheckResults()
|
|
1057
|
+
|
|
1058
|
+
print("=" * 70)
|
|
1059
|
+
print("SNOWBALL PDE SOLVER - BOUNDARY CHECKS")
|
|
1060
|
+
print("=" * 70)
|
|
1061
|
+
|
|
1062
|
+
# Extreme market cases
|
|
1063
|
+
print("\n>>> Running Extreme Market Case Tests...")
|
|
1064
|
+
test_low_volatility(results)
|
|
1065
|
+
test_high_volatility(results)
|
|
1066
|
+
test_near_expiry(results)
|
|
1067
|
+
test_deep_in_ko_region(results)
|
|
1068
|
+
test_deep_in_ki_region(results)
|
|
1069
|
+
test_zero_rate(results)
|
|
1070
|
+
|
|
1071
|
+
# Theoretical relationships
|
|
1072
|
+
print("\n>>> Running Theoretical Relationship Tests...")
|
|
1073
|
+
test_v0_v1_relationship(results)
|
|
1074
|
+
test_ko_barrier_effect(results)
|
|
1075
|
+
test_ki_barrier_effect(results)
|
|
1076
|
+
test_maturity_effect(results)
|
|
1077
|
+
test_ko_rate_effect(results)
|
|
1078
|
+
test_principal_bounds(results)
|
|
1079
|
+
test_continuous_vs_discrete_ki(results)
|
|
1080
|
+
|
|
1081
|
+
# Numerical stability
|
|
1082
|
+
print("\n>>> Running Numerical Stability Tests...")
|
|
1083
|
+
test_grid_convergence(results)
|
|
1084
|
+
test_spot_sensitivity(results)
|
|
1085
|
+
|
|
1086
|
+
# Snowball variant tests
|
|
1087
|
+
print("\n>>> Running Snowball Variant Tests...")
|
|
1088
|
+
test_stepdown_snowball_basics(results)
|
|
1089
|
+
test_stepdown_vs_flat_ko(results)
|
|
1090
|
+
test_european_ki_snowball_basics(results)
|
|
1091
|
+
test_european_ki_vs_continuous_ki(results)
|
|
1092
|
+
test_parachute_snowball_basics(results)
|
|
1093
|
+
test_parachute_vs_standard(results)
|
|
1094
|
+
test_airbag_snowball_basics(results)
|
|
1095
|
+
test_airbag_vs_standard(results)
|
|
1096
|
+
test_airbag_barrier_effect(results)
|
|
1097
|
+
test_variant_volatility_sensitivity(results)
|
|
1098
|
+
|
|
1099
|
+
# Print summary
|
|
1100
|
+
success = results.summary()
|
|
1101
|
+
sys.exit(0 if success else 1)
|