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,1242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Snowball (autocallable) option implementation.
|
|
3
|
+
|
|
4
|
+
Snowball options are structured products with knock-in and knock-out barriers,
|
|
5
|
+
coupon payments, and various protection/participation features.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from quantark.asset.equity.product.option.base_equity_option import BaseEquityOption
|
|
13
|
+
from quantark.util.calendar import calculate_year_fraction
|
|
14
|
+
from quantark.util.calendar.day_counter import DayCountConvention
|
|
15
|
+
from quantark.util.enum import (
|
|
16
|
+
BarrierType,
|
|
17
|
+
CouponPayType,
|
|
18
|
+
ExerciseType,
|
|
19
|
+
ObservationAggregation,
|
|
20
|
+
ObservationType,
|
|
21
|
+
OptionType,
|
|
22
|
+
ProtectionType,
|
|
23
|
+
TenorEnd,
|
|
24
|
+
)
|
|
25
|
+
from quantark.util.exceptions import ValidationError
|
|
26
|
+
|
|
27
|
+
from .observation_schedule import (
|
|
28
|
+
ObservationRecord,
|
|
29
|
+
ObservationSchedule,
|
|
30
|
+
PricingEnv,
|
|
31
|
+
ResolvedObservationRecord,
|
|
32
|
+
)
|
|
33
|
+
from .snowball_config import AccrualConfig, AirbagConfig, BarrierConfig, PayoffConfig
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SnowballOption(BaseEquityOption):
|
|
38
|
+
"""
|
|
39
|
+
Snowball (autocallable) structured product with knock-in and knock-out barriers.
|
|
40
|
+
|
|
41
|
+
A snowball option is an autocallable product that:
|
|
42
|
+
- Pays coupons if knock-out (KO) barrier is triggered (product terminates)
|
|
43
|
+
- Switches to knock-in (KI) state if KI barrier is breached
|
|
44
|
+
- Has different payoffs at maturity depending on KO/KI status
|
|
45
|
+
|
|
46
|
+
Product Types:
|
|
47
|
+
Standard Snowball (is_reverse=False):
|
|
48
|
+
- KO barrier: UP (above initial price)
|
|
49
|
+
- KI barrier: DOWN (below initial price)
|
|
50
|
+
- Embedded option: PUT (investor is short put on KI)
|
|
51
|
+
- option_type: PUT
|
|
52
|
+
|
|
53
|
+
Reverse Snowball (is_reverse=True):
|
|
54
|
+
- KO barrier: DOWN (below initial price)
|
|
55
|
+
- KI barrier: UP (above initial price)
|
|
56
|
+
- Embedded option: CALL (investor is short call on KI)
|
|
57
|
+
- option_type: CALL
|
|
58
|
+
|
|
59
|
+
Payoff Scenarios:
|
|
60
|
+
1. KO triggered: Principal (if included) + KO rate × accrued time
|
|
61
|
+
2. At maturity, never KO and never KI (V0):
|
|
62
|
+
Principal (if included) + fixed rebate or call-style rebate
|
|
63
|
+
3. At maturity, never KO but KI happened (V1):
|
|
64
|
+
Principal (if included) + participation × (Spot - strike), floored by protection
|
|
65
|
+
|
|
66
|
+
Core Attributes:
|
|
67
|
+
initial_price: Reference price for payoff calculations
|
|
68
|
+
strike: Strike for the embedded option (put for standard, call for reverse)
|
|
69
|
+
contract_multiplier: Underlying units represented by one contract
|
|
70
|
+
is_reverse: If True, reverse snowball; if False (default), standard snowball
|
|
71
|
+
option_type: CALL for reverse, PUT for standard (auto-set based on is_reverse)
|
|
72
|
+
exercise_type: EUROPEAN (autocallables are European-style)
|
|
73
|
+
|
|
74
|
+
Barrier Attributes (via barrier_config):
|
|
75
|
+
ko_barrier: Knock-out barrier level(s)
|
|
76
|
+
ko_rate: Knock-out return rate(s)
|
|
77
|
+
ko_observation_type: DISCRETE or CONTINUOUS monitoring for KO
|
|
78
|
+
ki_barrier: Knock-in barrier level(s)
|
|
79
|
+
ki_observation_type: DISCRETE or CONTINUOUS monitoring for KI
|
|
80
|
+
disable_ko_after_ki: If True, disable KO after KI is triggered
|
|
81
|
+
|
|
82
|
+
Payoff Attributes (via payoff_config):
|
|
83
|
+
rebate_rate: Fixed rebate rate for V0 maturity payoff
|
|
84
|
+
call_rebate_enabled: If True, use call-style rebate instead of fixed
|
|
85
|
+
include_principal: Whether principal is part of payouts
|
|
86
|
+
participation_rate: Downside participation after KI
|
|
87
|
+
protection_type: NONE, PARTIAL, or FULL protection
|
|
88
|
+
|
|
89
|
+
Accrual Attributes (via accrual_config):
|
|
90
|
+
coupon_pay_type: INSTANT (at KO date) or EXPIRY (discounted to maturity)
|
|
91
|
+
is_annualized: Flag for annualized accruals
|
|
92
|
+
|
|
93
|
+
Airbag Attributes (via airbag_config):
|
|
94
|
+
airbag_barrier: Barrier level for airbag protection
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# Core parameters
|
|
98
|
+
initial_price: float = 0.0
|
|
99
|
+
strike: float = 0.0
|
|
100
|
+
is_reverse: bool = False
|
|
101
|
+
|
|
102
|
+
# Option type parameters (inherited from BaseEquityOption)
|
|
103
|
+
# For standard snowball: embedded option is PUT (short put exposure on KI)
|
|
104
|
+
# For reverse snowball: embedded option is CALL (short call exposure on KI)
|
|
105
|
+
option_type: OptionType = OptionType.PUT
|
|
106
|
+
exercise_type: ExerciseType = ExerciseType.EUROPEAN
|
|
107
|
+
|
|
108
|
+
# Date-based maturity (inherited from base class concept, defined here as dataclass fields)
|
|
109
|
+
initial_date: Optional[datetime] = None
|
|
110
|
+
exercise_date: Optional[datetime] = None
|
|
111
|
+
settlement_date: Optional[datetime] = None
|
|
112
|
+
maturity_date: Optional[datetime] = None
|
|
113
|
+
tenor: Optional[float] = None
|
|
114
|
+
maturity: Optional[float] = None
|
|
115
|
+
tenor_end: TenorEnd = TenorEnd.EXERCISE
|
|
116
|
+
annualization_day_count: DayCountConvention = DayCountConvention.ACT_365
|
|
117
|
+
|
|
118
|
+
# Configuration objects (clean API)
|
|
119
|
+
barrier_config: BarrierConfig = field(
|
|
120
|
+
default_factory=lambda: BarrierConfig(ko_barrier=0.0, ko_rate=0.0)
|
|
121
|
+
)
|
|
122
|
+
payoff_config: PayoffConfig = field(default_factory=PayoffConfig)
|
|
123
|
+
accrual_config: AccrualConfig = field(default_factory=AccrualConfig)
|
|
124
|
+
airbag_config: AirbagConfig = field(default_factory=AirbagConfig)
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
initial_price: float,
|
|
129
|
+
strike: float,
|
|
130
|
+
barrier_config: BarrierConfig,
|
|
131
|
+
payoff_config: Optional[PayoffConfig] = None,
|
|
132
|
+
accrual_config: Optional[AccrualConfig] = None,
|
|
133
|
+
airbag_config: Optional[AirbagConfig] = None,
|
|
134
|
+
contract_multiplier: float = 1.0,
|
|
135
|
+
is_reverse: bool = False,
|
|
136
|
+
maturity: Optional[float] = None,
|
|
137
|
+
tenor: Optional[float] = None,
|
|
138
|
+
initial_date: Optional[datetime] = None,
|
|
139
|
+
exercise_date: Optional[datetime] = None,
|
|
140
|
+
settlement_date: Optional[datetime] = None,
|
|
141
|
+
maturity_date: Optional[datetime] = None,
|
|
142
|
+
tenor_end: TenorEnd = TenorEnd.EXERCISE,
|
|
143
|
+
annualization_day_count: DayCountConvention = DayCountConvention.ACT_365,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Initialize Snowball option.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
initial_price: Reference price for payoff calculations
|
|
150
|
+
strike: Strike for the embedded option (put for standard, call for reverse)
|
|
151
|
+
barrier_config: BarrierConfig with KO/KI barrier settings (required)
|
|
152
|
+
payoff_config: PayoffConfig with rebate/protection/participation settings
|
|
153
|
+
accrual_config: AccrualConfig with annualization flags
|
|
154
|
+
airbag_config: AirbagConfig with airbag barrier settings
|
|
155
|
+
contract_multiplier: Underlying units represented by one contract
|
|
156
|
+
is_reverse: If True, creates a reverse snowball with embedded call option;
|
|
157
|
+
if False (default), creates standard snowball with embedded put
|
|
158
|
+
maturity: Time to maturity from valuation (years)
|
|
159
|
+
tenor: Contract tenor in years (issue to expiry)
|
|
160
|
+
initial_date: Product start/issue date
|
|
161
|
+
exercise_date: Expiration date
|
|
162
|
+
settlement_date: Settlement date
|
|
163
|
+
maturity_date: Explicit maturity date
|
|
164
|
+
tenor_end: Tenor end-point selection
|
|
165
|
+
annualization_day_count: Day count basis
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ValidationError: If parameters are invalid
|
|
169
|
+
|
|
170
|
+
Note:
|
|
171
|
+
Standard Snowball (is_reverse=False):
|
|
172
|
+
- KO barrier is UP (above initial price)
|
|
173
|
+
- KI barrier is DOWN (below initial price)
|
|
174
|
+
- Embedded option is PUT (investor is short put on KI)
|
|
175
|
+
- V1 payoff: participation × (Spot - Strike), typically negative when spot < strike
|
|
176
|
+
|
|
177
|
+
Reverse Snowball (is_reverse=True):
|
|
178
|
+
- KO barrier is DOWN (below initial price)
|
|
179
|
+
- KI barrier is UP (above initial price)
|
|
180
|
+
- Embedded option is CALL (investor is short call on KI)
|
|
181
|
+
- V1 payoff: participation × (Strike - Spot), typically negative when spot > strike
|
|
182
|
+
"""
|
|
183
|
+
# Set base class attributes
|
|
184
|
+
self.initial_date = initial_date
|
|
185
|
+
self.exercise_date = exercise_date
|
|
186
|
+
self.settlement_date = settlement_date
|
|
187
|
+
self.maturity_date = maturity_date
|
|
188
|
+
self.tenor = tenor
|
|
189
|
+
self.maturity = maturity
|
|
190
|
+
self.tenor_end = tenor_end
|
|
191
|
+
self.annualization_day_count = annualization_day_count
|
|
192
|
+
|
|
193
|
+
# Set core attributes
|
|
194
|
+
self.initial_price = initial_price
|
|
195
|
+
self.strike = strike
|
|
196
|
+
self.contract_multiplier = contract_multiplier
|
|
197
|
+
self.is_reverse = is_reverse
|
|
198
|
+
|
|
199
|
+
# Set option type based on standard vs reverse snowball
|
|
200
|
+
# Standard snowball: embedded PUT (short put exposure on KI)
|
|
201
|
+
# Reverse snowball: embedded CALL (short call exposure on KI)
|
|
202
|
+
self.option_type = OptionType.CALL if is_reverse else OptionType.PUT
|
|
203
|
+
self.exercise_type = ExerciseType.EUROPEAN
|
|
204
|
+
|
|
205
|
+
# Set configuration objects
|
|
206
|
+
self.barrier_config = barrier_config
|
|
207
|
+
self.payoff_config = (
|
|
208
|
+
payoff_config if payoff_config is not None else PayoffConfig()
|
|
209
|
+
)
|
|
210
|
+
self.accrual_config = (
|
|
211
|
+
accrual_config if accrual_config is not None else AccrualConfig()
|
|
212
|
+
)
|
|
213
|
+
self.airbag_config = (
|
|
214
|
+
airbag_config if airbag_config is not None else AirbagConfig()
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self.validate()
|
|
218
|
+
|
|
219
|
+
def validate(self) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Validate Snowball option parameters.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ValidationError: If parameters are invalid
|
|
225
|
+
"""
|
|
226
|
+
self._validate_core_parameters()
|
|
227
|
+
self._validate_maturity_parameters()
|
|
228
|
+
super().validate()
|
|
229
|
+
self._validate_barrier_parameters()
|
|
230
|
+
self._validate_observation_parameters()
|
|
231
|
+
self._validate_payoff_parameters()
|
|
232
|
+
self._validate_accrual_parameters()
|
|
233
|
+
self._build_observation_schedules()
|
|
234
|
+
|
|
235
|
+
def _validate_core_parameters(self) -> None:
|
|
236
|
+
"""Validate core product parameters (initial_price, strike)."""
|
|
237
|
+
if self.initial_price <= 0:
|
|
238
|
+
raise ValidationError(
|
|
239
|
+
f"Initial price must be positive, got {self.initial_price}"
|
|
240
|
+
)
|
|
241
|
+
if self.strike <= 0:
|
|
242
|
+
raise ValidationError(f"Strike must be positive, got {self.strike}")
|
|
243
|
+
|
|
244
|
+
def _validate_maturity_parameters(self) -> None:
|
|
245
|
+
"""Validate maturity, tenor, and date-related parameters."""
|
|
246
|
+
if self.maturity is None and self.exercise_date is None:
|
|
247
|
+
raise ValidationError("Either maturity or exercise_date must be provided")
|
|
248
|
+
if self.maturity is not None and self.maturity <= 0:
|
|
249
|
+
raise ValidationError(f"Maturity must be positive, got {self.maturity}")
|
|
250
|
+
if self.tenor is not None and self.tenor <= 0:
|
|
251
|
+
raise ValidationError(f"Tenor must be positive, got {self.tenor}")
|
|
252
|
+
if not isinstance(self.tenor_end, TenorEnd):
|
|
253
|
+
raise ValidationError(f"Invalid tenor_end: {self.tenor_end}")
|
|
254
|
+
if not isinstance(self.annualization_day_count, DayCountConvention):
|
|
255
|
+
raise ValidationError(
|
|
256
|
+
f"annualization_day_count must be DayCountConvention, got {self.annualization_day_count}"
|
|
257
|
+
)
|
|
258
|
+
if self.tenor_end == TenorEnd.SETTLEMENT and self.settlement_date is None:
|
|
259
|
+
raise ValidationError(
|
|
260
|
+
"settlement_date required when tenor_end is SETTLEMENT"
|
|
261
|
+
)
|
|
262
|
+
if (
|
|
263
|
+
self.tenor_end == TenorEnd.MATURITY
|
|
264
|
+
and self.maturity_date is None
|
|
265
|
+
and self.exercise_date is None
|
|
266
|
+
):
|
|
267
|
+
raise ValidationError(
|
|
268
|
+
"maturity_date or exercise_date required when tenor_end is MATURITY"
|
|
269
|
+
)
|
|
270
|
+
if (
|
|
271
|
+
self.tenor is None
|
|
272
|
+
and any(
|
|
273
|
+
[
|
|
274
|
+
self.accrual_config.is_annualized_ko,
|
|
275
|
+
self.accrual_config.is_annualized_ki,
|
|
276
|
+
self.accrual_config.is_annualized_rebate,
|
|
277
|
+
]
|
|
278
|
+
)
|
|
279
|
+
and self.initial_date is None
|
|
280
|
+
and self.settlement_date is None
|
|
281
|
+
):
|
|
282
|
+
raise ValidationError(
|
|
283
|
+
"initial_date or settlement_date required for annualized accruals when tenor is not provided"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _validate_barrier_parameters(self) -> None:
|
|
287
|
+
"""Validate barrier configurations (KO/KI barriers and rates)."""
|
|
288
|
+
# Validate barrier config
|
|
289
|
+
self._validate_barrier_array(self.barrier_config.ko_barrier, "KO barrier")
|
|
290
|
+
self._validate_rate_array(self.barrier_config.ko_rate, "KO rate")
|
|
291
|
+
|
|
292
|
+
# Validate KO barrier/rate array lengths match observation dates
|
|
293
|
+
ko_obs_len = self._get_observation_length(BarrierType.UP_OUT)
|
|
294
|
+
if ko_obs_len is not None:
|
|
295
|
+
self._validate_array_length(
|
|
296
|
+
self.barrier_config.ko_barrier, ko_obs_len, "KO barrier"
|
|
297
|
+
)
|
|
298
|
+
self._validate_array_length(
|
|
299
|
+
self.barrier_config.ko_rate, ko_obs_len, "KO rate"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Validate KI barrier if provided
|
|
303
|
+
if self.barrier_config.ki_barrier is not None:
|
|
304
|
+
self._validate_barrier_array(self.barrier_config.ki_barrier, "KI barrier")
|
|
305
|
+
if (
|
|
306
|
+
self.barrier_config.ki_continuous
|
|
307
|
+
or self.barrier_config.ki_observation_type == ObservationType.CONTINUOUS
|
|
308
|
+
) and isinstance(self.barrier_config.ki_barrier, list):
|
|
309
|
+
raise ValidationError("Continuous KI requires scalar ki_barrier")
|
|
310
|
+
ki_obs_len = self._get_observation_length(BarrierType.DOWN_IN)
|
|
311
|
+
if ki_obs_len is not None:
|
|
312
|
+
self._validate_array_length(
|
|
313
|
+
self.barrier_config.ki_barrier, ki_obs_len, "KI barrier"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _validate_observation_parameters(self) -> None:
|
|
317
|
+
"""Validate observation types and discrete observation requirements."""
|
|
318
|
+
# Validate observation types
|
|
319
|
+
if not isinstance(self.barrier_config.ko_observation_type, ObservationType):
|
|
320
|
+
raise ValidationError(
|
|
321
|
+
f"Invalid KO observation type: {self.barrier_config.ko_observation_type}"
|
|
322
|
+
)
|
|
323
|
+
if not isinstance(self.barrier_config.ki_observation_type, ObservationType):
|
|
324
|
+
raise ValidationError(
|
|
325
|
+
f"Invalid KI observation type: {self.barrier_config.ki_observation_type}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Validate discrete KO observations
|
|
329
|
+
if self.barrier_config.ko_observation_type == ObservationType.DISCRETE:
|
|
330
|
+
if (
|
|
331
|
+
self.barrier_config.ko_observation_schedule is None
|
|
332
|
+
and self.barrier_config.ko_observation_dates is None
|
|
333
|
+
):
|
|
334
|
+
raise ValidationError(
|
|
335
|
+
"KO observation dates or schedule required for discrete monitoring"
|
|
336
|
+
)
|
|
337
|
+
self._validate_observation_dates(
|
|
338
|
+
self.barrier_config.ko_observation_dates, "KO"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Validate discrete KI observations (if KI barrier provided)
|
|
342
|
+
if (
|
|
343
|
+
self.barrier_config.ki_barrier is not None
|
|
344
|
+
and self.barrier_config.ki_observation_type == ObservationType.DISCRETE
|
|
345
|
+
and not self.barrier_config.ki_continuous
|
|
346
|
+
):
|
|
347
|
+
if (
|
|
348
|
+
self.barrier_config.ki_observation_schedule is None
|
|
349
|
+
and self.barrier_config.ki_observation_dates is None
|
|
350
|
+
):
|
|
351
|
+
raise ValidationError(
|
|
352
|
+
"KI observation dates or schedule required for discrete monitoring"
|
|
353
|
+
)
|
|
354
|
+
self._validate_observation_dates(
|
|
355
|
+
self.barrier_config.ki_observation_dates, "KI"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _validate_payoff_parameters(self) -> None:
|
|
359
|
+
"""Validate payoff configuration (rebate, protection, participation)."""
|
|
360
|
+
# Validate accrual config
|
|
361
|
+
if not isinstance(self.accrual_config.coupon_pay_type, CouponPayType):
|
|
362
|
+
raise ValidationError(
|
|
363
|
+
f"Invalid coupon pay type: {self.accrual_config.coupon_pay_type}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Validate call rebate parameters
|
|
367
|
+
if self.payoff_config.call_rebate_enabled:
|
|
368
|
+
if self.payoff_config.call_strike is None:
|
|
369
|
+
raise ValidationError(
|
|
370
|
+
"Call strike required when call_rebate_enabled is True"
|
|
371
|
+
)
|
|
372
|
+
if self.payoff_config.call_strike <= 0:
|
|
373
|
+
raise ValidationError(
|
|
374
|
+
f"Call strike must be positive, got {self.payoff_config.call_strike}"
|
|
375
|
+
)
|
|
376
|
+
if self.payoff_config.call_participation_rate <= 0:
|
|
377
|
+
raise ValidationError(
|
|
378
|
+
f"Call participation rate must be positive, got {self.payoff_config.call_participation_rate}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Validate protection parameters
|
|
382
|
+
if not isinstance(self.payoff_config.protection_type, ProtectionType):
|
|
383
|
+
raise ValidationError(
|
|
384
|
+
f"Invalid protection type: {self.payoff_config.protection_type}"
|
|
385
|
+
)
|
|
386
|
+
if self.payoff_config.protection_type == ProtectionType.PARTIAL:
|
|
387
|
+
if (
|
|
388
|
+
self.payoff_config.protection_rate < 0
|
|
389
|
+
or self.payoff_config.protection_rate > 1
|
|
390
|
+
):
|
|
391
|
+
raise ValidationError(
|
|
392
|
+
f"Protection rate must be in [0, 1], got {self.payoff_config.protection_rate}"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Validate participation rate
|
|
396
|
+
if self.payoff_config.participation_rate <= 0:
|
|
397
|
+
raise ValidationError(
|
|
398
|
+
f"Participation rate must be positive, got {self.payoff_config.participation_rate}"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def _validate_accrual_parameters(self) -> None:
|
|
402
|
+
"""Validate accrual configuration flags."""
|
|
403
|
+
# Validate accrual config flags
|
|
404
|
+
for flag_name, flag_value in [
|
|
405
|
+
("is_annualized", self.accrual_config.is_annualized),
|
|
406
|
+
("is_annualized_ko", self.accrual_config.is_annualized_ko),
|
|
407
|
+
("is_annualized_ki", self.accrual_config.is_annualized_ki),
|
|
408
|
+
("is_annualized_rebate", self.accrual_config.is_annualized_rebate),
|
|
409
|
+
]:
|
|
410
|
+
if flag_value is not None and not isinstance(flag_value, bool):
|
|
411
|
+
raise ValidationError(f"{flag_name} must be boolean, got {flag_value}")
|
|
412
|
+
accrual_factors = self.accrual_config.accrual_factors
|
|
413
|
+
if accrual_factors is not None:
|
|
414
|
+
ko_obs_len = self._get_observation_length(BarrierType.UP_OUT)
|
|
415
|
+
if ko_obs_len is not None and len(accrual_factors) != ko_obs_len:
|
|
416
|
+
raise ValidationError(
|
|
417
|
+
"accrual_factors length "
|
|
418
|
+
f"({len(accrual_factors)}) must match KO observation length "
|
|
419
|
+
f"({ko_obs_len})"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def _validate_barrier_array(
|
|
423
|
+
self, barrier: Union[float, List[float]], name: str
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Validate barrier level(s) are positive."""
|
|
426
|
+
if isinstance(barrier, list):
|
|
427
|
+
for i, b in enumerate(barrier):
|
|
428
|
+
if b <= 0:
|
|
429
|
+
raise ValidationError(f"{name}[{i}] must be positive, got {b}")
|
|
430
|
+
else:
|
|
431
|
+
if barrier <= 0:
|
|
432
|
+
raise ValidationError(f"{name} must be positive, got {barrier}")
|
|
433
|
+
|
|
434
|
+
def _validate_rate_array(self, rate: Union[float, List[float]], name: str) -> None:
|
|
435
|
+
"""Validate rate(s) - can be negative for some structures."""
|
|
436
|
+
if isinstance(rate, list):
|
|
437
|
+
for i, r in enumerate(rate):
|
|
438
|
+
if not isinstance(r, (int, float)):
|
|
439
|
+
raise ValidationError(f"{name}[{i}] must be numeric, got {r}")
|
|
440
|
+
else:
|
|
441
|
+
if not isinstance(rate, (int, float)):
|
|
442
|
+
raise ValidationError(f"{name} must be numeric, got {rate}")
|
|
443
|
+
|
|
444
|
+
def _validate_array_length(
|
|
445
|
+
self, value: Union[float, List[float]], expected_len: int, name: str
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Validate array length matches observation dates if it's an array."""
|
|
448
|
+
if isinstance(value, list) and len(value) != expected_len:
|
|
449
|
+
raise ValidationError(
|
|
450
|
+
f"{name} array length ({len(value)}) must match "
|
|
451
|
+
f"observation dates length ({expected_len})"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def _validate_observation_dates(
|
|
455
|
+
self, dates: Optional[List[float]], name: str
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Validate observation dates are non-negative and ordered."""
|
|
458
|
+
if dates is None:
|
|
459
|
+
return
|
|
460
|
+
for i, t in enumerate(dates):
|
|
461
|
+
if t < 0:
|
|
462
|
+
raise ValidationError(
|
|
463
|
+
f"{name} observation date[{i}] must be non-negative, got {t}"
|
|
464
|
+
)
|
|
465
|
+
# Check ordering
|
|
466
|
+
for i in range(1, len(dates)):
|
|
467
|
+
if dates[i] <= dates[i - 1]:
|
|
468
|
+
raise ValidationError(
|
|
469
|
+
f"{name} observation dates must be strictly increasing"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def _get_observation_length(self, barrier_type: BarrierType) -> Optional[int]:
|
|
473
|
+
"""Get the number of observation dates for a barrier type.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
barrier_type: BarrierType enum (e.g., UP_OUT, DOWN_IN)
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Number of observation dates or None if not available
|
|
480
|
+
"""
|
|
481
|
+
if barrier_type.is_knock_out:
|
|
482
|
+
if self.barrier_config.ko_observation_schedule is not None:
|
|
483
|
+
return len(self.barrier_config.ko_observation_schedule.records)
|
|
484
|
+
if self.barrier_config.ko_observation_dates is not None:
|
|
485
|
+
return len(self.barrier_config.ko_observation_dates)
|
|
486
|
+
elif barrier_type.is_knock_in:
|
|
487
|
+
if self.barrier_config.ki_observation_schedule is not None:
|
|
488
|
+
return len(self.barrier_config.ki_observation_schedule.records)
|
|
489
|
+
if self.barrier_config.ki_observation_dates is not None:
|
|
490
|
+
return len(self.barrier_config.ki_observation_dates)
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
def _build_observation_schedules(self) -> None:
|
|
494
|
+
"""Build ObservationSchedules from observation dates if needed (Legacy).
|
|
495
|
+
|
|
496
|
+
Note: This method works around frozen config classes by recreating them with schedules.
|
|
497
|
+
"""
|
|
498
|
+
# Check if we need to build schedules
|
|
499
|
+
needs_ko_schedule = (
|
|
500
|
+
self.barrier_config.ko_observation_schedule is None
|
|
501
|
+
and self.barrier_config.ko_observation_dates is not None
|
|
502
|
+
)
|
|
503
|
+
needs_ki_schedule = (
|
|
504
|
+
self.barrier_config.ki_barrier is not None
|
|
505
|
+
and self.barrier_config.ki_observation_schedule is None
|
|
506
|
+
and self.barrier_config.ki_observation_dates is not None
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if not needs_ko_schedule and not needs_ki_schedule:
|
|
510
|
+
return # Nothing to build
|
|
511
|
+
|
|
512
|
+
# Build KO schedule
|
|
513
|
+
ko_schedule = self.barrier_config.ko_observation_schedule
|
|
514
|
+
if needs_ko_schedule:
|
|
515
|
+
ko_barriers = (
|
|
516
|
+
self.barrier_config.ko_barrier
|
|
517
|
+
if isinstance(self.barrier_config.ko_barrier, list)
|
|
518
|
+
else None
|
|
519
|
+
)
|
|
520
|
+
ko_rates = (
|
|
521
|
+
self.barrier_config.ko_rate
|
|
522
|
+
if isinstance(self.barrier_config.ko_rate, list)
|
|
523
|
+
else None
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
records = []
|
|
527
|
+
for i, t in enumerate(self.barrier_config.ko_observation_dates):
|
|
528
|
+
barrier_val = (
|
|
529
|
+
ko_barriers[i] if ko_barriers else self.barrier_config.ko_barrier
|
|
530
|
+
)
|
|
531
|
+
rate_val = ko_rates[i] if ko_rates else self.barrier_config.ko_rate
|
|
532
|
+
records.append(
|
|
533
|
+
ObservationRecord(
|
|
534
|
+
observation_time=t,
|
|
535
|
+
barrier=barrier_val,
|
|
536
|
+
return_rate=rate_val,
|
|
537
|
+
is_rate_annualized=False,
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
ko_schedule = ObservationSchedule(
|
|
541
|
+
records=records,
|
|
542
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
# Build KI schedule
|
|
546
|
+
ki_schedule = self.barrier_config.ki_observation_schedule
|
|
547
|
+
if needs_ki_schedule:
|
|
548
|
+
ki_barriers = (
|
|
549
|
+
self.barrier_config.ki_barrier
|
|
550
|
+
if isinstance(self.barrier_config.ki_barrier, list)
|
|
551
|
+
else None
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
records = []
|
|
555
|
+
for i, t in enumerate(self.barrier_config.ki_observation_dates):
|
|
556
|
+
barrier_val = (
|
|
557
|
+
ki_barriers[i] if ki_barriers else self.barrier_config.ki_barrier
|
|
558
|
+
)
|
|
559
|
+
records.append(
|
|
560
|
+
ObservationRecord(
|
|
561
|
+
observation_time=t,
|
|
562
|
+
barrier=barrier_val,
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
ki_schedule = ObservationSchedule(
|
|
566
|
+
records=records,
|
|
567
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Create new barrier config with schedules (workaround for frozen dataclass)
|
|
571
|
+
from dataclasses import replace
|
|
572
|
+
|
|
573
|
+
self.barrier_config = replace(
|
|
574
|
+
self.barrier_config,
|
|
575
|
+
ko_observation_schedule=ko_schedule,
|
|
576
|
+
ki_observation_schedule=(
|
|
577
|
+
ki_schedule
|
|
578
|
+
if needs_ki_schedule
|
|
579
|
+
else self.barrier_config.ki_observation_schedule
|
|
580
|
+
),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def time_shift(self, time_bump: float, bumped_date: datetime, pricing_env) -> bool:
|
|
584
|
+
"""Shift barrier schedules for theta bumping."""
|
|
585
|
+
dropped_all = super().time_shift(time_bump, bumped_date, pricing_env)
|
|
586
|
+
if dropped_all:
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
if self.barrier_config is not None:
|
|
590
|
+
new_config, dropped_all = self.barrier_config.time_shift(
|
|
591
|
+
time_bump, bumped_date, pricing_env
|
|
592
|
+
)
|
|
593
|
+
self.barrier_config = new_config
|
|
594
|
+
|
|
595
|
+
return dropped_all
|
|
596
|
+
|
|
597
|
+
def get_maturity(self, pricing_env: PricingEnv = None) -> float:
|
|
598
|
+
"""
|
|
599
|
+
Get time to maturity in years.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
pricing_env: Optional pricing environment for date-based maturity
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Time to maturity in years
|
|
606
|
+
|
|
607
|
+
Raises:
|
|
608
|
+
ValidationError: If maturity cannot be determined
|
|
609
|
+
"""
|
|
610
|
+
return super().get_maturity(pricing_env)
|
|
611
|
+
|
|
612
|
+
def get_contract_tenor(self, pricing_env: PricingEnv = None) -> float:
|
|
613
|
+
"""
|
|
614
|
+
Get contract tenor in years.
|
|
615
|
+
|
|
616
|
+
Contract tenor is the time from initial date to exercise/maturity,
|
|
617
|
+
used for annualized coupon and rebate calculations.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
pricing_env: Optional pricing environment for date-based tenor calculation
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
Contract tenor in years
|
|
624
|
+
"""
|
|
625
|
+
# Use get_tenor from base class which handles various scenarios
|
|
626
|
+
return self.get_tenor(pricing_env)
|
|
627
|
+
|
|
628
|
+
def get_payoff(
|
|
629
|
+
self, spot: float, pricing_env: PricingEnv = None, **kwargs
|
|
630
|
+
) -> float:
|
|
631
|
+
"""
|
|
632
|
+
Calculate payoff at maturity (V0 or V1 state).
|
|
633
|
+
|
|
634
|
+
This method calculates the payoff assuming no KO has occurred.
|
|
635
|
+
The actual payoff depends on the path history (KO triggered, KI triggered).
|
|
636
|
+
|
|
637
|
+
For full path-dependent payoff calculation, use
|
|
638
|
+
resolve_ko_observations, get_maturity_payoff_v0, or
|
|
639
|
+
get_maturity_payoff_v1 methods.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
spot: Spot price at maturity
|
|
643
|
+
pricing_env: Optional pricing environment for date-based maturity
|
|
644
|
+
**kwargs:
|
|
645
|
+
knocked_in: Whether KI was triggered (default: False)
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Payoff at maturity
|
|
649
|
+
"""
|
|
650
|
+
if spot < 0:
|
|
651
|
+
raise ValidationError(f"Spot price must be non-negative, got {spot}")
|
|
652
|
+
|
|
653
|
+
knocked_in = kwargs.get("knocked_in", False)
|
|
654
|
+
|
|
655
|
+
if knocked_in:
|
|
656
|
+
return self.get_maturity_payoff_v1(spot, pricing_env=pricing_env)
|
|
657
|
+
else:
|
|
658
|
+
return self.get_maturity_payoff_v0(spot, pricing_env=pricing_env)
|
|
659
|
+
|
|
660
|
+
def get_maturity_payoff_v0(
|
|
661
|
+
self, spot: float, pricing_env: PricingEnv = None
|
|
662
|
+
) -> float:
|
|
663
|
+
"""
|
|
664
|
+
Calculate payoff at maturity when never KO and never KI (V0 state).
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
spot: Spot price at maturity
|
|
668
|
+
pricing_env: Optional pricing environment for resolving maturity
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
V0 maturity payoff
|
|
672
|
+
"""
|
|
673
|
+
principal = (
|
|
674
|
+
self.initial_price * self.contract_multiplier
|
|
675
|
+
if self.payoff_config.include_principal
|
|
676
|
+
else 0.0
|
|
677
|
+
)
|
|
678
|
+
contract_tenor: Optional[float] = None
|
|
679
|
+
|
|
680
|
+
if (
|
|
681
|
+
self.payoff_config.call_rebate_enabled
|
|
682
|
+
and self.payoff_config.call_strike is not None
|
|
683
|
+
):
|
|
684
|
+
# Call-style rebate
|
|
685
|
+
call_payoff = max(spot - self.payoff_config.call_strike, 0.0)
|
|
686
|
+
rebate = (
|
|
687
|
+
self.payoff_config.call_participation_rate
|
|
688
|
+
* self.contract_multiplier
|
|
689
|
+
* call_payoff
|
|
690
|
+
)
|
|
691
|
+
if self.accrual_config.is_annualized_rebate:
|
|
692
|
+
contract_tenor = contract_tenor or self.get_contract_tenor(pricing_env)
|
|
693
|
+
rebate *= contract_tenor
|
|
694
|
+
else:
|
|
695
|
+
# Fixed rebate
|
|
696
|
+
contract_tenor = contract_tenor or self.get_contract_tenor(pricing_env)
|
|
697
|
+
if self.accrual_config.is_annualized_rebate:
|
|
698
|
+
rebate = (
|
|
699
|
+
self.payoff_config.rebate_rate
|
|
700
|
+
* self.initial_price
|
|
701
|
+
* self.contract_multiplier
|
|
702
|
+
* contract_tenor
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
rebate = (
|
|
706
|
+
self.payoff_config.rebate_rate
|
|
707
|
+
* self.initial_price
|
|
708
|
+
* self.contract_multiplier
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
return principal + rebate
|
|
712
|
+
|
|
713
|
+
def get_maturity_payoff_v1(
|
|
714
|
+
self, spot: float, pricing_env: PricingEnv = None
|
|
715
|
+
) -> float:
|
|
716
|
+
"""
|
|
717
|
+
Calculate payoff at maturity when never KO but KI happened (V1 state).
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
spot: Spot price at maturity
|
|
721
|
+
pricing_env: Optional pricing environment for resolving maturity when annualizing KI return
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
V1 maturity payoff
|
|
725
|
+
"""
|
|
726
|
+
principal = (
|
|
727
|
+
self.initial_price * self.contract_multiplier
|
|
728
|
+
if self.payoff_config.include_principal
|
|
729
|
+
else 0.0
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# Determine if airbag logic applies
|
|
733
|
+
airbag_barrier = self.airbag_config.airbag_barrier
|
|
734
|
+
|
|
735
|
+
# Default to standard payoff configuration
|
|
736
|
+
participation_rate = self.payoff_config.participation_rate
|
|
737
|
+
effective_strike = self.strike
|
|
738
|
+
|
|
739
|
+
if airbag_barrier is not None:
|
|
740
|
+
# Standard snowball: airbag applies (unsafe) when spot < airbag_barrier
|
|
741
|
+
# Reverse snowball: airbag applies (unsafe) when spot > airbag_barrier
|
|
742
|
+
if self.is_reverse:
|
|
743
|
+
is_unsafe = spot > airbag_barrier
|
|
744
|
+
else:
|
|
745
|
+
is_unsafe = spot < airbag_barrier
|
|
746
|
+
|
|
747
|
+
if is_unsafe:
|
|
748
|
+
# In unsafe zone, use airbag participation and strike
|
|
749
|
+
participation_rate = self.airbag_config.airbag_participation_rate
|
|
750
|
+
effective_strike = (
|
|
751
|
+
self.airbag_config.airbag_strike
|
|
752
|
+
if self.airbag_config.airbag_strike is not None
|
|
753
|
+
else self.strike
|
|
754
|
+
)
|
|
755
|
+
# When not in unsafe zone, use standard participation rate (already set above)
|
|
756
|
+
|
|
757
|
+
# Downside participation
|
|
758
|
+
# Standard: Short Put (loss if spot < strike) -> downside = spot - strike
|
|
759
|
+
# Reverse: Short Call (loss if spot > strike) -> downside = strike - spot
|
|
760
|
+
if self.is_reverse:
|
|
761
|
+
raw_diff = effective_strike - spot
|
|
762
|
+
else:
|
|
763
|
+
raw_diff = spot - effective_strike
|
|
764
|
+
|
|
765
|
+
downside = (
|
|
766
|
+
participation_rate * min(raw_diff, 0.0) * self.contract_multiplier
|
|
767
|
+
)
|
|
768
|
+
if self.accrual_config.is_annualized_ki:
|
|
769
|
+
contract_tenor = self.get_contract_tenor(pricing_env)
|
|
770
|
+
downside *= contract_tenor
|
|
771
|
+
|
|
772
|
+
# Apply protection floor
|
|
773
|
+
if self.payoff_config.protection_type == ProtectionType.FULL:
|
|
774
|
+
# Full protection: can't lose more than principal (if included)
|
|
775
|
+
floor = 0.0
|
|
776
|
+
downside = max(downside, -floor)
|
|
777
|
+
elif self.payoff_config.protection_type == ProtectionType.PARTIAL:
|
|
778
|
+
# Partial protection: floor at -protection_rate × N
|
|
779
|
+
floor = (
|
|
780
|
+
self.payoff_config.protection_rate
|
|
781
|
+
* self.initial_price
|
|
782
|
+
* self.contract_multiplier
|
|
783
|
+
)
|
|
784
|
+
downside = max(downside, -floor)
|
|
785
|
+
|
|
786
|
+
return principal + downside
|
|
787
|
+
|
|
788
|
+
def is_ko_triggered(self, spot: float, observation_idx: int = 0) -> bool:
|
|
789
|
+
"""
|
|
790
|
+
Check if KO barrier would be triggered at given spot.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
spot: Current spot price
|
|
794
|
+
observation_idx: Index of observation date (for time-varying barriers)
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
True if KO barrier is triggered (spot >= KO barrier for up barrier)
|
|
798
|
+
"""
|
|
799
|
+
barrier = self._get_barrier_at(
|
|
800
|
+
self.barrier_config.ko_barrier, observation_idx, "KO barrier"
|
|
801
|
+
)
|
|
802
|
+
return spot >= barrier
|
|
803
|
+
|
|
804
|
+
def is_ki_triggered(self, spot: float, observation_idx: int = 0) -> bool:
|
|
805
|
+
"""
|
|
806
|
+
Check if KI barrier would be triggered at given spot.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
spot: Current spot price
|
|
810
|
+
observation_idx: Index of observation date (for time-varying barriers)
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
True if KI barrier is triggered (spot <= KI barrier for down barrier)
|
|
814
|
+
"""
|
|
815
|
+
if self.barrier_config.ki_barrier is None:
|
|
816
|
+
return False
|
|
817
|
+
|
|
818
|
+
barrier = self._get_barrier_at(
|
|
819
|
+
self.barrier_config.ki_barrier, observation_idx, "KI barrier"
|
|
820
|
+
)
|
|
821
|
+
return spot <= barrier
|
|
822
|
+
|
|
823
|
+
def _get_barrier_at(
|
|
824
|
+
self, barrier_value: Union[float, List[float]], index: int, barrier_type: str
|
|
825
|
+
) -> float:
|
|
826
|
+
"""Extract barrier value at given observation index.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
barrier_value: Single barrier or list of barriers
|
|
830
|
+
index: Observation index
|
|
831
|
+
barrier_type: Type description for error messages (e.g., "KO", "KI")
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Barrier level at the specified index
|
|
835
|
+
"""
|
|
836
|
+
if isinstance(barrier_value, list):
|
|
837
|
+
if index < 0 or index >= len(barrier_value):
|
|
838
|
+
raise ValidationError(
|
|
839
|
+
f"{barrier_type} observation index {index} out of range"
|
|
840
|
+
)
|
|
841
|
+
return barrier_value[index]
|
|
842
|
+
return barrier_value
|
|
843
|
+
|
|
844
|
+
def get_ko_barrier_at(self, observation_idx: int) -> float:
|
|
845
|
+
"""Get KO barrier level at given observation index."""
|
|
846
|
+
return self._get_barrier_at(
|
|
847
|
+
self.barrier_config.ko_barrier, observation_idx, "KO"
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
def get_ko_rate_at(self, observation_idx: int) -> float:
|
|
851
|
+
"""Get KO rate at given observation index."""
|
|
852
|
+
return self._get_barrier_at(
|
|
853
|
+
self.barrier_config.ko_rate, observation_idx, "KO rate"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def get_ki_barrier_at(self, observation_idx: int) -> Optional[float]:
|
|
857
|
+
"""Get KI barrier level at given observation index."""
|
|
858
|
+
if self.barrier_config.ki_barrier is None:
|
|
859
|
+
return None
|
|
860
|
+
return self._get_barrier_at(
|
|
861
|
+
self.barrier_config.ki_barrier, observation_idx, "KI"
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
def get_ko_direction(self) -> BarrierType:
|
|
865
|
+
"""
|
|
866
|
+
Get the direction of the KO barrier.
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
BarrierType enum indicating the direction (UP_OUT or DOWN_OUT).
|
|
870
|
+
"""
|
|
871
|
+
return BarrierType.DOWN_OUT if self.is_reverse else BarrierType.UP_OUT
|
|
872
|
+
|
|
873
|
+
def get_ki_direction(self) -> BarrierType:
|
|
874
|
+
"""
|
|
875
|
+
Get the direction of the KI barrier.
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
BarrierType enum indicating the direction (UP_IN or DOWN_IN).
|
|
879
|
+
"""
|
|
880
|
+
return BarrierType.UP_IN if self.is_reverse else BarrierType.DOWN_IN
|
|
881
|
+
|
|
882
|
+
@property
|
|
883
|
+
def has_ki_barrier(self) -> bool:
|
|
884
|
+
"""Check if product has a knock-in barrier."""
|
|
885
|
+
return self.barrier_config.ki_barrier is not None
|
|
886
|
+
|
|
887
|
+
@property
|
|
888
|
+
def num_ko_observations(self) -> int:
|
|
889
|
+
"""Get number of KO observation dates."""
|
|
890
|
+
if self.barrier_config.ko_observation_schedule is not None:
|
|
891
|
+
return len(self.barrier_config.ko_observation_schedule.records)
|
|
892
|
+
if self.barrier_config.ko_observation_dates is not None:
|
|
893
|
+
return len(self.barrier_config.ko_observation_dates)
|
|
894
|
+
return 0
|
|
895
|
+
|
|
896
|
+
@property
|
|
897
|
+
def num_ki_observations(self) -> int:
|
|
898
|
+
"""Get number of KI observation dates."""
|
|
899
|
+
if self.barrier_config.ki_observation_schedule is not None:
|
|
900
|
+
return len(self.barrier_config.ki_observation_schedule.records)
|
|
901
|
+
if self.barrier_config.ki_observation_dates is not None:
|
|
902
|
+
return len(self.barrier_config.ki_observation_dates)
|
|
903
|
+
return 0
|
|
904
|
+
|
|
905
|
+
@property
|
|
906
|
+
def is_standard(self) -> bool:
|
|
907
|
+
"""
|
|
908
|
+
Check if this is a standard snowball (not reverse).
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
True if standard snowball (embedded put), False if reverse (embedded call)
|
|
912
|
+
"""
|
|
913
|
+
return not self.is_reverse
|
|
914
|
+
|
|
915
|
+
def intrinsic_value(self, spot: float) -> float:
|
|
916
|
+
"""
|
|
917
|
+
Calculate intrinsic value of the embedded option.
|
|
918
|
+
|
|
919
|
+
For standard snowball (PUT): max(strike - spot, 0)
|
|
920
|
+
For reverse snowball (CALL): max(spot - strike, 0)
|
|
921
|
+
|
|
922
|
+
Note: This represents the intrinsic value of the embedded option component,
|
|
923
|
+
not the full V1 payoff which includes participation and protection.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
spot: Current spot price
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
Intrinsic value (non-negative)
|
|
930
|
+
|
|
931
|
+
Raises:
|
|
932
|
+
ValidationError: If spot is negative
|
|
933
|
+
"""
|
|
934
|
+
if spot < 0:
|
|
935
|
+
raise ValidationError(f"Spot must be non-negative, got {spot}")
|
|
936
|
+
|
|
937
|
+
if self.is_reverse:
|
|
938
|
+
intrinsic = max(spot - self.strike, 0.0)
|
|
939
|
+
else:
|
|
940
|
+
intrinsic = max(self.strike - spot, 0.0)
|
|
941
|
+
return intrinsic * self.contract_multiplier
|
|
942
|
+
|
|
943
|
+
def _effective_annualized_flag(self, flag: Optional[bool]) -> bool:
|
|
944
|
+
"""Resolve specific annualized flag with product-level default."""
|
|
945
|
+
if flag is None:
|
|
946
|
+
return bool(self.accrual_config.is_annualized)
|
|
947
|
+
return flag
|
|
948
|
+
|
|
949
|
+
def resolve_ko_observations(self, pricing_env) -> List[ResolvedObservationRecord]:
|
|
950
|
+
"""
|
|
951
|
+
Resolve KO observation schedule to concrete times, barriers, payoffs, and settlement times.
|
|
952
|
+
|
|
953
|
+
The payoff includes principal (when configured) plus KO coupon scaled by annualization settings.
|
|
954
|
+
"""
|
|
955
|
+
if self.barrier_config.ko_observation_type != ObservationType.DISCRETE:
|
|
956
|
+
raise ValidationError(
|
|
957
|
+
"resolve_ko_observations currently supports discrete KO monitoring."
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
schedule = self.barrier_config.ko_observation_schedule
|
|
961
|
+
if schedule is None:
|
|
962
|
+
raise ValidationError(
|
|
963
|
+
"KO observation schedule is required to resolve KO observations."
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
default_barrier = (
|
|
967
|
+
None
|
|
968
|
+
if isinstance(self.barrier_config.ko_barrier, list)
|
|
969
|
+
else self.barrier_config.ko_barrier
|
|
970
|
+
)
|
|
971
|
+
resolved_schedule = schedule.resolve(
|
|
972
|
+
pricing_env=pricing_env,
|
|
973
|
+
default_barrier=default_barrier,
|
|
974
|
+
default_payoff=0.0,
|
|
975
|
+
require_single=True,
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
annualized_ko = self._effective_annualized_flag(
|
|
979
|
+
self.accrual_config.is_annualized_ko
|
|
980
|
+
)
|
|
981
|
+
principal_component = (
|
|
982
|
+
self.initial_price * self.contract_multiplier
|
|
983
|
+
if self.payoff_config.include_principal
|
|
984
|
+
else 0.0
|
|
985
|
+
)
|
|
986
|
+
maturity_time: Optional[float] = None
|
|
987
|
+
bus_days_in_year = (
|
|
988
|
+
pricing_env.bus_days_in_year if pricing_env is not None else 252
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
ko_records: List[ResolvedObservationRecord] = []
|
|
992
|
+
accrual_factors = self.accrual_config.accrual_factors
|
|
993
|
+
for idx, rec in enumerate(resolved_schedule):
|
|
994
|
+
rate = schedule.records[idx].return_rate
|
|
995
|
+
if rate is None:
|
|
996
|
+
rate = self.get_ko_rate_at(idx)
|
|
997
|
+
|
|
998
|
+
if accrual_factors is not None:
|
|
999
|
+
accrual_factor = float(accrual_factors[idx])
|
|
1000
|
+
elif annualized_ko:
|
|
1001
|
+
schedule_record = schedule.records[idx]
|
|
1002
|
+
accrual_start_date = self.initial_date
|
|
1003
|
+
if schedule_record.observation_date is not None:
|
|
1004
|
+
if accrual_start_date is None:
|
|
1005
|
+
if pricing_env is None:
|
|
1006
|
+
raise ValidationError(
|
|
1007
|
+
"PricingEnvironment required to resolve KO accrual from observation_date."
|
|
1008
|
+
)
|
|
1009
|
+
accrual_start_date = pricing_env.valuation_date
|
|
1010
|
+
accrual_factor = calculate_year_fraction(
|
|
1011
|
+
accrual_start_date,
|
|
1012
|
+
schedule_record.observation_date,
|
|
1013
|
+
self.annualization_day_count,
|
|
1014
|
+
bus_days_in_year,
|
|
1015
|
+
calendar=getattr(pricing_env, "calendar", None),
|
|
1016
|
+
)
|
|
1017
|
+
else:
|
|
1018
|
+
if accrual_start_date is None:
|
|
1019
|
+
accrual_factor = rec.observation_time
|
|
1020
|
+
else:
|
|
1021
|
+
if pricing_env is None:
|
|
1022
|
+
raise ValidationError(
|
|
1023
|
+
"PricingEnvironment required to resolve KO accrual without observation_date."
|
|
1024
|
+
)
|
|
1025
|
+
if pricing_env.valuation_date < accrual_start_date:
|
|
1026
|
+
raise ValidationError(
|
|
1027
|
+
"valuation_date must be on or after initial_date to resolve KO accrual."
|
|
1028
|
+
)
|
|
1029
|
+
if pricing_env.valuation_date == accrual_start_date:
|
|
1030
|
+
initial_to_valuation = 0.0
|
|
1031
|
+
else:
|
|
1032
|
+
initial_to_valuation = calculate_year_fraction(
|
|
1033
|
+
accrual_start_date,
|
|
1034
|
+
pricing_env.valuation_date,
|
|
1035
|
+
self.annualization_day_count,
|
|
1036
|
+
pricing_env.bus_days_in_year,
|
|
1037
|
+
calendar=getattr(pricing_env, "calendar", None),
|
|
1038
|
+
)
|
|
1039
|
+
accrual_factor = initial_to_valuation + rec.observation_time
|
|
1040
|
+
else:
|
|
1041
|
+
accrual_factor = 1.0
|
|
1042
|
+
coupon_payoff = (
|
|
1043
|
+
self.initial_price * self.contract_multiplier * rate * accrual_factor
|
|
1044
|
+
)
|
|
1045
|
+
payoff = principal_component + coupon_payoff
|
|
1046
|
+
|
|
1047
|
+
settlement_time = rec.settlement_time
|
|
1048
|
+
if self.accrual_config.coupon_pay_type == CouponPayType.EXPIRY:
|
|
1049
|
+
maturity_time = (
|
|
1050
|
+
maturity_time
|
|
1051
|
+
if maturity_time is not None
|
|
1052
|
+
else self.get_maturity(pricing_env)
|
|
1053
|
+
)
|
|
1054
|
+
settlement_time = maturity_time
|
|
1055
|
+
|
|
1056
|
+
ko_records.append(
|
|
1057
|
+
ResolvedObservationRecord(
|
|
1058
|
+
observation_time=rec.observation_time,
|
|
1059
|
+
barrier=rec.barrier,
|
|
1060
|
+
payoff=payoff,
|
|
1061
|
+
settlement_time=settlement_time,
|
|
1062
|
+
)
|
|
1063
|
+
)
|
|
1064
|
+
return ko_records
|
|
1065
|
+
|
|
1066
|
+
def resolve_ki_observations(self, pricing_env) -> List[ResolvedObservationRecord]:
|
|
1067
|
+
"""
|
|
1068
|
+
Resolve KI observation schedule to times and barrier levels (no immediate payoff).
|
|
1069
|
+
"""
|
|
1070
|
+
if self.barrier_config.ki_barrier is None:
|
|
1071
|
+
raise ValidationError("KI barrier configuration is missing.")
|
|
1072
|
+
if (
|
|
1073
|
+
self.barrier_config.ki_observation_type != ObservationType.DISCRETE
|
|
1074
|
+
or self.barrier_config.ki_continuous
|
|
1075
|
+
):
|
|
1076
|
+
raise ValidationError(
|
|
1077
|
+
"resolve_ki_observations currently supports discrete KI monitoring."
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
schedule = self.barrier_config.ki_observation_schedule
|
|
1081
|
+
if schedule is None:
|
|
1082
|
+
raise ValidationError(
|
|
1083
|
+
"KI observation schedule is required to resolve KI observations."
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
default_barrier = (
|
|
1087
|
+
None
|
|
1088
|
+
if isinstance(self.barrier_config.ki_barrier, list)
|
|
1089
|
+
else self.barrier_config.ki_barrier
|
|
1090
|
+
)
|
|
1091
|
+
resolved_schedule = schedule.resolve(
|
|
1092
|
+
pricing_env=pricing_env,
|
|
1093
|
+
default_barrier=default_barrier,
|
|
1094
|
+
default_payoff=0.0,
|
|
1095
|
+
require_single=True,
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
return [
|
|
1099
|
+
ResolvedObservationRecord(
|
|
1100
|
+
observation_time=rec.observation_time,
|
|
1101
|
+
barrier=rec.barrier,
|
|
1102
|
+
payoff=0.0,
|
|
1103
|
+
settlement_time=rec.settlement_time,
|
|
1104
|
+
)
|
|
1105
|
+
for rec in resolved_schedule
|
|
1106
|
+
]
|
|
1107
|
+
|
|
1108
|
+
def get_ko_observation_profile(
|
|
1109
|
+
self, pricing_env
|
|
1110
|
+
) -> Dict[str, List[Optional[float]]]:
|
|
1111
|
+
"""
|
|
1112
|
+
Convenience helper returning KO observation attributes for engine consumption.
|
|
1113
|
+
"""
|
|
1114
|
+
records = self.resolve_ko_observations(pricing_env)
|
|
1115
|
+
return {
|
|
1116
|
+
"observation_times": [rec.observation_time for rec in records],
|
|
1117
|
+
"barriers": [rec.barrier for rec in records],
|
|
1118
|
+
"payoffs": [rec.payoff for rec in records],
|
|
1119
|
+
"settlement_times": [rec.settlement_time for rec in records],
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
def get_ki_observation_profile(
|
|
1123
|
+
self, pricing_env
|
|
1124
|
+
) -> Dict[str, List[Optional[float]]]:
|
|
1125
|
+
"""
|
|
1126
|
+
Convenience helper returning KI observation attributes for engine consumption.
|
|
1127
|
+
"""
|
|
1128
|
+
ki_continuous = (
|
|
1129
|
+
self.barrier_config.ki_continuous
|
|
1130
|
+
or self.barrier_config.ki_observation_type == ObservationType.CONTINUOUS
|
|
1131
|
+
)
|
|
1132
|
+
if ki_continuous:
|
|
1133
|
+
if self.barrier_config.ki_barrier is None:
|
|
1134
|
+
raise ValidationError("KI barrier configuration is missing.")
|
|
1135
|
+
if isinstance(self.barrier_config.ki_barrier, list):
|
|
1136
|
+
raise ValidationError("Continuous KI requires scalar ki_barrier")
|
|
1137
|
+
# For continuous KI, the engine generates its own time grid.
|
|
1138
|
+
# We return the base KI barrier as a scalar (in a list for consistency)
|
|
1139
|
+
# and empty lists for other attributes.
|
|
1140
|
+
return {
|
|
1141
|
+
"observation_times": [],
|
|
1142
|
+
"barriers": [self.barrier_config.ki_barrier],
|
|
1143
|
+
"payoffs": [],
|
|
1144
|
+
"settlement_times": [],
|
|
1145
|
+
}
|
|
1146
|
+
records = self.resolve_ki_observations(pricing_env)
|
|
1147
|
+
return {
|
|
1148
|
+
"observation_times": [rec.observation_time for rec in records],
|
|
1149
|
+
"barriers": [rec.barrier for rec in records],
|
|
1150
|
+
"payoffs": [rec.payoff for rec in records],
|
|
1151
|
+
"settlement_times": [rec.settlement_time for rec in records],
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
def cache_key(self) -> Dict[str, object]:
|
|
1155
|
+
def _serialize_dt(value: Optional[datetime]) -> Optional[str]:
|
|
1156
|
+
return value.isoformat() if isinstance(value, datetime) else None
|
|
1157
|
+
|
|
1158
|
+
def _serialize_enum(value) -> Optional[str]:
|
|
1159
|
+
return value.name if hasattr(value, "name") else None
|
|
1160
|
+
|
|
1161
|
+
def _serialize_schedule(
|
|
1162
|
+
schedule: Optional[ObservationSchedule],
|
|
1163
|
+
) -> Optional[Dict[str, object]]:
|
|
1164
|
+
if schedule is None:
|
|
1165
|
+
return None
|
|
1166
|
+
frequency = schedule.frequency
|
|
1167
|
+
if hasattr(frequency, "name"):
|
|
1168
|
+
frequency_value = frequency.name
|
|
1169
|
+
else:
|
|
1170
|
+
frequency_value = frequency
|
|
1171
|
+
return {
|
|
1172
|
+
"aggregation_mode": _serialize_enum(schedule.aggregation_mode),
|
|
1173
|
+
"frequency": frequency_value,
|
|
1174
|
+
"records": [_serialize_value(rec) for rec in schedule.records],
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
def _serialize_value(value):
|
|
1178
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
1179
|
+
return value
|
|
1180
|
+
if isinstance(value, datetime):
|
|
1181
|
+
return value.isoformat()
|
|
1182
|
+
if hasattr(value, "name"):
|
|
1183
|
+
return value.name
|
|
1184
|
+
if isinstance(value, ObservationSchedule):
|
|
1185
|
+
return _serialize_schedule(value)
|
|
1186
|
+
if is_dataclass(value):
|
|
1187
|
+
return {
|
|
1188
|
+
f.name: _serialize_value(getattr(value, f.name))
|
|
1189
|
+
for f in fields(value)
|
|
1190
|
+
}
|
|
1191
|
+
if isinstance(value, dict):
|
|
1192
|
+
return {k: _serialize_value(v) for k, v in value.items()}
|
|
1193
|
+
if isinstance(value, (list, tuple)):
|
|
1194
|
+
return [_serialize_value(v) for v in value]
|
|
1195
|
+
return repr(value)
|
|
1196
|
+
|
|
1197
|
+
key = {f.name: _serialize_value(getattr(self, f.name)) for f in fields(self)}
|
|
1198
|
+
key["contract_multiplier"] = _serialize_value(self.contract_multiplier)
|
|
1199
|
+
return key
|
|
1200
|
+
|
|
1201
|
+
def __repr__(self) -> str:
|
|
1202
|
+
ko_barrier_str = (
|
|
1203
|
+
f"{self.barrier_config.ko_barrier[0]:.4f}..."
|
|
1204
|
+
if isinstance(self.barrier_config.ko_barrier, list)
|
|
1205
|
+
else f"{self.barrier_config.ko_barrier:.4f}"
|
|
1206
|
+
)
|
|
1207
|
+
ko_rate_str = (
|
|
1208
|
+
f"{self.barrier_config.ko_rate[0]:.4f}..."
|
|
1209
|
+
if isinstance(self.barrier_config.ko_rate, list)
|
|
1210
|
+
else f"{self.barrier_config.ko_rate:.4f}"
|
|
1211
|
+
)
|
|
1212
|
+
ki_barrier_str = "None"
|
|
1213
|
+
if self.barrier_config.ki_barrier is not None:
|
|
1214
|
+
ki_barrier_str = (
|
|
1215
|
+
f"{self.barrier_config.ki_barrier[0]:.4f}..."
|
|
1216
|
+
if isinstance(self.barrier_config.ki_barrier, list)
|
|
1217
|
+
else f"{self.barrier_config.ki_barrier:.4f}"
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
ko_obs_desc = (
|
|
1221
|
+
f"{self.barrier_config.ko_observation_type.name.lower()}-{self.num_ko_observations}obs"
|
|
1222
|
+
if self.num_ko_observations
|
|
1223
|
+
else self.barrier_config.ko_observation_type.name.lower()
|
|
1224
|
+
)
|
|
1225
|
+
ki_obs_desc = (
|
|
1226
|
+
f"{self.barrier_config.ki_observation_type.name.lower()}-{self.num_ki_observations}obs"
|
|
1227
|
+
if self.num_ki_observations
|
|
1228
|
+
else self.barrier_config.ki_observation_type.name.lower()
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
protection = self.payoff_config.protection_type.name.lower()
|
|
1232
|
+
pay_timing = self.accrual_config.coupon_pay_type.name.lower()
|
|
1233
|
+
principal_flag = "inclN" if self.payoff_config.include_principal else "exN"
|
|
1234
|
+
|
|
1235
|
+
return (
|
|
1236
|
+
f"SnowballOption("
|
|
1237
|
+
f"S0={self.initial_price:.4f}, K={self.strike:.4f}, "
|
|
1238
|
+
f"mult={self.contract_multiplier:.4f}, "
|
|
1239
|
+
f"KO={ko_barrier_str} [{ko_obs_desc}] @rate={ko_rate_str}, "
|
|
1240
|
+
f"KI={ki_barrier_str} [{ki_obs_desc}], "
|
|
1241
|
+
f"pay={pay_timing}, protection={protection}, {principal_flag})"
|
|
1242
|
+
)
|