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,738 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monte Carlo pricing engine for Range Accrual options.
|
|
3
|
+
|
|
4
|
+
This engine prices Range Accrual options using Monte Carlo simulation with support for:
|
|
5
|
+
- Weighted observations (e.g., Friday = 3 for weekend carry)
|
|
6
|
+
- Historical (past) observations with recorded in-range outcomes
|
|
7
|
+
- Time-varying upper and lower barriers
|
|
8
|
+
- Reverse mode (pay when outside range instead of inside)
|
|
9
|
+
- Annualized or non-annualized accrual rates
|
|
10
|
+
- Three Monte Carlo methods (PSEUDO, QUASI, RANDOMIZED_QUASI)
|
|
11
|
+
- Vectorized NumPy operations for efficiency
|
|
12
|
+
|
|
13
|
+
For options where valuation_date > initial_date, past observations with recorded
|
|
14
|
+
outcomes are combined with simulated future observations for payoff calculations.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from quantark.asset.equity.engine.base_engine import BaseEngine
|
|
24
|
+
from quantark.asset.equity.param import MCParams
|
|
25
|
+
from quantark.asset.equity.process.bsm.qmc_path_generator import GBMPathGenerator
|
|
26
|
+
from quantark.asset.equity.process.bsm.qmc_rqmc_driver import run_rqmc
|
|
27
|
+
from quantark.asset.equity.process.bsm.qmc_sobol import (
|
|
28
|
+
PseudoRandomNormalGenerator,
|
|
29
|
+
SobolNormalGenerator,
|
|
30
|
+
)
|
|
31
|
+
from quantark.asset.equity.process.bsm.qmc_variance_reduction import VarianceReductionConfig
|
|
32
|
+
from quantark.asset.equity.product.base_equity_product import BaseEquityProduct
|
|
33
|
+
from quantark.asset.equity.product.option.range_accrual_option import RangeAccrualOption
|
|
34
|
+
from quantark.priceenv import PricingEnvironment
|
|
35
|
+
from quantark.util.enum.engine_enums import EngineType, MonteCarloMethod
|
|
36
|
+
from quantark.util.exceptions import PricingError, ValidationError
|
|
37
|
+
from quantark.util.numerical import is_zero
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class RangeAccrualMCResult:
|
|
42
|
+
"""Result container for Range Accrual option MC pricing."""
|
|
43
|
+
|
|
44
|
+
price: float
|
|
45
|
+
std_error: float
|
|
46
|
+
num_paths: int
|
|
47
|
+
in_range_ratio_mean: float # Mean of in-range weight ratios across paths
|
|
48
|
+
in_range_ratio_std: float # Std of in-range weight ratios across paths
|
|
49
|
+
num_past_observations: int = 0 # Number of already-observed outcomes
|
|
50
|
+
num_future_observations: int = 0 # Number of simulated observations
|
|
51
|
+
past_in_range_weights: float = 0.0 # Accumulated in-range weights from past
|
|
52
|
+
total_weights: float = 0.0 # Total weight sum
|
|
53
|
+
batches_used: Optional[int] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RangeAccrualMCEngine(BaseEngine):
|
|
57
|
+
"""
|
|
58
|
+
Monte Carlo pricing engine for Range Accrual options.
|
|
59
|
+
|
|
60
|
+
Supports three Monte Carlo methods:
|
|
61
|
+
- PSEUDO: Standard Monte Carlo with pseudorandom numbers
|
|
62
|
+
- QUASI: Quasi-Monte Carlo with Sobol sequences
|
|
63
|
+
- RANDOMIZED_QUASI: Randomized QMC with adaptive batching
|
|
64
|
+
|
|
65
|
+
Range Accrual options pay based on the proportion of observations where the
|
|
66
|
+
underlying stays within a defined price range. The payoff formula is:
|
|
67
|
+
|
|
68
|
+
Payoff = initial_price * contract_multiplier * accrual_rate
|
|
69
|
+
* (sum_in_range_weights / sum_total_weights) * year_fraction
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
# Preferred: Two-level enum pattern
|
|
73
|
+
engine = RangeAccrualMCEngine(
|
|
74
|
+
params=MCParams(num_paths=100000),
|
|
75
|
+
method=EngineType.MONTE_CARLO(MonteCarloMethod.QUASI)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Alternative: Direct method enum
|
|
79
|
+
engine = RangeAccrualMCEngine(
|
|
80
|
+
params=MCParams(num_paths=100000),
|
|
81
|
+
method=MonteCarloMethod.QUASI
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Backward compatibility: String
|
|
85
|
+
engine = RangeAccrualMCEngine(method="quasi")
|
|
86
|
+
|
|
87
|
+
The engine creates a GBMPathGenerator internally based on the pricing
|
|
88
|
+
environment and product observation schedule.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
engine_type = EngineType.MONTE_CARLO
|
|
92
|
+
|
|
93
|
+
DEFAULT_METHOD = MonteCarloMethod.PSEUDO
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
params: Optional[MCParams] = None,
|
|
98
|
+
method: Union[str, MonteCarloMethod, tuple, None] = None,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Initialize Range Accrual Monte Carlo engine.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
params: Monte Carlo configuration parameters (MCParams)
|
|
105
|
+
method: Monte Carlo method selection, one of:
|
|
106
|
+
- EngineType.MONTE_CARLO(MonteCarloMethod.XXX) (preferred)
|
|
107
|
+
- MonteCarloMethod.XXX
|
|
108
|
+
- String: "pseudo", "quasi", "randomized_quasi"
|
|
109
|
+
- None: defaults to MonteCarloMethod.PSEUDO
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValidationError: If method is invalid or params are invalid
|
|
113
|
+
"""
|
|
114
|
+
if params is None:
|
|
115
|
+
params = MCParams()
|
|
116
|
+
|
|
117
|
+
if not isinstance(params, MCParams):
|
|
118
|
+
raise ValidationError(
|
|
119
|
+
f"params must be MCParams instance, got {type(params).__name__}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
super().__init__(params)
|
|
123
|
+
|
|
124
|
+
if method is None:
|
|
125
|
+
self.method = self.DEFAULT_METHOD
|
|
126
|
+
elif isinstance(method, tuple):
|
|
127
|
+
engine_type, mc_method = method
|
|
128
|
+
if engine_type != EngineType.MONTE_CARLO:
|
|
129
|
+
raise ValidationError(
|
|
130
|
+
f"Expected EngineType.MONTE_CARLO, got {engine_type}"
|
|
131
|
+
)
|
|
132
|
+
if not isinstance(mc_method, MonteCarloMethod):
|
|
133
|
+
raise ValidationError(
|
|
134
|
+
f"Expected MonteCarloMethod, got {type(mc_method).__name__}"
|
|
135
|
+
)
|
|
136
|
+
self.method = mc_method
|
|
137
|
+
elif isinstance(method, MonteCarloMethod):
|
|
138
|
+
self.method = method
|
|
139
|
+
elif isinstance(method, str):
|
|
140
|
+
try:
|
|
141
|
+
self.method = MonteCarloMethod[method.upper()]
|
|
142
|
+
except KeyError:
|
|
143
|
+
valid_methods = [m.name for m in MonteCarloMethod]
|
|
144
|
+
raise ValidationError(
|
|
145
|
+
f"Invalid method string '{method}'. Valid methods: {valid_methods}"
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
raise ValidationError(
|
|
149
|
+
f"Invalid method type {type(method).__name__}. "
|
|
150
|
+
"Expected MonteCarloMethod, tuple, str, or None"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Result storage
|
|
154
|
+
self._last_result: Optional[RangeAccrualMCResult] = None
|
|
155
|
+
|
|
156
|
+
def price(
|
|
157
|
+
self, product: BaseEquityProduct, pricing_env: PricingEnvironment
|
|
158
|
+
) -> float:
|
|
159
|
+
"""
|
|
160
|
+
Price a Range Accrual option using Monte Carlo simulation.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
product: Range Accrual option to price
|
|
164
|
+
pricing_env: Pricing environment with market data
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Option price
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
PricingError: If product is not a RangeAccrualOption
|
|
171
|
+
ValidationError: If pricing parameters are invalid
|
|
172
|
+
"""
|
|
173
|
+
if not isinstance(product, RangeAccrualOption):
|
|
174
|
+
raise PricingError(
|
|
175
|
+
f"RangeAccrualMCEngine only supports RangeAccrualOption, "
|
|
176
|
+
f"got {type(product).__name__}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Extract market data
|
|
180
|
+
S = pricing_env.spot
|
|
181
|
+
T = product.get_maturity(pricing_env)
|
|
182
|
+
r = pricing_env.get_rate(T)
|
|
183
|
+
q = pricing_env.get_div_yield(T)
|
|
184
|
+
sigma = pricing_env.get_vol(product.initial_price, T)
|
|
185
|
+
|
|
186
|
+
self._validate_inputs(S, T, r, q, sigma, product)
|
|
187
|
+
|
|
188
|
+
# Handle near-expiry case
|
|
189
|
+
if is_zero(T):
|
|
190
|
+
# At expiry, use product's payoff calculation with known weights
|
|
191
|
+
past_in_range, past_total = product.get_past_accrual(pricing_env)
|
|
192
|
+
total_weights = product.get_total_weights()
|
|
193
|
+
return product.get_payoff(
|
|
194
|
+
S,
|
|
195
|
+
in_range_weights=past_in_range,
|
|
196
|
+
total_weights=total_weights,
|
|
197
|
+
pricing_env=pricing_env,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Price using appropriate method
|
|
201
|
+
if self.method == MonteCarloMethod.RANDOMIZED_QUASI:
|
|
202
|
+
result = self._price_rqmc(product, pricing_env, S, T, r, q, sigma)
|
|
203
|
+
else:
|
|
204
|
+
result = self._price_mc_or_qmc(product, pricing_env, S, T, r, q, sigma)
|
|
205
|
+
|
|
206
|
+
self._last_result = result
|
|
207
|
+
|
|
208
|
+
# Range accrual payoffs are non-negative by construction
|
|
209
|
+
if result.price < 0:
|
|
210
|
+
raise PricingError(f"Negative price computed: {result.price}")
|
|
211
|
+
|
|
212
|
+
return result.price
|
|
213
|
+
|
|
214
|
+
def _validate_inputs(
|
|
215
|
+
self,
|
|
216
|
+
S: float,
|
|
217
|
+
T: float,
|
|
218
|
+
r: float,
|
|
219
|
+
q: float,
|
|
220
|
+
sigma: float,
|
|
221
|
+
product: RangeAccrualOption,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Validate pricing inputs."""
|
|
224
|
+
if S <= 0:
|
|
225
|
+
raise ValidationError(f"Spot price must be positive, got {S}")
|
|
226
|
+
if T < 0:
|
|
227
|
+
raise ValidationError(f"Time to maturity must be non-negative, got {T}")
|
|
228
|
+
if sigma <= 0:
|
|
229
|
+
raise ValidationError(f"Volatility must be positive, got {sigma}")
|
|
230
|
+
if q < 0:
|
|
231
|
+
raise ValidationError(f"Dividend yield must be non-negative, got {q}")
|
|
232
|
+
|
|
233
|
+
if product.range_config is None:
|
|
234
|
+
raise ValidationError("range_config is required for Range Accrual option")
|
|
235
|
+
|
|
236
|
+
def _build_observation_grid(
|
|
237
|
+
self,
|
|
238
|
+
product: RangeAccrualOption,
|
|
239
|
+
pricing_env: PricingEnvironment,
|
|
240
|
+
T: float,
|
|
241
|
+
) -> Tuple[
|
|
242
|
+
np.ndarray, # all_times
|
|
243
|
+
np.ndarray, # dt_array
|
|
244
|
+
np.ndarray, # future_obs_indices
|
|
245
|
+
List[Tuple[float, bool]], # past_observations: (weight, in_range)
|
|
246
|
+
List[Tuple[float, float, int]], # future_observations: (weight, time, obs_idx)
|
|
247
|
+
float, # total_weights
|
|
248
|
+
]:
|
|
249
|
+
"""
|
|
250
|
+
Build time grid aligned with future observation times.
|
|
251
|
+
|
|
252
|
+
Uses resolve_observations() to separate past (already observed) from
|
|
253
|
+
future (to be simulated) observations.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
product: Range Accrual option product
|
|
257
|
+
pricing_env: Pricing environment with valuation date
|
|
258
|
+
T: Time to maturity
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Tuple of:
|
|
262
|
+
- all_times: Sorted unique times including future observations and maturity
|
|
263
|
+
- dt_array: Time increments between times
|
|
264
|
+
- future_obs_indices: Indices into paths for future observations
|
|
265
|
+
- past_observations: List of (weight, in_range) for past observations
|
|
266
|
+
- future_observations: List of (weight, time, obs_idx) for future observations
|
|
267
|
+
- total_weights: Sum of all observation weights
|
|
268
|
+
"""
|
|
269
|
+
past_obs, future_obs, total_weights = product.resolve_observations(pricing_env)
|
|
270
|
+
|
|
271
|
+
# If no future observations, we only need to compute payoff from past data
|
|
272
|
+
if len(future_obs) == 0:
|
|
273
|
+
return (
|
|
274
|
+
np.array([T]),
|
|
275
|
+
np.array([T]),
|
|
276
|
+
np.array([], dtype=int),
|
|
277
|
+
past_obs,
|
|
278
|
+
future_obs,
|
|
279
|
+
total_weights,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Extract future observation times
|
|
283
|
+
future_times = np.array([t for _, t, _ in future_obs])
|
|
284
|
+
|
|
285
|
+
# Ensure maturity is included in the time grid
|
|
286
|
+
all_times_set = set(future_times.tolist()) | {T}
|
|
287
|
+
all_times = np.array(sorted(all_times_set))
|
|
288
|
+
|
|
289
|
+
# Build dt_array
|
|
290
|
+
times_with_zero = np.concatenate([[0.0], all_times])
|
|
291
|
+
dt_array = np.diff(times_with_zero)
|
|
292
|
+
|
|
293
|
+
# Find indices for future observation times in the path array
|
|
294
|
+
# Paths have shape (num_paths, num_times + 1) where index 0 is t=0
|
|
295
|
+
future_obs_indices = np.searchsorted(all_times, future_times) + 1
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
all_times,
|
|
299
|
+
dt_array,
|
|
300
|
+
future_obs_indices,
|
|
301
|
+
past_obs,
|
|
302
|
+
future_obs,
|
|
303
|
+
total_weights,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _create_path_generator(
|
|
307
|
+
self,
|
|
308
|
+
S: float,
|
|
309
|
+
r: float,
|
|
310
|
+
q: float,
|
|
311
|
+
sigma: float,
|
|
312
|
+
T: float,
|
|
313
|
+
dt_array: np.ndarray,
|
|
314
|
+
batch_id: Optional[int] = None,
|
|
315
|
+
num_paths: Optional[int] = None,
|
|
316
|
+
) -> GBMPathGenerator:
|
|
317
|
+
"""
|
|
318
|
+
Create a GBMPathGenerator configured for the observation grid.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
S: Spot price
|
|
322
|
+
r: Risk-free rate
|
|
323
|
+
q: Dividend yield
|
|
324
|
+
sigma: Volatility
|
|
325
|
+
T: Time to maturity
|
|
326
|
+
dt_array: Non-uniform time increments
|
|
327
|
+
batch_id: Batch identifier for RQMC
|
|
328
|
+
num_paths: Override for number of paths
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Configured GBMPathGenerator
|
|
332
|
+
"""
|
|
333
|
+
params = self.params
|
|
334
|
+
effective_num_paths = params.num_paths if num_paths is None else int(num_paths)
|
|
335
|
+
if effective_num_paths <= 0:
|
|
336
|
+
raise ValidationError(
|
|
337
|
+
f"num_paths must be positive, got {effective_num_paths}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if self.method == MonteCarloMethod.PSEUDO:
|
|
341
|
+
seed = params.seed + (batch_id or 0) * 1000
|
|
342
|
+
random_stream = PseudoRandomNormalGenerator(seed=seed)
|
|
343
|
+
is_qmc = False
|
|
344
|
+
elif self.method in (MonteCarloMethod.QUASI, MonteCarloMethod.RANDOMIZED_QUASI):
|
|
345
|
+
random_stream = SobolNormalGenerator(base_seed=params.seed)
|
|
346
|
+
is_qmc = True
|
|
347
|
+
else:
|
|
348
|
+
raise ValidationError(f"Unknown Monte Carlo method: {self.method}")
|
|
349
|
+
|
|
350
|
+
# Use antithetic variates for non-QMC (no barriers to worry about)
|
|
351
|
+
vr_config = None
|
|
352
|
+
if params.use_antithetic and not is_qmc:
|
|
353
|
+
vr_config = VarianceReductionConfig(antithetic=True)
|
|
354
|
+
|
|
355
|
+
generator = GBMPathGenerator(
|
|
356
|
+
initial_value=S,
|
|
357
|
+
vol=sigma,
|
|
358
|
+
rrf=r,
|
|
359
|
+
div=q,
|
|
360
|
+
maturity=T,
|
|
361
|
+
time_steps=len(dt_array),
|
|
362
|
+
num_paths=effective_num_paths,
|
|
363
|
+
model="bsm",
|
|
364
|
+
random_stream=random_stream,
|
|
365
|
+
use_brownian_bridge=False,
|
|
366
|
+
vr_config=vr_config,
|
|
367
|
+
is_qmc=is_qmc,
|
|
368
|
+
dt_array=dt_array,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return generator
|
|
372
|
+
|
|
373
|
+
def _check_in_range(
|
|
374
|
+
self,
|
|
375
|
+
product: RangeAccrualOption,
|
|
376
|
+
spots: np.ndarray,
|
|
377
|
+
future_obs: List[Tuple[float, float, int]],
|
|
378
|
+
) -> np.ndarray:
|
|
379
|
+
"""
|
|
380
|
+
Check which observations are in range for all paths.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
product: Range Accrual option product
|
|
384
|
+
spots: Spot prices at observation times, shape (num_paths, num_future_obs)
|
|
385
|
+
future_obs: List of (weight, time, obs_idx) for future observations
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Boolean array of shape (num_paths, num_future_obs) indicating in-range
|
|
389
|
+
"""
|
|
390
|
+
num_paths, num_future = spots.shape
|
|
391
|
+
in_range = np.zeros((num_paths, num_future), dtype=bool)
|
|
392
|
+
|
|
393
|
+
for i, (_, _, obs_idx) in enumerate(future_obs):
|
|
394
|
+
# Get barriers for this observation
|
|
395
|
+
upper = product.range_config.get_upper_barrier(obs_idx)
|
|
396
|
+
lower = product.range_config.get_lower_barrier(obs_idx)
|
|
397
|
+
|
|
398
|
+
# Check if in range
|
|
399
|
+
spot_col = spots[:, i]
|
|
400
|
+
in_range_col = (spot_col >= lower) & (spot_col <= upper)
|
|
401
|
+
|
|
402
|
+
# Apply reverse mode if configured
|
|
403
|
+
if product.range_config.is_reverse:
|
|
404
|
+
in_range_col = ~in_range_col
|
|
405
|
+
|
|
406
|
+
in_range[:, i] = in_range_col
|
|
407
|
+
|
|
408
|
+
return in_range
|
|
409
|
+
|
|
410
|
+
def _compute_payoffs(
|
|
411
|
+
self,
|
|
412
|
+
product: RangeAccrualOption,
|
|
413
|
+
pricing_env: PricingEnvironment,
|
|
414
|
+
paths: np.ndarray,
|
|
415
|
+
future_obs_indices: np.ndarray,
|
|
416
|
+
past_obs: List[Tuple[float, bool]],
|
|
417
|
+
future_obs: List[Tuple[float, float, int]],
|
|
418
|
+
total_weights: float,
|
|
419
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
420
|
+
"""
|
|
421
|
+
Compute payoffs for all paths.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
product: Range Accrual option product
|
|
425
|
+
pricing_env: Pricing environment
|
|
426
|
+
paths: Simulated paths, shape (num_paths, num_times + 1)
|
|
427
|
+
future_obs_indices: Indices into paths for future observations
|
|
428
|
+
past_obs: List of (weight, in_range) for past observations
|
|
429
|
+
future_obs: List of (weight, time, obs_idx) for future observations
|
|
430
|
+
total_weights: Sum of all observation weights
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Tuple of:
|
|
434
|
+
- payoffs: Undiscounted payoffs, shape (num_paths,)
|
|
435
|
+
- in_range_ratios: In-range weight ratios, shape (num_paths,)
|
|
436
|
+
"""
|
|
437
|
+
num_paths = paths.shape[0]
|
|
438
|
+
num_future = len(future_obs)
|
|
439
|
+
|
|
440
|
+
# Calculate past contribution (same for all paths)
|
|
441
|
+
past_in_range_weights = sum(w for w, in_range in past_obs if in_range)
|
|
442
|
+
|
|
443
|
+
# Handle case with no future observations
|
|
444
|
+
if num_future == 0:
|
|
445
|
+
in_range_ratio = past_in_range_weights / total_weights if total_weights > 0 else 0.0
|
|
446
|
+
in_range_ratios = np.full(num_paths, in_range_ratio)
|
|
447
|
+
else:
|
|
448
|
+
# Extract simulated prices at future observation times
|
|
449
|
+
future_spots = paths[:, future_obs_indices] # (num_paths, num_future)
|
|
450
|
+
|
|
451
|
+
# Check in-range status for all future observations
|
|
452
|
+
in_range = self._check_in_range(product, future_spots, future_obs)
|
|
453
|
+
|
|
454
|
+
# Get future weights as array
|
|
455
|
+
future_weights = np.array([w for w, _, _ in future_obs])
|
|
456
|
+
|
|
457
|
+
# Calculate in-range weights for each path
|
|
458
|
+
# Weight is added if in_range is True
|
|
459
|
+
future_in_range_weights = (in_range * future_weights).sum(axis=1)
|
|
460
|
+
|
|
461
|
+
# Total in-range weights = past + future
|
|
462
|
+
total_in_range_weights = past_in_range_weights + future_in_range_weights
|
|
463
|
+
|
|
464
|
+
# Compute in-range ratio
|
|
465
|
+
in_range_ratios = total_in_range_weights / total_weights if total_weights > 0 else np.zeros(num_paths)
|
|
466
|
+
|
|
467
|
+
# Compute payoffs using product's formula
|
|
468
|
+
# Payoff = initial_price * contract_multiplier * accrual_rate * ratio * year_fraction
|
|
469
|
+
year_fraction = product.get_year_fraction(pricing_env)
|
|
470
|
+
accrual_rate = product.range_config.accrual_rate
|
|
471
|
+
|
|
472
|
+
payoffs = (
|
|
473
|
+
product.initial_price
|
|
474
|
+
* product.contract_multiplier
|
|
475
|
+
* accrual_rate
|
|
476
|
+
* in_range_ratios
|
|
477
|
+
* year_fraction
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return payoffs, in_range_ratios
|
|
481
|
+
|
|
482
|
+
def _price_mc_or_qmc(
|
|
483
|
+
self,
|
|
484
|
+
product: RangeAccrualOption,
|
|
485
|
+
pricing_env: PricingEnvironment,
|
|
486
|
+
S: float,
|
|
487
|
+
T: float,
|
|
488
|
+
r: float,
|
|
489
|
+
q: float,
|
|
490
|
+
sigma: float,
|
|
491
|
+
) -> RangeAccrualMCResult:
|
|
492
|
+
"""
|
|
493
|
+
Price using normal MC or QMC (non-randomized).
|
|
494
|
+
"""
|
|
495
|
+
# Build observation grid
|
|
496
|
+
(
|
|
497
|
+
all_times,
|
|
498
|
+
dt_array,
|
|
499
|
+
future_obs_indices,
|
|
500
|
+
past_obs,
|
|
501
|
+
future_obs,
|
|
502
|
+
total_weights,
|
|
503
|
+
) = self._build_observation_grid(product, pricing_env, T)
|
|
504
|
+
|
|
505
|
+
num_past = len(past_obs)
|
|
506
|
+
num_future = len(future_obs)
|
|
507
|
+
past_in_range_weights = sum(w for w, in_range in past_obs if in_range)
|
|
508
|
+
|
|
509
|
+
# Handle special case: all observations are in the past
|
|
510
|
+
if num_future == 0:
|
|
511
|
+
# Compute payoff from past observations only
|
|
512
|
+
in_range_ratio = past_in_range_weights / total_weights if total_weights > 0 else 0.0
|
|
513
|
+
|
|
514
|
+
year_fraction = product.get_year_fraction(pricing_env)
|
|
515
|
+
accrual_rate = product.range_config.accrual_rate
|
|
516
|
+
payoff = (
|
|
517
|
+
product.initial_price
|
|
518
|
+
* product.contract_multiplier
|
|
519
|
+
* accrual_rate
|
|
520
|
+
* in_range_ratio
|
|
521
|
+
* year_fraction
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Discount payoff
|
|
525
|
+
discount_factor = math.exp(-r * T)
|
|
526
|
+
price = discount_factor * payoff
|
|
527
|
+
|
|
528
|
+
return RangeAccrualMCResult(
|
|
529
|
+
price=price,
|
|
530
|
+
std_error=0.0, # No simulation uncertainty
|
|
531
|
+
num_paths=0,
|
|
532
|
+
in_range_ratio_mean=in_range_ratio,
|
|
533
|
+
in_range_ratio_std=0.0,
|
|
534
|
+
num_past_observations=num_past,
|
|
535
|
+
num_future_observations=0,
|
|
536
|
+
past_in_range_weights=past_in_range_weights,
|
|
537
|
+
total_weights=total_weights,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Create path generator for future observations
|
|
541
|
+
maturity_for_sim = all_times[-1]
|
|
542
|
+
generator = self._create_path_generator(
|
|
543
|
+
S, r, q, sigma, maturity_for_sim, dt_array
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Generate paths
|
|
547
|
+
paths, _ = generator.generate_paths(return_aux=False)
|
|
548
|
+
|
|
549
|
+
# Compute payoffs
|
|
550
|
+
payoffs, in_range_ratios = self._compute_payoffs(
|
|
551
|
+
product,
|
|
552
|
+
pricing_env,
|
|
553
|
+
paths,
|
|
554
|
+
future_obs_indices,
|
|
555
|
+
past_obs,
|
|
556
|
+
future_obs,
|
|
557
|
+
total_weights,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Discount payoffs
|
|
561
|
+
discount_factor = math.exp(-r * T)
|
|
562
|
+
discounted_payoffs = discount_factor * payoffs
|
|
563
|
+
|
|
564
|
+
# Compute price and standard error
|
|
565
|
+
price = float(discounted_payoffs.mean())
|
|
566
|
+
std_payoff = float(discounted_payoffs.std(ddof=1))
|
|
567
|
+
std_error = std_payoff / math.sqrt(len(payoffs))
|
|
568
|
+
|
|
569
|
+
return RangeAccrualMCResult(
|
|
570
|
+
price=price,
|
|
571
|
+
std_error=std_error,
|
|
572
|
+
num_paths=len(paths),
|
|
573
|
+
in_range_ratio_mean=float(in_range_ratios.mean()),
|
|
574
|
+
in_range_ratio_std=float(in_range_ratios.std(ddof=1)),
|
|
575
|
+
num_past_observations=num_past,
|
|
576
|
+
num_future_observations=num_future,
|
|
577
|
+
past_in_range_weights=past_in_range_weights,
|
|
578
|
+
total_weights=total_weights,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def _price_rqmc(
|
|
582
|
+
self,
|
|
583
|
+
product: RangeAccrualOption,
|
|
584
|
+
pricing_env: PricingEnvironment,
|
|
585
|
+
S: float,
|
|
586
|
+
T: float,
|
|
587
|
+
r: float,
|
|
588
|
+
q: float,
|
|
589
|
+
sigma: float,
|
|
590
|
+
) -> RangeAccrualMCResult:
|
|
591
|
+
"""
|
|
592
|
+
Price using Randomized QMC with adaptive batching.
|
|
593
|
+
"""
|
|
594
|
+
# Build observation grid
|
|
595
|
+
(
|
|
596
|
+
all_times,
|
|
597
|
+
dt_array,
|
|
598
|
+
future_obs_indices,
|
|
599
|
+
past_obs,
|
|
600
|
+
future_obs,
|
|
601
|
+
total_weights,
|
|
602
|
+
) = self._build_observation_grid(product, pricing_env, T)
|
|
603
|
+
|
|
604
|
+
num_past = len(past_obs)
|
|
605
|
+
num_future = len(future_obs)
|
|
606
|
+
past_in_range_weights = sum(w for w, in_range in past_obs if in_range)
|
|
607
|
+
|
|
608
|
+
# Handle special case: all observations are in the past
|
|
609
|
+
if num_future == 0:
|
|
610
|
+
in_range_ratio = past_in_range_weights / total_weights if total_weights > 0 else 0.0
|
|
611
|
+
|
|
612
|
+
year_fraction = product.get_year_fraction(pricing_env)
|
|
613
|
+
accrual_rate = product.range_config.accrual_rate
|
|
614
|
+
payoff = (
|
|
615
|
+
product.initial_price
|
|
616
|
+
* product.contract_multiplier
|
|
617
|
+
* accrual_rate
|
|
618
|
+
* in_range_ratio
|
|
619
|
+
* year_fraction
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
discount_factor = math.exp(-r * T)
|
|
623
|
+
price = discount_factor * payoff
|
|
624
|
+
|
|
625
|
+
return RangeAccrualMCResult(
|
|
626
|
+
price=price,
|
|
627
|
+
std_error=0.0,
|
|
628
|
+
num_paths=0,
|
|
629
|
+
in_range_ratio_mean=in_range_ratio,
|
|
630
|
+
in_range_ratio_std=0.0,
|
|
631
|
+
num_past_observations=num_past,
|
|
632
|
+
num_future_observations=0,
|
|
633
|
+
past_in_range_weights=past_in_range_weights,
|
|
634
|
+
total_weights=total_weights,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
params = self.params
|
|
638
|
+
max_batches = getattr(
|
|
639
|
+
params, "rqmc_max_batches", getattr(params, "max_batches", 32)
|
|
640
|
+
)
|
|
641
|
+
min_batches = getattr(
|
|
642
|
+
params, "rqmc_min_batches", getattr(params, "min_batches", 4)
|
|
643
|
+
)
|
|
644
|
+
if hasattr(params, "resolve_rqmc_target_std"):
|
|
645
|
+
target_std = params.resolve_rqmc_target_std(
|
|
646
|
+
product=product, pricing_env=pricing_env
|
|
647
|
+
)
|
|
648
|
+
else:
|
|
649
|
+
target_std = getattr(params, "target_std", 1e-4)
|
|
650
|
+
if hasattr(params, "resolve_rqmc_paths_per_batch"):
|
|
651
|
+
per_batch_paths = params.resolve_rqmc_paths_per_batch(
|
|
652
|
+
max_batches=max_batches
|
|
653
|
+
)
|
|
654
|
+
else:
|
|
655
|
+
per_batch_paths = params.num_paths
|
|
656
|
+
|
|
657
|
+
# Create path generator
|
|
658
|
+
maturity_for_sim = all_times[-1]
|
|
659
|
+
generator = self._create_path_generator(
|
|
660
|
+
S,
|
|
661
|
+
r,
|
|
662
|
+
q,
|
|
663
|
+
sigma,
|
|
664
|
+
maturity_for_sim,
|
|
665
|
+
dt_array,
|
|
666
|
+
num_paths=per_batch_paths,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
discount_factor = math.exp(-r * T)
|
|
670
|
+
|
|
671
|
+
def pricer_fn(paths, aux):
|
|
672
|
+
"""Pricer function for RQMC driver."""
|
|
673
|
+
payoffs, _ = self._compute_payoffs(
|
|
674
|
+
product,
|
|
675
|
+
pricing_env,
|
|
676
|
+
paths,
|
|
677
|
+
future_obs_indices,
|
|
678
|
+
past_obs,
|
|
679
|
+
future_obs,
|
|
680
|
+
total_weights,
|
|
681
|
+
)
|
|
682
|
+
return discount_factor * payoffs
|
|
683
|
+
|
|
684
|
+
result = run_rqmc(
|
|
685
|
+
pricer_fn=pricer_fn,
|
|
686
|
+
path_generator=generator,
|
|
687
|
+
max_batches=max_batches,
|
|
688
|
+
target_std=target_std,
|
|
689
|
+
min_batches=min_batches,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Run one more batch to get ratio statistics
|
|
693
|
+
paths, _ = generator.generate_paths(return_aux=False, batch_id=0)
|
|
694
|
+
_, in_range_ratios = self._compute_payoffs(
|
|
695
|
+
product,
|
|
696
|
+
pricing_env,
|
|
697
|
+
paths,
|
|
698
|
+
future_obs_indices,
|
|
699
|
+
past_obs,
|
|
700
|
+
future_obs,
|
|
701
|
+
total_weights,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
return RangeAccrualMCResult(
|
|
705
|
+
price=result.price,
|
|
706
|
+
std_error=result.std_error,
|
|
707
|
+
num_paths=result.total_paths,
|
|
708
|
+
in_range_ratio_mean=float(in_range_ratios.mean()),
|
|
709
|
+
in_range_ratio_std=float(in_range_ratios.std(ddof=1)),
|
|
710
|
+
num_past_observations=num_past,
|
|
711
|
+
num_future_observations=num_future,
|
|
712
|
+
past_in_range_weights=past_in_range_weights,
|
|
713
|
+
total_weights=total_weights,
|
|
714
|
+
batches_used=result.batches_used,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def get_last_result(self) -> Optional[RangeAccrualMCResult]:
|
|
718
|
+
"""
|
|
719
|
+
Get the full result from the last pricing run.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
RangeAccrualMCResult object, or None if no pricing has been performed
|
|
723
|
+
"""
|
|
724
|
+
return self._last_result
|
|
725
|
+
|
|
726
|
+
def get_last_std_error(self) -> Optional[float]:
|
|
727
|
+
"""
|
|
728
|
+
Get the standard error from the last pricing run.
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Standard error, or None if no pricing has been performed
|
|
732
|
+
"""
|
|
733
|
+
if self._last_result is None:
|
|
734
|
+
return None
|
|
735
|
+
return self._last_result.std_error
|
|
736
|
+
|
|
737
|
+
def __repr__(self):
|
|
738
|
+
return f"RangeAccrualMCEngine(method={self.method.name})"
|