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,530 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observation schedule structures for barrier-like products.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from math import isclose
|
|
8
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
|
|
11
|
+
from quantark.util.enum import ObservationAggregation, TenorEnd, ObservationFrequency
|
|
12
|
+
from quantark.util.calendar import DayCountConvention
|
|
13
|
+
from quantark.util.exceptions import ValidationError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from quantark.priceenv import PricingEnvironment
|
|
17
|
+
|
|
18
|
+
# Type alias for cleaner signatures
|
|
19
|
+
PricingEnv = Optional["PricingEnvironment"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from quantark.util.calendar import calculate_year_fraction, calculate_day_count_fraction
|
|
24
|
+
except ImportError:
|
|
25
|
+
# Fallback for environments where calendar utilities are unavailable at import time.
|
|
26
|
+
calculate_year_fraction = None
|
|
27
|
+
calculate_day_count_fraction = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ObservationRecord:
|
|
32
|
+
"""
|
|
33
|
+
Single observation entry with optional per-date barrier and payoff data.
|
|
34
|
+
|
|
35
|
+
Fields for annualized rate handling:
|
|
36
|
+
is_rate_annualized: Whether return_rate is annualized (default: False)
|
|
37
|
+
initial_date: Start date for accrual period calculation
|
|
38
|
+
settlement_date: Settlement date (used if tenor_end = SETTLEMENT)
|
|
39
|
+
maturity_date: Maturity date (used if tenor_end = MATURITY)
|
|
40
|
+
day_count_convention: Per-observation day count convention override
|
|
41
|
+
tenor_end: Defines end date for accrual (SETTLEMENT/MATURITY/default to observation)
|
|
42
|
+
day_count_fraction: Pre-calculated day count fraction (optional, computed if not provided)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
observation_time: Optional[float] = None
|
|
46
|
+
observation_date: Optional[datetime] = None
|
|
47
|
+
barrier: Optional[float] = None
|
|
48
|
+
upper_barrier: Optional[float] = None
|
|
49
|
+
lower_barrier: Optional[float] = None
|
|
50
|
+
payoff: Optional[float] = None
|
|
51
|
+
return_rate: Optional[float] = None
|
|
52
|
+
is_rate_annualized: Optional[bool] = False
|
|
53
|
+
|
|
54
|
+
# Date-based accrual fields
|
|
55
|
+
initial_date: Optional[datetime] = None
|
|
56
|
+
settlement_date: Optional[datetime] = None
|
|
57
|
+
maturity_date: Optional[datetime] = None
|
|
58
|
+
day_count_convention: Optional[DayCountConvention] = None
|
|
59
|
+
tenor_end: Optional[TenorEnd] = None
|
|
60
|
+
day_count_fraction: Optional[float] = None
|
|
61
|
+
|
|
62
|
+
def resolve_time(self, pricing_env: PricingEnv) -> float:
|
|
63
|
+
"""Resolve observation time to a year fraction.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
pricing_env: PricingEnvironment containing valuation date and calendar conventions
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Year fraction from valuation date to observation date
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValidationError: If observation time/date cannot be resolved
|
|
73
|
+
"""
|
|
74
|
+
if self.observation_time is not None:
|
|
75
|
+
return self.observation_time
|
|
76
|
+
if self.observation_date is not None:
|
|
77
|
+
if pricing_env is None or calculate_year_fraction is None:
|
|
78
|
+
raise ValidationError(
|
|
79
|
+
"PricingEnvironment is required to resolve observation_date."
|
|
80
|
+
)
|
|
81
|
+
return calculate_year_fraction(
|
|
82
|
+
pricing_env.valuation_date,
|
|
83
|
+
self.observation_date,
|
|
84
|
+
pricing_env.day_count_convention,
|
|
85
|
+
pricing_env.bus_days_in_year,
|
|
86
|
+
calendar=getattr(pricing_env, "calendar", None),
|
|
87
|
+
)
|
|
88
|
+
raise ValidationError("ObservationRecord requires observation_time or observation_date.")
|
|
89
|
+
|
|
90
|
+
def get_payoff(self, default_payoff: float, pricing_env: PricingEnv = None) -> float:
|
|
91
|
+
"""
|
|
92
|
+
Return the payoff for this record, following the calculation flow:
|
|
93
|
+
1. If payoff is explicitly set, return it
|
|
94
|
+
2. If return_rate is not annualized, return return_rate
|
|
95
|
+
3. If day_count_fraction is pre-calculated, return day_count_fraction * return_rate
|
|
96
|
+
4. Calculate day_count_fraction based on tenor_end and dates, then return day_count_fraction * return_rate
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
default_payoff: Default payoff value if not specified
|
|
100
|
+
pricing_env: Optional pricing environment for date resolution
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Resolved payoff value for this observation
|
|
104
|
+
"""
|
|
105
|
+
# If explicit payoff is set, use it
|
|
106
|
+
if self.payoff is not None:
|
|
107
|
+
return self.payoff
|
|
108
|
+
|
|
109
|
+
# If no return_rate, use default
|
|
110
|
+
if self.return_rate is None:
|
|
111
|
+
return default_payoff
|
|
112
|
+
|
|
113
|
+
# If return_rate is not annualized, return it directly
|
|
114
|
+
if not self.is_rate_annualized:
|
|
115
|
+
return self.return_rate
|
|
116
|
+
|
|
117
|
+
# If day_count_fraction is pre-calculated, use it
|
|
118
|
+
if self.day_count_fraction is not None:
|
|
119
|
+
return self.day_count_fraction * self.return_rate
|
|
120
|
+
|
|
121
|
+
# Calculate day_count_fraction based on tenor_end
|
|
122
|
+
if self.initial_date is None:
|
|
123
|
+
# Cannot calculate day_count_fraction without initial_date
|
|
124
|
+
return self.return_rate
|
|
125
|
+
|
|
126
|
+
# Determine end_date based on tenor_end
|
|
127
|
+
end_date = self._determine_end_date()
|
|
128
|
+
if end_date is None:
|
|
129
|
+
# Cannot calculate day_count_fraction without end_date
|
|
130
|
+
return self.return_rate
|
|
131
|
+
|
|
132
|
+
# Calculate day_count_fraction
|
|
133
|
+
return self._calculate_annualized_payoff(end_date)
|
|
134
|
+
|
|
135
|
+
def _determine_end_date(self) -> Optional[datetime]:
|
|
136
|
+
"""Determine the end date for accrual period based on tenor_end setting."""
|
|
137
|
+
if self.tenor_end == TenorEnd.SETTLEMENT and self.settlement_date is not None:
|
|
138
|
+
return self.settlement_date
|
|
139
|
+
if self.tenor_end == TenorEnd.MATURITY and self.maturity_date is not None:
|
|
140
|
+
return self.maturity_date
|
|
141
|
+
if self.observation_date is not None:
|
|
142
|
+
return self.observation_date
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def _calculate_annualized_payoff(self, end_date: datetime) -> float:
|
|
146
|
+
"""Calculate annualized payoff using day count fraction."""
|
|
147
|
+
if self.day_count_convention is not None and calculate_day_count_fraction is not None:
|
|
148
|
+
try:
|
|
149
|
+
dcf = calculate_day_count_fraction(
|
|
150
|
+
self.initial_date, end_date, self.day_count_convention
|
|
151
|
+
)
|
|
152
|
+
return dcf * self.return_rate
|
|
153
|
+
except ImportError:
|
|
154
|
+
# If calculation fails due to import issues, return raw return_rate
|
|
155
|
+
return self.return_rate
|
|
156
|
+
|
|
157
|
+
# Fallback to raw return_rate if no day count convention
|
|
158
|
+
return self.return_rate
|
|
159
|
+
|
|
160
|
+
def validate(self, require_single: bool = False, require_double: bool = False) -> None:
|
|
161
|
+
"""Validate record fields."""
|
|
162
|
+
if self.observation_time is None and self.observation_date is None:
|
|
163
|
+
raise ValidationError("ObservationRecord must provide observation_time or observation_date.")
|
|
164
|
+
if require_single:
|
|
165
|
+
if self.barrier is None:
|
|
166
|
+
raise ValidationError("Single-barrier observation requires barrier level.")
|
|
167
|
+
if self.upper_barrier is not None or self.lower_barrier is not None:
|
|
168
|
+
raise ValidationError("Single-barrier observation must not include upper/lower barriers.")
|
|
169
|
+
if require_double:
|
|
170
|
+
if self.upper_barrier is None or self.lower_barrier is None:
|
|
171
|
+
raise ValidationError("Double-barrier observation requires both upper_barrier and lower_barrier.")
|
|
172
|
+
if self.upper_barrier <= 0 or self.lower_barrier <= 0:
|
|
173
|
+
raise ValidationError("Barrier levels must be positive.")
|
|
174
|
+
if self.lower_barrier >= self.upper_barrier:
|
|
175
|
+
raise ValidationError("lower_barrier must be less than upper_barrier.")
|
|
176
|
+
|
|
177
|
+
# Validate date consistency for annualized rates
|
|
178
|
+
if self.is_rate_annualized and self.return_rate is not None:
|
|
179
|
+
if self.day_count_fraction is None and self.initial_date is not None:
|
|
180
|
+
# Check that we have sufficient info to calculate day_count_fraction
|
|
181
|
+
has_end_date = (
|
|
182
|
+
(self.tenor_end == TenorEnd.SETTLEMENT and self.settlement_date is not None)
|
|
183
|
+
or (self.tenor_end == TenorEnd.MATURITY and self.maturity_date is not None)
|
|
184
|
+
or self.observation_date is not None
|
|
185
|
+
)
|
|
186
|
+
if not has_end_date:
|
|
187
|
+
raise ValidationError(
|
|
188
|
+
"Annualized return_rate requires either day_count_fraction, "
|
|
189
|
+
"or initial_date with appropriate tenor_end and corresponding date."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class ResolvedObservationRecord:
|
|
195
|
+
"""
|
|
196
|
+
ObservationRecord with resolved time and concrete barrier/payoff values.
|
|
197
|
+
|
|
198
|
+
This is the final state used by pricing engines. It contains only:
|
|
199
|
+
- observation_time: When to check for barrier breach
|
|
200
|
+
- barrier levels: What levels to check against
|
|
201
|
+
- payoff: The final payoff amount if barrier is hit
|
|
202
|
+
- settlement_time: When the payoff is settled (for discounting)
|
|
203
|
+
|
|
204
|
+
All intermediate calculation values (return_rate, day_count_fraction)
|
|
205
|
+
have been consumed during resolution to produce the final payoff.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
observation_time: float
|
|
209
|
+
barrier: Optional[float] = None
|
|
210
|
+
upper_barrier: Optional[float] = None
|
|
211
|
+
lower_barrier: Optional[float] = None
|
|
212
|
+
payoff: float = 0.0
|
|
213
|
+
settlement_time: Optional[float] = None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass
|
|
217
|
+
class ObservationSchedule:
|
|
218
|
+
"""Ordered schedule of observation records with aggregation semantics."""
|
|
219
|
+
|
|
220
|
+
records: List[ObservationRecord] = field(default_factory=list)
|
|
221
|
+
aggregation_mode: ObservationAggregation = ObservationAggregation.STOP_FIRST_HIT
|
|
222
|
+
frequency: ObservationFrequency = ObservationFrequency.CUSTOM
|
|
223
|
+
|
|
224
|
+
def validate(self, require_single: bool = False, require_double: bool = False) -> None:
|
|
225
|
+
"""Validate schedule ordering and record completeness."""
|
|
226
|
+
if len(self.records) == 0:
|
|
227
|
+
raise ValidationError("ObservationSchedule must contain at least one record for discrete monitoring.")
|
|
228
|
+
if not isinstance(self.aggregation_mode, ObservationAggregation):
|
|
229
|
+
raise ValidationError(f"Invalid aggregation mode: {self.aggregation_mode}")
|
|
230
|
+
times: List[float] = []
|
|
231
|
+
for rec in self.records:
|
|
232
|
+
rec.validate(require_single=require_single, require_double=require_double)
|
|
233
|
+
if rec.observation_time is not None:
|
|
234
|
+
times.append(rec.observation_time)
|
|
235
|
+
if times and not self._is_sorted(times):
|
|
236
|
+
raise ValidationError("Observation records must be ordered by observation_time.")
|
|
237
|
+
|
|
238
|
+
def uses_dates(self) -> bool:
|
|
239
|
+
"""Return True if any record uses observation_date."""
|
|
240
|
+
return any(
|
|
241
|
+
getattr(rec, "observation_date", None) is not None for rec in self.records
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def uses_times(self) -> bool:
|
|
245
|
+
"""Return True if any record uses observation_time."""
|
|
246
|
+
return any(
|
|
247
|
+
getattr(rec, "observation_time", None) is not None for rec in self.records
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def time_shift(
|
|
251
|
+
self, time_bump: float, bumped_date: Optional[datetime] = None
|
|
252
|
+
) -> Optional["ObservationSchedule"]:
|
|
253
|
+
"""
|
|
254
|
+
Shift observation schedule by time_bump and drop past records.
|
|
255
|
+
|
|
256
|
+
- observation_time is reduced by time_bump
|
|
257
|
+
- observation_date records are dropped if <= bumped_date
|
|
258
|
+
"""
|
|
259
|
+
if not self.records:
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
updated_records = []
|
|
263
|
+
for rec in self.records:
|
|
264
|
+
rec_copy = deepcopy(rec)
|
|
265
|
+
|
|
266
|
+
if getattr(rec_copy, "observation_date", None) is not None:
|
|
267
|
+
if bumped_date is not None and rec_copy.observation_date <= bumped_date:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if getattr(rec_copy, "observation_time", None) is not None:
|
|
271
|
+
adjusted_time = rec_copy.observation_time - time_bump
|
|
272
|
+
if adjusted_time <= 0:
|
|
273
|
+
continue
|
|
274
|
+
rec_copy.observation_time = adjusted_time
|
|
275
|
+
|
|
276
|
+
updated_records.append(rec_copy)
|
|
277
|
+
|
|
278
|
+
if not updated_records:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
return ObservationSchedule(
|
|
282
|
+
records=updated_records,
|
|
283
|
+
aggregation_mode=self.aggregation_mode,
|
|
284
|
+
frequency=self.frequency,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def resolve(
|
|
288
|
+
self,
|
|
289
|
+
pricing_env,
|
|
290
|
+
default_barrier: Optional[float] = None,
|
|
291
|
+
default_upper: Optional[float] = None,
|
|
292
|
+
default_lower: Optional[float] = None,
|
|
293
|
+
default_payoff: float = 0.0,
|
|
294
|
+
require_single: bool = False,
|
|
295
|
+
require_double: bool = False,
|
|
296
|
+
) -> List[ResolvedObservationRecord]:
|
|
297
|
+
"""
|
|
298
|
+
Resolve records to concrete numeric times and defaults.
|
|
299
|
+
Payoff calculation is delegated to ObservationRecord.get_payoff().
|
|
300
|
+
"""
|
|
301
|
+
self.validate(require_single=require_single, require_double=require_double)
|
|
302
|
+
|
|
303
|
+
resolved: List[ResolvedObservationRecord] = []
|
|
304
|
+
times: List[float] = []
|
|
305
|
+
|
|
306
|
+
for rec in self.records:
|
|
307
|
+
# Resolve observation time
|
|
308
|
+
t = rec.resolve_time(pricing_env)
|
|
309
|
+
times.append(t)
|
|
310
|
+
|
|
311
|
+
# Get payoff using the record's get_payoff method
|
|
312
|
+
payoff_value = rec.get_payoff(default_payoff, pricing_env)
|
|
313
|
+
|
|
314
|
+
# Resolve settlement time
|
|
315
|
+
settlement_t: Optional[float] = None
|
|
316
|
+
if rec.settlement_date is not None and calculate_year_fraction is not None:
|
|
317
|
+
try:
|
|
318
|
+
settlement_t = calculate_year_fraction(
|
|
319
|
+
pricing_env.valuation_date,
|
|
320
|
+
rec.settlement_date,
|
|
321
|
+
pricing_env.day_count_convention,
|
|
322
|
+
pricing_env.bus_days_in_year,
|
|
323
|
+
calendar=getattr(pricing_env, "calendar", None),
|
|
324
|
+
)
|
|
325
|
+
except Exception:
|
|
326
|
+
# If calculation fails, default to observation time
|
|
327
|
+
settlement_t = t
|
|
328
|
+
else:
|
|
329
|
+
# Default: settlement at observation time
|
|
330
|
+
settlement_t = t
|
|
331
|
+
|
|
332
|
+
resolved.append(
|
|
333
|
+
ResolvedObservationRecord(
|
|
334
|
+
observation_time=t,
|
|
335
|
+
barrier=rec.barrier if rec.barrier is not None else default_barrier,
|
|
336
|
+
upper_barrier=rec.upper_barrier if rec.upper_barrier is not None else default_upper,
|
|
337
|
+
lower_barrier=rec.lower_barrier if rec.lower_barrier is not None else default_lower,
|
|
338
|
+
payoff=payoff_value,
|
|
339
|
+
settlement_time=settlement_t,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if times and not self._is_sorted(times):
|
|
344
|
+
raise ValidationError("Observation records must be ordered by resolved observation time.")
|
|
345
|
+
return resolved
|
|
346
|
+
|
|
347
|
+
def ensure_regular_frequency(
|
|
348
|
+
self,
|
|
349
|
+
times: List[float],
|
|
350
|
+
tolerance: float = 1e-8,
|
|
351
|
+
business_days_in_year: float = 252.0,
|
|
352
|
+
) -> float:
|
|
353
|
+
"""Ensure schedule has a regular observation frequency; return the interval.
|
|
354
|
+
|
|
355
|
+
This method supports three usage modes:
|
|
356
|
+
1) `frequency` is `ObservationFrequency.CUSTOM` or None: infer dt from `times`.
|
|
357
|
+
2) `frequency` is a numeric dt (legacy): validate against that dt.
|
|
358
|
+
3) `frequency` is an `ObservationFrequency`: validate against calendar or business-day dt.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
times: List of observation times in years
|
|
362
|
+
tolerance: Tolerance for comparing floating-point intervals (default: 1e-8)
|
|
363
|
+
business_days_in_year: Business-day year length used when validating
|
|
364
|
+
business-day frequency conventions (default: 252.0).
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
The regular frequency interval (float)
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ValidationError: If spacing is irregular or does not match declared frequency.
|
|
371
|
+
"""
|
|
372
|
+
if len(times) < 2:
|
|
373
|
+
raise ValidationError("Need at least 2 observation times to determine interval.")
|
|
374
|
+
|
|
375
|
+
inferred_dt = times[1] - times[0]
|
|
376
|
+
if inferred_dt <= 0.0:
|
|
377
|
+
raise ValidationError("Observation times must be strictly increasing.")
|
|
378
|
+
|
|
379
|
+
for i in range(1, len(times) - 1):
|
|
380
|
+
dt_i = times[i + 1] - times[i]
|
|
381
|
+
if dt_i <= 0.0:
|
|
382
|
+
raise ValidationError("Observation times must be strictly increasing.")
|
|
383
|
+
if not isclose(dt_i, inferred_dt, rel_tol=tolerance, abs_tol=tolerance):
|
|
384
|
+
raise ValidationError(
|
|
385
|
+
"ObservationSchedule requires a regular frequency for analytical usage, "
|
|
386
|
+
"but observation_times are not equally spaced."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
frequency = self.frequency
|
|
390
|
+
|
|
391
|
+
if frequency is None or frequency == ObservationFrequency.CUSTOM:
|
|
392
|
+
return inferred_dt
|
|
393
|
+
|
|
394
|
+
if isinstance(frequency, (int, float)):
|
|
395
|
+
expected_dt = float(frequency)
|
|
396
|
+
if not isclose(
|
|
397
|
+
inferred_dt, expected_dt, rel_tol=tolerance, abs_tol=tolerance
|
|
398
|
+
):
|
|
399
|
+
raise ValidationError(
|
|
400
|
+
f"ObservationSchedule declared frequency '{frequency}' "
|
|
401
|
+
f"does not match inferred interval {inferred_dt:.6f} from observation_times."
|
|
402
|
+
)
|
|
403
|
+
return expected_dt
|
|
404
|
+
|
|
405
|
+
if not isinstance(frequency, ObservationFrequency):
|
|
406
|
+
raise ValidationError(
|
|
407
|
+
f"Invalid schedule frequency type: {type(frequency).__name__}"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
expected_dt_calendar = frequency.to_year_fraction(use_business_days=False)
|
|
411
|
+
expected_dt_business = frequency.to_year_fraction(
|
|
412
|
+
use_business_days=True, days_in_year=business_days_in_year
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
matches_calendar = isclose(
|
|
416
|
+
inferred_dt, expected_dt_calendar, rel_tol=tolerance, abs_tol=tolerance
|
|
417
|
+
)
|
|
418
|
+
matches_business = isclose(
|
|
419
|
+
inferred_dt, expected_dt_business, rel_tol=tolerance, abs_tol=tolerance
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if not (matches_calendar or matches_business):
|
|
423
|
+
raise ValidationError(
|
|
424
|
+
f"ObservationSchedule declared frequency '{frequency.name}' "
|
|
425
|
+
f"(expected dt={expected_dt_calendar:.6f} or {expected_dt_business:.6f}) "
|
|
426
|
+
f"does not match inferred interval {inferred_dt:.6f} from observation_times."
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return inferred_dt
|
|
430
|
+
|
|
431
|
+
def has_fixed_payoff(self, default_payoff: float = 0.0, tolerance: float = 1e-8) -> bool:
|
|
432
|
+
"""Check if all records share the same payoff within tolerance.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
default_payoff: Default payoff value to use for records without explicit payoffs
|
|
436
|
+
tolerance: Tolerance for comparing payoff values (default: 1e-8 for floating-point precision)
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
True if all payoffs are equal within tolerance, False otherwise
|
|
440
|
+
|
|
441
|
+
Note:
|
|
442
|
+
Payoffs are computed once per record and cached for efficiency.
|
|
443
|
+
"""
|
|
444
|
+
if not self.records:
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
# Compute payoffs once and cache in list
|
|
448
|
+
payoffs = [rec.get_payoff(default_payoff) for rec in self.records]
|
|
449
|
+
base = payoffs[0]
|
|
450
|
+
|
|
451
|
+
for payoff in payoffs[1:]:
|
|
452
|
+
if not isclose(payoff, base, rel_tol=tolerance, abs_tol=tolerance):
|
|
453
|
+
return False
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
def assert_analytical_ready(
|
|
457
|
+
self,
|
|
458
|
+
default_payoff: float = 0.0,
|
|
459
|
+
tolerance: float = 1e-8,
|
|
460
|
+
business_days_in_year: float = 252.0,
|
|
461
|
+
) -> None:
|
|
462
|
+
"""
|
|
463
|
+
Validate preconditions for analytical engines that rely on barrier shift:
|
|
464
|
+
regular observation frequency and fixed payoff across observations.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
default_payoff: Default payoff value to use for records without explicit payoffs
|
|
468
|
+
tolerance: Tolerance for validation checks (default: 1e-8 for numerical stability)
|
|
469
|
+
business_days_in_year: Number of business days in a year used for validating
|
|
470
|
+
business-day frequency conventions (default: 252.0).
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
ValidationError: If schedule is not suitable for analytical pricing
|
|
474
|
+
"""
|
|
475
|
+
times = [
|
|
476
|
+
rec.observation_time
|
|
477
|
+
for rec in self.records
|
|
478
|
+
if rec.observation_time is not None
|
|
479
|
+
]
|
|
480
|
+
self.ensure_regular_frequency(
|
|
481
|
+
times, tolerance=tolerance, business_days_in_year=business_days_in_year
|
|
482
|
+
)
|
|
483
|
+
if not self.has_fixed_payoff(
|
|
484
|
+
default_payoff=default_payoff, tolerance=tolerance
|
|
485
|
+
):
|
|
486
|
+
raise ValidationError(
|
|
487
|
+
"Analytical barrier shift requires a fixed payoff across observation records."
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def times(self) -> List[float]:
|
|
492
|
+
"""Return observation_time values when present."""
|
|
493
|
+
return [rec.observation_time for rec in self.records if rec.observation_time is not None]
|
|
494
|
+
|
|
495
|
+
@classmethod
|
|
496
|
+
def from_legacy(
|
|
497
|
+
cls,
|
|
498
|
+
observation_dates: List[float],
|
|
499
|
+
default_barrier: Optional[float],
|
|
500
|
+
default_payoff: float,
|
|
501
|
+
aggregation_mode: ObservationAggregation = ObservationAggregation.STOP_FIRST_HIT,
|
|
502
|
+
frequency: ObservationFrequency = ObservationFrequency.CUSTOM,
|
|
503
|
+
upper_barrier: Optional[float] = None,
|
|
504
|
+
lower_barrier: Optional[float] = None,
|
|
505
|
+
) -> "ObservationSchedule":
|
|
506
|
+
"""Create a schedule from legacy observation_dates and uniform barrier/payoff."""
|
|
507
|
+
if observation_dates is None or len(observation_dates) == 0:
|
|
508
|
+
raise ValidationError("observation_dates required to build ObservationSchedule from legacy fields.")
|
|
509
|
+
records = [
|
|
510
|
+
ObservationRecord(
|
|
511
|
+
observation_time=t,
|
|
512
|
+
barrier=default_barrier,
|
|
513
|
+
upper_barrier=upper_barrier,
|
|
514
|
+
lower_barrier=lower_barrier,
|
|
515
|
+
payoff=default_payoff,
|
|
516
|
+
)
|
|
517
|
+
for t in observation_dates
|
|
518
|
+
]
|
|
519
|
+
schedule = cls(
|
|
520
|
+
records=records,
|
|
521
|
+
aggregation_mode=aggregation_mode,
|
|
522
|
+
frequency=frequency,
|
|
523
|
+
)
|
|
524
|
+
schedule.validate(require_single=upper_barrier is None and lower_barrier is None, require_double=upper_barrier is not None or lower_barrier is not None)
|
|
525
|
+
return schedule
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def _is_sorted(values: List[float]) -> bool:
|
|
529
|
+
"""Check if list is non-decreasing."""
|
|
530
|
+
return all(values[i] <= values[i + 1] for i in range(len(values) - 1))
|