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,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDE-based pricing engines for bonds.
|
|
3
|
+
"""
|
|
4
|
+
from quantark.asset.bond.engine.pde.convertible import (
|
|
5
|
+
ConvertibleBondPDEParams,
|
|
6
|
+
ConvertibleBondJumpDiffusionEngine,
|
|
7
|
+
ConvertibleBondTFEngine,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ConvertibleBondPDEParams",
|
|
12
|
+
"ConvertibleBondJumpDiffusionEngine",
|
|
13
|
+
"ConvertibleBondTFEngine",
|
|
14
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDE-based pricing engines for convertible bonds.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- ConvertibleBondPDEParams: Configuration for PDE-based pricing
|
|
6
|
+
- ConvertibleBondJumpDiffusionEngine: Bloomberg OVCV jump-diffusion model
|
|
7
|
+
- ConvertibleBondTFEngine: Tsiveriotis-Fernandes decomposition model
|
|
8
|
+
"""
|
|
9
|
+
from quantark.asset.bond.engine.pde.convertible.pde_params import ConvertibleBondPDEParams
|
|
10
|
+
from quantark.asset.bond.engine.pde.convertible.jump_diffusion_engine import (
|
|
11
|
+
ConvertibleBondJumpDiffusionEngine,
|
|
12
|
+
)
|
|
13
|
+
from quantark.asset.bond.engine.pde.convertible.tf_engine import (
|
|
14
|
+
ConvertibleBondTFEngine,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ConvertibleBondPDEParams",
|
|
19
|
+
"ConvertibleBondJumpDiffusionEngine",
|
|
20
|
+
"ConvertibleBondTFEngine",
|
|
21
|
+
]
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jump-diffusion PDE engine for convertible bond pricing.
|
|
3
|
+
|
|
4
|
+
Implements the Bloomberg OVCV model where the stock follows a jump-diffusion
|
|
5
|
+
process with credit risk modeled via hazard rate.
|
|
6
|
+
"""
|
|
7
|
+
import math
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy import sparse
|
|
14
|
+
from scipy.sparse.linalg import spsolve
|
|
15
|
+
|
|
16
|
+
from quantark.asset.bond.product.convertible.convertible_bond import ConvertibleBond
|
|
17
|
+
from quantark.asset.bond.engine.pde.convertible.pde_params import ConvertibleBondPDEParams
|
|
18
|
+
from quantark.priceenv import PricingEnvironment
|
|
19
|
+
from quantark.util.exceptions import ValidationError, PricingError
|
|
20
|
+
from quantark.util.numerical import Tolerance, safe_exp, safe_sqrt, safe_log, is_zero
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ConvertibleBondJumpDiffusionResult:
|
|
25
|
+
"""
|
|
26
|
+
Result container for jump-diffusion PDE engine pricing.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
price: Clean price of the convertible bond
|
|
30
|
+
dirty_price: Dirty price including accrued interest
|
|
31
|
+
delta: Price sensitivity to stock price
|
|
32
|
+
gamma: Second derivative of price with respect to stock
|
|
33
|
+
theta: Time decay (daily)
|
|
34
|
+
conversion_probability: Risk-neutral probability of eventual conversion
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
price: float
|
|
38
|
+
dirty_price: float
|
|
39
|
+
delta: float = 0.0
|
|
40
|
+
gamma: float = 0.0
|
|
41
|
+
theta: float = 0.0
|
|
42
|
+
conversion_probability: float = 0.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConvertibleBondJumpDiffusionEngine:
|
|
46
|
+
"""
|
|
47
|
+
Bloomberg OVCV jump-diffusion model for convertible bonds.
|
|
48
|
+
|
|
49
|
+
This engine implements a finite-difference PDE solver where the stock
|
|
50
|
+
follows:
|
|
51
|
+
dS = (r - q - lambda * eta) * S * dt + sigma * S * dW - eta * S * dN
|
|
52
|
+
|
|
53
|
+
where:
|
|
54
|
+
lambda = hazard rate (default intensity)
|
|
55
|
+
eta = stock price drop on default (stock_jump_on_default)
|
|
56
|
+
dN = Poisson process with intensity lambda
|
|
57
|
+
|
|
58
|
+
The convertible bond PDE becomes:
|
|
59
|
+
V_t + 0.5 * sigma^2 * S^2 * V_SS + (r - q - lambda*eta) * S * V_S
|
|
60
|
+
- r * V - lambda * (V - recovery) = 0
|
|
61
|
+
|
|
62
|
+
Boundary conditions:
|
|
63
|
+
- At S=0: V = recovery * face_value
|
|
64
|
+
- At S=S_max: V = conversion_ratio * S (pure equity)
|
|
65
|
+
- At maturity: V = max(face_value, conversion_ratio * S)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
pricing_env: PricingEnvironment,
|
|
71
|
+
params: Optional[ConvertibleBondPDEParams] = None,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize the jump-diffusion PDE engine.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
pricing_env: Pricing environment with market data
|
|
78
|
+
params: PDE configuration parameters (optional)
|
|
79
|
+
"""
|
|
80
|
+
if pricing_env is None:
|
|
81
|
+
raise ValidationError("Pricing environment is required")
|
|
82
|
+
|
|
83
|
+
self.pricing_env = pricing_env
|
|
84
|
+
self.params = params if params is not None else ConvertibleBondPDEParams()
|
|
85
|
+
|
|
86
|
+
def price(self, bond: ConvertibleBond) -> float:
|
|
87
|
+
"""
|
|
88
|
+
Calculate the clean price of the convertible bond.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
bond: Convertible bond to price
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Clean price of the bond
|
|
95
|
+
"""
|
|
96
|
+
result = self.price_with_details(bond)
|
|
97
|
+
return result.price
|
|
98
|
+
|
|
99
|
+
def price_with_details(
|
|
100
|
+
self, bond: ConvertibleBond
|
|
101
|
+
) -> ConvertibleBondJumpDiffusionResult:
|
|
102
|
+
"""
|
|
103
|
+
Calculate price with detailed results including Greeks.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
bond: Convertible bond to price
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
ConvertibleBondJumpDiffusionResult with full pricing details
|
|
110
|
+
"""
|
|
111
|
+
valuation_date = self.pricing_env.valuation_date
|
|
112
|
+
|
|
113
|
+
# Validate inputs
|
|
114
|
+
if bond.is_expired(valuation_date):
|
|
115
|
+
raise PricingError("Cannot price expired bond")
|
|
116
|
+
|
|
117
|
+
# Get market data
|
|
118
|
+
spot = self.pricing_env.spot
|
|
119
|
+
T = bond.time_to_maturity(valuation_date)
|
|
120
|
+
|
|
121
|
+
# Credit parameters from bond
|
|
122
|
+
lambda_h = bond.hazard_rate # Hazard rate
|
|
123
|
+
eta = bond.stock_jump_on_default # Stock drop on default
|
|
124
|
+
recovery = bond.recovery_rate
|
|
125
|
+
|
|
126
|
+
# Dividend yield
|
|
127
|
+
q = bond.continuous_dividend_yield
|
|
128
|
+
|
|
129
|
+
# Build grid
|
|
130
|
+
S_min = spot * self.params.min_stock_multiple
|
|
131
|
+
S_max = spot * self.params.max_stock_multiple
|
|
132
|
+
N_s = self.params.num_space_steps
|
|
133
|
+
N_t = self.params.num_time_steps
|
|
134
|
+
dt = T / N_t
|
|
135
|
+
|
|
136
|
+
# Use log-space grid for better accuracy
|
|
137
|
+
log_S_min = safe_log(S_min)
|
|
138
|
+
log_S_max = safe_log(S_max)
|
|
139
|
+
log_S = np.linspace(log_S_min, log_S_max, N_s + 1)
|
|
140
|
+
S = np.exp(log_S)
|
|
141
|
+
dS = S[1:] - S[:-1] # Variable spacing
|
|
142
|
+
|
|
143
|
+
# Initialize solution
|
|
144
|
+
V = self._terminal_condition(bond, S)
|
|
145
|
+
P = self._terminal_conversion_probability(bond, S)
|
|
146
|
+
|
|
147
|
+
# Get coupon schedule
|
|
148
|
+
coupon_schedule = self._build_coupon_schedule(bond, valuation_date)
|
|
149
|
+
|
|
150
|
+
# Time stepping - backward from T to 0
|
|
151
|
+
# All times are measured in years from valuation date
|
|
152
|
+
for n in range(N_t - 1, -1, -1):
|
|
153
|
+
t = n * dt # Current time (years from valuation)
|
|
154
|
+
t_next = t + dt # End of this step (closer to maturity)
|
|
155
|
+
node_date = valuation_date + timedelta(days=int(t * 365))
|
|
156
|
+
|
|
157
|
+
# Use Rannacher smoothing for first few steps
|
|
158
|
+
use_implicit = n >= N_t - self.params.rannacher_steps
|
|
159
|
+
|
|
160
|
+
# Query time-local forward rate for this step
|
|
161
|
+
r_local = self.pricing_env.rate_curve.get_forward_rate(t, t_next)
|
|
162
|
+
|
|
163
|
+
# Query time-local effective volatility for this step
|
|
164
|
+
vol_local = self.pricing_env.get_step_volatility(spot, t, t_next)
|
|
165
|
+
|
|
166
|
+
# Apply coupon payments (jump condition) to the known later-time state
|
|
167
|
+
coupon_amount = 0.0
|
|
168
|
+
for ct, ca in coupon_schedule:
|
|
169
|
+
if t < ct <= t_next:
|
|
170
|
+
coupon_amount += ca
|
|
171
|
+
if coupon_amount > 0.0:
|
|
172
|
+
V = V + coupon_amount
|
|
173
|
+
|
|
174
|
+
# Build discretization matrices with time-local parameters
|
|
175
|
+
A, b = self._build_matrices(
|
|
176
|
+
S, V, r_local, q, vol_local, lambda_h, eta, recovery, bond, dt, use_implicit
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Solve system
|
|
180
|
+
if use_implicit or self.params.scheme == "implicit_euler":
|
|
181
|
+
# Implicit: solve A * V_new = b
|
|
182
|
+
V = spsolve(A.tocsr(), b)
|
|
183
|
+
else:
|
|
184
|
+
# Crank-Nicolson or explicit
|
|
185
|
+
V = spsolve(A.tocsr(), b)
|
|
186
|
+
|
|
187
|
+
conversion_possible = bond.conversion_end_date >= node_date
|
|
188
|
+
p_upper_boundary = 1.0 if conversion_possible else 0.0
|
|
189
|
+
A_p, b_p = self._build_probability_matrices(
|
|
190
|
+
S,
|
|
191
|
+
P,
|
|
192
|
+
r_local,
|
|
193
|
+
q,
|
|
194
|
+
vol_local,
|
|
195
|
+
lambda_h,
|
|
196
|
+
eta,
|
|
197
|
+
dt,
|
|
198
|
+
use_implicit,
|
|
199
|
+
p_upper_boundary=p_upper_boundary,
|
|
200
|
+
)
|
|
201
|
+
P = spsolve(A_p.tocsr(), b_p)
|
|
202
|
+
P = np.clip(P, 0.0, 1.0)
|
|
203
|
+
|
|
204
|
+
# Apply early exercise constraints to both value and probability
|
|
205
|
+
V, P = self._apply_constraints(bond, S, V, node_date, P)
|
|
206
|
+
P = np.clip(P, 0.0, 1.0)
|
|
207
|
+
|
|
208
|
+
# Interpolate to get value at spot
|
|
209
|
+
spot_idx = np.searchsorted(S, spot)
|
|
210
|
+
if spot_idx == 0:
|
|
211
|
+
dirty_price = V[0]
|
|
212
|
+
conv_prob = P[0]
|
|
213
|
+
elif spot_idx >= len(S):
|
|
214
|
+
dirty_price = V[-1]
|
|
215
|
+
conv_prob = P[-1]
|
|
216
|
+
else:
|
|
217
|
+
# Linear interpolation
|
|
218
|
+
w = (spot - S[spot_idx - 1]) / (S[spot_idx] - S[spot_idx - 1])
|
|
219
|
+
dirty_price = (1 - w) * V[spot_idx - 1] + w * V[spot_idx]
|
|
220
|
+
conv_prob = (1 - w) * P[spot_idx - 1] + w * P[spot_idx]
|
|
221
|
+
conv_prob = float(np.clip(conv_prob, 0.0, 1.0))
|
|
222
|
+
|
|
223
|
+
# Calculate Greeks
|
|
224
|
+
delta, gamma = self._calculate_greeks(S, V, spot, spot_idx)
|
|
225
|
+
theta = self._calculate_theta(bond, dirty_price)
|
|
226
|
+
|
|
227
|
+
# Calculate accrued interest
|
|
228
|
+
accrued = bond.calculate_accrued_interest(valuation_date)
|
|
229
|
+
clean_price = dirty_price - accrued
|
|
230
|
+
|
|
231
|
+
return ConvertibleBondJumpDiffusionResult(
|
|
232
|
+
price=clean_price,
|
|
233
|
+
dirty_price=dirty_price,
|
|
234
|
+
delta=delta,
|
|
235
|
+
gamma=gamma,
|
|
236
|
+
theta=theta,
|
|
237
|
+
conversion_probability=conv_prob,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _terminal_condition(
|
|
241
|
+
self, bond: ConvertibleBond, S: np.ndarray
|
|
242
|
+
) -> np.ndarray:
|
|
243
|
+
"""
|
|
244
|
+
Compute terminal condition at maturity.
|
|
245
|
+
|
|
246
|
+
At maturity: V = max(face_value, conversion_ratio * S)
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
bond: Convertible bond
|
|
250
|
+
S: Stock price grid
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Array of terminal values
|
|
254
|
+
"""
|
|
255
|
+
face_value = bond.face_value
|
|
256
|
+
conversion_value = bond.conversion_ratio * S
|
|
257
|
+
if bond.is_convertible_at(bond.maturity_date):
|
|
258
|
+
return np.maximum(face_value, conversion_value)
|
|
259
|
+
return face_value * np.ones_like(S)
|
|
260
|
+
|
|
261
|
+
def _terminal_conversion_probability(
|
|
262
|
+
self, bond: ConvertibleBond, S: np.ndarray
|
|
263
|
+
) -> np.ndarray:
|
|
264
|
+
"""
|
|
265
|
+
Terminal condition for the eventual conversion probability.
|
|
266
|
+
|
|
267
|
+
At maturity: P = 1 if conversion is optimal and allowed, else 0.
|
|
268
|
+
"""
|
|
269
|
+
if not bond.is_convertible_at(bond.maturity_date):
|
|
270
|
+
return np.zeros_like(S)
|
|
271
|
+
conversion_value = bond.conversion_ratio * S
|
|
272
|
+
return (conversion_value > bond.face_value).astype(float)
|
|
273
|
+
|
|
274
|
+
def _build_matrices(
|
|
275
|
+
self,
|
|
276
|
+
S: np.ndarray,
|
|
277
|
+
V: np.ndarray,
|
|
278
|
+
r: float,
|
|
279
|
+
q: float,
|
|
280
|
+
vol: float,
|
|
281
|
+
lambda_h: float,
|
|
282
|
+
eta: float,
|
|
283
|
+
recovery: float,
|
|
284
|
+
bond: ConvertibleBond,
|
|
285
|
+
dt: float,
|
|
286
|
+
use_implicit: bool,
|
|
287
|
+
) -> Tuple[sparse.csr_matrix, np.ndarray]:
|
|
288
|
+
"""
|
|
289
|
+
Build finite difference matrices for the PDE.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
S: Stock price grid
|
|
293
|
+
V: Current solution
|
|
294
|
+
r, q: Rate and dividend yield
|
|
295
|
+
vol: Volatility
|
|
296
|
+
lambda_h: Hazard rate
|
|
297
|
+
eta: Stock jump on default
|
|
298
|
+
recovery: Recovery rate
|
|
299
|
+
bond: Convertible bond
|
|
300
|
+
dt: Time step
|
|
301
|
+
use_implicit: Whether to use fully implicit scheme
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Tuple of (A matrix, b vector) for A*V_new = b
|
|
305
|
+
"""
|
|
306
|
+
N = len(S)
|
|
307
|
+
recovery_value = recovery * bond.face_value
|
|
308
|
+
|
|
309
|
+
# Coefficient for drift term (adjusted for default jump)
|
|
310
|
+
drift = r - q - lambda_h * eta
|
|
311
|
+
|
|
312
|
+
# Build tridiagonal matrix coefficients
|
|
313
|
+
# Using central differences for interior points
|
|
314
|
+
diag = np.zeros(N)
|
|
315
|
+
lower = np.zeros(N - 1)
|
|
316
|
+
upper = np.zeros(N - 1)
|
|
317
|
+
|
|
318
|
+
for i in range(1, N - 1):
|
|
319
|
+
h_minus = S[i] - S[i - 1]
|
|
320
|
+
h_plus = S[i + 1] - S[i]
|
|
321
|
+
h = 0.5 * (h_minus + h_plus)
|
|
322
|
+
|
|
323
|
+
# Diffusion coefficient: 0.5 * sigma^2 * S^2
|
|
324
|
+
D = 0.5 * vol * vol * S[i] * S[i]
|
|
325
|
+
|
|
326
|
+
# Convection coefficient: drift * S
|
|
327
|
+
C = drift * S[i]
|
|
328
|
+
|
|
329
|
+
# Reaction coefficient: -(r + lambda)
|
|
330
|
+
R = -(r + lambda_h)
|
|
331
|
+
|
|
332
|
+
# Central difference approximation
|
|
333
|
+
lower[i - 1] = D / (h_minus * h) - C / (2 * h)
|
|
334
|
+
diag[i] = -2 * D / (h_minus * h_plus) + R
|
|
335
|
+
upper[i] = D / (h_plus * h) + C / (2 * h)
|
|
336
|
+
|
|
337
|
+
# Boundary conditions
|
|
338
|
+
# At S=0: V = recovery_value (Dirichlet)
|
|
339
|
+
diag[0] = 1.0
|
|
340
|
+
upper[0] = 0.0
|
|
341
|
+
|
|
342
|
+
# At S_max: V = conversion_ratio * S (Dirichlet for deep ITM)
|
|
343
|
+
diag[-1] = 1.0
|
|
344
|
+
lower[-1] = 0.0
|
|
345
|
+
|
|
346
|
+
# Build matrices based on scheme
|
|
347
|
+
if use_implicit:
|
|
348
|
+
# Fully implicit: (I - dt*A) * V_new = V + dt * source
|
|
349
|
+
theta_scheme = 1.0
|
|
350
|
+
elif self.params.scheme == "explicit_euler":
|
|
351
|
+
theta_scheme = 0.0
|
|
352
|
+
else: # crank_nicolson
|
|
353
|
+
theta_scheme = 0.5
|
|
354
|
+
|
|
355
|
+
# A_new = I - theta * dt * L
|
|
356
|
+
# A_old = I + (1 - theta) * dt * L
|
|
357
|
+
# A_new * V_new = A_old * V + dt * source
|
|
358
|
+
|
|
359
|
+
L = sparse.diags(
|
|
360
|
+
[lower, diag, upper], [-1, 0, 1], shape=(N, N), format="csr"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
I = sparse.eye(N, format="csr")
|
|
364
|
+
|
|
365
|
+
A = I - theta_scheme * dt * L
|
|
366
|
+
|
|
367
|
+
# Right-hand side
|
|
368
|
+
if theta_scheme < 1.0:
|
|
369
|
+
A_old = I + (1 - theta_scheme) * dt * L
|
|
370
|
+
b = A_old @ V
|
|
371
|
+
else:
|
|
372
|
+
b = V.copy()
|
|
373
|
+
|
|
374
|
+
# Add jump term contribution (recovery on default)
|
|
375
|
+
b[1:-1] += dt * lambda_h * recovery_value
|
|
376
|
+
|
|
377
|
+
# Apply boundary conditions
|
|
378
|
+
b[0] = recovery_value
|
|
379
|
+
b[-1] = bond.conversion_ratio * S[-1]
|
|
380
|
+
|
|
381
|
+
return A, b
|
|
382
|
+
|
|
383
|
+
def _build_probability_matrices(
|
|
384
|
+
self,
|
|
385
|
+
S: np.ndarray,
|
|
386
|
+
P: np.ndarray,
|
|
387
|
+
r: float,
|
|
388
|
+
q: float,
|
|
389
|
+
vol: float,
|
|
390
|
+
lambda_h: float,
|
|
391
|
+
eta: float,
|
|
392
|
+
dt: float,
|
|
393
|
+
use_implicit: bool,
|
|
394
|
+
p_upper_boundary: float,
|
|
395
|
+
) -> Tuple[sparse.csr_matrix, np.ndarray]:
|
|
396
|
+
"""
|
|
397
|
+
Build finite difference matrices for the eventual conversion probability.
|
|
398
|
+
|
|
399
|
+
The probability PDE incorporates default as a killing term (-lambda * P)
|
|
400
|
+
and uses Dirichlet boundaries at the grid endpoints.
|
|
401
|
+
"""
|
|
402
|
+
N = len(S)
|
|
403
|
+
|
|
404
|
+
drift = r - q - lambda_h * eta
|
|
405
|
+
|
|
406
|
+
diag = np.zeros(N)
|
|
407
|
+
lower = np.zeros(N - 1)
|
|
408
|
+
upper = np.zeros(N - 1)
|
|
409
|
+
|
|
410
|
+
for i in range(1, N - 1):
|
|
411
|
+
h_minus = S[i] - S[i - 1]
|
|
412
|
+
h_plus = S[i + 1] - S[i]
|
|
413
|
+
h = 0.5 * (h_minus + h_plus)
|
|
414
|
+
|
|
415
|
+
D = 0.5 * vol * vol * S[i] * S[i]
|
|
416
|
+
C = drift * S[i]
|
|
417
|
+
R = -lambda_h
|
|
418
|
+
|
|
419
|
+
lower[i - 1] = D / (h_minus * h) - C / (2 * h)
|
|
420
|
+
diag[i] = -2 * D / (h_minus * h_plus) + R
|
|
421
|
+
upper[i] = D / (h_plus * h) + C / (2 * h)
|
|
422
|
+
|
|
423
|
+
# Dirichlet boundaries: enforce via zero operator rows
|
|
424
|
+
diag[0] = 0.0
|
|
425
|
+
diag[-1] = 0.0
|
|
426
|
+
upper[0] = 0.0
|
|
427
|
+
lower[-1] = 0.0
|
|
428
|
+
|
|
429
|
+
if use_implicit:
|
|
430
|
+
theta_scheme = 1.0
|
|
431
|
+
elif self.params.scheme == "explicit_euler":
|
|
432
|
+
theta_scheme = 0.0
|
|
433
|
+
else: # crank_nicolson
|
|
434
|
+
theta_scheme = 0.5
|
|
435
|
+
|
|
436
|
+
L = sparse.diags(
|
|
437
|
+
[lower, diag, upper], [-1, 0, 1], shape=(N, N), format="csr"
|
|
438
|
+
)
|
|
439
|
+
I = sparse.eye(N, format="csr")
|
|
440
|
+
|
|
441
|
+
A = I - theta_scheme * dt * L
|
|
442
|
+
|
|
443
|
+
if theta_scheme < 1.0:
|
|
444
|
+
A_old = I + (1 - theta_scheme) * dt * L
|
|
445
|
+
b = A_old @ P
|
|
446
|
+
else:
|
|
447
|
+
b = P.copy()
|
|
448
|
+
|
|
449
|
+
b[0] = 0.0
|
|
450
|
+
b[-1] = p_upper_boundary
|
|
451
|
+
|
|
452
|
+
return A, b
|
|
453
|
+
|
|
454
|
+
def _apply_constraints(
|
|
455
|
+
self,
|
|
456
|
+
bond: ConvertibleBond,
|
|
457
|
+
S: np.ndarray,
|
|
458
|
+
V: np.ndarray,
|
|
459
|
+
node_date: datetime,
|
|
460
|
+
P: np.ndarray,
|
|
461
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
462
|
+
"""
|
|
463
|
+
Apply early exercise constraints (conversion, call, put).
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
bond: Convertible bond
|
|
467
|
+
S: Stock price grid
|
|
468
|
+
V: Current values
|
|
469
|
+
node_date: Current date in backward induction
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Tuple of (updated values, updated conversion probability) with
|
|
473
|
+
constraints applied
|
|
474
|
+
"""
|
|
475
|
+
V_new = V.copy()
|
|
476
|
+
P_new = P.copy()
|
|
477
|
+
|
|
478
|
+
# Conversion constraint (holder's right)
|
|
479
|
+
if bond.is_convertible_at(node_date):
|
|
480
|
+
conversion_value = bond.conversion_ratio * S
|
|
481
|
+
convert_mask = conversion_value >= (V_new - Tolerance.PRECISION)
|
|
482
|
+
V_new = np.maximum(V_new, conversion_value)
|
|
483
|
+
if np.any(convert_mask):
|
|
484
|
+
P_new[convert_mask] = 1.0
|
|
485
|
+
|
|
486
|
+
# Call constraint (issuer's right)
|
|
487
|
+
call_price = bond.get_call_price_at(node_date)
|
|
488
|
+
if call_price is not None:
|
|
489
|
+
for i, stock in enumerate(S):
|
|
490
|
+
if bond.is_callable_at(node_date, stock):
|
|
491
|
+
# Issuer calls if V > call_price
|
|
492
|
+
# Holder chooses max(conversion, call_price)
|
|
493
|
+
if V_new[i] > call_price:
|
|
494
|
+
conversion_value = bond.conversion_ratio * stock
|
|
495
|
+
if bond.is_convertible_at(node_date):
|
|
496
|
+
V_new[i] = max(conversion_value, call_price)
|
|
497
|
+
if conversion_value > call_price:
|
|
498
|
+
P_new[i] = 1.0
|
|
499
|
+
else:
|
|
500
|
+
P_new[i] = 0.0
|
|
501
|
+
else:
|
|
502
|
+
V_new[i] = call_price
|
|
503
|
+
P_new[i] = 0.0
|
|
504
|
+
|
|
505
|
+
# Put constraint (holder's right)
|
|
506
|
+
put_price = bond.get_put_price_at(node_date)
|
|
507
|
+
if put_price is not None:
|
|
508
|
+
put_mask = put_price >= (V_new - Tolerance.PRECISION)
|
|
509
|
+
V_new = np.maximum(V_new, put_price)
|
|
510
|
+
if np.any(put_mask):
|
|
511
|
+
P_new[put_mask] = 0.0
|
|
512
|
+
|
|
513
|
+
return V_new, P_new
|
|
514
|
+
|
|
515
|
+
def _build_coupon_schedule(
|
|
516
|
+
self, bond: ConvertibleBond, valuation_date: datetime
|
|
517
|
+
) -> list:
|
|
518
|
+
"""
|
|
519
|
+
Build list of (time, amount) tuples for coupon payments.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
bond: Convertible bond
|
|
523
|
+
valuation_date: Valuation date
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
List of (time_to_payment, coupon_amount) tuples
|
|
527
|
+
"""
|
|
528
|
+
T = bond.time_to_maturity(valuation_date)
|
|
529
|
+
schedule = []
|
|
530
|
+
|
|
531
|
+
for cf in bond.get_all_cashflows():
|
|
532
|
+
cf_time = (cf.payment_date - valuation_date).days / 365.0
|
|
533
|
+
if 0 < cf_time <= T:
|
|
534
|
+
# Extract just the coupon (not principal)
|
|
535
|
+
coupon_amount = cf.amount
|
|
536
|
+
if cf.payment_date >= bond.maturity_date:
|
|
537
|
+
coupon_amount -= bond.face_value
|
|
538
|
+
if coupon_amount > 0:
|
|
539
|
+
schedule.append((cf_time, coupon_amount))
|
|
540
|
+
|
|
541
|
+
return schedule
|
|
542
|
+
|
|
543
|
+
def _calculate_greeks(
|
|
544
|
+
self,
|
|
545
|
+
S: np.ndarray,
|
|
546
|
+
V: np.ndarray,
|
|
547
|
+
spot: float,
|
|
548
|
+
spot_idx: int,
|
|
549
|
+
) -> Tuple[float, float]:
|
|
550
|
+
"""
|
|
551
|
+
Calculate delta and gamma from the PDE grid.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
S: Stock price grid
|
|
555
|
+
V: Solution values
|
|
556
|
+
spot: Current spot price
|
|
557
|
+
spot_idx: Index of spot in grid
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Tuple of (delta, gamma)
|
|
561
|
+
"""
|
|
562
|
+
if spot_idx <= 1 or spot_idx >= len(S) - 1:
|
|
563
|
+
return 0.0, 0.0
|
|
564
|
+
|
|
565
|
+
# Use central differences
|
|
566
|
+
i = spot_idx
|
|
567
|
+
h_minus = S[i] - S[i - 1]
|
|
568
|
+
h_plus = S[i + 1] - S[i]
|
|
569
|
+
|
|
570
|
+
# Delta: dV/dS
|
|
571
|
+
delta = (V[i + 1] - V[i - 1]) / (h_plus + h_minus)
|
|
572
|
+
|
|
573
|
+
# Gamma: d2V/dS2
|
|
574
|
+
gamma = 2.0 * (
|
|
575
|
+
V[i + 1] / (h_plus * (h_plus + h_minus))
|
|
576
|
+
- V[i] / (h_plus * h_minus)
|
|
577
|
+
+ V[i - 1] / (h_minus * (h_plus + h_minus))
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
return delta, gamma
|
|
581
|
+
|
|
582
|
+
def _calculate_theta(
|
|
583
|
+
self, bond: ConvertibleBond, current_price: float
|
|
584
|
+
) -> float:
|
|
585
|
+
"""
|
|
586
|
+
Calculate theta (time decay) by finite difference.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
bond: Convertible bond
|
|
590
|
+
current_price: Current dirty price
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
Daily theta
|
|
594
|
+
"""
|
|
595
|
+
# Theta calculation would require pricing at t+dt
|
|
596
|
+
# For now, return 0 (could be implemented with bump-and-reprice)
|
|
597
|
+
return 0.0
|
|
598
|
+
|
|
599
|
+
def __repr__(self):
|
|
600
|
+
return (
|
|
601
|
+
f"ConvertibleBondJumpDiffusionEngine("
|
|
602
|
+
f"scheme={self.params.scheme})"
|
|
603
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration parameters for PDE-based convertible bond pricing engines.
|
|
3
|
+
"""
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from quantark.util.enum.engine_enums import PDEMethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ConvertibleBondPDEParams:
|
|
10
|
+
"""
|
|
11
|
+
Configuration parameters for PDE-based convertible bond pricing.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
num_space_steps: Number of grid points in stock price dimension (default: 200)
|
|
15
|
+
num_time_steps: Number of time steps (default: 500)
|
|
16
|
+
scheme: PDE scheme (crank_nicolson, implicit_euler, explicit_euler)
|
|
17
|
+
min_stock_multiple: Minimum stock price as multiple of current (default: 0.01)
|
|
18
|
+
max_stock_multiple: Maximum stock price as multiple of current (default: 5.0)
|
|
19
|
+
rannacher_steps: Number of initial Rannacher smoothing steps (default: 4)
|
|
20
|
+
bump_size: Bump size for finite difference Greeks (default: 0.01)
|
|
21
|
+
tolerance: Convergence tolerance for iterative schemes (default: 1e-8)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
num_space_steps: int = 200
|
|
25
|
+
num_time_steps: int = 500
|
|
26
|
+
scheme: str = "crank_nicolson"
|
|
27
|
+
min_stock_multiple: float = 0.01
|
|
28
|
+
max_stock_multiple: float = 5.0
|
|
29
|
+
rannacher_steps: int = 4
|
|
30
|
+
bump_size: float = 0.01
|
|
31
|
+
tolerance: float = 1e-8
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
"""Validate parameters."""
|
|
35
|
+
if self.num_space_steps < 10:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"num_space_steps must be >= 10, got {self.num_space_steps}"
|
|
38
|
+
)
|
|
39
|
+
if self.num_time_steps < 10:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"num_time_steps must be >= 10, got {self.num_time_steps}"
|
|
42
|
+
)
|
|
43
|
+
valid_schemes = ["crank_nicolson", "implicit_euler", "explicit_euler"]
|
|
44
|
+
if self.scheme not in valid_schemes:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"scheme must be one of {valid_schemes}, got {self.scheme}"
|
|
47
|
+
)
|
|
48
|
+
if self.min_stock_multiple <= 0:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"min_stock_multiple must be positive, got {self.min_stock_multiple}"
|
|
51
|
+
)
|
|
52
|
+
if self.max_stock_multiple <= 1:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"max_stock_multiple must be > 1, got {self.max_stock_multiple}"
|
|
55
|
+
)
|
|
56
|
+
if self.rannacher_steps < 0:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"rannacher_steps must be >= 0, got {self.rannacher_steps}"
|
|
59
|
+
)
|