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,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single sharkfin option product definition.
|
|
3
|
+
|
|
4
|
+
Single sharkfin options are capped single-barrier structures. A call sharkfin
|
|
5
|
+
uses an upper knock-out barrier; a put sharkfin uses a lower knock-out barrier.
|
|
6
|
+
If the barrier is hit, the product pays a fixed knock-out rebate. Otherwise it
|
|
7
|
+
pays a capped vanilla-style payoff plus any no-hit rebate.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from math import ceil
|
|
13
|
+
from typing import List, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from quantark.util.enum import (
|
|
16
|
+
ExerciseType,
|
|
17
|
+
ObservationAggregation,
|
|
18
|
+
ObservationFrequency,
|
|
19
|
+
ObservationType,
|
|
20
|
+
OptionType,
|
|
21
|
+
)
|
|
22
|
+
from quantark.util.exceptions import ValidationError
|
|
23
|
+
from quantark.util.numerical import is_close
|
|
24
|
+
|
|
25
|
+
from .base_equity_option import BaseEquityOption
|
|
26
|
+
from .observation_schedule import ObservationRecord, ObservationSchedule
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SingleSharkfinOption(BaseEquityOption):
|
|
31
|
+
"""
|
|
32
|
+
Single sharkfin option with one knock-out barrier.
|
|
33
|
+
|
|
34
|
+
Payoff states:
|
|
35
|
+
1. Barrier hit: knock_out_rebate
|
|
36
|
+
2. Barrier not hit:
|
|
37
|
+
- CALL: no_hit_rebate + participation_rate * max(min(S, B) - K, 0)
|
|
38
|
+
- PUT: no_hit_rebate + participation_rate * max(K - max(S, B), 0)
|
|
39
|
+
|
|
40
|
+
Monitoring styles:
|
|
41
|
+
- EXPIRY: barrier checked only at terminal spot
|
|
42
|
+
- DISCRETE: barrier checked on observation dates, including daily schedules
|
|
43
|
+
- CONTINUOUS: barrier checked continuously by the pricing engine/path logic
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
strike: Strike price
|
|
47
|
+
option_type: CALL for upper-barrier sharkfin, PUT for lower-barrier sharkfin
|
|
48
|
+
barrier: Knock-out barrier level
|
|
49
|
+
maturity: Time to maturity in years
|
|
50
|
+
participation_rate: Participation in the capped vanilla payoff
|
|
51
|
+
knock_out_rebate: Fixed payoff if the barrier is hit
|
|
52
|
+
no_hit_rebate: Fixed payoff added when the barrier is not hit
|
|
53
|
+
pay_at_hit: If True, pay knock-out rebate immediately on hit;
|
|
54
|
+
otherwise pay it at expiry
|
|
55
|
+
observation_type: EXPIRY, DISCRETE, or CONTINUOUS monitoring
|
|
56
|
+
observation_dates: Legacy discrete observation times in years
|
|
57
|
+
observation_schedule: Preferred discrete observation schedule
|
|
58
|
+
observation_frequency: Frequency used when generating a regular schedule
|
|
59
|
+
contract_multiplier: Underlying units represented by one contract
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
barrier: float = 0.0
|
|
63
|
+
participation_rate: float = 1.0
|
|
64
|
+
knock_out_rebate: float = 0.0
|
|
65
|
+
no_hit_rebate: float = 0.0
|
|
66
|
+
pay_at_hit: bool = False
|
|
67
|
+
observation_type: ObservationType = ObservationType.CONTINUOUS
|
|
68
|
+
observation_dates: Optional[List[float]] = None
|
|
69
|
+
observation_schedule: Optional[ObservationSchedule] = None
|
|
70
|
+
observation_frequency: ObservationFrequency = ObservationFrequency.CUSTOM
|
|
71
|
+
use_business_days_for_frequency: bool = True
|
|
72
|
+
business_days_in_year: float = 252.0
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
strike: float,
|
|
77
|
+
option_type: OptionType,
|
|
78
|
+
barrier: float,
|
|
79
|
+
maturity: Optional[float] = None,
|
|
80
|
+
exercise_date: Optional[datetime] = None,
|
|
81
|
+
settlement_date: Optional[datetime] = None,
|
|
82
|
+
participation_rate: float = 1.0,
|
|
83
|
+
knock_out_rebate: float = 0.0,
|
|
84
|
+
no_hit_rebate: float = 0.0,
|
|
85
|
+
pay_at_hit: bool = False,
|
|
86
|
+
observation_type: ObservationType = ObservationType.CONTINUOUS,
|
|
87
|
+
observation_dates: Optional[List[float]] = None,
|
|
88
|
+
observation_schedule: Optional[ObservationSchedule] = None,
|
|
89
|
+
observation_frequency: ObservationFrequency = ObservationFrequency.CUSTOM,
|
|
90
|
+
use_business_days_for_frequency: bool = True,
|
|
91
|
+
business_days_in_year: float = 252.0,
|
|
92
|
+
contract_multiplier: float = 1.0,
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Initialize a single sharkfin option.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
strike: Strike price.
|
|
99
|
+
option_type: CALL for upper sharkfin or PUT for lower sharkfin.
|
|
100
|
+
barrier: Knock-out barrier level.
|
|
101
|
+
maturity: Time to maturity in years (optional if exercise_date provided).
|
|
102
|
+
exercise_date: Expiration date (optional if maturity provided).
|
|
103
|
+
settlement_date: Settlement date.
|
|
104
|
+
participation_rate: Participation in the capped no-hit payoff.
|
|
105
|
+
knock_out_rebate: Fixed payoff if barrier is hit.
|
|
106
|
+
no_hit_rebate: Fixed payoff added if barrier is not hit.
|
|
107
|
+
pay_at_hit: If True, knock-out rebate is paid immediately on hit.
|
|
108
|
+
If False, it is paid at expiry.
|
|
109
|
+
observation_type: EXPIRY, DISCRETE, or CONTINUOUS monitoring.
|
|
110
|
+
observation_dates: Discrete observation times in year fractions.
|
|
111
|
+
observation_schedule: Preferred discrete observation schedule.
|
|
112
|
+
observation_frequency: Frequency for generated discrete schedules.
|
|
113
|
+
use_business_days_for_frequency: Use business-day spacing for frequency.
|
|
114
|
+
business_days_in_year: Business-day count for generated schedules.
|
|
115
|
+
contract_multiplier: Underlying units represented by one contract.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValidationError: If parameters are invalid.
|
|
119
|
+
"""
|
|
120
|
+
if maturity is None and exercise_date is None:
|
|
121
|
+
maturity = 0.0
|
|
122
|
+
elif maturity is None:
|
|
123
|
+
maturity = 0.0
|
|
124
|
+
|
|
125
|
+
self.barrier = barrier
|
|
126
|
+
self.participation_rate = participation_rate
|
|
127
|
+
self.knock_out_rebate = knock_out_rebate
|
|
128
|
+
self.no_hit_rebate = no_hit_rebate
|
|
129
|
+
self.pay_at_hit = pay_at_hit
|
|
130
|
+
self.observation_type = observation_type
|
|
131
|
+
self.observation_dates = observation_dates
|
|
132
|
+
self.observation_schedule = observation_schedule
|
|
133
|
+
self.observation_frequency = observation_frequency
|
|
134
|
+
self.use_business_days_for_frequency = use_business_days_for_frequency
|
|
135
|
+
self.business_days_in_year = business_days_in_year
|
|
136
|
+
|
|
137
|
+
super().__init__(
|
|
138
|
+
strike=strike,
|
|
139
|
+
option_type=option_type,
|
|
140
|
+
exercise_type=ExerciseType.EUROPEAN,
|
|
141
|
+
maturity=maturity,
|
|
142
|
+
exercise_date=exercise_date,
|
|
143
|
+
settlement_date=settlement_date,
|
|
144
|
+
contract_multiplier=contract_multiplier,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def validate(self) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Validate single sharkfin option parameters.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ValidationError: If any parameter is invalid.
|
|
153
|
+
"""
|
|
154
|
+
super().validate()
|
|
155
|
+
|
|
156
|
+
if self.barrier <= 0:
|
|
157
|
+
raise ValidationError(f"Barrier must be positive, got {self.barrier}")
|
|
158
|
+
if self.participation_rate < 0:
|
|
159
|
+
raise ValidationError(
|
|
160
|
+
f"Participation rate must be non-negative, got {self.participation_rate}"
|
|
161
|
+
)
|
|
162
|
+
if self.knock_out_rebate < 0:
|
|
163
|
+
raise ValidationError(
|
|
164
|
+
f"Knock-out rebate must be non-negative, got {self.knock_out_rebate}"
|
|
165
|
+
)
|
|
166
|
+
if self.no_hit_rebate < 0:
|
|
167
|
+
raise ValidationError(
|
|
168
|
+
f"No-hit rebate must be non-negative, got {self.no_hit_rebate}"
|
|
169
|
+
)
|
|
170
|
+
if not isinstance(self.pay_at_hit, bool):
|
|
171
|
+
raise ValidationError(f"pay_at_hit must be boolean, got {self.pay_at_hit}")
|
|
172
|
+
if not isinstance(self.observation_type, ObservationType):
|
|
173
|
+
raise ValidationError(f"Invalid observation type: {self.observation_type}")
|
|
174
|
+
if not isinstance(self.observation_frequency, ObservationFrequency):
|
|
175
|
+
raise ValidationError(
|
|
176
|
+
f"Invalid observation frequency: {self.observation_frequency}"
|
|
177
|
+
)
|
|
178
|
+
if self.business_days_in_year <= 0:
|
|
179
|
+
raise ValidationError(
|
|
180
|
+
f"business_days_in_year must be positive, got {self.business_days_in_year}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self._validate_barrier_orientation()
|
|
184
|
+
self._normalize_observation_schedule()
|
|
185
|
+
|
|
186
|
+
def _validate_barrier_orientation(self) -> None:
|
|
187
|
+
"""Validate the barrier is on the sharkfin side of the strike."""
|
|
188
|
+
if self.is_call() and self.barrier <= self.strike:
|
|
189
|
+
raise ValidationError(
|
|
190
|
+
"Call sharkfin requires an upper barrier above strike, "
|
|
191
|
+
f"got barrier={self.barrier}, strike={self.strike}"
|
|
192
|
+
)
|
|
193
|
+
if self.is_put() and self.barrier >= self.strike:
|
|
194
|
+
raise ValidationError(
|
|
195
|
+
"Put sharkfin requires a lower barrier below strike, "
|
|
196
|
+
f"got barrier={self.barrier}, strike={self.strike}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _normalize_observation_schedule(self) -> None:
|
|
200
|
+
"""Normalize discrete monitoring inputs into ObservationSchedule."""
|
|
201
|
+
if self.observation_type in (ObservationType.CONTINUOUS, ObservationType.EXPIRY):
|
|
202
|
+
if self.observation_schedule is not None:
|
|
203
|
+
raise ValidationError(
|
|
204
|
+
"ObservationSchedule requires DISCRETE observation_type."
|
|
205
|
+
)
|
|
206
|
+
self.observation_dates = self.observation_dates or []
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if self.observation_schedule is not None:
|
|
210
|
+
normalized_schedule = ObservationSchedule(
|
|
211
|
+
records=[
|
|
212
|
+
ObservationRecord(
|
|
213
|
+
observation_time=rec.observation_time,
|
|
214
|
+
observation_date=rec.observation_date,
|
|
215
|
+
barrier=rec.barrier if rec.barrier is not None else self.barrier,
|
|
216
|
+
payoff=(
|
|
217
|
+
rec.payoff
|
|
218
|
+
if rec.payoff is not None
|
|
219
|
+
else self.knock_out_rebate
|
|
220
|
+
),
|
|
221
|
+
return_rate=rec.return_rate,
|
|
222
|
+
is_rate_annualized=rec.is_rate_annualized,
|
|
223
|
+
initial_date=rec.initial_date,
|
|
224
|
+
settlement_date=rec.settlement_date,
|
|
225
|
+
maturity_date=rec.maturity_date,
|
|
226
|
+
day_count_convention=rec.day_count_convention,
|
|
227
|
+
tenor_end=rec.tenor_end,
|
|
228
|
+
day_count_fraction=rec.day_count_fraction,
|
|
229
|
+
)
|
|
230
|
+
for rec in self.observation_schedule.records
|
|
231
|
+
],
|
|
232
|
+
aggregation_mode=self.observation_schedule.aggregation_mode,
|
|
233
|
+
frequency=self.observation_schedule.frequency,
|
|
234
|
+
)
|
|
235
|
+
self._validate_observation_times(normalized_schedule.times)
|
|
236
|
+
normalized_schedule.validate(require_single=True)
|
|
237
|
+
self.observation_schedule = normalized_schedule
|
|
238
|
+
if self.observation_schedule.times:
|
|
239
|
+
self.observation_dates = self.observation_schedule.times
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
if self.observation_dates is None or len(self.observation_dates) == 0:
|
|
243
|
+
self.observation_dates = self._generate_observation_dates()
|
|
244
|
+
self._validate_observation_times(self.observation_dates)
|
|
245
|
+
|
|
246
|
+
self.observation_schedule = ObservationSchedule.from_legacy(
|
|
247
|
+
observation_dates=self.observation_dates,
|
|
248
|
+
default_barrier=self.barrier,
|
|
249
|
+
default_payoff=self.knock_out_rebate,
|
|
250
|
+
aggregation_mode=ObservationAggregation.STOP_FIRST_HIT,
|
|
251
|
+
frequency=self.observation_frequency,
|
|
252
|
+
)
|
|
253
|
+
self.observation_dates = self.observation_schedule.times
|
|
254
|
+
|
|
255
|
+
def _validate_observation_times(self, times: List[float]) -> None:
|
|
256
|
+
"""Validate numeric observation times when present."""
|
|
257
|
+
if any(t < 0 for t in times):
|
|
258
|
+
raise ValidationError("Observation dates must be non-negative.")
|
|
259
|
+
if times != sorted(times):
|
|
260
|
+
raise ValidationError("Observation dates must be sorted in ascending order.")
|
|
261
|
+
|
|
262
|
+
def _generate_observation_dates(self) -> List[float]:
|
|
263
|
+
"""
|
|
264
|
+
Generate regular discrete observation times from observation_frequency.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Observation times in years.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
ValidationError: If a schedule cannot be generated.
|
|
271
|
+
"""
|
|
272
|
+
if self.observation_frequency == ObservationFrequency.CUSTOM:
|
|
273
|
+
raise ValidationError(
|
|
274
|
+
"Observation dates required for discrete sharkfin monitoring "
|
|
275
|
+
"when observation_frequency is CUSTOM."
|
|
276
|
+
)
|
|
277
|
+
if self.maturity is None or self.maturity <= 0:
|
|
278
|
+
raise ValidationError(
|
|
279
|
+
"Positive maturity is required to generate discrete observations."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
days_in_year = (
|
|
283
|
+
self.business_days_in_year
|
|
284
|
+
if self.use_business_days_for_frequency
|
|
285
|
+
else 365.0
|
|
286
|
+
)
|
|
287
|
+
dt = self.observation_frequency.to_year_fraction(
|
|
288
|
+
use_business_days=self.use_business_days_for_frequency,
|
|
289
|
+
days_in_year=days_in_year,
|
|
290
|
+
)
|
|
291
|
+
if dt <= 0:
|
|
292
|
+
raise ValidationError(f"Observation frequency produced invalid dt={dt}")
|
|
293
|
+
|
|
294
|
+
count = max(1, int(ceil(self.maturity / dt)))
|
|
295
|
+
times = [min((idx + 1) * dt, self.maturity) for idx in range(count)]
|
|
296
|
+
if not is_close(times[-1], self.maturity):
|
|
297
|
+
times.append(self.maturity)
|
|
298
|
+
return times
|
|
299
|
+
|
|
300
|
+
def is_barrier_hit(self, spot: float) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Check whether a single observed spot hits the sharkfin barrier.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
spot: Observed underlying price.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
True if the barrier is hit.
|
|
309
|
+
"""
|
|
310
|
+
if spot < 0:
|
|
311
|
+
raise ValidationError(f"Spot price must be non-negative, got {spot}")
|
|
312
|
+
|
|
313
|
+
if self.is_call():
|
|
314
|
+
return spot >= self.barrier
|
|
315
|
+
return spot <= self.barrier
|
|
316
|
+
|
|
317
|
+
def has_barrier_hit(self, path: Sequence[float]) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Check whether any observed spot in a path hits the barrier.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
path: Sequence of observed underlying prices.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
True if any observation hits the barrier.
|
|
326
|
+
"""
|
|
327
|
+
if len(path) == 0:
|
|
328
|
+
raise ValidationError("Path must contain at least one observed spot.")
|
|
329
|
+
return any(self.is_barrier_hit(spot) for spot in path)
|
|
330
|
+
|
|
331
|
+
def get_no_hit_payoff(self, spot: float) -> float:
|
|
332
|
+
"""
|
|
333
|
+
Calculate the capped no-hit payoff.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
spot: Terminal underlying price.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Payoff assuming the barrier was not hit.
|
|
340
|
+
"""
|
|
341
|
+
if spot < 0:
|
|
342
|
+
raise ValidationError(f"Spot price must be non-negative, got {spot}")
|
|
343
|
+
|
|
344
|
+
if self.is_call():
|
|
345
|
+
capped_spot = min(spot, self.barrier)
|
|
346
|
+
intrinsic = max(capped_spot - self.strike, 0.0)
|
|
347
|
+
else:
|
|
348
|
+
capped_spot = max(spot, self.barrier)
|
|
349
|
+
intrinsic = max(self.strike - capped_spot, 0.0)
|
|
350
|
+
|
|
351
|
+
payoff = self.no_hit_rebate + self.participation_rate * intrinsic
|
|
352
|
+
return payoff * self.contract_multiplier
|
|
353
|
+
|
|
354
|
+
def get_barrier_payoff(self) -> float:
|
|
355
|
+
"""
|
|
356
|
+
Calculate payoff when the sharkfin barrier has been hit.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Knock-out rebate scaled by contract multiplier.
|
|
360
|
+
"""
|
|
361
|
+
return self.knock_out_rebate * self.contract_multiplier
|
|
362
|
+
|
|
363
|
+
def get_payoff(self, spot: float, barrier_hit: Optional[bool] = None) -> float:
|
|
364
|
+
"""
|
|
365
|
+
Calculate the terminal sharkfin payoff.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
spot: Terminal underlying price.
|
|
369
|
+
barrier_hit: Optional observed barrier state. When omitted, terminal
|
|
370
|
+
spot is used to infer a hit. Pricing engines for DISCRETE or
|
|
371
|
+
CONTINUOUS monitoring should pass the path-derived barrier state.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Sharkfin payoff scaled by contract multiplier.
|
|
375
|
+
"""
|
|
376
|
+
if spot < 0:
|
|
377
|
+
raise ValidationError(f"Spot price must be non-negative, got {spot}")
|
|
378
|
+
|
|
379
|
+
if barrier_hit is None:
|
|
380
|
+
barrier_hit = self.is_barrier_hit(spot)
|
|
381
|
+
|
|
382
|
+
if barrier_hit:
|
|
383
|
+
return self.get_barrier_payoff()
|
|
384
|
+
return self.get_no_hit_payoff(spot)
|
|
385
|
+
|
|
386
|
+
def get_observation_times(self) -> List[float]:
|
|
387
|
+
"""
|
|
388
|
+
Get discrete observation times.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Observation times in years. Empty for EXPIRY or CONTINUOUS monitoring.
|
|
392
|
+
"""
|
|
393
|
+
if self.observation_schedule is not None:
|
|
394
|
+
return self.observation_schedule.times
|
|
395
|
+
return self.observation_dates or []
|
|
396
|
+
|
|
397
|
+
def time_shift(self, time_bump: float, bumped_date: datetime, pricing_env) -> bool:
|
|
398
|
+
"""Shift observation schedule and maturity for theta bumping."""
|
|
399
|
+
schedule = getattr(self, "observation_schedule", None)
|
|
400
|
+
if schedule is not None:
|
|
401
|
+
if schedule.uses_dates():
|
|
402
|
+
pricing_env.valuation_date = bumped_date
|
|
403
|
+
bumped_schedule = schedule.time_shift(time_bump, bumped_date)
|
|
404
|
+
if bumped_schedule is None:
|
|
405
|
+
return True
|
|
406
|
+
self.observation_schedule = bumped_schedule
|
|
407
|
+
if bumped_schedule.uses_times():
|
|
408
|
+
self.observation_dates = bumped_schedule.times
|
|
409
|
+
|
|
410
|
+
if getattr(self, "exercise_date", None) is None and self.maturity is not None:
|
|
411
|
+
self.maturity -= time_bump
|
|
412
|
+
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
def __repr__(self) -> str:
|
|
416
|
+
return (
|
|
417
|
+
"SingleSharkfinOption("
|
|
418
|
+
f"{self.option_type}, K={self.strike:.2f}, "
|
|
419
|
+
f"B={self.barrier:.2f}, T={self.maturity:.4f})"
|
|
420
|
+
)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration classes for Snowball (autocallable) options.
|
|
3
|
+
|
|
4
|
+
These classes group related parameters to simplify the SnowballOption API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Optional, Union
|
|
10
|
+
|
|
11
|
+
from quantark.util.enum import (
|
|
12
|
+
ObservationType,
|
|
13
|
+
CouponPayType,
|
|
14
|
+
ProtectionType,
|
|
15
|
+
)
|
|
16
|
+
from .observation_schedule import ObservationSchedule
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class BarrierConfig:
|
|
21
|
+
"""
|
|
22
|
+
Configuration for knock-out (KO) and knock-in (KI) barriers.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
ko_barrier: Knock-out barrier level(s), up barrier by default
|
|
26
|
+
ko_rate: Knock-out return rate(s)
|
|
27
|
+
ko_observation_type: DISCRETE or CONTINUOUS monitoring for KO
|
|
28
|
+
ko_observation_dates: Year fractions for KO observations (legacy)
|
|
29
|
+
ko_observation_schedule: ObservationSchedule for KO (preferred)
|
|
30
|
+
ki_barrier: Optional knock-in barrier level(s), down barrier by default
|
|
31
|
+
ki_observation_type: DISCRETE or CONTINUOUS monitoring for KI
|
|
32
|
+
ki_observation_dates: Year fractions for KI observations (legacy)
|
|
33
|
+
ki_observation_schedule: ObservationSchedule for KI (preferred)
|
|
34
|
+
ki_continuous: If True, KI monitored continuously
|
|
35
|
+
disable_ko_after_ki: If True, disable KO after KI is triggered
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Knock-out barrier (required)
|
|
39
|
+
ko_barrier: Union[float, List[float]]
|
|
40
|
+
ko_rate: Union[float, List[float]]
|
|
41
|
+
ko_observation_type: ObservationType = ObservationType.DISCRETE
|
|
42
|
+
ko_observation_dates: Optional[List[float]] = None
|
|
43
|
+
ko_observation_schedule: Optional[ObservationSchedule] = None
|
|
44
|
+
|
|
45
|
+
# Knock-in barrier (optional)
|
|
46
|
+
ki_barrier: Optional[Union[float, List[float]]] = None
|
|
47
|
+
ki_observation_type: ObservationType = ObservationType.DISCRETE
|
|
48
|
+
ki_observation_dates: Optional[List[float]] = None
|
|
49
|
+
ki_observation_schedule: Optional[ObservationSchedule] = None
|
|
50
|
+
ki_continuous: bool = False
|
|
51
|
+
|
|
52
|
+
# Interaction
|
|
53
|
+
disable_ko_after_ki: bool = False
|
|
54
|
+
|
|
55
|
+
def __post_init__(self):
|
|
56
|
+
"""Validate configuration after initialization."""
|
|
57
|
+
# Validate ko_barrier is positive
|
|
58
|
+
self._validate_barrier_positive(self.ko_barrier, "ko_barrier")
|
|
59
|
+
|
|
60
|
+
# Validate ki_barrier is positive if provided
|
|
61
|
+
if self.ki_barrier is not None:
|
|
62
|
+
self._validate_barrier_positive(self.ki_barrier, "ki_barrier")
|
|
63
|
+
|
|
64
|
+
# Validate observation types are enums
|
|
65
|
+
if not isinstance(self.ko_observation_type, ObservationType):
|
|
66
|
+
raise ValueError(f"ko_observation_type must be ObservationType, got {type(self.ko_observation_type)}")
|
|
67
|
+
if not isinstance(self.ki_observation_type, ObservationType):
|
|
68
|
+
raise ValueError(f"ki_observation_type must be ObservationType, got {type(self.ki_observation_type)}")
|
|
69
|
+
|
|
70
|
+
def time_shift(self, time_bump: float, bumped_date, pricing_env) -> tuple["BarrierConfig", bool]:
|
|
71
|
+
"""
|
|
72
|
+
Shift KO/KI observation schedules and legacy dates for theta bumps.
|
|
73
|
+
|
|
74
|
+
Returns a new BarrierConfig and a flag indicating if all observations were dropped.
|
|
75
|
+
"""
|
|
76
|
+
dropped_all = False
|
|
77
|
+
|
|
78
|
+
ko_schedule = self.ko_observation_schedule
|
|
79
|
+
if ko_schedule is not None:
|
|
80
|
+
if ko_schedule.uses_dates():
|
|
81
|
+
pricing_env.valuation_date = bumped_date
|
|
82
|
+
ko_schedule = ko_schedule.time_shift(time_bump, bumped_date)
|
|
83
|
+
if ko_schedule is None:
|
|
84
|
+
dropped_all = True
|
|
85
|
+
|
|
86
|
+
ki_schedule = self.ki_observation_schedule
|
|
87
|
+
if ki_schedule is not None:
|
|
88
|
+
if ki_schedule.uses_dates():
|
|
89
|
+
pricing_env.valuation_date = bumped_date
|
|
90
|
+
ki_schedule = ki_schedule.time_shift(time_bump, bumped_date)
|
|
91
|
+
if ki_schedule is None:
|
|
92
|
+
dropped_all = True
|
|
93
|
+
|
|
94
|
+
ko_dates = self.ko_observation_dates
|
|
95
|
+
if ko_schedule is not None and ko_schedule.uses_times():
|
|
96
|
+
ko_dates = ko_schedule.times
|
|
97
|
+
elif ko_schedule is None and ko_dates:
|
|
98
|
+
ko_dates = [t - time_bump for t in ko_dates if t - time_bump > 0]
|
|
99
|
+
if not ko_dates:
|
|
100
|
+
dropped_all = True
|
|
101
|
+
|
|
102
|
+
ki_dates = self.ki_observation_dates
|
|
103
|
+
if ki_schedule is not None and ki_schedule.uses_times():
|
|
104
|
+
ki_dates = ki_schedule.times
|
|
105
|
+
elif ki_schedule is None and ki_dates:
|
|
106
|
+
ki_dates = [t - time_bump for t in ki_dates if t - time_bump > 0]
|
|
107
|
+
if not ki_dates:
|
|
108
|
+
dropped_all = True
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
replace(
|
|
112
|
+
self,
|
|
113
|
+
ko_observation_schedule=ko_schedule,
|
|
114
|
+
ki_observation_schedule=ki_schedule,
|
|
115
|
+
ko_observation_dates=ko_dates,
|
|
116
|
+
ki_observation_dates=ki_dates,
|
|
117
|
+
),
|
|
118
|
+
dropped_all,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _validate_barrier_positive(barrier: Union[float, List[float]], name: str) -> None:
|
|
123
|
+
"""Validate that barrier level(s) are positive."""
|
|
124
|
+
if isinstance(barrier, list):
|
|
125
|
+
if not barrier:
|
|
126
|
+
raise ValueError(f"{name} list cannot be empty")
|
|
127
|
+
for i, b in enumerate(barrier):
|
|
128
|
+
if not isinstance(b, (int, float)) or b <= 0:
|
|
129
|
+
raise ValueError(f"{name}[{i}] must be positive number, got {b}")
|
|
130
|
+
else:
|
|
131
|
+
if not isinstance(barrier, (int, float)) or barrier <= 0:
|
|
132
|
+
raise ValueError(f"{name} must be positive number, got {barrier}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class PayoffConfig:
|
|
137
|
+
"""
|
|
138
|
+
Configuration for payoff features (rebate, protection, participation).
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
rebate_rate: Fixed rebate rate for V0 maturity payoff
|
|
142
|
+
call_rebate_enabled: If True, use call-style rebate instead of fixed
|
|
143
|
+
call_strike: Strike for call rebate
|
|
144
|
+
call_participation_rate: Participation rate for call rebate
|
|
145
|
+
include_principal: Whether principal is part of payouts
|
|
146
|
+
participation_rate: Downside participation rate after KI
|
|
147
|
+
protection_type: NONE, PARTIAL, or FULL protection
|
|
148
|
+
protection_rate: Rate for partial protection floor
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# Rebate (V0 maturity payoff)
|
|
152
|
+
rebate_rate: float = 0.0
|
|
153
|
+
call_rebate_enabled: bool = False
|
|
154
|
+
call_strike: Optional[float] = None
|
|
155
|
+
call_participation_rate: float = 1.0
|
|
156
|
+
|
|
157
|
+
# Participation and protection (V1 maturity payoff)
|
|
158
|
+
include_principal: bool = True
|
|
159
|
+
participation_rate: float = 1.0
|
|
160
|
+
protection_type: ProtectionType = ProtectionType.NONE
|
|
161
|
+
protection_rate: float = 0.0
|
|
162
|
+
|
|
163
|
+
def __post_init__(self):
|
|
164
|
+
"""Validate configuration after initialization."""
|
|
165
|
+
# Validate participation rate is positive
|
|
166
|
+
if self.participation_rate <= 0:
|
|
167
|
+
raise ValueError(f"participation_rate must be positive, got {self.participation_rate}")
|
|
168
|
+
|
|
169
|
+
# Validate call rebate parameters if enabled
|
|
170
|
+
if self.call_rebate_enabled:
|
|
171
|
+
if self.call_strike is None:
|
|
172
|
+
raise ValueError("call_strike required when call_rebate_enabled is True")
|
|
173
|
+
if self.call_strike <= 0:
|
|
174
|
+
raise ValueError(f"call_strike must be positive, got {self.call_strike}")
|
|
175
|
+
if self.call_participation_rate <= 0:
|
|
176
|
+
raise ValueError(f"call_participation_rate must be positive, got {self.call_participation_rate}")
|
|
177
|
+
|
|
178
|
+
# Validate protection type is enum
|
|
179
|
+
if not isinstance(self.protection_type, ProtectionType):
|
|
180
|
+
raise ValueError(f"protection_type must be ProtectionType, got {type(self.protection_type)}")
|
|
181
|
+
|
|
182
|
+
# Validate protection rate for partial protection
|
|
183
|
+
if self.protection_type == ProtectionType.PARTIAL:
|
|
184
|
+
if not 0 <= self.protection_rate <= 1:
|
|
185
|
+
raise ValueError(f"protection_rate must be in [0, 1], got {self.protection_rate}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class AccrualConfig:
|
|
190
|
+
"""
|
|
191
|
+
Configuration for accrual and coupon payment settings.
|
|
192
|
+
|
|
193
|
+
Attributes:
|
|
194
|
+
coupon_pay_type: INSTANT (at KO date) or EXPIRY (discounted to maturity)
|
|
195
|
+
is_annualized: Backward-compatible flag for annualized accruals (default for all)
|
|
196
|
+
is_annualized_ko: If True, KO return accrues with year fraction
|
|
197
|
+
is_annualized_ki: If True, KI return accrues with year fraction
|
|
198
|
+
is_annualized_rebate: If True, rebate accrues with year fraction
|
|
199
|
+
accrual_dates: Calendar dates for annualized coupon calculation
|
|
200
|
+
accrual_factors: Optional externally supplied accrual factors by
|
|
201
|
+
KO/coupon observation
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
coupon_pay_type: CouponPayType = CouponPayType.INSTANT
|
|
205
|
+
is_annualized: bool = True
|
|
206
|
+
is_annualized_ko: Optional[bool] = None
|
|
207
|
+
is_annualized_ki: Optional[bool] = None
|
|
208
|
+
is_annualized_rebate: Optional[bool] = None
|
|
209
|
+
accrual_dates: Optional[List[datetime]] = None
|
|
210
|
+
accrual_factors: Optional[List[float]] = None
|
|
211
|
+
|
|
212
|
+
def __post_init__(self):
|
|
213
|
+
"""Validate configuration after initialization."""
|
|
214
|
+
# Validate coupon_pay_type is enum
|
|
215
|
+
if not isinstance(self.coupon_pay_type, CouponPayType):
|
|
216
|
+
raise ValueError(f"coupon_pay_type must be CouponPayType, got {type(self.coupon_pay_type)}")
|
|
217
|
+
if self.accrual_factors is not None:
|
|
218
|
+
if not isinstance(self.accrual_factors, list):
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"accrual_factors must be a list, got {type(self.accrual_factors)}"
|
|
221
|
+
)
|
|
222
|
+
for i, factor in enumerate(self.accrual_factors):
|
|
223
|
+
if isinstance(factor, bool) or not isinstance(factor, (int, float)):
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"accrual_factors[{i}] must be numeric, got {factor}"
|
|
226
|
+
)
|
|
227
|
+
if factor < 0:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"accrual_factors[{i}] must be non-negative, got {factor}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True)
|
|
233
|
+
class AirbagConfig:
|
|
234
|
+
"""
|
|
235
|
+
Configuration for airbag features.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
airbag_barrier: Barrier level for airbag protection.
|
|
239
|
+
If spot > airbag_barrier, principal is fully protected (subject to other terms).
|
|
240
|
+
If spot < airbag_barrier, investor participates in downside.
|
|
241
|
+
airbag_participation_rate: Participation rate when spot < airbag_barrier (default: 1.0).
|
|
242
|
+
airbag_strike: Strike price for airbag payoff calculation (optional).
|
|
243
|
+
If None, defaults to the product's strike.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
airbag_barrier: Optional[float] = None
|
|
247
|
+
airbag_participation_rate: float = 1.0
|
|
248
|
+
airbag_strike: Optional[float] = None
|
|
249
|
+
|
|
250
|
+
def __post_init__(self):
|
|
251
|
+
"""Validate configuration after initialization."""
|
|
252
|
+
if self.airbag_barrier is not None:
|
|
253
|
+
if not isinstance(self.airbag_barrier, (int, float)) or self.airbag_barrier <= 0:
|
|
254
|
+
raise ValueError(f"airbag_barrier must be a positive number, got {self.airbag_barrier}")
|
|
255
|
+
|
|
256
|
+
if self.airbag_participation_rate <= 0:
|
|
257
|
+
raise ValueError(f"airbag_participation_rate must be positive, got {self.airbag_participation_rate}")
|
|
258
|
+
|
|
259
|
+
if self.airbag_strike is not None:
|
|
260
|
+
if not isinstance(self.airbag_strike, (int, float)) or self.airbag_strike <= 0:
|
|
261
|
+
raise ValueError(f"airbag_strike must be a positive number, got {self.airbag_strike}")
|