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