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,1260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter layer for building quadrature core inputs from product types.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Sequence, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from quantark.asset.equity.engine.quad.quad_core import QuadCoreInputs
|
|
15
|
+
from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
|
|
16
|
+
from quantark.asset.equity.product.option import (
|
|
17
|
+
BarrierOption,
|
|
18
|
+
DoubleBarrierOption,
|
|
19
|
+
DoubleOneTouchOption,
|
|
20
|
+
EuropeanVanillaOption,
|
|
21
|
+
OneTouchOption,
|
|
22
|
+
)
|
|
23
|
+
from quantark.asset.equity.product.option.observation_schedule import (
|
|
24
|
+
ObservationSchedule,
|
|
25
|
+
ResolvedObservationRecord,
|
|
26
|
+
)
|
|
27
|
+
from quantark.priceenv import PricingEnvironment
|
|
28
|
+
from quantark.util.enum import ObservationAggregation, ObservationType, TouchType
|
|
29
|
+
from quantark.util.exceptions import PricingError, ValidationError
|
|
30
|
+
from quantark.util.numerical import (
|
|
31
|
+
Tolerance,
|
|
32
|
+
is_close,
|
|
33
|
+
is_zero,
|
|
34
|
+
validate_non_negative,
|
|
35
|
+
validate_positive,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from quantark.asset.equity.engine.quad.discrete_quad_engine import DiscreteQuadEngine
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class QuadPricingContext:
|
|
44
|
+
"""Shared pricing context for discrete quadrature adapters."""
|
|
45
|
+
|
|
46
|
+
spot: float
|
|
47
|
+
maturity: float
|
|
48
|
+
rate: float
|
|
49
|
+
div: float
|
|
50
|
+
vol: float
|
|
51
|
+
contract_multiplier: float
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class QuadInputAdapter(ABC):
|
|
55
|
+
"""Interface for building quad core inputs from a product."""
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def can_handle(self, product: BaseEquityProduct) -> bool:
|
|
59
|
+
"""Return True if adapter can handle the product."""
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def build_pricing_context(
|
|
63
|
+
self,
|
|
64
|
+
product: BaseEquityProduct,
|
|
65
|
+
pricing_env: PricingEnvironment,
|
|
66
|
+
engine: "DiscreteQuadEngine",
|
|
67
|
+
) -> QuadPricingContext:
|
|
68
|
+
"""Build shared pricing context for the product."""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def early_price(
|
|
72
|
+
self,
|
|
73
|
+
product: BaseEquityProduct,
|
|
74
|
+
pricing_env: PricingEnvironment,
|
|
75
|
+
context: QuadPricingContext,
|
|
76
|
+
engine: "DiscreteQuadEngine",
|
|
77
|
+
) -> float | None:
|
|
78
|
+
"""Return a price if no core evaluation is needed; otherwise None."""
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def resolve_schedule(
|
|
82
|
+
self,
|
|
83
|
+
product: BaseEquityProduct,
|
|
84
|
+
pricing_env: PricingEnvironment,
|
|
85
|
+
context: QuadPricingContext,
|
|
86
|
+
) -> Sequence[ResolvedObservationRecord]:
|
|
87
|
+
"""Resolve observation schedule into concrete records."""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def build_inputs(
|
|
91
|
+
self,
|
|
92
|
+
product: BaseEquityProduct,
|
|
93
|
+
resolved: Sequence[ResolvedObservationRecord],
|
|
94
|
+
context: QuadPricingContext,
|
|
95
|
+
) -> QuadCoreInputs:
|
|
96
|
+
"""Construct quad core inputs for the product."""
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def finalize_price(
|
|
100
|
+
self,
|
|
101
|
+
product: BaseEquityProduct,
|
|
102
|
+
pricing_env: PricingEnvironment,
|
|
103
|
+
context: QuadPricingContext,
|
|
104
|
+
core_price: float,
|
|
105
|
+
engine: "DiscreteQuadEngine",
|
|
106
|
+
) -> float:
|
|
107
|
+
"""Post-process the core output into a final product price."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class BaseDiscreteQuadAdapter(QuadInputAdapter):
|
|
111
|
+
"""Base adapter with shared schedule handling helpers."""
|
|
112
|
+
|
|
113
|
+
def resolve_schedule(
|
|
114
|
+
self,
|
|
115
|
+
product: BaseEquityProduct,
|
|
116
|
+
pricing_env: PricingEnvironment,
|
|
117
|
+
context: QuadPricingContext,
|
|
118
|
+
) -> Sequence[ResolvedObservationRecord]:
|
|
119
|
+
maturity = context.maturity
|
|
120
|
+
default_payoff = self._default_payoff(product)
|
|
121
|
+
|
|
122
|
+
if product.observation_type == ObservationType.EXPIRY:
|
|
123
|
+
schedule = ObservationSchedule.from_legacy(
|
|
124
|
+
observation_dates=[maturity],
|
|
125
|
+
default_barrier=product.barrier,
|
|
126
|
+
default_payoff=default_payoff,
|
|
127
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
schedule = product.observation_schedule
|
|
131
|
+
if schedule is None and product.observation_dates:
|
|
132
|
+
schedule = ObservationSchedule.from_legacy(
|
|
133
|
+
observation_dates=product.observation_dates,
|
|
134
|
+
default_barrier=product.barrier,
|
|
135
|
+
default_payoff=default_payoff,
|
|
136
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if schedule is None or not schedule.records:
|
|
140
|
+
raise PricingError("Discrete monitoring requires ObservationSchedule.")
|
|
141
|
+
if schedule.aggregation_mode != ObservationAggregation.STOP_FIRST_HIT:
|
|
142
|
+
raise PricingError("DiscreteQuadEngine requires STOP_FIRST_HIT aggregation.")
|
|
143
|
+
|
|
144
|
+
resolved = schedule.resolve(
|
|
145
|
+
pricing_env,
|
|
146
|
+
default_barrier=product.barrier,
|
|
147
|
+
default_payoff=default_payoff,
|
|
148
|
+
require_single=True,
|
|
149
|
+
)
|
|
150
|
+
return resolved
|
|
151
|
+
|
|
152
|
+
def _default_payoff(self, product: BaseEquityProduct) -> float:
|
|
153
|
+
return float(getattr(product, "rebate", 0.0))
|
|
154
|
+
|
|
155
|
+
def _extract_observations(
|
|
156
|
+
self,
|
|
157
|
+
resolved: Sequence[ResolvedObservationRecord],
|
|
158
|
+
maturity: float,
|
|
159
|
+
is_up_barrier: bool,
|
|
160
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
161
|
+
obs_times = np.array([rec.observation_time for rec in resolved], dtype=float)
|
|
162
|
+
barriers = np.array([rec.barrier for rec in resolved], dtype=float)
|
|
163
|
+
payoffs = np.array([rec.payoff for rec in resolved], dtype=float)
|
|
164
|
+
settlement_times = np.array(
|
|
165
|
+
[
|
|
166
|
+
rec.settlement_time if rec.settlement_time is not None else rec.observation_time
|
|
167
|
+
for rec in resolved
|
|
168
|
+
],
|
|
169
|
+
dtype=float,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if obs_times.size == 0:
|
|
173
|
+
raise ValidationError("Resolved observation schedule is empty.")
|
|
174
|
+
|
|
175
|
+
if maturity - obs_times[-1] > Tolerance.ZERO:
|
|
176
|
+
obs_times = np.append(obs_times, maturity)
|
|
177
|
+
barriers = np.append(barriers, math.inf if is_up_barrier else 0.0)
|
|
178
|
+
payoffs = np.append(payoffs, 0.0)
|
|
179
|
+
settlement_times = np.append(settlement_times, maturity)
|
|
180
|
+
|
|
181
|
+
return obs_times, barriers, payoffs, settlement_times
|
|
182
|
+
|
|
183
|
+
def _discount_rebates(
|
|
184
|
+
self,
|
|
185
|
+
payoffs: np.ndarray,
|
|
186
|
+
observation_times: np.ndarray,
|
|
187
|
+
settlement_times: np.ndarray,
|
|
188
|
+
maturity: float,
|
|
189
|
+
rate: float,
|
|
190
|
+
pay_at_hit: bool,
|
|
191
|
+
) -> np.ndarray:
|
|
192
|
+
if payoffs.size == 0:
|
|
193
|
+
return payoffs
|
|
194
|
+
if pay_at_hit:
|
|
195
|
+
delays = np.maximum(settlement_times - observation_times, 0.0)
|
|
196
|
+
discount = np.exp(-rate * delays)
|
|
197
|
+
else:
|
|
198
|
+
discount = np.exp(-rate * (maturity - observation_times))
|
|
199
|
+
return payoffs * discount
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class BarrierQuadInputAdapter(BaseDiscreteQuadAdapter):
|
|
203
|
+
"""Adapter for barrier options in discrete quadrature."""
|
|
204
|
+
|
|
205
|
+
def can_handle(self, product: BaseEquityProduct) -> bool:
|
|
206
|
+
return isinstance(product, BarrierOption)
|
|
207
|
+
|
|
208
|
+
def build_pricing_context(
|
|
209
|
+
self,
|
|
210
|
+
product: BarrierOption,
|
|
211
|
+
pricing_env: PricingEnvironment,
|
|
212
|
+
engine: "DiscreteQuadEngine",
|
|
213
|
+
) -> QuadPricingContext:
|
|
214
|
+
spot = pricing_env.spot
|
|
215
|
+
maturity = product.get_maturity(pricing_env)
|
|
216
|
+
rate = pricing_env.get_rate(maturity)
|
|
217
|
+
div = pricing_env.get_div_yield(maturity)
|
|
218
|
+
vol = pricing_env.get_vol(product.strike, maturity)
|
|
219
|
+
contract_multiplier = product.contract_multiplier
|
|
220
|
+
|
|
221
|
+
self._validate_inputs(
|
|
222
|
+
spot, product.strike, maturity, rate, div, vol, product.barrier, engine
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return QuadPricingContext(
|
|
226
|
+
spot=spot,
|
|
227
|
+
maturity=maturity,
|
|
228
|
+
rate=rate,
|
|
229
|
+
div=div,
|
|
230
|
+
vol=vol,
|
|
231
|
+
contract_multiplier=contract_multiplier,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def early_price(
|
|
235
|
+
self,
|
|
236
|
+
product: BarrierOption,
|
|
237
|
+
pricing_env: PricingEnvironment,
|
|
238
|
+
context: QuadPricingContext,
|
|
239
|
+
engine: "DiscreteQuadEngine",
|
|
240
|
+
) -> float | None:
|
|
241
|
+
if context.maturity < engine.MIN_MATURITY:
|
|
242
|
+
value = self._barrier_expired_price(
|
|
243
|
+
product, context.spot, context.rate, context.maturity
|
|
244
|
+
)
|
|
245
|
+
return value * context.contract_multiplier
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
product.observation_type != ObservationType.EXPIRY
|
|
249
|
+
and product.is_barrier_hit(context.spot)
|
|
250
|
+
):
|
|
251
|
+
if product.is_knock_out:
|
|
252
|
+
value = self._barrier_immediate_ko_price(
|
|
253
|
+
product, context.rate, context.maturity
|
|
254
|
+
)
|
|
255
|
+
return value * context.contract_multiplier
|
|
256
|
+
value = self._vanilla_price(product, pricing_env, engine)
|
|
257
|
+
return value * context.contract_multiplier
|
|
258
|
+
|
|
259
|
+
if product.observation_type == ObservationType.CONTINUOUS:
|
|
260
|
+
raise PricingError(
|
|
261
|
+
"DiscreteQuadEngine supports discrete monitoring only. "
|
|
262
|
+
"Use BarrierAnalyticalEngine for continuous barriers."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
def resolve_schedule(
|
|
268
|
+
self,
|
|
269
|
+
product: BarrierOption,
|
|
270
|
+
pricing_env: PricingEnvironment,
|
|
271
|
+
context: QuadPricingContext,
|
|
272
|
+
) -> Sequence[ResolvedObservationRecord]:
|
|
273
|
+
return super().resolve_schedule(product, pricing_env, context)
|
|
274
|
+
|
|
275
|
+
def build_inputs(
|
|
276
|
+
self,
|
|
277
|
+
product: BarrierOption,
|
|
278
|
+
resolved: Sequence[ResolvedObservationRecord],
|
|
279
|
+
context: QuadPricingContext,
|
|
280
|
+
) -> QuadCoreInputs:
|
|
281
|
+
obs_times, barriers, payoffs, settlement_times = self._extract_observations(
|
|
282
|
+
resolved, context.maturity, product.is_up_barrier
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if product.is_up_barrier:
|
|
286
|
+
k_plus = np.array(barriers, dtype=float)
|
|
287
|
+
k_minus = np.zeros_like(k_plus)
|
|
288
|
+
else:
|
|
289
|
+
k_minus = np.array(barriers, dtype=float)
|
|
290
|
+
k_plus = np.full_like(k_minus, math.inf, dtype=float)
|
|
291
|
+
|
|
292
|
+
a_minus = np.zeros_like(k_minus, dtype=float)
|
|
293
|
+
b_minus = np.zeros_like(k_minus, dtype=float)
|
|
294
|
+
a_plus = np.zeros_like(k_plus, dtype=float)
|
|
295
|
+
b_plus = np.zeros_like(k_plus, dtype=float)
|
|
296
|
+
|
|
297
|
+
if product.rebate > 0.0:
|
|
298
|
+
rebate_values = self._discount_rebates(
|
|
299
|
+
payoffs,
|
|
300
|
+
obs_times,
|
|
301
|
+
settlement_times,
|
|
302
|
+
context.maturity,
|
|
303
|
+
context.rate,
|
|
304
|
+
product.pay_at_hit,
|
|
305
|
+
)
|
|
306
|
+
if product.is_up_barrier:
|
|
307
|
+
b_plus = rebate_values
|
|
308
|
+
else:
|
|
309
|
+
b_minus = rebate_values
|
|
310
|
+
|
|
311
|
+
maturity_barrier = self._resolve_maturity_barrier(
|
|
312
|
+
obs_times, barriers, context.maturity
|
|
313
|
+
)
|
|
314
|
+
a_terminal, b_terminal = self._apply_terminal_payoff_structure(
|
|
315
|
+
product,
|
|
316
|
+
maturity_barrier,
|
|
317
|
+
k_minus,
|
|
318
|
+
k_plus,
|
|
319
|
+
a_minus,
|
|
320
|
+
b_minus,
|
|
321
|
+
a_plus,
|
|
322
|
+
b_plus,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return QuadCoreInputs(
|
|
326
|
+
observation_times=obs_times,
|
|
327
|
+
k_minus=k_minus,
|
|
328
|
+
k_plus=k_plus,
|
|
329
|
+
a_minus=a_minus,
|
|
330
|
+
b_minus=b_minus,
|
|
331
|
+
a_plus=a_plus,
|
|
332
|
+
b_plus=b_plus,
|
|
333
|
+
a_terminal=a_terminal,
|
|
334
|
+
b_terminal=b_terminal,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def finalize_price(
|
|
338
|
+
self,
|
|
339
|
+
product: BarrierOption,
|
|
340
|
+
pricing_env: PricingEnvironment,
|
|
341
|
+
context: QuadPricingContext,
|
|
342
|
+
core_price: float,
|
|
343
|
+
engine: "DiscreteQuadEngine",
|
|
344
|
+
) -> float:
|
|
345
|
+
if product.is_knock_in:
|
|
346
|
+
vanilla_price = self._vanilla_price(product, pricing_env, engine)
|
|
347
|
+
rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
|
|
348
|
+
value = vanilla_price + rebate_discount - core_price
|
|
349
|
+
return value * context.contract_multiplier
|
|
350
|
+
return core_price * context.contract_multiplier
|
|
351
|
+
|
|
352
|
+
def _validate_inputs(
|
|
353
|
+
self,
|
|
354
|
+
spot: float,
|
|
355
|
+
strike: float,
|
|
356
|
+
maturity: float,
|
|
357
|
+
rate: float,
|
|
358
|
+
div: float,
|
|
359
|
+
vol: float,
|
|
360
|
+
barrier: float,
|
|
361
|
+
engine: "DiscreteQuadEngine",
|
|
362
|
+
) -> None:
|
|
363
|
+
if spot <= 0:
|
|
364
|
+
raise ValidationError(f"Spot price must be positive, got {spot}")
|
|
365
|
+
if strike <= 0:
|
|
366
|
+
raise ValidationError(f"Strike price must be positive, got {strike}")
|
|
367
|
+
if maturity < 0:
|
|
368
|
+
raise ValidationError(f"Time to maturity must be non-negative, got {maturity}")
|
|
369
|
+
if vol <= 0:
|
|
370
|
+
raise ValidationError(f"Volatility must be positive, got {vol}")
|
|
371
|
+
if barrier <= 0:
|
|
372
|
+
raise ValidationError(f"Barrier must be positive, got {barrier}")
|
|
373
|
+
if div < 0:
|
|
374
|
+
raise ValidationError(f"Dividend yield must be non-negative, got {div}")
|
|
375
|
+
if abs(rate) > 1.0:
|
|
376
|
+
raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
|
|
377
|
+
if vol > engine.MAX_VOL:
|
|
378
|
+
raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
|
|
379
|
+
|
|
380
|
+
def _resolve_maturity_barrier(
|
|
381
|
+
self, observation_times: np.ndarray, barriers: np.ndarray, maturity: float
|
|
382
|
+
) -> float | None:
|
|
383
|
+
maturity_mask = np.isclose(
|
|
384
|
+
observation_times, maturity, atol=Tolerance.ZERO, rtol=0.0
|
|
385
|
+
)
|
|
386
|
+
if not np.any(maturity_mask):
|
|
387
|
+
return None
|
|
388
|
+
value = float(barriers[np.where(maturity_mask)[0][-1]])
|
|
389
|
+
if not math.isfinite(value) or value <= 0.0:
|
|
390
|
+
return None
|
|
391
|
+
return value
|
|
392
|
+
|
|
393
|
+
def _apply_terminal_payoff_structure(
|
|
394
|
+
self,
|
|
395
|
+
product: BarrierOption,
|
|
396
|
+
maturity_barrier: float | None,
|
|
397
|
+
k_minus: np.ndarray,
|
|
398
|
+
k_plus: np.ndarray,
|
|
399
|
+
a_minus: np.ndarray,
|
|
400
|
+
b_minus: np.ndarray,
|
|
401
|
+
a_plus: np.ndarray,
|
|
402
|
+
b_plus: np.ndarray,
|
|
403
|
+
) -> tuple[float, float]:
|
|
404
|
+
participation = product.participation_rate
|
|
405
|
+
strike = product.strike
|
|
406
|
+
is_call = product.is_call()
|
|
407
|
+
a_terminal = 0.0
|
|
408
|
+
b_terminal = 0.0
|
|
409
|
+
|
|
410
|
+
if maturity_barrier is None:
|
|
411
|
+
if is_call:
|
|
412
|
+
k_minus[-1] = strike
|
|
413
|
+
a_terminal = participation
|
|
414
|
+
b_terminal = -participation * strike
|
|
415
|
+
else:
|
|
416
|
+
k_plus[-1] = strike
|
|
417
|
+
a_terminal = -participation
|
|
418
|
+
b_terminal = participation * strike
|
|
419
|
+
return a_terminal, b_terminal
|
|
420
|
+
|
|
421
|
+
barrier = maturity_barrier
|
|
422
|
+
|
|
423
|
+
if product.is_up_barrier:
|
|
424
|
+
k_plus[-1] = barrier
|
|
425
|
+
if is_call:
|
|
426
|
+
if barrier > strike:
|
|
427
|
+
k_minus[-1] = strike
|
|
428
|
+
a_terminal = participation
|
|
429
|
+
b_terminal = -participation * strike
|
|
430
|
+
else:
|
|
431
|
+
if barrier > strike:
|
|
432
|
+
k_minus[-1] = strike
|
|
433
|
+
a_minus[-1] = -participation
|
|
434
|
+
b_minus[-1] = participation * strike
|
|
435
|
+
else:
|
|
436
|
+
a_terminal = -participation
|
|
437
|
+
b_terminal = participation * strike
|
|
438
|
+
return a_terminal, b_terminal
|
|
439
|
+
|
|
440
|
+
k_minus[-1] = barrier
|
|
441
|
+
if is_call:
|
|
442
|
+
if barrier < strike:
|
|
443
|
+
k_plus[-1] = strike
|
|
444
|
+
a_plus[-1] = participation
|
|
445
|
+
b_plus[-1] = -participation * strike
|
|
446
|
+
else:
|
|
447
|
+
a_terminal = participation
|
|
448
|
+
b_terminal = -participation * strike
|
|
449
|
+
else:
|
|
450
|
+
if barrier < strike:
|
|
451
|
+
k_plus[-1] = strike
|
|
452
|
+
a_terminal = -participation
|
|
453
|
+
b_terminal = participation * strike
|
|
454
|
+
|
|
455
|
+
return a_terminal, b_terminal
|
|
456
|
+
|
|
457
|
+
def _barrier_expired_price(
|
|
458
|
+
self, product: BarrierOption, spot: float, rate: float, maturity: float
|
|
459
|
+
) -> float:
|
|
460
|
+
hit = product.is_barrier_hit(spot)
|
|
461
|
+
if product.is_call():
|
|
462
|
+
intrinsic = max(spot - product.strike, 0.0)
|
|
463
|
+
else:
|
|
464
|
+
intrinsic = max(product.strike - spot, 0.0)
|
|
465
|
+
vanilla = intrinsic * product.participation_rate
|
|
466
|
+
if product.is_knock_out:
|
|
467
|
+
value = product.rebate if hit else vanilla
|
|
468
|
+
else:
|
|
469
|
+
value = vanilla if hit else product.rebate
|
|
470
|
+
if maturity <= 0.0:
|
|
471
|
+
return value
|
|
472
|
+
return value * math.exp(-rate * maturity)
|
|
473
|
+
|
|
474
|
+
def _barrier_immediate_ko_price(
|
|
475
|
+
self, product: BarrierOption, rate: float, maturity: float
|
|
476
|
+
) -> float:
|
|
477
|
+
if product.pay_at_hit:
|
|
478
|
+
return product.rebate
|
|
479
|
+
return product.rebate * math.exp(-rate * maturity)
|
|
480
|
+
|
|
481
|
+
def _vanilla_price(
|
|
482
|
+
self,
|
|
483
|
+
product: BarrierOption,
|
|
484
|
+
pricing_env: PricingEnvironment,
|
|
485
|
+
engine: "DiscreteQuadEngine",
|
|
486
|
+
) -> float:
|
|
487
|
+
vanilla = EuropeanVanillaOption(
|
|
488
|
+
strike=product.strike,
|
|
489
|
+
option_type=product.option_type,
|
|
490
|
+
maturity=product.maturity,
|
|
491
|
+
exercise_date=product.exercise_date,
|
|
492
|
+
settlement_date=product.settlement_date,
|
|
493
|
+
contract_multiplier=1.0,
|
|
494
|
+
)
|
|
495
|
+
price = engine.vanilla_engine.price(vanilla, pricing_env)
|
|
496
|
+
return price * product.participation_rate
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class OneTouchQuadInputAdapter(BaseDiscreteQuadAdapter):
|
|
500
|
+
"""Adapter for one-touch/no-touch options in discrete quadrature."""
|
|
501
|
+
|
|
502
|
+
def can_handle(self, product: BaseEquityProduct) -> bool:
|
|
503
|
+
return isinstance(product, OneTouchOption)
|
|
504
|
+
|
|
505
|
+
def build_pricing_context(
|
|
506
|
+
self,
|
|
507
|
+
product: OneTouchOption,
|
|
508
|
+
pricing_env: PricingEnvironment,
|
|
509
|
+
engine: "DiscreteQuadEngine",
|
|
510
|
+
) -> QuadPricingContext:
|
|
511
|
+
spot = pricing_env.spot
|
|
512
|
+
maturity = product.get_maturity(pricing_env)
|
|
513
|
+
rate = pricing_env.get_rate(maturity)
|
|
514
|
+
div = pricing_env.get_div_yield(maturity)
|
|
515
|
+
vol = pricing_env.get_vol(product.barrier, maturity)
|
|
516
|
+
contract_multiplier = getattr(product, "contract_multiplier", 1.0)
|
|
517
|
+
|
|
518
|
+
self._validate_inputs(
|
|
519
|
+
spot, product.barrier, maturity, rate, div, vol, product.rebate, engine
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return QuadPricingContext(
|
|
523
|
+
spot=spot,
|
|
524
|
+
maturity=maturity,
|
|
525
|
+
rate=rate,
|
|
526
|
+
div=div,
|
|
527
|
+
vol=vol,
|
|
528
|
+
contract_multiplier=contract_multiplier,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def early_price(
|
|
532
|
+
self,
|
|
533
|
+
product: OneTouchOption,
|
|
534
|
+
pricing_env: PricingEnvironment,
|
|
535
|
+
context: QuadPricingContext,
|
|
536
|
+
engine: "DiscreteQuadEngine",
|
|
537
|
+
) -> float | None:
|
|
538
|
+
if is_zero(context.maturity, tol=engine.MIN_MATURITY):
|
|
539
|
+
pay_at_hit = product.payment_at_hit if product.is_one_touch else False
|
|
540
|
+
value = self._one_touch_instant_payoff(
|
|
541
|
+
product, context.spot, context.maturity, context.rate, pay_at_hit
|
|
542
|
+
)
|
|
543
|
+
return value * context.contract_multiplier
|
|
544
|
+
|
|
545
|
+
if (
|
|
546
|
+
product.observation_type != ObservationType.EXPIRY
|
|
547
|
+
and product.is_barrier_hit(context.spot)
|
|
548
|
+
):
|
|
549
|
+
if product.is_one_touch:
|
|
550
|
+
pay_at_hit = product.payment_at_hit
|
|
551
|
+
value = product.rebate if pay_at_hit else product.rebate * math.exp(
|
|
552
|
+
-context.rate * context.maturity
|
|
553
|
+
)
|
|
554
|
+
return value * context.contract_multiplier
|
|
555
|
+
return 0.0
|
|
556
|
+
|
|
557
|
+
if product.observation_type == ObservationType.CONTINUOUS:
|
|
558
|
+
raise PricingError(
|
|
559
|
+
"DiscreteQuadEngine supports discrete or expiry monitoring only. "
|
|
560
|
+
"Use OneTouchAnalyticalEngine for continuous monitoring."
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if product.rebate <= 0.0:
|
|
564
|
+
return 0.0
|
|
565
|
+
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
def resolve_schedule(
|
|
569
|
+
self,
|
|
570
|
+
product: OneTouchOption,
|
|
571
|
+
pricing_env: PricingEnvironment,
|
|
572
|
+
context: QuadPricingContext,
|
|
573
|
+
) -> Sequence[ResolvedObservationRecord]:
|
|
574
|
+
return super().resolve_schedule(product, pricing_env, context)
|
|
575
|
+
|
|
576
|
+
def build_inputs(
|
|
577
|
+
self,
|
|
578
|
+
product: OneTouchOption,
|
|
579
|
+
resolved: Sequence[ResolvedObservationRecord],
|
|
580
|
+
context: QuadPricingContext,
|
|
581
|
+
) -> QuadCoreInputs:
|
|
582
|
+
obs_times, barriers, payoffs, settlement_times = self._extract_observations(
|
|
583
|
+
resolved, context.maturity, product.is_up_barrier
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if product.is_up_barrier:
|
|
587
|
+
k_plus = np.array(barriers, dtype=float)
|
|
588
|
+
k_minus = np.zeros_like(k_plus)
|
|
589
|
+
else:
|
|
590
|
+
k_minus = np.array(barriers, dtype=float)
|
|
591
|
+
k_plus = np.full_like(k_minus, math.inf, dtype=float)
|
|
592
|
+
|
|
593
|
+
a_minus = np.zeros_like(k_minus, dtype=float)
|
|
594
|
+
b_minus = np.zeros_like(k_minus, dtype=float)
|
|
595
|
+
a_plus = np.zeros_like(k_plus, dtype=float)
|
|
596
|
+
b_plus = np.zeros_like(k_plus, dtype=float)
|
|
597
|
+
|
|
598
|
+
if product.rebate > 0.0:
|
|
599
|
+
rebate_values = self._discount_rebates(
|
|
600
|
+
payoffs,
|
|
601
|
+
obs_times,
|
|
602
|
+
settlement_times,
|
|
603
|
+
context.maturity,
|
|
604
|
+
context.rate,
|
|
605
|
+
product.payment_at_hit,
|
|
606
|
+
)
|
|
607
|
+
if product.is_up_barrier:
|
|
608
|
+
b_plus = rebate_values
|
|
609
|
+
else:
|
|
610
|
+
b_minus = rebate_values
|
|
611
|
+
|
|
612
|
+
return QuadCoreInputs(
|
|
613
|
+
observation_times=obs_times,
|
|
614
|
+
k_minus=k_minus,
|
|
615
|
+
k_plus=k_plus,
|
|
616
|
+
a_minus=a_minus,
|
|
617
|
+
b_minus=b_minus,
|
|
618
|
+
a_plus=a_plus,
|
|
619
|
+
b_plus=b_plus,
|
|
620
|
+
a_terminal=0.0,
|
|
621
|
+
b_terminal=0.0,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def finalize_price(
|
|
625
|
+
self,
|
|
626
|
+
product: OneTouchOption,
|
|
627
|
+
pricing_env: PricingEnvironment,
|
|
628
|
+
context: QuadPricingContext,
|
|
629
|
+
core_price: float,
|
|
630
|
+
engine: "DiscreteQuadEngine",
|
|
631
|
+
) -> float:
|
|
632
|
+
if product.is_no_touch:
|
|
633
|
+
rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
|
|
634
|
+
value = rebate_discount - core_price
|
|
635
|
+
return max(0.0, value) * context.contract_multiplier
|
|
636
|
+
return core_price * context.contract_multiplier
|
|
637
|
+
|
|
638
|
+
def _validate_inputs(
|
|
639
|
+
self,
|
|
640
|
+
spot: float,
|
|
641
|
+
barrier: float,
|
|
642
|
+
maturity: float,
|
|
643
|
+
rate: float,
|
|
644
|
+
div: float,
|
|
645
|
+
vol: float,
|
|
646
|
+
rebate: float,
|
|
647
|
+
engine: "DiscreteQuadEngine",
|
|
648
|
+
) -> None:
|
|
649
|
+
validate_positive(spot, "spot")
|
|
650
|
+
validate_positive(barrier, "barrier")
|
|
651
|
+
validate_positive(vol, "volatility")
|
|
652
|
+
validate_positive(maturity, "maturity", allow_zero=True)
|
|
653
|
+
validate_non_negative(rebate, "rebate")
|
|
654
|
+
validate_non_negative(div, "dividend_yield")
|
|
655
|
+
|
|
656
|
+
if abs(rate) > 1.0:
|
|
657
|
+
raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
|
|
658
|
+
if vol > engine.MAX_VOL:
|
|
659
|
+
raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
|
|
660
|
+
|
|
661
|
+
def _one_touch_instant_payoff(
|
|
662
|
+
self,
|
|
663
|
+
product: OneTouchOption,
|
|
664
|
+
spot: float,
|
|
665
|
+
maturity: float,
|
|
666
|
+
rate: float,
|
|
667
|
+
pay_at_hit: bool,
|
|
668
|
+
) -> float:
|
|
669
|
+
touched = product.is_barrier_hit(spot)
|
|
670
|
+
discount = math.exp(-rate * maturity)
|
|
671
|
+
if product.is_one_touch:
|
|
672
|
+
if touched:
|
|
673
|
+
return product.rebate if pay_at_hit else product.rebate * discount
|
|
674
|
+
return 0.0
|
|
675
|
+
return product.rebate * discount if not touched else 0.0
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class DoubleBarrierQuadInputAdapter(BaseDiscreteQuadAdapter):
|
|
679
|
+
"""Adapter for double barrier options in discrete quadrature."""
|
|
680
|
+
|
|
681
|
+
def can_handle(self, product: BaseEquityProduct) -> bool:
|
|
682
|
+
return isinstance(product, DoubleBarrierOption)
|
|
683
|
+
|
|
684
|
+
def build_pricing_context(
|
|
685
|
+
self,
|
|
686
|
+
product: DoubleBarrierOption,
|
|
687
|
+
pricing_env: PricingEnvironment,
|
|
688
|
+
engine: "DiscreteQuadEngine",
|
|
689
|
+
) -> QuadPricingContext:
|
|
690
|
+
spot = pricing_env.spot
|
|
691
|
+
maturity = product.get_maturity(pricing_env)
|
|
692
|
+
rate = pricing_env.get_rate(maturity)
|
|
693
|
+
div = pricing_env.get_div_yield(maturity)
|
|
694
|
+
vol = pricing_env.get_vol(product.strike, maturity)
|
|
695
|
+
contract_multiplier = product.contract_multiplier
|
|
696
|
+
|
|
697
|
+
self._validate_inputs(
|
|
698
|
+
spot,
|
|
699
|
+
product.strike,
|
|
700
|
+
maturity,
|
|
701
|
+
rate,
|
|
702
|
+
div,
|
|
703
|
+
vol,
|
|
704
|
+
product.lower_barrier,
|
|
705
|
+
product.upper_barrier,
|
|
706
|
+
engine,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
return QuadPricingContext(
|
|
710
|
+
spot=spot,
|
|
711
|
+
maturity=maturity,
|
|
712
|
+
rate=rate,
|
|
713
|
+
div=div,
|
|
714
|
+
vol=vol,
|
|
715
|
+
contract_multiplier=contract_multiplier,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def early_price(
|
|
719
|
+
self,
|
|
720
|
+
product: DoubleBarrierOption,
|
|
721
|
+
pricing_env: PricingEnvironment,
|
|
722
|
+
context: QuadPricingContext,
|
|
723
|
+
engine: "DiscreteQuadEngine",
|
|
724
|
+
) -> float | None:
|
|
725
|
+
if is_zero(context.maturity, tol=engine.MIN_MATURITY):
|
|
726
|
+
value = self._double_barrier_expired_price(
|
|
727
|
+
product, context.spot, context.rate, context.maturity
|
|
728
|
+
)
|
|
729
|
+
return value * context.contract_multiplier
|
|
730
|
+
|
|
731
|
+
if (
|
|
732
|
+
product.observation_type != ObservationType.EXPIRY
|
|
733
|
+
and product.is_barrier_hit(context.spot)
|
|
734
|
+
):
|
|
735
|
+
if product.is_knock_out:
|
|
736
|
+
rebate_discount = math.exp(-context.rate * context.maturity)
|
|
737
|
+
return product.rebate * rebate_discount * context.contract_multiplier
|
|
738
|
+
vanilla_price = self._vanilla_price(product, pricing_env, engine)
|
|
739
|
+
return vanilla_price * context.contract_multiplier
|
|
740
|
+
|
|
741
|
+
if product.observation_type == ObservationType.CONTINUOUS:
|
|
742
|
+
raise PricingError(
|
|
743
|
+
"DiscreteQuadEngine supports discrete monitoring only. "
|
|
744
|
+
"Use a continuous barrier engine for continuous barriers."
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
return None
|
|
748
|
+
|
|
749
|
+
def resolve_schedule(
|
|
750
|
+
self,
|
|
751
|
+
product: DoubleBarrierOption,
|
|
752
|
+
pricing_env: PricingEnvironment,
|
|
753
|
+
context: QuadPricingContext,
|
|
754
|
+
) -> Sequence[ResolvedObservationRecord]:
|
|
755
|
+
if product.observation_type == ObservationType.EXPIRY:
|
|
756
|
+
schedule = ObservationSchedule.from_legacy(
|
|
757
|
+
observation_dates=[context.maturity],
|
|
758
|
+
default_barrier=None,
|
|
759
|
+
default_payoff=product.rebate,
|
|
760
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
761
|
+
upper_barrier=product.upper_barrier,
|
|
762
|
+
lower_barrier=product.lower_barrier,
|
|
763
|
+
)
|
|
764
|
+
else:
|
|
765
|
+
schedule = product.observation_schedule
|
|
766
|
+
if schedule is None and product.observation_dates:
|
|
767
|
+
schedule = ObservationSchedule.from_legacy(
|
|
768
|
+
observation_dates=product.observation_dates,
|
|
769
|
+
default_barrier=None,
|
|
770
|
+
default_payoff=product.rebate,
|
|
771
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
772
|
+
upper_barrier=product.upper_barrier,
|
|
773
|
+
lower_barrier=product.lower_barrier,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if schedule is None or not schedule.records:
|
|
777
|
+
raise PricingError("Discrete monitoring requires ObservationSchedule.")
|
|
778
|
+
if schedule.aggregation_mode != ObservationAggregation.STOP_FIRST_HIT:
|
|
779
|
+
raise PricingError("DiscreteQuadEngine requires STOP_FIRST_HIT aggregation.")
|
|
780
|
+
|
|
781
|
+
return schedule.resolve(
|
|
782
|
+
pricing_env,
|
|
783
|
+
default_upper=product.upper_barrier,
|
|
784
|
+
default_lower=product.lower_barrier,
|
|
785
|
+
default_payoff=product.rebate,
|
|
786
|
+
require_double=True,
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
def build_inputs(
|
|
790
|
+
self,
|
|
791
|
+
product: DoubleBarrierOption,
|
|
792
|
+
resolved: Sequence[ResolvedObservationRecord],
|
|
793
|
+
context: QuadPricingContext,
|
|
794
|
+
) -> QuadCoreInputs:
|
|
795
|
+
(
|
|
796
|
+
obs_times,
|
|
797
|
+
upper_barriers,
|
|
798
|
+
lower_barriers,
|
|
799
|
+
payoffs,
|
|
800
|
+
settlement_times,
|
|
801
|
+
) = self._extract_double_observations(resolved, context.maturity)
|
|
802
|
+
|
|
803
|
+
k_plus = np.array(upper_barriers, dtype=float)
|
|
804
|
+
k_minus = np.array(lower_barriers, dtype=float)
|
|
805
|
+
a_minus = np.zeros_like(k_minus, dtype=float)
|
|
806
|
+
b_minus = np.zeros_like(k_minus, dtype=float)
|
|
807
|
+
a_plus = np.zeros_like(k_plus, dtype=float)
|
|
808
|
+
b_plus = np.zeros_like(k_plus, dtype=float)
|
|
809
|
+
|
|
810
|
+
if np.any(payoffs > 0.0):
|
|
811
|
+
pay_at_hit = self._pay_at_hit(settlement_times, context.maturity)
|
|
812
|
+
rebate_values = self._discount_rebates(
|
|
813
|
+
payoffs,
|
|
814
|
+
obs_times,
|
|
815
|
+
settlement_times,
|
|
816
|
+
context.maturity,
|
|
817
|
+
context.rate,
|
|
818
|
+
pay_at_hit,
|
|
819
|
+
)
|
|
820
|
+
b_minus = rebate_values
|
|
821
|
+
b_plus = rebate_values
|
|
822
|
+
|
|
823
|
+
a_terminal, b_terminal = self._terminal_payoff_coefficients(
|
|
824
|
+
product, k_minus, k_plus
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
return QuadCoreInputs(
|
|
828
|
+
observation_times=obs_times,
|
|
829
|
+
k_minus=k_minus,
|
|
830
|
+
k_plus=k_plus,
|
|
831
|
+
a_minus=a_minus,
|
|
832
|
+
b_minus=b_minus,
|
|
833
|
+
a_plus=a_plus,
|
|
834
|
+
b_plus=b_plus,
|
|
835
|
+
a_terminal=a_terminal,
|
|
836
|
+
b_terminal=b_terminal,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
def finalize_price(
|
|
840
|
+
self,
|
|
841
|
+
product: DoubleBarrierOption,
|
|
842
|
+
pricing_env: PricingEnvironment,
|
|
843
|
+
context: QuadPricingContext,
|
|
844
|
+
core_price: float,
|
|
845
|
+
engine: "DiscreteQuadEngine",
|
|
846
|
+
) -> float:
|
|
847
|
+
if product.is_knock_in:
|
|
848
|
+
vanilla_price = self._vanilla_price(product, pricing_env, engine)
|
|
849
|
+
rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
|
|
850
|
+
value = vanilla_price + rebate_discount - core_price
|
|
851
|
+
return value * context.contract_multiplier
|
|
852
|
+
return core_price * context.contract_multiplier
|
|
853
|
+
|
|
854
|
+
def _extract_double_observations(
|
|
855
|
+
self, resolved: Sequence[ResolvedObservationRecord], maturity: float
|
|
856
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
857
|
+
obs_times = np.array([rec.observation_time for rec in resolved], dtype=float)
|
|
858
|
+
upper = np.array([rec.upper_barrier for rec in resolved], dtype=float)
|
|
859
|
+
lower = np.array([rec.lower_barrier for rec in resolved], dtype=float)
|
|
860
|
+
payoffs = np.array([rec.payoff for rec in resolved], dtype=float)
|
|
861
|
+
settlement_times = np.array(
|
|
862
|
+
[
|
|
863
|
+
rec.settlement_time if rec.settlement_time is not None else rec.observation_time
|
|
864
|
+
for rec in resolved
|
|
865
|
+
],
|
|
866
|
+
dtype=float,
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
if obs_times.size == 0:
|
|
870
|
+
raise ValidationError("Resolved observation schedule is empty.")
|
|
871
|
+
|
|
872
|
+
if maturity - obs_times[-1] > Tolerance.ZERO:
|
|
873
|
+
obs_times = np.append(obs_times, maturity)
|
|
874
|
+
upper = np.append(upper, math.inf)
|
|
875
|
+
lower = np.append(lower, 0.0)
|
|
876
|
+
payoffs = np.append(payoffs, 0.0)
|
|
877
|
+
settlement_times = np.append(settlement_times, maturity)
|
|
878
|
+
|
|
879
|
+
return obs_times, upper, lower, payoffs, settlement_times
|
|
880
|
+
|
|
881
|
+
def _terminal_payoff_coefficients(
|
|
882
|
+
self,
|
|
883
|
+
product: DoubleBarrierOption,
|
|
884
|
+
k_minus: np.ndarray,
|
|
885
|
+
k_plus: np.ndarray,
|
|
886
|
+
) -> tuple[float, float]:
|
|
887
|
+
strike = product.strike
|
|
888
|
+
lower = float(k_minus[-1])
|
|
889
|
+
upper = float(k_plus[-1])
|
|
890
|
+
|
|
891
|
+
if product.is_call():
|
|
892
|
+
if strike <= lower:
|
|
893
|
+
return 1.0, -strike
|
|
894
|
+
if strike >= upper:
|
|
895
|
+
return 0.0, 0.0
|
|
896
|
+
k_minus[-1] = strike
|
|
897
|
+
return 1.0, -strike
|
|
898
|
+
|
|
899
|
+
if strike >= upper:
|
|
900
|
+
return -1.0, strike
|
|
901
|
+
if strike <= lower:
|
|
902
|
+
return 0.0, 0.0
|
|
903
|
+
k_plus[-1] = strike
|
|
904
|
+
return -1.0, strike
|
|
905
|
+
|
|
906
|
+
def _double_barrier_expired_price(
|
|
907
|
+
self,
|
|
908
|
+
product: DoubleBarrierOption,
|
|
909
|
+
spot: float,
|
|
910
|
+
rate: float,
|
|
911
|
+
maturity: float,
|
|
912
|
+
) -> float:
|
|
913
|
+
hit = product.is_barrier_hit(spot)
|
|
914
|
+
if product.is_call():
|
|
915
|
+
intrinsic = max(spot - product.strike, 0.0)
|
|
916
|
+
else:
|
|
917
|
+
intrinsic = max(product.strike - spot, 0.0)
|
|
918
|
+
if product.is_knock_out:
|
|
919
|
+
value = product.rebate if hit else intrinsic
|
|
920
|
+
else:
|
|
921
|
+
value = intrinsic if hit else product.rebate
|
|
922
|
+
if maturity <= 0.0:
|
|
923
|
+
return value
|
|
924
|
+
return value * math.exp(-rate * maturity)
|
|
925
|
+
|
|
926
|
+
def _pay_at_hit(self, settlement_times: np.ndarray, maturity: float) -> bool:
|
|
927
|
+
if settlement_times.size == 0:
|
|
928
|
+
return False
|
|
929
|
+
return not np.all(
|
|
930
|
+
[is_close(t, maturity, abs_tol=Tolerance.PRECISION) for t in settlement_times]
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
def _vanilla_price(
|
|
934
|
+
self,
|
|
935
|
+
product: DoubleBarrierOption,
|
|
936
|
+
pricing_env: PricingEnvironment,
|
|
937
|
+
engine: "DiscreteQuadEngine",
|
|
938
|
+
) -> float:
|
|
939
|
+
vanilla = EuropeanVanillaOption(
|
|
940
|
+
strike=product.strike,
|
|
941
|
+
option_type=product.option_type,
|
|
942
|
+
maturity=product.maturity,
|
|
943
|
+
exercise_date=product.exercise_date,
|
|
944
|
+
settlement_date=product.settlement_date,
|
|
945
|
+
contract_multiplier=1.0,
|
|
946
|
+
)
|
|
947
|
+
return engine.vanilla_engine.price(vanilla, pricing_env)
|
|
948
|
+
|
|
949
|
+
def _validate_inputs(
|
|
950
|
+
self,
|
|
951
|
+
spot: float,
|
|
952
|
+
strike: float,
|
|
953
|
+
maturity: float,
|
|
954
|
+
rate: float,
|
|
955
|
+
div: float,
|
|
956
|
+
vol: float,
|
|
957
|
+
lower: float,
|
|
958
|
+
upper: float,
|
|
959
|
+
engine: "DiscreteQuadEngine",
|
|
960
|
+
) -> None:
|
|
961
|
+
validate_positive(spot, "spot")
|
|
962
|
+
validate_positive(strike, "strike")
|
|
963
|
+
validate_positive(vol, "volatility")
|
|
964
|
+
validate_positive(maturity, "maturity", allow_zero=True)
|
|
965
|
+
validate_positive(lower, "lower_barrier")
|
|
966
|
+
validate_positive(upper, "upper_barrier")
|
|
967
|
+
validate_non_negative(div, "dividend_yield")
|
|
968
|
+
|
|
969
|
+
if lower >= upper:
|
|
970
|
+
raise ValidationError(
|
|
971
|
+
f"lower_barrier ({lower}) must be less than upper_barrier ({upper})"
|
|
972
|
+
)
|
|
973
|
+
if abs(rate) > 1.0:
|
|
974
|
+
raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
|
|
975
|
+
if vol > engine.MAX_VOL:
|
|
976
|
+
raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
class DoubleOneTouchQuadInputAdapter(BaseDiscreteQuadAdapter):
|
|
980
|
+
"""Adapter for double one-touch/no-touch options in discrete quadrature."""
|
|
981
|
+
|
|
982
|
+
def can_handle(self, product: BaseEquityProduct) -> bool:
|
|
983
|
+
return isinstance(product, DoubleOneTouchOption)
|
|
984
|
+
|
|
985
|
+
def build_pricing_context(
|
|
986
|
+
self,
|
|
987
|
+
product: DoubleOneTouchOption,
|
|
988
|
+
pricing_env: PricingEnvironment,
|
|
989
|
+
engine: "DiscreteQuadEngine",
|
|
990
|
+
) -> QuadPricingContext:
|
|
991
|
+
spot = pricing_env.spot
|
|
992
|
+
maturity = product.get_maturity(pricing_env)
|
|
993
|
+
rate = pricing_env.get_rate(maturity)
|
|
994
|
+
div = pricing_env.get_div_yield(maturity)
|
|
995
|
+
barrier_ref = math.sqrt(product.upper_barrier * product.lower_barrier)
|
|
996
|
+
vol = pricing_env.get_vol(barrier_ref, maturity)
|
|
997
|
+
contract_multiplier = getattr(product, "contract_multiplier", 1.0)
|
|
998
|
+
|
|
999
|
+
self._validate_inputs(
|
|
1000
|
+
spot,
|
|
1001
|
+
product.lower_barrier,
|
|
1002
|
+
product.upper_barrier,
|
|
1003
|
+
maturity,
|
|
1004
|
+
rate,
|
|
1005
|
+
div,
|
|
1006
|
+
vol,
|
|
1007
|
+
product.rebate,
|
|
1008
|
+
engine,
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
return QuadPricingContext(
|
|
1012
|
+
spot=spot,
|
|
1013
|
+
maturity=maturity,
|
|
1014
|
+
rate=rate,
|
|
1015
|
+
div=div,
|
|
1016
|
+
vol=vol,
|
|
1017
|
+
contract_multiplier=contract_multiplier,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
def early_price(
|
|
1021
|
+
self,
|
|
1022
|
+
product: DoubleOneTouchOption,
|
|
1023
|
+
pricing_env: PricingEnvironment,
|
|
1024
|
+
context: QuadPricingContext,
|
|
1025
|
+
engine: "DiscreteQuadEngine",
|
|
1026
|
+
) -> float | None:
|
|
1027
|
+
if is_zero(context.maturity, tol=engine.MIN_MATURITY):
|
|
1028
|
+
value = self._double_touch_instant_payoff(
|
|
1029
|
+
product, context.spot, context.maturity, context.rate
|
|
1030
|
+
)
|
|
1031
|
+
return value * context.contract_multiplier
|
|
1032
|
+
|
|
1033
|
+
touched = (
|
|
1034
|
+
context.spot >= product.upper_barrier
|
|
1035
|
+
or context.spot <= product.lower_barrier
|
|
1036
|
+
)
|
|
1037
|
+
if product.observation_type != ObservationType.EXPIRY and touched:
|
|
1038
|
+
if product.touch_type == TouchType.DOUBLE_ONE_TOUCH:
|
|
1039
|
+
pay_at_hit = product.payment_at_hit
|
|
1040
|
+
value = product.rebate if pay_at_hit else product.rebate * math.exp(
|
|
1041
|
+
-context.rate * context.maturity
|
|
1042
|
+
)
|
|
1043
|
+
return value * context.contract_multiplier
|
|
1044
|
+
return 0.0
|
|
1045
|
+
|
|
1046
|
+
if product.observation_type == ObservationType.CONTINUOUS:
|
|
1047
|
+
raise PricingError(
|
|
1048
|
+
"DiscreteQuadEngine supports discrete monitoring only. "
|
|
1049
|
+
"Use a continuous engine for continuous barriers."
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
if product.rebate <= 0.0:
|
|
1053
|
+
return 0.0
|
|
1054
|
+
|
|
1055
|
+
return None
|
|
1056
|
+
|
|
1057
|
+
def resolve_schedule(
|
|
1058
|
+
self,
|
|
1059
|
+
product: DoubleOneTouchOption,
|
|
1060
|
+
pricing_env: PricingEnvironment,
|
|
1061
|
+
context: QuadPricingContext,
|
|
1062
|
+
) -> Sequence[ResolvedObservationRecord]:
|
|
1063
|
+
if product.observation_type == ObservationType.EXPIRY:
|
|
1064
|
+
schedule = ObservationSchedule.from_legacy(
|
|
1065
|
+
observation_dates=[context.maturity],
|
|
1066
|
+
default_barrier=None,
|
|
1067
|
+
default_payoff=product.rebate,
|
|
1068
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
1069
|
+
upper_barrier=product.upper_barrier,
|
|
1070
|
+
lower_barrier=product.lower_barrier,
|
|
1071
|
+
)
|
|
1072
|
+
else:
|
|
1073
|
+
schedule = product.observation_schedule
|
|
1074
|
+
if schedule is None and product.observation_dates:
|
|
1075
|
+
schedule = ObservationSchedule.from_legacy(
|
|
1076
|
+
observation_dates=product.observation_dates,
|
|
1077
|
+
default_barrier=None,
|
|
1078
|
+
default_payoff=product.rebate,
|
|
1079
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
1080
|
+
upper_barrier=product.upper_barrier,
|
|
1081
|
+
lower_barrier=product.lower_barrier,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
if schedule is None or not schedule.records:
|
|
1085
|
+
raise PricingError("Discrete monitoring requires ObservationSchedule.")
|
|
1086
|
+
if schedule.aggregation_mode != ObservationAggregation.STOP_FIRST_HIT:
|
|
1087
|
+
raise PricingError("DiscreteQuadEngine requires STOP_FIRST_HIT aggregation.")
|
|
1088
|
+
|
|
1089
|
+
return schedule.resolve(
|
|
1090
|
+
pricing_env,
|
|
1091
|
+
default_upper=product.upper_barrier,
|
|
1092
|
+
default_lower=product.lower_barrier,
|
|
1093
|
+
default_payoff=product.rebate,
|
|
1094
|
+
require_double=True,
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
def build_inputs(
|
|
1098
|
+
self,
|
|
1099
|
+
product: DoubleOneTouchOption,
|
|
1100
|
+
resolved: Sequence[ResolvedObservationRecord],
|
|
1101
|
+
context: QuadPricingContext,
|
|
1102
|
+
) -> QuadCoreInputs:
|
|
1103
|
+
(
|
|
1104
|
+
obs_times,
|
|
1105
|
+
upper_barriers,
|
|
1106
|
+
lower_barriers,
|
|
1107
|
+
payoffs,
|
|
1108
|
+
settlement_times,
|
|
1109
|
+
) = self._extract_double_observations(resolved, context.maturity)
|
|
1110
|
+
|
|
1111
|
+
k_plus = np.array(upper_barriers, dtype=float)
|
|
1112
|
+
k_minus = np.array(lower_barriers, dtype=float)
|
|
1113
|
+
a_minus = np.zeros_like(k_minus, dtype=float)
|
|
1114
|
+
b_minus = np.zeros_like(k_minus, dtype=float)
|
|
1115
|
+
a_plus = np.zeros_like(k_plus, dtype=float)
|
|
1116
|
+
b_plus = np.zeros_like(k_plus, dtype=float)
|
|
1117
|
+
|
|
1118
|
+
if np.any(payoffs > 0.0):
|
|
1119
|
+
rebate_values = self._discount_rebates(
|
|
1120
|
+
payoffs,
|
|
1121
|
+
obs_times,
|
|
1122
|
+
settlement_times,
|
|
1123
|
+
context.maturity,
|
|
1124
|
+
context.rate,
|
|
1125
|
+
product.payment_at_hit,
|
|
1126
|
+
)
|
|
1127
|
+
b_minus = rebate_values
|
|
1128
|
+
b_plus = rebate_values
|
|
1129
|
+
|
|
1130
|
+
return QuadCoreInputs(
|
|
1131
|
+
observation_times=obs_times,
|
|
1132
|
+
k_minus=k_minus,
|
|
1133
|
+
k_plus=k_plus,
|
|
1134
|
+
a_minus=a_minus,
|
|
1135
|
+
b_minus=b_minus,
|
|
1136
|
+
a_plus=a_plus,
|
|
1137
|
+
b_plus=b_plus,
|
|
1138
|
+
a_terminal=0.0,
|
|
1139
|
+
b_terminal=0.0,
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
def finalize_price(
|
|
1143
|
+
self,
|
|
1144
|
+
product: DoubleOneTouchOption,
|
|
1145
|
+
pricing_env: PricingEnvironment,
|
|
1146
|
+
context: QuadPricingContext,
|
|
1147
|
+
core_price: float,
|
|
1148
|
+
engine: "DiscreteQuadEngine",
|
|
1149
|
+
) -> float:
|
|
1150
|
+
if product.touch_type == TouchType.DOUBLE_NO_TOUCH:
|
|
1151
|
+
rebate_discount = product.rebate * math.exp(-context.rate * context.maturity)
|
|
1152
|
+
value = rebate_discount - core_price
|
|
1153
|
+
return max(0.0, value) * context.contract_multiplier
|
|
1154
|
+
return core_price * context.contract_multiplier
|
|
1155
|
+
|
|
1156
|
+
def _extract_double_observations(
|
|
1157
|
+
self, resolved: Sequence[ResolvedObservationRecord], maturity: float
|
|
1158
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
1159
|
+
obs_times = np.array([rec.observation_time for rec in resolved], dtype=float)
|
|
1160
|
+
upper = np.array([rec.upper_barrier for rec in resolved], dtype=float)
|
|
1161
|
+
lower = np.array([rec.lower_barrier for rec in resolved], dtype=float)
|
|
1162
|
+
payoffs = np.array([rec.payoff for rec in resolved], dtype=float)
|
|
1163
|
+
settlement_times = np.array(
|
|
1164
|
+
[
|
|
1165
|
+
rec.settlement_time if rec.settlement_time is not None else rec.observation_time
|
|
1166
|
+
for rec in resolved
|
|
1167
|
+
],
|
|
1168
|
+
dtype=float,
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
if obs_times.size == 0:
|
|
1172
|
+
raise ValidationError("Resolved observation schedule is empty.")
|
|
1173
|
+
|
|
1174
|
+
if maturity - obs_times[-1] > Tolerance.ZERO:
|
|
1175
|
+
obs_times = np.append(obs_times, maturity)
|
|
1176
|
+
upper = np.append(upper, math.inf)
|
|
1177
|
+
lower = np.append(lower, 0.0)
|
|
1178
|
+
payoffs = np.append(payoffs, 0.0)
|
|
1179
|
+
settlement_times = np.append(settlement_times, maturity)
|
|
1180
|
+
|
|
1181
|
+
return obs_times, upper, lower, payoffs, settlement_times
|
|
1182
|
+
|
|
1183
|
+
def _double_touch_instant_payoff(
|
|
1184
|
+
self,
|
|
1185
|
+
product: DoubleOneTouchOption,
|
|
1186
|
+
spot: float,
|
|
1187
|
+
maturity: float,
|
|
1188
|
+
rate: float,
|
|
1189
|
+
) -> float:
|
|
1190
|
+
touched = spot >= product.upper_barrier or spot <= product.lower_barrier
|
|
1191
|
+
discount = math.exp(-rate * maturity)
|
|
1192
|
+
if product.touch_type == TouchType.DOUBLE_ONE_TOUCH:
|
|
1193
|
+
if touched:
|
|
1194
|
+
return product.rebate if product.payment_at_hit else product.rebate * discount
|
|
1195
|
+
return 0.0
|
|
1196
|
+
return product.rebate * discount if not touched else 0.0
|
|
1197
|
+
|
|
1198
|
+
def _validate_inputs(
|
|
1199
|
+
self,
|
|
1200
|
+
spot: float,
|
|
1201
|
+
lower: float,
|
|
1202
|
+
upper: float,
|
|
1203
|
+
maturity: float,
|
|
1204
|
+
rate: float,
|
|
1205
|
+
div: float,
|
|
1206
|
+
vol: float,
|
|
1207
|
+
rebate: float,
|
|
1208
|
+
engine: "DiscreteQuadEngine",
|
|
1209
|
+
) -> None:
|
|
1210
|
+
validate_positive(spot, "spot")
|
|
1211
|
+
validate_positive(lower, "lower_barrier")
|
|
1212
|
+
validate_positive(upper, "upper_barrier")
|
|
1213
|
+
validate_positive(vol, "volatility")
|
|
1214
|
+
validate_positive(maturity, "maturity", allow_zero=True)
|
|
1215
|
+
validate_non_negative(rebate, "rebate")
|
|
1216
|
+
validate_non_negative(div, "dividend_yield")
|
|
1217
|
+
|
|
1218
|
+
if lower >= upper:
|
|
1219
|
+
raise ValidationError(
|
|
1220
|
+
f"lower_barrier ({lower}) must be less than upper_barrier ({upper})"
|
|
1221
|
+
)
|
|
1222
|
+
if abs(rate) > 1.0:
|
|
1223
|
+
raise ValidationError(f"Risk-free rate outside reasonable bounds: {rate}")
|
|
1224
|
+
if vol > engine.MAX_VOL:
|
|
1225
|
+
raise ValidationError(f"Volatility too high for quadrature stability: {vol}")
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
class QuadInputAdapterRegistry:
|
|
1229
|
+
"""Registry for quad input adapters."""
|
|
1230
|
+
|
|
1231
|
+
def __init__(self) -> None:
|
|
1232
|
+
self._adapters: list[QuadInputAdapter] = []
|
|
1233
|
+
|
|
1234
|
+
def register(self, adapter: QuadInputAdapter) -> None:
|
|
1235
|
+
self._adapters.append(adapter)
|
|
1236
|
+
|
|
1237
|
+
def resolve(self, product: BaseEquityProduct) -> QuadInputAdapter:
|
|
1238
|
+
for adapter in self._adapters:
|
|
1239
|
+
if adapter.can_handle(product):
|
|
1240
|
+
return adapter
|
|
1241
|
+
raise PricingError(
|
|
1242
|
+
f"DiscreteQuadEngine has no adapter for product type {type(product).__name__}"
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
_ADAPTER_REGISTRY = QuadInputAdapterRegistry()
|
|
1247
|
+
_ADAPTER_REGISTRY.register(BarrierQuadInputAdapter())
|
|
1248
|
+
_ADAPTER_REGISTRY.register(OneTouchQuadInputAdapter())
|
|
1249
|
+
_ADAPTER_REGISTRY.register(DoubleBarrierQuadInputAdapter())
|
|
1250
|
+
_ADAPTER_REGISTRY.register(DoubleOneTouchQuadInputAdapter())
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def register_quad_adapter(adapter: QuadInputAdapter) -> None:
|
|
1254
|
+
"""Register a custom quad input adapter."""
|
|
1255
|
+
_ADAPTER_REGISTRY.register(adapter)
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def resolve_quad_adapter(product: BaseEquityProduct) -> QuadInputAdapter:
|
|
1259
|
+
"""Resolve the quad input adapter for a product."""
|
|
1260
|
+
return _ADAPTER_REGISTRY.resolve(product)
|