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,694 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on Mon Nov 17 2025
|
|
3
|
+
|
|
4
|
+
@author: yaofuxin
|
|
5
|
+
@description: MC/QMC GBM/BSM path generators built on top of generic random
|
|
6
|
+
streams, Brownian bridge utilities and variance reduction
|
|
7
|
+
helpers. The generators are NumPy-first and do not depend on
|
|
8
|
+
Dask, making them suitable for use in production pricing
|
|
9
|
+
engines.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Dict, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from .qmc_brownian_bridge import apply_brownian_bridge
|
|
20
|
+
from .qmc_sobol import PseudoRandomNormalGenerator, RandomStream, SobolNormalGenerator
|
|
21
|
+
from .qmc_variance_reduction import (
|
|
22
|
+
VarianceReductionConfig,
|
|
23
|
+
apply_variance_reduction_to_normals,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_time_grid(
|
|
28
|
+
maturity: float,
|
|
29
|
+
time_steps: int,
|
|
30
|
+
dt_array: Optional[np.ndarray] = None,
|
|
31
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
32
|
+
"""
|
|
33
|
+
Build a time grid and associated dt array.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
maturity : float
|
|
38
|
+
Option maturity.
|
|
39
|
+
time_steps : int
|
|
40
|
+
Number of time steps.
|
|
41
|
+
dt_array : np.ndarray, optional
|
|
42
|
+
Custom time step sizes. If provided, it must have length equal to
|
|
43
|
+
time_steps and sum to maturity.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
times : np.ndarray
|
|
48
|
+
Time grid of shape (time_steps,) with strictly increasing values.
|
|
49
|
+
dt : np.ndarray
|
|
50
|
+
Time increments of shape (time_steps,).
|
|
51
|
+
"""
|
|
52
|
+
if time_steps <= 0:
|
|
53
|
+
raise ValueError("time_steps must be positive")
|
|
54
|
+
if maturity <= 0.0:
|
|
55
|
+
raise ValueError("maturity must be positive")
|
|
56
|
+
|
|
57
|
+
if dt_array is not None:
|
|
58
|
+
dt = np.asarray(dt_array, dtype=float)
|
|
59
|
+
if dt.ndim != 1 or dt.shape[0] != time_steps:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"dt_array must be one-dimensional with length {time_steps}, "
|
|
62
|
+
f"got shape {dt.shape}."
|
|
63
|
+
)
|
|
64
|
+
if np.any(dt <= 0.0):
|
|
65
|
+
raise ValueError("dt_array must have strictly positive entries")
|
|
66
|
+
if not np.isclose(dt.sum(), maturity):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Sum of dt_array ({dt.sum()}) must equal maturity ({maturity})."
|
|
69
|
+
)
|
|
70
|
+
times = np.cumsum(dt)
|
|
71
|
+
else:
|
|
72
|
+
dt_value = maturity / float(time_steps)
|
|
73
|
+
dt = np.full(time_steps, dt_value, dtype=float)
|
|
74
|
+
times = np.cumsum(dt)
|
|
75
|
+
|
|
76
|
+
return times, dt
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class GBMPathGenerator:
|
|
81
|
+
"""
|
|
82
|
+
General-purpose GBM/BSM path generator driven by a RandomStream.
|
|
83
|
+
|
|
84
|
+
This class supports both classical MC (with PseudoRandomNormalGenerator)
|
|
85
|
+
and QMC (with SobolNormalGenerator), with optional Brownian bridge and
|
|
86
|
+
variance reduction techniques.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
initial_value: float = 100.0
|
|
90
|
+
vol: float = 0.2
|
|
91
|
+
rrf: float = 0.0
|
|
92
|
+
div: float = 0.0
|
|
93
|
+
maturity: float = 1.0
|
|
94
|
+
time_steps: int = 244
|
|
95
|
+
num_paths: int = 10000
|
|
96
|
+
model: str = "bsm"
|
|
97
|
+
random_stream: Optional[RandomStream] = None
|
|
98
|
+
use_brownian_bridge: bool = False
|
|
99
|
+
vr_config: Optional[VarianceReductionConfig] = None
|
|
100
|
+
is_qmc: bool = False
|
|
101
|
+
dt_array: Optional[np.ndarray] = None
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
if self.initial_value <= 0.0:
|
|
105
|
+
raise ValueError("initial_value must be positive")
|
|
106
|
+
if self.vol < 0.0:
|
|
107
|
+
raise ValueError("vol must be non-negative")
|
|
108
|
+
if self.num_paths <= 0:
|
|
109
|
+
raise ValueError("num_paths must be positive")
|
|
110
|
+
|
|
111
|
+
valid_models = ["bsm", "black", "gbm"]
|
|
112
|
+
if self.model not in valid_models:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Invalid model '{self.model}', choose from {valid_models}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Default to pseudorandom generator if none is provided
|
|
118
|
+
if self.random_stream is None:
|
|
119
|
+
self.random_stream = PseudoRandomNormalGenerator()
|
|
120
|
+
self.is_qmc = False
|
|
121
|
+
|
|
122
|
+
# Precompute drift based on model type
|
|
123
|
+
self._set_drift()
|
|
124
|
+
|
|
125
|
+
# Build time grid
|
|
126
|
+
self.times, self.dt_vector = _build_time_grid(
|
|
127
|
+
maturity=self.maturity,
|
|
128
|
+
time_steps=self.time_steps,
|
|
129
|
+
dt_array=self.dt_array,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _set_drift(self) -> None:
|
|
133
|
+
if self.model == "black":
|
|
134
|
+
self.drift = self.rrf
|
|
135
|
+
elif self.model == "bsm":
|
|
136
|
+
self.drift = self.rrf - self.div
|
|
137
|
+
elif self.model == "gbm":
|
|
138
|
+
self.drift = 0.0
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(f"Invalid model '{self.model}'")
|
|
141
|
+
|
|
142
|
+
def _generate_base_normals(self, batch_id: Optional[int]) -> np.ndarray:
|
|
143
|
+
"""
|
|
144
|
+
Generate base standard normals before variance reduction is applied.
|
|
145
|
+
|
|
146
|
+
For antithetic variates in MC mode, we generate ceil(num_paths / 2)
|
|
147
|
+
base paths so that the expanded matrix after pairing reaches at least
|
|
148
|
+
num_paths paths.
|
|
149
|
+
"""
|
|
150
|
+
dim = self.time_steps
|
|
151
|
+
|
|
152
|
+
if self.vr_config is not None and self.vr_config.antithetic and not self.is_qmc:
|
|
153
|
+
n_base = (self.num_paths + 1) // 2
|
|
154
|
+
else:
|
|
155
|
+
n_base = self.num_paths
|
|
156
|
+
|
|
157
|
+
if self.random_stream is None:
|
|
158
|
+
raise RuntimeError("Random stream is not configured")
|
|
159
|
+
|
|
160
|
+
return self.random_stream.normal(n_paths=n_base, dim=dim, batch_id=batch_id)
|
|
161
|
+
|
|
162
|
+
def _build_brownian_increments(self, z: np.ndarray) -> np.ndarray:
|
|
163
|
+
"""
|
|
164
|
+
Convert standard normals to Brownian increments dW_t.
|
|
165
|
+
|
|
166
|
+
If use_brownian_bridge is True, a Brownian bridge is used to map the
|
|
167
|
+
normals into Brownian motion and then to increments. Otherwise, dW_t
|
|
168
|
+
= sqrt(dt_t) * Z_t is used directly.
|
|
169
|
+
"""
|
|
170
|
+
if self.use_brownian_bridge:
|
|
171
|
+
return apply_brownian_bridge(z, self.times)
|
|
172
|
+
|
|
173
|
+
# Direct construction without Brownian bridge
|
|
174
|
+
sqrt_dt = np.sqrt(self.dt_vector).reshape(1, -1)
|
|
175
|
+
return sqrt_dt * z
|
|
176
|
+
|
|
177
|
+
def generate_paths(
|
|
178
|
+
self,
|
|
179
|
+
seed: Optional[int] = None,
|
|
180
|
+
batch_id: Optional[int] = None,
|
|
181
|
+
return_aux: bool = False,
|
|
182
|
+
) -> Tuple[np.ndarray, Optional[Dict[str, np.ndarray]]]:
|
|
183
|
+
"""
|
|
184
|
+
Generate GBM/BSM paths.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
seed : int, optional
|
|
189
|
+
Seed used for pseudorandom streams. If the underlying random
|
|
190
|
+
stream supports seeding via constructor only (e.g., Sobol),
|
|
191
|
+
this argument is ignored.
|
|
192
|
+
batch_id : int, optional
|
|
193
|
+
Batch identifier for RQMC usage. Different batch_ids produce
|
|
194
|
+
independent randomized Sobol batches.
|
|
195
|
+
return_aux : bool
|
|
196
|
+
If True, also return auxiliary data such as importance sampling
|
|
197
|
+
weights and control variates.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
paths : np.ndarray
|
|
202
|
+
Simulated paths of shape (num_paths, time_steps + 1).
|
|
203
|
+
aux : dict or None
|
|
204
|
+
Dictionary containing auxiliary arrays if return_aux is True.
|
|
205
|
+
Keys:
|
|
206
|
+
- "weights": importance sampling weights (if enabled)
|
|
207
|
+
- "control_variate": GBM control variate (if enabled)
|
|
208
|
+
"""
|
|
209
|
+
# Optional reseeding for pseudorandom generator
|
|
210
|
+
if seed is not None and isinstance(
|
|
211
|
+
self.random_stream, PseudoRandomNormalGenerator
|
|
212
|
+
):
|
|
213
|
+
self.random_stream = PseudoRandomNormalGenerator(seed=seed)
|
|
214
|
+
|
|
215
|
+
base_normals = self._generate_base_normals(batch_id=batch_id)
|
|
216
|
+
|
|
217
|
+
# Variance reduction applied at the level of standard normals
|
|
218
|
+
z_processed, weights, control_variate = apply_variance_reduction_to_normals(
|
|
219
|
+
n_paths=self.num_paths,
|
|
220
|
+
dim=self.time_steps,
|
|
221
|
+
base_normals=base_normals,
|
|
222
|
+
vr_config=self.vr_config,
|
|
223
|
+
is_qmc=self.is_qmc,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Convert to Brownian increments
|
|
227
|
+
dW = self._build_brownian_increments(z_processed)
|
|
228
|
+
|
|
229
|
+
# Build GBM/BSM paths
|
|
230
|
+
paths = np.zeros((self.num_paths, self.time_steps + 1), dtype=float)
|
|
231
|
+
paths[:, 0] = self.initial_value
|
|
232
|
+
|
|
233
|
+
drift_term = (self.drift - 0.5 * self.vol * self.vol) * self.dt_vector
|
|
234
|
+
drift_term = drift_term.reshape(1, -1)
|
|
235
|
+
|
|
236
|
+
diffusion_term = self.vol * dW
|
|
237
|
+
exp_term = np.exp(drift_term + diffusion_term)
|
|
238
|
+
|
|
239
|
+
paths[:, 1:] = self.initial_value * np.cumprod(exp_term, axis=1)
|
|
240
|
+
|
|
241
|
+
aux: Optional[Dict[str, np.ndarray]] = None
|
|
242
|
+
if return_aux:
|
|
243
|
+
aux = {}
|
|
244
|
+
# Include batch identifier for downstream engines (e.g., barrier corrections)
|
|
245
|
+
aux["batch_id"] = np.array(batch_id if batch_id is not None else 0)
|
|
246
|
+
if weights is not None:
|
|
247
|
+
aux["weights"] = weights
|
|
248
|
+
if control_variate is not None:
|
|
249
|
+
aux["control_variate"] = control_variate
|
|
250
|
+
|
|
251
|
+
return paths, aux
|
|
252
|
+
|
|
253
|
+
def generate_terminal_values_qmc(
|
|
254
|
+
self,
|
|
255
|
+
batch_id: Optional[int] = None,
|
|
256
|
+
seed: Optional[int] = None,
|
|
257
|
+
dim: int = 1,
|
|
258
|
+
) -> np.ndarray:
|
|
259
|
+
"""
|
|
260
|
+
Generate terminal asset values S_T using low-dimensional QMC.
|
|
261
|
+
|
|
262
|
+
This method is intended for path-independent payoffs, where using a
|
|
263
|
+
low-dimensional Sobol sequence (e.g., 1D) provides excellent
|
|
264
|
+
effectiveness and avoids the high-dimensional degradation that occurs
|
|
265
|
+
when full paths are simulated.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
batch_id : int, optional
|
|
270
|
+
Batch identifier for RQMC usage.
|
|
271
|
+
seed : int, optional
|
|
272
|
+
Seed for pseudorandom streams; ignored for Sobol-based streams.
|
|
273
|
+
dim : int
|
|
274
|
+
Sobol dimension to use (typically 1 or 2). Only the first column
|
|
275
|
+
is used to construct the terminal value.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
np.ndarray
|
|
280
|
+
Array of terminal values S_T of shape (num_paths,).
|
|
281
|
+
"""
|
|
282
|
+
if seed is not None and isinstance(
|
|
283
|
+
self.random_stream, PseudoRandomNormalGenerator
|
|
284
|
+
):
|
|
285
|
+
self.random_stream = PseudoRandomNormalGenerator(seed=seed)
|
|
286
|
+
|
|
287
|
+
# Use low-dimensional normals for the terminal value
|
|
288
|
+
z = self.random_stream.normal(self.num_paths, dim, batch_id=batch_id)
|
|
289
|
+
z_1d = z[:, 0]
|
|
290
|
+
|
|
291
|
+
T = self.maturity
|
|
292
|
+
drift_term = (self.drift - 0.5 * self.vol * self.vol) * T
|
|
293
|
+
diffusion_term = self.vol * np.sqrt(T) * z_1d
|
|
294
|
+
|
|
295
|
+
return self.initial_value * np.exp(drift_term + diffusion_term)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class GBMPathGeneratorQMC(GBMPathGenerator):
|
|
300
|
+
"""
|
|
301
|
+
Convenience subclass for QMC-based GBM path generation.
|
|
302
|
+
|
|
303
|
+
This simply defaults to using SobolNormalGenerator and sets is_qmc=True.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __post_init__(self) -> None:
|
|
307
|
+
if self.random_stream is None:
|
|
308
|
+
self.random_stream = SobolNormalGenerator()
|
|
309
|
+
self.is_qmc = True
|
|
310
|
+
super().__post_init__()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass
|
|
314
|
+
class MultiAssetGBMPathGenerator:
|
|
315
|
+
"""
|
|
316
|
+
Multi-asset GBM/BSM path generator with correlation support.
|
|
317
|
+
|
|
318
|
+
This class generates correlated paths for multiple assets using Cholesky
|
|
319
|
+
decomposition to transform independent standard normals into correlated
|
|
320
|
+
normals. It supports both classical MC (with PseudoRandomNormalGenerator)
|
|
321
|
+
and QMC (with SobolNormalGenerator), with optional Brownian bridge per
|
|
322
|
+
asset for improved QMC effectiveness.
|
|
323
|
+
|
|
324
|
+
The dimension layout for QMC is: dimensions 0 to T-1 for asset 0,
|
|
325
|
+
dimensions T to 2T-1 for asset 1, etc. This grouping allows per-asset
|
|
326
|
+
Brownian bridge construction.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
initial_values : np.ndarray
|
|
331
|
+
Initial spot prices for each asset, shape (n_assets,).
|
|
332
|
+
vols : np.ndarray
|
|
333
|
+
Volatilities for each asset, shape (n_assets,).
|
|
334
|
+
rrfs : np.ndarray
|
|
335
|
+
Risk-free rates for each asset, shape (n_assets,).
|
|
336
|
+
divs : np.ndarray
|
|
337
|
+
Dividend yields for each asset, shape (n_assets,).
|
|
338
|
+
correlation_matrix : np.ndarray
|
|
339
|
+
Correlation matrix of shape (n_assets, n_assets). Must be symmetric
|
|
340
|
+
and positive semi-definite.
|
|
341
|
+
maturity : float
|
|
342
|
+
Time to maturity in years.
|
|
343
|
+
time_steps : int
|
|
344
|
+
Number of time steps for path discretization.
|
|
345
|
+
num_paths : int
|
|
346
|
+
Number of Monte Carlo paths to generate.
|
|
347
|
+
model : str
|
|
348
|
+
Model type: "bsm" (Black-Scholes-Merton), "black", or "gbm".
|
|
349
|
+
random_stream : RandomStream, optional
|
|
350
|
+
Random stream for generating normals. Defaults to pseudorandom.
|
|
351
|
+
use_brownian_bridge : bool
|
|
352
|
+
Whether to use Brownian bridge construction for each asset.
|
|
353
|
+
vr_config : VarianceReductionConfig, optional
|
|
354
|
+
Variance reduction configuration (antithetic not supported for QMC).
|
|
355
|
+
is_qmc : bool
|
|
356
|
+
Whether this is a QMC-based generator.
|
|
357
|
+
dt_array : np.ndarray, optional
|
|
358
|
+
Custom time step sizes.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
initial_values: np.ndarray = None
|
|
362
|
+
vols: np.ndarray = None
|
|
363
|
+
rrfs: np.ndarray = None
|
|
364
|
+
divs: np.ndarray = None
|
|
365
|
+
correlation_matrix: np.ndarray = None
|
|
366
|
+
maturity: float = 1.0
|
|
367
|
+
time_steps: int = 244
|
|
368
|
+
num_paths: int = 10000
|
|
369
|
+
model: str = "bsm"
|
|
370
|
+
random_stream: Optional[RandomStream] = None
|
|
371
|
+
use_brownian_bridge: bool = False
|
|
372
|
+
vr_config: Optional[VarianceReductionConfig] = None
|
|
373
|
+
is_qmc: bool = False
|
|
374
|
+
dt_array: Optional[np.ndarray] = None
|
|
375
|
+
|
|
376
|
+
def __post_init__(self) -> None:
|
|
377
|
+
# Convert inputs to numpy arrays
|
|
378
|
+
self.initial_values = np.asarray(self.initial_values, dtype=float)
|
|
379
|
+
self.vols = np.asarray(self.vols, dtype=float)
|
|
380
|
+
self.rrfs = np.asarray(self.rrfs, dtype=float)
|
|
381
|
+
self.divs = np.asarray(self.divs, dtype=float)
|
|
382
|
+
self.correlation_matrix = np.asarray(self.correlation_matrix, dtype=float)
|
|
383
|
+
|
|
384
|
+
# Determine number of assets
|
|
385
|
+
self.n_assets = self.initial_values.shape[0]
|
|
386
|
+
|
|
387
|
+
# Validate shapes
|
|
388
|
+
if self.vols.shape != (self.n_assets,):
|
|
389
|
+
raise ValueError(
|
|
390
|
+
f"vols must have shape ({self.n_assets},), got {self.vols.shape}"
|
|
391
|
+
)
|
|
392
|
+
if self.rrfs.shape != (self.n_assets,):
|
|
393
|
+
raise ValueError(
|
|
394
|
+
f"rrfs must have shape ({self.n_assets},), got {self.rrfs.shape}"
|
|
395
|
+
)
|
|
396
|
+
if self.divs.shape != (self.n_assets,):
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"divs must have shape ({self.n_assets},), got {self.divs.shape}"
|
|
399
|
+
)
|
|
400
|
+
if self.correlation_matrix.shape != (self.n_assets, self.n_assets):
|
|
401
|
+
raise ValueError(
|
|
402
|
+
f"correlation_matrix must have shape ({self.n_assets}, {self.n_assets}), "
|
|
403
|
+
f"got {self.correlation_matrix.shape}"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Validate values
|
|
407
|
+
if np.any(self.initial_values <= 0.0):
|
|
408
|
+
raise ValueError("All initial_values must be positive")
|
|
409
|
+
if np.any(self.vols < 0.0):
|
|
410
|
+
raise ValueError("All vols must be non-negative")
|
|
411
|
+
if self.num_paths <= 0:
|
|
412
|
+
raise ValueError("num_paths must be positive")
|
|
413
|
+
|
|
414
|
+
# Validate correlation matrix
|
|
415
|
+
if not np.allclose(self.correlation_matrix, self.correlation_matrix.T):
|
|
416
|
+
raise ValueError("correlation_matrix must be symmetric")
|
|
417
|
+
if not np.allclose(np.diag(self.correlation_matrix), 1.0):
|
|
418
|
+
raise ValueError("correlation_matrix diagonal must be all ones")
|
|
419
|
+
|
|
420
|
+
valid_models = ["bsm", "black", "gbm"]
|
|
421
|
+
if self.model not in valid_models:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Invalid model '{self.model}', choose from {valid_models}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Default to pseudorandom generator if none is provided
|
|
427
|
+
if self.random_stream is None:
|
|
428
|
+
self.random_stream = PseudoRandomNormalGenerator()
|
|
429
|
+
self.is_qmc = False
|
|
430
|
+
|
|
431
|
+
# Compute Cholesky decomposition for correlation
|
|
432
|
+
try:
|
|
433
|
+
self.cholesky_L = np.linalg.cholesky(self.correlation_matrix)
|
|
434
|
+
except np.linalg.LinAlgError:
|
|
435
|
+
raise ValueError(
|
|
436
|
+
"correlation_matrix must be positive semi-definite. "
|
|
437
|
+
"Cholesky decomposition failed."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Precompute drifts based on model type
|
|
441
|
+
self._set_drifts()
|
|
442
|
+
|
|
443
|
+
# Build time grid
|
|
444
|
+
self.times, self.dt_vector = _build_time_grid(
|
|
445
|
+
maturity=self.maturity,
|
|
446
|
+
time_steps=self.time_steps,
|
|
447
|
+
dt_array=self.dt_array,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def _set_drifts(self) -> None:
|
|
451
|
+
"""Set drift for each asset based on model type."""
|
|
452
|
+
if self.model == "black":
|
|
453
|
+
self.drifts = self.rrfs.copy()
|
|
454
|
+
elif self.model == "bsm":
|
|
455
|
+
self.drifts = self.rrfs - self.divs
|
|
456
|
+
elif self.model == "gbm":
|
|
457
|
+
self.drifts = np.zeros(self.n_assets, dtype=float)
|
|
458
|
+
else:
|
|
459
|
+
raise ValueError(f"Invalid model '{self.model}'")
|
|
460
|
+
|
|
461
|
+
def _generate_base_normals(self, batch_id: Optional[int]) -> np.ndarray:
|
|
462
|
+
"""
|
|
463
|
+
Generate base independent standard normals for all assets and time steps.
|
|
464
|
+
|
|
465
|
+
The dimension is n_assets * time_steps, laid out so that dimensions
|
|
466
|
+
0 to time_steps-1 correspond to asset 0, etc.
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
np.ndarray
|
|
471
|
+
Array of shape (n_paths, n_assets * time_steps).
|
|
472
|
+
"""
|
|
473
|
+
total_dim = self.n_assets * self.time_steps
|
|
474
|
+
|
|
475
|
+
if self.vr_config is not None and self.vr_config.antithetic and not self.is_qmc:
|
|
476
|
+
n_base = (self.num_paths + 1) // 2
|
|
477
|
+
else:
|
|
478
|
+
n_base = self.num_paths
|
|
479
|
+
|
|
480
|
+
if self.random_stream is None:
|
|
481
|
+
raise RuntimeError("Random stream is not configured")
|
|
482
|
+
|
|
483
|
+
return self.random_stream.normal(
|
|
484
|
+
n_paths=n_base, dim=total_dim, batch_id=batch_id
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def _apply_correlation(self, z_independent: np.ndarray) -> np.ndarray:
|
|
488
|
+
"""
|
|
489
|
+
Apply Cholesky correlation to independent normals.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
z_independent : np.ndarray
|
|
494
|
+
Independent standard normals of shape (n_paths, n_assets, time_steps).
|
|
495
|
+
|
|
496
|
+
Returns
|
|
497
|
+
-------
|
|
498
|
+
np.ndarray
|
|
499
|
+
Correlated standard normals of shape (n_paths, n_assets, time_steps).
|
|
500
|
+
"""
|
|
501
|
+
n_paths, n_assets, time_steps = z_independent.shape
|
|
502
|
+
|
|
503
|
+
# Apply Cholesky: for each time step, correlate across assets
|
|
504
|
+
# z_correlated[i, :, t] = L @ z_independent[i, :, t]
|
|
505
|
+
# Using einsum for efficient batch matrix multiplication
|
|
506
|
+
z_correlated = np.einsum("ij,kjt->kit", self.cholesky_L, z_independent)
|
|
507
|
+
|
|
508
|
+
return z_correlated
|
|
509
|
+
|
|
510
|
+
def _build_brownian_increments_per_asset(
|
|
511
|
+
self, z: np.ndarray, asset_idx: int
|
|
512
|
+
) -> np.ndarray:
|
|
513
|
+
"""
|
|
514
|
+
Convert standard normals to Brownian increments for a single asset.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
z : np.ndarray
|
|
519
|
+
Standard normals for one asset, shape (n_paths, time_steps).
|
|
520
|
+
asset_idx : int
|
|
521
|
+
Asset index (for debugging/logging purposes).
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
np.ndarray
|
|
526
|
+
Brownian increments dW of shape (n_paths, time_steps).
|
|
527
|
+
"""
|
|
528
|
+
if self.use_brownian_bridge:
|
|
529
|
+
return apply_brownian_bridge(z, self.times)
|
|
530
|
+
|
|
531
|
+
# Direct construction without Brownian bridge
|
|
532
|
+
sqrt_dt = np.sqrt(self.dt_vector).reshape(1, -1)
|
|
533
|
+
return sqrt_dt * z
|
|
534
|
+
|
|
535
|
+
def generate_paths(
|
|
536
|
+
self,
|
|
537
|
+
seed: Optional[int] = None,
|
|
538
|
+
batch_id: Optional[int] = None,
|
|
539
|
+
return_aux: bool = False,
|
|
540
|
+
) -> Tuple[np.ndarray, Optional[Dict[str, np.ndarray]]]:
|
|
541
|
+
"""
|
|
542
|
+
Generate correlated GBM/BSM paths for all assets.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
seed : int, optional
|
|
547
|
+
Seed used for pseudorandom streams. Ignored for Sobol-based streams.
|
|
548
|
+
batch_id : int, optional
|
|
549
|
+
Batch identifier for RQMC usage. Different batch_ids produce
|
|
550
|
+
independent randomized Sobol batches.
|
|
551
|
+
return_aux : bool
|
|
552
|
+
If True, also return auxiliary data such as importance sampling
|
|
553
|
+
weights.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
paths : np.ndarray
|
|
558
|
+
Simulated paths of shape (n_assets, num_paths, time_steps + 1).
|
|
559
|
+
aux : dict or None
|
|
560
|
+
Dictionary containing auxiliary arrays if return_aux is True.
|
|
561
|
+
Keys:
|
|
562
|
+
- "weights": importance sampling weights (if enabled)
|
|
563
|
+
"""
|
|
564
|
+
# Optional reseeding for pseudorandom generator
|
|
565
|
+
if seed is not None and isinstance(
|
|
566
|
+
self.random_stream, PseudoRandomNormalGenerator
|
|
567
|
+
):
|
|
568
|
+
self.random_stream = PseudoRandomNormalGenerator(seed=seed)
|
|
569
|
+
|
|
570
|
+
# Generate base normals: shape (n_paths or n_base, n_assets * time_steps)
|
|
571
|
+
base_normals = self._generate_base_normals(batch_id=batch_id)
|
|
572
|
+
|
|
573
|
+
# Apply variance reduction if configured
|
|
574
|
+
total_dim = self.n_assets * self.time_steps
|
|
575
|
+
z_processed, weights, _ = apply_variance_reduction_to_normals(
|
|
576
|
+
n_paths=self.num_paths,
|
|
577
|
+
dim=total_dim,
|
|
578
|
+
base_normals=base_normals,
|
|
579
|
+
vr_config=self.vr_config,
|
|
580
|
+
is_qmc=self.is_qmc,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Reshape to (n_paths, n_assets, time_steps)
|
|
584
|
+
# Layout: first time_steps columns are asset 0, next are asset 1, etc.
|
|
585
|
+
z_reshaped = z_processed.reshape(self.num_paths, self.n_assets, self.time_steps)
|
|
586
|
+
|
|
587
|
+
# Build Brownian increments per asset independently (with optional Brownian bridge)
|
|
588
|
+
# This preserves QMC effectiveness for each asset's marginal distribution
|
|
589
|
+
dW_independent = np.zeros((self.n_assets, self.num_paths, self.time_steps))
|
|
590
|
+
for i in range(self.n_assets):
|
|
591
|
+
dW_independent[i] = self._build_brownian_increments_per_asset(
|
|
592
|
+
z_reshaped[:, i, :], asset_idx=i
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Apply correlation to the Brownian increments
|
|
596
|
+
# Reshape for correlation: (n_paths, n_assets, time_steps)
|
|
597
|
+
dW_independent_transposed = np.transpose(dW_independent, (1, 0, 2))
|
|
598
|
+
dW_correlated = self._apply_correlation(dW_independent_transposed)
|
|
599
|
+
# Transpose back to (n_assets, n_paths, time_steps)
|
|
600
|
+
dW = np.transpose(dW_correlated, (1, 0, 2))
|
|
601
|
+
|
|
602
|
+
# Build GBM/BSM paths for each asset
|
|
603
|
+
paths = np.zeros(
|
|
604
|
+
(self.n_assets, self.num_paths, self.time_steps + 1), dtype=float
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
for i in range(self.n_assets):
|
|
608
|
+
paths[i, :, 0] = self.initial_values[i]
|
|
609
|
+
|
|
610
|
+
drift_term = (self.drifts[i] - 0.5 * self.vols[i] ** 2) * self.dt_vector
|
|
611
|
+
drift_term = drift_term.reshape(1, -1)
|
|
612
|
+
|
|
613
|
+
diffusion_term = self.vols[i] * dW[i]
|
|
614
|
+
exp_term = np.exp(drift_term + diffusion_term)
|
|
615
|
+
|
|
616
|
+
paths[i, :, 1:] = self.initial_values[i] * np.cumprod(exp_term, axis=1)
|
|
617
|
+
|
|
618
|
+
aux: Optional[Dict[str, np.ndarray]] = None
|
|
619
|
+
if return_aux:
|
|
620
|
+
aux = {}
|
|
621
|
+
if weights is not None:
|
|
622
|
+
aux["weights"] = weights
|
|
623
|
+
|
|
624
|
+
return paths, aux
|
|
625
|
+
|
|
626
|
+
def generate_terminal_values(
|
|
627
|
+
self,
|
|
628
|
+
batch_id: Optional[int] = None,
|
|
629
|
+
seed: Optional[int] = None,
|
|
630
|
+
) -> np.ndarray:
|
|
631
|
+
"""
|
|
632
|
+
Generate terminal asset values S_T for all assets using low-dimensional QMC.
|
|
633
|
+
|
|
634
|
+
This method is intended for path-independent payoffs on multiple assets,
|
|
635
|
+
where using a low-dimensional Sobol sequence (n_assets dimensions) provides
|
|
636
|
+
excellent effectiveness.
|
|
637
|
+
|
|
638
|
+
Parameters
|
|
639
|
+
----------
|
|
640
|
+
batch_id : int, optional
|
|
641
|
+
Batch identifier for RQMC usage.
|
|
642
|
+
seed : int, optional
|
|
643
|
+
Seed for pseudorandom streams; ignored for Sobol-based streams.
|
|
644
|
+
|
|
645
|
+
Returns
|
|
646
|
+
-------
|
|
647
|
+
np.ndarray
|
|
648
|
+
Array of terminal values of shape (n_assets, num_paths).
|
|
649
|
+
"""
|
|
650
|
+
if seed is not None and isinstance(
|
|
651
|
+
self.random_stream, PseudoRandomNormalGenerator
|
|
652
|
+
):
|
|
653
|
+
self.random_stream = PseudoRandomNormalGenerator(seed=seed)
|
|
654
|
+
|
|
655
|
+
# Generate low-dimensional normals: one dimension per asset
|
|
656
|
+
z = self.random_stream.normal(self.num_paths, self.n_assets, batch_id=batch_id)
|
|
657
|
+
|
|
658
|
+
# Apply correlation
|
|
659
|
+
z_correlated = (self.cholesky_L @ z.T).T # shape (num_paths, n_assets)
|
|
660
|
+
|
|
661
|
+
T = self.maturity
|
|
662
|
+
terminal_values = np.zeros((self.n_assets, self.num_paths), dtype=float)
|
|
663
|
+
|
|
664
|
+
for i in range(self.n_assets):
|
|
665
|
+
drift_term = (self.drifts[i] - 0.5 * self.vols[i] ** 2) * T
|
|
666
|
+
diffusion_term = self.vols[i] * np.sqrt(T) * z_correlated[:, i]
|
|
667
|
+
terminal_values[i] = self.initial_values[i] * np.exp(
|
|
668
|
+
drift_term + diffusion_term
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
return terminal_values
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@dataclass
|
|
675
|
+
class MultiAssetGBMPathGeneratorQMC(MultiAssetGBMPathGenerator):
|
|
676
|
+
"""
|
|
677
|
+
Convenience subclass for QMC-based multi-asset GBM path generation.
|
|
678
|
+
|
|
679
|
+
This simply defaults to using SobolNormalGenerator and sets is_qmc=True.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def __post_init__(self) -> None:
|
|
683
|
+
if self.random_stream is None:
|
|
684
|
+
self.random_stream = SobolNormalGenerator()
|
|
685
|
+
self.is_qmc = True
|
|
686
|
+
super().__post_init__()
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
__all__ = [
|
|
690
|
+
"GBMPathGenerator",
|
|
691
|
+
"GBMPathGeneratorQMC",
|
|
692
|
+
"MultiAssetGBMPathGenerator",
|
|
693
|
+
"MultiAssetGBMPathGeneratorQMC",
|
|
694
|
+
]
|