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,913 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Discount-based pricing engine for Floating Rate Notes (FRNs).
|
|
3
|
+
|
|
4
|
+
Provides pricing, yield calculations (Discount Margin and Simple Margin),
|
|
5
|
+
and risk metrics for floating rate bonds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
from quantark.asset.bond.product.couponbond.frn import FloatingRateBond
|
|
14
|
+
from quantark.asset.bond.schedule.cashflow import FloatingCashFlow
|
|
15
|
+
from quantark.priceenv import PricingEnvironment
|
|
16
|
+
from quantark.param.rrf import RateCurve
|
|
17
|
+
from quantark.util.exceptions import ValidationError, MarketDataError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FRNPricingResults:
|
|
22
|
+
"""
|
|
23
|
+
Results container for FRN pricing.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
dirty_price: Price including accrued interest
|
|
27
|
+
clean_price: Price excluding accrued interest
|
|
28
|
+
accrued_interest: Accrued interest amount
|
|
29
|
+
discount_margin: Spread over forward that equates price to PV
|
|
30
|
+
simple_margin: Simple yield-based margin
|
|
31
|
+
yield_to_maturity: YTM assuming constant index rate
|
|
32
|
+
effective_duration: Interest rate sensitivity
|
|
33
|
+
spread_duration: Credit spread sensitivity
|
|
34
|
+
weighted_average_life: WAL in years
|
|
35
|
+
current_coupon: Current period coupon rate
|
|
36
|
+
assumed_index_rate: Index rate used for YTM calculation
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
dirty_price: float
|
|
40
|
+
clean_price: float
|
|
41
|
+
accrued_interest: float
|
|
42
|
+
discount_margin: Optional[float] = None
|
|
43
|
+
simple_margin: Optional[float] = None
|
|
44
|
+
yield_to_maturity: Optional[float] = None
|
|
45
|
+
effective_duration: Optional[float] = None
|
|
46
|
+
spread_duration: Optional[float] = None
|
|
47
|
+
weighted_average_life: Optional[float] = None
|
|
48
|
+
current_coupon: Optional[float] = None
|
|
49
|
+
assumed_index_rate: Optional[float] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FRNDiscountEngine:
|
|
53
|
+
"""
|
|
54
|
+
Pricing engine for Floating Rate Notes.
|
|
55
|
+
|
|
56
|
+
Supports:
|
|
57
|
+
- Dirty/Clean price calculation using forward rates
|
|
58
|
+
- Discount Margin (DM) calculation
|
|
59
|
+
- Simple Margin calculation
|
|
60
|
+
- Effective duration and spread duration
|
|
61
|
+
- Weighted average life
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, pricing_env: PricingEnvironment):
|
|
65
|
+
"""
|
|
66
|
+
Initialize FRN discount engine.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
pricing_env: Pricing environment with rate curve
|
|
70
|
+
"""
|
|
71
|
+
if pricing_env is None:
|
|
72
|
+
raise ValidationError("Pricing environment is required")
|
|
73
|
+
|
|
74
|
+
if pricing_env.rate_curve is None:
|
|
75
|
+
raise MarketDataError("Rate curve is required for FRN pricing")
|
|
76
|
+
|
|
77
|
+
self.pricing_env = pricing_env
|
|
78
|
+
|
|
79
|
+
def price(
|
|
80
|
+
self,
|
|
81
|
+
frn: FloatingRateBond,
|
|
82
|
+
valuation_date: Optional[datetime] = None,
|
|
83
|
+
settlement_date: Optional[datetime] = None,
|
|
84
|
+
spread_adjustment: float = 0.0,
|
|
85
|
+
) -> float:
|
|
86
|
+
"""
|
|
87
|
+
Calculate FRN dirty price (present value including accrued interest).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
frn: Floating rate bond to price
|
|
91
|
+
valuation_date: Date to value the bond (default: pricing env date)
|
|
92
|
+
settlement_date: Settlement date for trade (default: valuation_date)
|
|
93
|
+
spread_adjustment: Additional spread for discount margin calculation
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dirty price (present value of all future cashflows)
|
|
97
|
+
"""
|
|
98
|
+
if valuation_date is None:
|
|
99
|
+
valuation_date = self.pricing_env.valuation_date
|
|
100
|
+
|
|
101
|
+
if settlement_date is None:
|
|
102
|
+
settlement_date = valuation_date
|
|
103
|
+
|
|
104
|
+
# Check if bond has matured
|
|
105
|
+
if frn.is_expired(valuation_date):
|
|
106
|
+
return 0.0
|
|
107
|
+
|
|
108
|
+
# Update forward rates in the FRN
|
|
109
|
+
frn.update_forward_rates(self.pricing_env.rate_curve, valuation_date)
|
|
110
|
+
|
|
111
|
+
# Get future floating cashflows
|
|
112
|
+
floating_cfs = frn.get_floating_cashflows(settlement_date)
|
|
113
|
+
|
|
114
|
+
if not floating_cfs:
|
|
115
|
+
return 0.0
|
|
116
|
+
|
|
117
|
+
# Discount each cashflow
|
|
118
|
+
pv = 0.0
|
|
119
|
+
for cf in floating_cfs:
|
|
120
|
+
# Calculate time to payment
|
|
121
|
+
time_to_payment = (cf.payment_date - valuation_date).days / 365.0
|
|
122
|
+
|
|
123
|
+
if time_to_payment < 0:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Get discount rate (base rate + spread adjustment)
|
|
127
|
+
base_discount_rate = self.pricing_env.get_rate(time_to_payment)
|
|
128
|
+
discount_rate = base_discount_rate + spread_adjustment
|
|
129
|
+
|
|
130
|
+
# Discount factor
|
|
131
|
+
discount_factor = math.exp(-discount_rate * time_to_payment)
|
|
132
|
+
|
|
133
|
+
# Add discounted cashflow
|
|
134
|
+
pv += cf.amount * discount_factor
|
|
135
|
+
|
|
136
|
+
# Add discounted principal (at maturity)
|
|
137
|
+
time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
|
|
138
|
+
if time_to_maturity > 0:
|
|
139
|
+
base_rate = self.pricing_env.get_rate(time_to_maturity)
|
|
140
|
+
discount_rate = base_rate + spread_adjustment
|
|
141
|
+
df_maturity = math.exp(-discount_rate * time_to_maturity)
|
|
142
|
+
pv += frn.denominator * df_maturity
|
|
143
|
+
|
|
144
|
+
return pv
|
|
145
|
+
|
|
146
|
+
def dirty_price(
|
|
147
|
+
self,
|
|
148
|
+
frn: FloatingRateBond,
|
|
149
|
+
valuation_date: Optional[datetime] = None,
|
|
150
|
+
settlement_date: Optional[datetime] = None,
|
|
151
|
+
) -> float:
|
|
152
|
+
"""
|
|
153
|
+
Calculate FRN dirty price (alias for price method).
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
frn: Floating rate bond to price
|
|
157
|
+
valuation_date: Date to value the bond
|
|
158
|
+
settlement_date: Settlement date for trade
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dirty price
|
|
162
|
+
"""
|
|
163
|
+
return self.price(frn, valuation_date, settlement_date)
|
|
164
|
+
|
|
165
|
+
def clean_price(
|
|
166
|
+
self,
|
|
167
|
+
frn: FloatingRateBond,
|
|
168
|
+
valuation_date: Optional[datetime] = None,
|
|
169
|
+
settlement_date: Optional[datetime] = None,
|
|
170
|
+
) -> float:
|
|
171
|
+
"""
|
|
172
|
+
Calculate FRN clean price (dirty price - accrued interest).
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
frn: Floating rate bond to price
|
|
176
|
+
valuation_date: Date to value the bond
|
|
177
|
+
settlement_date: Settlement date for trade
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Clean price
|
|
181
|
+
"""
|
|
182
|
+
if valuation_date is None:
|
|
183
|
+
valuation_date = self.pricing_env.valuation_date
|
|
184
|
+
|
|
185
|
+
if settlement_date is None:
|
|
186
|
+
settlement_date = valuation_date
|
|
187
|
+
|
|
188
|
+
dirty = self.dirty_price(frn, valuation_date, settlement_date)
|
|
189
|
+
accrued = frn.calculate_accrued_interest(settlement_date)
|
|
190
|
+
|
|
191
|
+
return dirty - accrued
|
|
192
|
+
|
|
193
|
+
def accrued_interest(
|
|
194
|
+
self, frn: FloatingRateBond, settlement_date: Optional[datetime] = None
|
|
195
|
+
) -> float:
|
|
196
|
+
"""
|
|
197
|
+
Calculate accrued interest.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
frn: Floating rate bond
|
|
201
|
+
settlement_date: Settlement date
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Accrued interest amount
|
|
205
|
+
"""
|
|
206
|
+
if settlement_date is None:
|
|
207
|
+
settlement_date = self.pricing_env.valuation_date
|
|
208
|
+
|
|
209
|
+
return frn.calculate_accrued_interest(settlement_date)
|
|
210
|
+
|
|
211
|
+
def discount_margin(
|
|
212
|
+
self,
|
|
213
|
+
frn: FloatingRateBond,
|
|
214
|
+
market_price: float,
|
|
215
|
+
valuation_date: Optional[datetime] = None,
|
|
216
|
+
settlement_date: Optional[datetime] = None,
|
|
217
|
+
clean_price: bool = True,
|
|
218
|
+
max_iterations: int = 100,
|
|
219
|
+
tolerance: float = 1e-6,
|
|
220
|
+
) -> float:
|
|
221
|
+
"""
|
|
222
|
+
Calculate Discount Margin (DM) using Newton-Raphson iteration.
|
|
223
|
+
|
|
224
|
+
The discount margin is the spread over the forward rate curve that
|
|
225
|
+
makes the present value of future cashflows equal to the market price.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
frn: Floating rate bond
|
|
229
|
+
market_price: Market price (clean or dirty based on clean_price flag)
|
|
230
|
+
valuation_date: Valuation date
|
|
231
|
+
settlement_date: Settlement date
|
|
232
|
+
clean_price: Whether price is clean price (default: True)
|
|
233
|
+
max_iterations: Maximum iterations for solver
|
|
234
|
+
tolerance: Convergence tolerance
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Discount margin (annualized spread)
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValidationError: If convergence fails
|
|
241
|
+
"""
|
|
242
|
+
if valuation_date is None:
|
|
243
|
+
valuation_date = self.pricing_env.valuation_date
|
|
244
|
+
|
|
245
|
+
if settlement_date is None:
|
|
246
|
+
settlement_date = valuation_date
|
|
247
|
+
|
|
248
|
+
if market_price <= 0:
|
|
249
|
+
raise ValidationError(f"Price must be positive, got {market_price}")
|
|
250
|
+
|
|
251
|
+
# Convert clean price to dirty price if needed
|
|
252
|
+
if clean_price:
|
|
253
|
+
accrued = frn.calculate_accrued_interest(settlement_date)
|
|
254
|
+
target_price = market_price + accrued
|
|
255
|
+
else:
|
|
256
|
+
target_price = market_price
|
|
257
|
+
|
|
258
|
+
# Initial guess: use bond spread
|
|
259
|
+
dm = frn.spread
|
|
260
|
+
|
|
261
|
+
# Newton-Raphson iteration
|
|
262
|
+
for iteration in range(max_iterations):
|
|
263
|
+
# Calculate price at current spread
|
|
264
|
+
current_price = self.price(
|
|
265
|
+
frn, valuation_date, settlement_date, spread_adjustment=dm
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Price difference
|
|
269
|
+
price_diff = current_price - target_price
|
|
270
|
+
|
|
271
|
+
if abs(price_diff) < tolerance:
|
|
272
|
+
return dm
|
|
273
|
+
|
|
274
|
+
# Calculate numerical derivative (sensitivity to spread)
|
|
275
|
+
bump = 0.0001 # 1bp bump
|
|
276
|
+
bumped_price = self.price(
|
|
277
|
+
frn, valuation_date, settlement_date, spread_adjustment=dm + bump
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
dP_dDM = (bumped_price - current_price) / bump
|
|
281
|
+
|
|
282
|
+
if abs(dP_dDM) < 1e-10:
|
|
283
|
+
raise ValidationError("Derivative too small for DM calculation")
|
|
284
|
+
|
|
285
|
+
# Newton-Raphson update
|
|
286
|
+
dm = dm - price_diff / dP_dDM
|
|
287
|
+
|
|
288
|
+
# Sanity check on DM
|
|
289
|
+
if dm < -0.10 or dm > 0.50:
|
|
290
|
+
dm = max(-0.10, min(0.50, dm))
|
|
291
|
+
|
|
292
|
+
raise ValidationError(
|
|
293
|
+
f"Discount margin did not converge after {max_iterations} iterations"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def price_from_yield(
|
|
297
|
+
self,
|
|
298
|
+
frn: FloatingRateBond,
|
|
299
|
+
ytm: float,
|
|
300
|
+
valuation_date: Optional[datetime] = None,
|
|
301
|
+
settlement_date: Optional[datetime] = None,
|
|
302
|
+
clean_price: bool = True,
|
|
303
|
+
assumed_index_rate: Optional[float] = None,
|
|
304
|
+
) -> float:
|
|
305
|
+
"""
|
|
306
|
+
Calculate FRN price given yield to maturity.
|
|
307
|
+
|
|
308
|
+
This is the inverse of yield_to_maturity - given a yield, calculate
|
|
309
|
+
the corresponding FRN price by projecting cashflows with an assumed
|
|
310
|
+
constant index rate and discounting with the given yield.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
frn: Floating rate bond
|
|
314
|
+
ytm: Yield to maturity (annualized, continuously compounded)
|
|
315
|
+
valuation_date: Valuation date
|
|
316
|
+
settlement_date: Settlement date
|
|
317
|
+
clean_price: Whether to return clean price (default: True)
|
|
318
|
+
assumed_index_rate: Assumed constant index rate for projection
|
|
319
|
+
(if None, uses current fixing or forward rate)
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
FRN price (clean or dirty based on clean_price flag)
|
|
323
|
+
"""
|
|
324
|
+
if valuation_date is None:
|
|
325
|
+
valuation_date = self.pricing_env.valuation_date
|
|
326
|
+
|
|
327
|
+
if settlement_date is None:
|
|
328
|
+
settlement_date = valuation_date
|
|
329
|
+
|
|
330
|
+
# Check if bond has matured
|
|
331
|
+
if frn.is_expired(valuation_date):
|
|
332
|
+
return 0.0
|
|
333
|
+
|
|
334
|
+
# Update forward rates in the FRN
|
|
335
|
+
frn.update_forward_rates(self.pricing_env.rate_curve, valuation_date)
|
|
336
|
+
|
|
337
|
+
# Determine the assumed index rate for projection
|
|
338
|
+
if assumed_index_rate is None:
|
|
339
|
+
current_rate = frn.get_current_coupon_rate(valuation_date)
|
|
340
|
+
if current_rate is not None:
|
|
341
|
+
assumed_index_rate = current_rate - frn.spread
|
|
342
|
+
else:
|
|
343
|
+
assumed_index_rate = self.pricing_env.rate_curve.get_rate(0.25)
|
|
344
|
+
|
|
345
|
+
# Get future floating cashflows
|
|
346
|
+
floating_cfs = frn.get_floating_cashflows(settlement_date)
|
|
347
|
+
|
|
348
|
+
if not floating_cfs:
|
|
349
|
+
return 0.0
|
|
350
|
+
|
|
351
|
+
# Build projected cashflows and discount with given yield
|
|
352
|
+
dirty_price = 0.0
|
|
353
|
+
last_cf_time = 0.0
|
|
354
|
+
last_coupon = 0.0
|
|
355
|
+
|
|
356
|
+
for cf in floating_cfs:
|
|
357
|
+
time_to_payment = (cf.payment_date - valuation_date).days / 365.0
|
|
358
|
+
if time_to_payment <= 0:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# Calculate coupon amount using assumed rate
|
|
362
|
+
if cf.is_projected:
|
|
363
|
+
total_rate = assumed_index_rate + frn.spread
|
|
364
|
+
if cf.rate_cap is not None:
|
|
365
|
+
total_rate = min(total_rate, cf.rate_cap)
|
|
366
|
+
if cf.rate_floor is not None:
|
|
367
|
+
total_rate = max(total_rate, cf.rate_floor)
|
|
368
|
+
coupon_amount = cf.notional * total_rate * cf.day_count_fraction
|
|
369
|
+
else:
|
|
370
|
+
coupon_amount = cf.amount
|
|
371
|
+
|
|
372
|
+
# Discount with given yield
|
|
373
|
+
df = math.exp(-ytm * time_to_payment)
|
|
374
|
+
dirty_price += coupon_amount * df
|
|
375
|
+
|
|
376
|
+
last_cf_time = time_to_payment
|
|
377
|
+
last_coupon = coupon_amount
|
|
378
|
+
|
|
379
|
+
# Add principal at maturity
|
|
380
|
+
time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
|
|
381
|
+
if time_to_maturity > 0:
|
|
382
|
+
df = math.exp(-ytm * time_to_maturity)
|
|
383
|
+
dirty_price += frn.denominator * df
|
|
384
|
+
|
|
385
|
+
if clean_price:
|
|
386
|
+
accrued = frn.calculate_accrued_interest(settlement_date)
|
|
387
|
+
return dirty_price - accrued
|
|
388
|
+
|
|
389
|
+
return dirty_price
|
|
390
|
+
|
|
391
|
+
def dirty_price_from_yield(
|
|
392
|
+
self,
|
|
393
|
+
frn: FloatingRateBond,
|
|
394
|
+
ytm: float,
|
|
395
|
+
valuation_date: Optional[datetime] = None,
|
|
396
|
+
settlement_date: Optional[datetime] = None,
|
|
397
|
+
assumed_index_rate: Optional[float] = None,
|
|
398
|
+
) -> float:
|
|
399
|
+
"""
|
|
400
|
+
Calculate dirty price given yield to maturity.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
frn: Floating rate bond
|
|
404
|
+
ytm: Yield to maturity (annualized, continuously compounded)
|
|
405
|
+
valuation_date: Valuation date
|
|
406
|
+
settlement_date: Settlement date
|
|
407
|
+
assumed_index_rate: Assumed constant index rate for projection
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dirty price
|
|
411
|
+
"""
|
|
412
|
+
return self.price_from_yield(
|
|
413
|
+
frn,
|
|
414
|
+
ytm,
|
|
415
|
+
valuation_date,
|
|
416
|
+
settlement_date,
|
|
417
|
+
clean_price=False,
|
|
418
|
+
assumed_index_rate=assumed_index_rate,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def clean_price_from_yield(
|
|
422
|
+
self,
|
|
423
|
+
frn: FloatingRateBond,
|
|
424
|
+
ytm: float,
|
|
425
|
+
valuation_date: Optional[datetime] = None,
|
|
426
|
+
settlement_date: Optional[datetime] = None,
|
|
427
|
+
assumed_index_rate: Optional[float] = None,
|
|
428
|
+
) -> float:
|
|
429
|
+
"""
|
|
430
|
+
Calculate clean price given yield to maturity.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
frn: Floating rate bond
|
|
434
|
+
ytm: Yield to maturity (annualized, continuously compounded)
|
|
435
|
+
valuation_date: Valuation date
|
|
436
|
+
settlement_date: Settlement date
|
|
437
|
+
assumed_index_rate: Assumed constant index rate for projection
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Clean price
|
|
441
|
+
"""
|
|
442
|
+
return self.price_from_yield(
|
|
443
|
+
frn,
|
|
444
|
+
ytm,
|
|
445
|
+
valuation_date,
|
|
446
|
+
settlement_date,
|
|
447
|
+
clean_price=True,
|
|
448
|
+
assumed_index_rate=assumed_index_rate,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def yield_to_maturity(
|
|
452
|
+
self,
|
|
453
|
+
frn: FloatingRateBond,
|
|
454
|
+
market_price: float,
|
|
455
|
+
valuation_date: Optional[datetime] = None,
|
|
456
|
+
settlement_date: Optional[datetime] = None,
|
|
457
|
+
clean_price: bool = True,
|
|
458
|
+
assumed_index_rate: Optional[float] = None,
|
|
459
|
+
max_iterations: int = 100,
|
|
460
|
+
tolerance: float = 1e-8,
|
|
461
|
+
) -> float:
|
|
462
|
+
"""
|
|
463
|
+
Calculate Yield to Maturity for an FRN.
|
|
464
|
+
|
|
465
|
+
For FRNs, YTM is calculated by assuming the floating rate remains
|
|
466
|
+
constant at either:
|
|
467
|
+
1. The provided assumed_index_rate
|
|
468
|
+
2. The current/latest fixing rate
|
|
469
|
+
3. The current forward rate from the curve
|
|
470
|
+
|
|
471
|
+
Then finds the single discount rate that equates PV to market price.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
frn: Floating rate bond
|
|
475
|
+
market_price: Market price (clean or dirty based on clean_price flag)
|
|
476
|
+
valuation_date: Valuation date
|
|
477
|
+
settlement_date: Settlement date
|
|
478
|
+
clean_price: Whether price is clean price (default: True)
|
|
479
|
+
assumed_index_rate: Assumed constant index rate for projection
|
|
480
|
+
(if None, uses current fixing or forward rate)
|
|
481
|
+
max_iterations: Maximum iterations for solver
|
|
482
|
+
tolerance: Convergence tolerance
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Yield to maturity (annualized, continuously compounded)
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
ValidationError: If convergence fails
|
|
489
|
+
"""
|
|
490
|
+
if valuation_date is None:
|
|
491
|
+
valuation_date = self.pricing_env.valuation_date
|
|
492
|
+
|
|
493
|
+
if settlement_date is None:
|
|
494
|
+
settlement_date = valuation_date
|
|
495
|
+
|
|
496
|
+
if market_price <= 0:
|
|
497
|
+
raise ValidationError(f"Price must be positive, got {market_price}")
|
|
498
|
+
|
|
499
|
+
# Convert clean price to dirty price if needed
|
|
500
|
+
if clean_price:
|
|
501
|
+
accrued = frn.calculate_accrued_interest(settlement_date)
|
|
502
|
+
target_price = market_price + accrued
|
|
503
|
+
else:
|
|
504
|
+
target_price = market_price
|
|
505
|
+
|
|
506
|
+
# Update forward rates in the FRN
|
|
507
|
+
frn.update_forward_rates(self.pricing_env.rate_curve, valuation_date)
|
|
508
|
+
|
|
509
|
+
# Determine the assumed index rate for projection
|
|
510
|
+
if assumed_index_rate is None:
|
|
511
|
+
# Try to get from latest fixing or current period
|
|
512
|
+
current_rate = frn.get_current_coupon_rate(valuation_date)
|
|
513
|
+
if current_rate is not None:
|
|
514
|
+
# Current coupon includes spread, so extract index rate
|
|
515
|
+
assumed_index_rate = current_rate - frn.spread
|
|
516
|
+
else:
|
|
517
|
+
# Use forward rate at short tenor
|
|
518
|
+
assumed_index_rate = self.pricing_env.rate_curve.get_rate(0.25)
|
|
519
|
+
|
|
520
|
+
# Get future floating cashflows and build projected cashflows
|
|
521
|
+
floating_cfs = frn.get_floating_cashflows(settlement_date)
|
|
522
|
+
|
|
523
|
+
if not floating_cfs:
|
|
524
|
+
raise ValidationError("No future cashflows to calculate yield")
|
|
525
|
+
|
|
526
|
+
# Build list of (time, amount) for all cashflows
|
|
527
|
+
cashflow_data = []
|
|
528
|
+
for cf in floating_cfs:
|
|
529
|
+
time_to_payment = (cf.payment_date - valuation_date).days / 365.0
|
|
530
|
+
if time_to_payment <= 0:
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
# Calculate coupon amount using assumed rate
|
|
534
|
+
if cf.is_projected:
|
|
535
|
+
# Use assumed index rate + spread
|
|
536
|
+
total_rate = assumed_index_rate + frn.spread
|
|
537
|
+
# Apply cap/floor if present
|
|
538
|
+
if cf.rate_cap is not None:
|
|
539
|
+
total_rate = min(total_rate, cf.rate_cap)
|
|
540
|
+
if cf.rate_floor is not None:
|
|
541
|
+
total_rate = max(total_rate, cf.rate_floor)
|
|
542
|
+
coupon_amount = cf.notional * total_rate * cf.day_count_fraction
|
|
543
|
+
else:
|
|
544
|
+
# Use actual fixing
|
|
545
|
+
coupon_amount = cf.amount
|
|
546
|
+
|
|
547
|
+
cashflow_data.append((time_to_payment, coupon_amount))
|
|
548
|
+
|
|
549
|
+
# Add principal at maturity
|
|
550
|
+
time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
|
|
551
|
+
if time_to_maturity > 0:
|
|
552
|
+
# Find the last cashflow time and add principal there
|
|
553
|
+
if cashflow_data:
|
|
554
|
+
last_time = cashflow_data[-1][0]
|
|
555
|
+
last_coupon = cashflow_data[-1][1]
|
|
556
|
+
cashflow_data[-1] = (last_time, last_coupon + frn.denominator)
|
|
557
|
+
else:
|
|
558
|
+
cashflow_data.append((time_to_maturity, frn.denominator))
|
|
559
|
+
|
|
560
|
+
if not cashflow_data:
|
|
561
|
+
raise ValidationError("No valid cashflows for YTM calculation")
|
|
562
|
+
|
|
563
|
+
# Initial guess: use current rate plus spread
|
|
564
|
+
ytm = assumed_index_rate + frn.spread
|
|
565
|
+
|
|
566
|
+
# Newton-Raphson iteration
|
|
567
|
+
for iteration in range(max_iterations):
|
|
568
|
+
# Calculate price and duration at current yield
|
|
569
|
+
pv = 0.0
|
|
570
|
+
duration = 0.0
|
|
571
|
+
|
|
572
|
+
for t, amount in cashflow_data:
|
|
573
|
+
df = math.exp(-ytm * t)
|
|
574
|
+
pv += amount * df
|
|
575
|
+
duration += amount * t * df
|
|
576
|
+
|
|
577
|
+
# Check convergence
|
|
578
|
+
price_diff = pv - target_price
|
|
579
|
+
|
|
580
|
+
if abs(price_diff) < tolerance:
|
|
581
|
+
return ytm
|
|
582
|
+
|
|
583
|
+
# Newton-Raphson update
|
|
584
|
+
# f(y) = PV(y) - target_price
|
|
585
|
+
# f'(y) = -duration
|
|
586
|
+
if abs(duration) < 1e-10:
|
|
587
|
+
raise ValidationError("Duration too small for yield calculation")
|
|
588
|
+
|
|
589
|
+
ytm = ytm - price_diff / (-duration)
|
|
590
|
+
|
|
591
|
+
# Sanity check on yield
|
|
592
|
+
if ytm < -0.20 or ytm > 1.0:
|
|
593
|
+
ytm = max(-0.20, min(1.0, ytm))
|
|
594
|
+
|
|
595
|
+
raise ValidationError(
|
|
596
|
+
f"Yield to maturity did not converge after {max_iterations} iterations"
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def simple_margin(
|
|
600
|
+
self,
|
|
601
|
+
frn: FloatingRateBond,
|
|
602
|
+
market_price: float,
|
|
603
|
+
valuation_date: Optional[datetime] = None,
|
|
604
|
+
clean_price: bool = True,
|
|
605
|
+
) -> float:
|
|
606
|
+
"""
|
|
607
|
+
Calculate Simple Margin.
|
|
608
|
+
|
|
609
|
+
Simple Margin is an approximation:
|
|
610
|
+
SM = (100 - Price) / WAL + Quoted Spread
|
|
611
|
+
|
|
612
|
+
Where WAL is weighted average life.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
frn: Floating rate bond
|
|
616
|
+
market_price: Market price (as % of par, e.g., 99.5)
|
|
617
|
+
valuation_date: Valuation date
|
|
618
|
+
clean_price: Whether price is clean price
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Simple margin (annualized)
|
|
622
|
+
"""
|
|
623
|
+
if valuation_date is None:
|
|
624
|
+
valuation_date = self.pricing_env.valuation_date
|
|
625
|
+
|
|
626
|
+
# Calculate weighted average life
|
|
627
|
+
wal = self.weighted_average_life(frn, valuation_date)
|
|
628
|
+
|
|
629
|
+
if wal <= 0:
|
|
630
|
+
return frn.spread
|
|
631
|
+
|
|
632
|
+
# Convert price to percentage of par
|
|
633
|
+
if clean_price:
|
|
634
|
+
price_pct = (market_price / frn.denominator) * 100
|
|
635
|
+
else:
|
|
636
|
+
accrued = frn.calculate_accrued_interest(valuation_date)
|
|
637
|
+
clean = market_price - accrued
|
|
638
|
+
price_pct = (clean / frn.denominator) * 100
|
|
639
|
+
|
|
640
|
+
# Simple margin formula
|
|
641
|
+
# (100 - Price) / WAL gives annualized capital gain/loss
|
|
642
|
+
capital_component = (100.0 - price_pct) / (100.0 * wal)
|
|
643
|
+
|
|
644
|
+
return capital_component + frn.spread
|
|
645
|
+
|
|
646
|
+
def weighted_average_life(
|
|
647
|
+
self, frn: FloatingRateBond, valuation_date: Optional[datetime] = None
|
|
648
|
+
) -> float:
|
|
649
|
+
"""
|
|
650
|
+
Calculate Weighted Average Life (WAL).
|
|
651
|
+
|
|
652
|
+
WAL = sum(t_i * P_i) / sum(P_i)
|
|
653
|
+
|
|
654
|
+
Where t_i is time to principal payment and P_i is principal amount.
|
|
655
|
+
For bullet FRNs, this equals time to maturity.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
frn: Floating rate bond
|
|
659
|
+
valuation_date: Valuation date
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Weighted average life in years
|
|
663
|
+
"""
|
|
664
|
+
if valuation_date is None:
|
|
665
|
+
valuation_date = self.pricing_env.valuation_date
|
|
666
|
+
|
|
667
|
+
# For bullet FRNs (single principal payment at maturity)
|
|
668
|
+
time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
|
|
669
|
+
|
|
670
|
+
return max(0.0, time_to_maturity)
|
|
671
|
+
|
|
672
|
+
def effective_duration(
|
|
673
|
+
self,
|
|
674
|
+
frn: FloatingRateBond,
|
|
675
|
+
valuation_date: Optional[datetime] = None,
|
|
676
|
+
settlement_date: Optional[datetime] = None,
|
|
677
|
+
rate_bump: float = 0.0001,
|
|
678
|
+
) -> float:
|
|
679
|
+
"""
|
|
680
|
+
Calculate effective duration (sensitivity to parallel rate shift).
|
|
681
|
+
|
|
682
|
+
For FRNs, effective duration is typically very low since cashflows
|
|
683
|
+
reset to market rates. The duration is mainly from the time to
|
|
684
|
+
next reset.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
frn: Floating rate bond
|
|
688
|
+
valuation_date: Valuation date
|
|
689
|
+
settlement_date: Settlement date
|
|
690
|
+
rate_bump: Size of rate bump (default: 1bp)
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Effective duration
|
|
694
|
+
"""
|
|
695
|
+
if valuation_date is None:
|
|
696
|
+
valuation_date = self.pricing_env.valuation_date
|
|
697
|
+
|
|
698
|
+
if settlement_date is None:
|
|
699
|
+
settlement_date = valuation_date
|
|
700
|
+
|
|
701
|
+
base_price = self.price(frn, valuation_date, settlement_date)
|
|
702
|
+
|
|
703
|
+
if base_price <= 0:
|
|
704
|
+
return 0.0
|
|
705
|
+
|
|
706
|
+
# Price with rates bumped up
|
|
707
|
+
from quantark.param.rrf import FlatRateCurve
|
|
708
|
+
|
|
709
|
+
original_curve = self.pricing_env.rate_curve
|
|
710
|
+
|
|
711
|
+
# Create bumped curve (simple flat bump)
|
|
712
|
+
base_rate = original_curve.get_rate(1.0)
|
|
713
|
+
|
|
714
|
+
up_curve = FlatRateCurve(rate=base_rate + rate_bump)
|
|
715
|
+
down_curve = FlatRateCurve(rate=base_rate - rate_bump)
|
|
716
|
+
|
|
717
|
+
# Price with up curve
|
|
718
|
+
self.pricing_env.rate_curve = up_curve
|
|
719
|
+
price_up = self.price(frn, valuation_date, settlement_date)
|
|
720
|
+
|
|
721
|
+
# Price with down curve
|
|
722
|
+
self.pricing_env.rate_curve = down_curve
|
|
723
|
+
price_down = self.price(frn, valuation_date, settlement_date)
|
|
724
|
+
|
|
725
|
+
# Restore original curve
|
|
726
|
+
self.pricing_env.rate_curve = original_curve
|
|
727
|
+
|
|
728
|
+
# Effective duration = -(P+ - P-) / (2 * dY * P0)
|
|
729
|
+
duration = -(price_up - price_down) / (2 * rate_bump * base_price)
|
|
730
|
+
|
|
731
|
+
return duration
|
|
732
|
+
|
|
733
|
+
def spread_duration(
|
|
734
|
+
self,
|
|
735
|
+
frn: FloatingRateBond,
|
|
736
|
+
valuation_date: Optional[datetime] = None,
|
|
737
|
+
settlement_date: Optional[datetime] = None,
|
|
738
|
+
spread_bump: float = 0.0001,
|
|
739
|
+
) -> float:
|
|
740
|
+
"""
|
|
741
|
+
Calculate spread duration (sensitivity to credit spread changes).
|
|
742
|
+
|
|
743
|
+
Spread duration measures the price sensitivity to changes in the
|
|
744
|
+
discount margin / credit spread. For FRNs, this is approximately
|
|
745
|
+
equal to the weighted average life.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
frn: Floating rate bond
|
|
749
|
+
valuation_date: Valuation date
|
|
750
|
+
settlement_date: Settlement date
|
|
751
|
+
spread_bump: Size of spread bump (default: 1bp)
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Spread duration
|
|
755
|
+
"""
|
|
756
|
+
if valuation_date is None:
|
|
757
|
+
valuation_date = self.pricing_env.valuation_date
|
|
758
|
+
|
|
759
|
+
if settlement_date is None:
|
|
760
|
+
settlement_date = valuation_date
|
|
761
|
+
|
|
762
|
+
base_price = self.price(frn, valuation_date, settlement_date)
|
|
763
|
+
|
|
764
|
+
if base_price <= 0:
|
|
765
|
+
return 0.0
|
|
766
|
+
|
|
767
|
+
# Price with spread bumped up and down
|
|
768
|
+
price_up = self.price(
|
|
769
|
+
frn, valuation_date, settlement_date, spread_adjustment=spread_bump
|
|
770
|
+
)
|
|
771
|
+
price_down = self.price(
|
|
772
|
+
frn, valuation_date, settlement_date, spread_adjustment=-spread_bump
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Spread duration = -(P+ - P-) / (2 * dS * P0)
|
|
776
|
+
spread_dur = -(price_up - price_down) / (2 * spread_bump * base_price)
|
|
777
|
+
|
|
778
|
+
return spread_dur
|
|
779
|
+
|
|
780
|
+
def dv01(
|
|
781
|
+
self,
|
|
782
|
+
frn: FloatingRateBond,
|
|
783
|
+
valuation_date: Optional[datetime] = None,
|
|
784
|
+
settlement_date: Optional[datetime] = None,
|
|
785
|
+
) -> float:
|
|
786
|
+
"""
|
|
787
|
+
Calculate DV01 (dollar value of one basis point).
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
frn: Floating rate bond
|
|
791
|
+
valuation_date: Valuation date
|
|
792
|
+
settlement_date: Settlement date
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
DV01 (price change per basis point)
|
|
796
|
+
"""
|
|
797
|
+
eff_dur = self.effective_duration(frn, valuation_date, settlement_date)
|
|
798
|
+
price = self.price(frn, valuation_date, settlement_date)
|
|
799
|
+
|
|
800
|
+
return eff_dur * price * 0.0001
|
|
801
|
+
|
|
802
|
+
def cs01(
|
|
803
|
+
self,
|
|
804
|
+
frn: FloatingRateBond,
|
|
805
|
+
valuation_date: Optional[datetime] = None,
|
|
806
|
+
settlement_date: Optional[datetime] = None,
|
|
807
|
+
) -> float:
|
|
808
|
+
"""
|
|
809
|
+
Calculate CS01 (credit spread 01 - price change per 1bp spread change).
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
frn: Floating rate bond
|
|
813
|
+
valuation_date: Valuation date
|
|
814
|
+
settlement_date: Settlement date
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
CS01
|
|
818
|
+
"""
|
|
819
|
+
spread_dur = self.spread_duration(frn, valuation_date, settlement_date)
|
|
820
|
+
price = self.price(frn, valuation_date, settlement_date)
|
|
821
|
+
|
|
822
|
+
return spread_dur * price * 0.0001
|
|
823
|
+
|
|
824
|
+
def full_analysis(
|
|
825
|
+
self,
|
|
826
|
+
frn: FloatingRateBond,
|
|
827
|
+
market_price: Optional[float] = None,
|
|
828
|
+
valuation_date: Optional[datetime] = None,
|
|
829
|
+
settlement_date: Optional[datetime] = None,
|
|
830
|
+
clean_price: bool = True,
|
|
831
|
+
assumed_index_rate: Optional[float] = None,
|
|
832
|
+
) -> FRNPricingResults:
|
|
833
|
+
"""
|
|
834
|
+
Perform full analysis of an FRN.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
frn: Floating rate bond
|
|
838
|
+
market_price: Market price (if provided, calculates margins and YTM)
|
|
839
|
+
valuation_date: Valuation date
|
|
840
|
+
settlement_date: Settlement date
|
|
841
|
+
clean_price: Whether market_price is clean
|
|
842
|
+
assumed_index_rate: Index rate to assume for YTM calculation
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
FRNPricingResults with all metrics
|
|
846
|
+
"""
|
|
847
|
+
if valuation_date is None:
|
|
848
|
+
valuation_date = self.pricing_env.valuation_date
|
|
849
|
+
|
|
850
|
+
if settlement_date is None:
|
|
851
|
+
settlement_date = valuation_date
|
|
852
|
+
|
|
853
|
+
dirty = self.dirty_price(frn, valuation_date, settlement_date)
|
|
854
|
+
accrued = frn.calculate_accrued_interest(settlement_date)
|
|
855
|
+
clean = dirty - accrued
|
|
856
|
+
|
|
857
|
+
# Calculate margins and YTM if market price provided
|
|
858
|
+
dm = None
|
|
859
|
+
sm = None
|
|
860
|
+
ytm = None
|
|
861
|
+
index_rate_used = assumed_index_rate
|
|
862
|
+
|
|
863
|
+
if market_price is not None:
|
|
864
|
+
try:
|
|
865
|
+
dm = self.discount_margin(
|
|
866
|
+
frn, market_price, valuation_date, settlement_date, clean_price
|
|
867
|
+
)
|
|
868
|
+
except ValidationError:
|
|
869
|
+
pass
|
|
870
|
+
|
|
871
|
+
sm = self.simple_margin(frn, market_price, valuation_date, clean_price)
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
ytm = self.yield_to_maturity(
|
|
875
|
+
frn,
|
|
876
|
+
market_price,
|
|
877
|
+
valuation_date,
|
|
878
|
+
settlement_date,
|
|
879
|
+
clean_price,
|
|
880
|
+
assumed_index_rate,
|
|
881
|
+
)
|
|
882
|
+
# Determine the index rate that was actually used
|
|
883
|
+
if index_rate_used is None:
|
|
884
|
+
current_coupon = frn.get_current_coupon_rate(valuation_date)
|
|
885
|
+
if current_coupon is not None:
|
|
886
|
+
index_rate_used = current_coupon - frn.spread
|
|
887
|
+
else:
|
|
888
|
+
index_rate_used = self.pricing_env.rate_curve.get_rate(0.25)
|
|
889
|
+
except ValidationError:
|
|
890
|
+
pass
|
|
891
|
+
|
|
892
|
+
# Risk metrics
|
|
893
|
+
eff_dur = self.effective_duration(frn, valuation_date, settlement_date)
|
|
894
|
+
spread_dur = self.spread_duration(frn, valuation_date, settlement_date)
|
|
895
|
+
wal = self.weighted_average_life(frn, valuation_date)
|
|
896
|
+
current_coupon = frn.get_current_coupon_rate(valuation_date)
|
|
897
|
+
|
|
898
|
+
return FRNPricingResults(
|
|
899
|
+
dirty_price=dirty,
|
|
900
|
+
clean_price=clean,
|
|
901
|
+
accrued_interest=accrued,
|
|
902
|
+
discount_margin=dm,
|
|
903
|
+
simple_margin=sm,
|
|
904
|
+
yield_to_maturity=ytm,
|
|
905
|
+
effective_duration=eff_dur,
|
|
906
|
+
spread_duration=spread_dur,
|
|
907
|
+
weighted_average_life=wal,
|
|
908
|
+
current_coupon=current_coupon,
|
|
909
|
+
assumed_index_rate=index_rate_used,
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
def __repr__(self):
|
|
913
|
+
return f"FRNDiscountEngine(valuation_date={self.pricing_env.valuation_date.date()})"
|