From 81f96400bfa8b75b1efc608b5e61d37abbcbfb26 Mon Sep 17 00:00:00 2001 From: Sebastian Schlenkrich Date: Thu, 4 Jan 2024 18:02:50 +0100 Subject: [PATCH] Add bermudan regression based on exercise trigger --- src/products/BermudanSwaptionLeg.jl | 87 +++++++++++++++---- .../scenarios/swaptions_expected_exposure.jl | 26 +++++- .../sensitivities/swaptions_delta_vega.jl | 2 +- .../products/bermudan_swaption_leg.jl | 39 +++++++++ 4 files changed, 136 insertions(+), 18 deletions(-) diff --git a/src/products/BermudanSwaptionLeg.jl b/src/products/BermudanSwaptionLeg.jl index f335c3cf..e9b85f2d 100644 --- a/src/products/BermudanSwaptionLeg.jl +++ b/src/products/BermudanSwaptionLeg.jl @@ -124,6 +124,11 @@ and `make_regression_variables` are passed on to `BermudanSwaptionLeg`. `path` and `make_regression` are used to create an `AmcPayoffRegression` object for `AmcPayoff`s. This data is supposed to be updated subsequent to leg cretion. + +`regression_on_exercise_trigger = true` specifies AMC regression strategy. +If `regression_on_exercise_trigger = then` then regression on regression +is used. `regression_on_exercise_trigger = true` is recommended for +accurate sensitivity calculation. """ function bermudan_swaption_leg( alias::String, @@ -133,6 +138,7 @@ function bermudan_swaption_leg( make_regression_variables::Function, path::Union{AbstractPath, Nothing}, make_regression::Union{Function, Nothing}, + regression_on_exercise_trigger = true, ) # @assert length(bermudan_exercises) > 0 @@ -157,22 +163,52 @@ function bermudan_swaption_leg( exercise_triggers = [ Cache(Hk > Uk), ] # backward sweep for ex in reverse(bermudan_exercises[begin:end-1]) - Hk = AmcSum( - ex.exercise_time, - [hold_values[end],], - ex.make_regression_variables(ex.exercise_time), - path, - make_regression, - numeraire_curve_key, - ) - Hk = Cache(Hk) - Uk = vcat([ - discounted_cashflows(leg, ex.exercise_time) - for leg in ex.cashflow_legs - ]...) - Uk = Cache(sum(Uk)) - hold_values = vcat(hold_values, Cache(Max(Hk, Uk))) - exercise_triggers = vcat(exercise_triggers, Cache(Hk > Uk)) + if regression_on_exercise_trigger + Uk = vcat([ + discounted_cashflows(leg, ex.exercise_time) + for leg in ex.cashflow_legs + ]...) + Uk = Cache(sum(Uk)) + Hk = AmcMax( + ex.exercise_time, + [ hold_values[end], ], + [ Uk, ], + ex.make_regression_variables(ex.exercise_time), + path, + make_regression, + numeraire_curve_key, + ) + Hk = Cache(Hk) + Ik = AmcOne( + ex.exercise_time, + [ hold_values[end], ], + [ Uk, ], + ex.make_regression_variables(ex.exercise_time), + path, + make_regression, + numeraire_curve_key, + ) + Ik = Cache(Ik) + hold_values = vcat(hold_values, Hk) + exercise_triggers = vcat(exercise_triggers, Ik) + else + Hk = AmcSum( + ex.exercise_time, + [hold_values[end],], + ex.make_regression_variables(ex.exercise_time), + path, + make_regression, + numeraire_curve_key, + ) + Hk = Cache(Hk) + Uk = vcat([ + discounted_cashflows(leg, ex.exercise_time) + for leg in ex.cashflow_legs + ]...) + Uk = Cache(sum(Uk)) + hold_values = vcat(hold_values, Cache(Max(Hk, Uk))) + exercise_triggers = vcat(exercise_triggers, Cache(Hk > Uk)) + end end # return BermudanSwaptionLeg( @@ -383,12 +419,30 @@ function make_bermudan_exercises( end +""" + bermudan_swaption_leg( + alias::String, + fixed_leg::DeterministicCashFlowLeg, + float_leg::DeterministicCashFlowLeg, + exercise_times::AbstractVector, + option_long_short::ModelValue, + regression_on_exercise_trigger = false, + ) + +Create a `BermudanSwaptionLeg` using simplified interface. + +`regression_on_exercise_trigger = true` specifies AMC regression strategy. +If `regression_on_exercise_trigger = then` then regression on regression +is used. `regression_on_exercise_trigger = true` is recommended for +accurate sensitivity calculation. +""" function bermudan_swaption_leg( alias::String, fixed_leg::DeterministicCashFlowLeg, float_leg::DeterministicCashFlowLeg, exercise_times::AbstractVector, option_long_short::ModelValue, + regression_on_exercise_trigger = true, ) # bermudan_exercises = make_bermudan_exercises(fixed_leg, float_leg, exercise_times) @@ -409,5 +463,6 @@ function bermudan_swaption_leg( make_regression_variables_, path_, make_regression_, + regression_on_exercise_trigger, ) end diff --git a/test/componenttests/scenarios/swaptions_expected_exposure.jl b/test/componenttests/scenarios/swaptions_expected_exposure.jl index a7890891..1cc4b337 100644 --- a/test/componenttests/scenarios/swaptions_expected_exposure.jl +++ b/test/componenttests/scenarios/swaptions_expected_exposure.jl @@ -191,8 +191,20 @@ using UnicodePlots make_regression_variables, nothing, # path nothing, # make_regression + true, # regression_on_exercise_trigger ) - + + berm_2 = DiffFusion.bermudan_swaption_leg( + "berm/10-nc-2 (regr_on_regr)", + [ exercise_2y, exercise_4y, exercise_6y, exercise_8y, ], + 1.0, # long option + "", # default discounting (curve key) + make_regression_variables, + nothing, # path + nothing, # make_regression + false, # regression_on_exercise_trigger + ) + # AMC Regression @@ -200,6 +212,7 @@ using UnicodePlots make_regression = (C, O) -> DiffFusion.piecewise_regression(C, O, 2, [3]) DiffFusion.reset_regression!(berm, path, make_regression) + DiffFusion.reset_regression!(berm_2, path, make_regression) # Scenario Calculation @@ -213,6 +226,7 @@ using UnicodePlots swaption_8y_scens = DiffFusion.scenarios([swaption_8y], times, path, "", with_progress_bar=false) berm_scens = DiffFusion.scenarios([berm], times, path, "", with_progress_bar=false) + berm_2_scens = DiffFusion.scenarios([berm_2], times, path, "", with_progress_bar=false) vanilla_swap_ee = DiffFusion.expected_exposure(vanilla_swap_scens) swaption_2y_ee = DiffFusion.expected_exposure(swaption_2y_scens) @@ -220,6 +234,7 @@ using UnicodePlots swaption_6y_ee = DiffFusion.expected_exposure(swaption_6y_scens) swaption_8y_ee = DiffFusion.expected_exposure(swaption_8y_scens) berm_ee = DiffFusion.expected_exposure(berm_scens) + berm_2_ee = DiffFusion.expected_exposure(berm_2_scens) portfolo = DiffFusion.join_scenarios([ vanilla_swap_ee, @@ -230,6 +245,11 @@ using UnicodePlots berm_ee, ]) + portfolo_berms = DiffFusion.join_scenarios([ + berm_ee, + berm_2_ee, + ]) + function plot_scens(scens, title) plt = lineplot(scens.times, scens.X[1,:,:], title = title, @@ -245,8 +265,12 @@ using UnicodePlots end plot_scens(portfolo, "Rates Derivatives") + plot_scens(portfolo_berms, "Bermudans") max_european_ee = maximum(portfolo.X[1,:,2:5], dims=2)[:,1] berm_ee = portfolo.X[1,:,6] @test berm_ee ≥ max_european_ee + + rel_error_berms = (portfolo_berms.X[1,:,1] .+ 1.0e-8) ./ (portfolo_berms.X[1,:,2] .+ 1.0e-8) .- 1.0 + @test maximum(abs.(rel_error_berms)) < 0.007 end \ No newline at end of file diff --git a/test/componenttests/sensitivities/swaptions_delta_vega.jl b/test/componenttests/sensitivities/swaptions_delta_vega.jl index 56970eb7..d249c9e3 100644 --- a/test/componenttests/sensitivities/swaptions_delta_vega.jl +++ b/test/componenttests/sensitivities/swaptions_delta_vega.jl @@ -267,7 +267,7 @@ using UnicodePlots @test abs(sum(g2[5:8])/sum(g1[5:8]) - 1.0) < 7.5e-2 # berm_4y ESTR # @test abs(sum(g3[1:4])/sum(g1[1:4]) - 1.0) < 0.27 # berm_10nc2 EURIBOR6M Delta - @test abs(sum(g3[5:8])/sum(g1[5:8]) - 1.0) < 0.17 # berm_10nc2 ESTR + @test abs(sum(g3[5:8])/sum(g1[5:8]) - 1.0) < 0.28 # berm_10nc2 ESTR end diff --git a/test/unittests/products/bermudan_swaption_leg.jl b/test/unittests/products/bermudan_swaption_leg.jl index 50799797..39c8dcfe 100644 --- a/test/unittests/products/bermudan_swaption_leg.jl +++ b/test/unittests/products/bermudan_swaption_leg.jl @@ -261,6 +261,30 @@ berm_35 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} berm_40 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} > {"*underl_20*"})} * {(0.0000 > {"*underl_30*"})}) * "*underl_40*" @ 4.00)" berm_45 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} > {"*underl_20*"})} * {(0.0000 > {"*underl_30*"})}) * "*underl_45*" @ 4.50)" +berm_2_00 = +"AmcSum(0.00, [{AmcMax(1.00, [{AmcMax(2.00, [{Max(0.0000, {(((" * +"(P(EUR:OIS, 3.00, 4.00) * 1.0000 * 0.0300 * 1.0000 @ 3.00) + " * +"(P(EUR:OIS, 3.00, 5.00) * 1.0000 * 0.0300 * 1.0000 @ 3.00)) + " * +"(P(EUR:OIS, 3.00, 4.00) * -1.0000 * L(EURIBOR12M, 3.00; 3.00, 4.00) * 1.0000 @ 3.00)) + " * +"(P(EUR:OIS, 3.00, 5.00) * -1.0000 * L(EURIBOR12M, 3.00; 4.00, 5.00) * 1.0000 @ 3.00))})}], [{(((((" * +"(P(EUR:OIS, 2.00, 3.00) * 1.0000 * 0.0300 * 1.0000 @ 2.00) + " * +"(P(EUR:OIS, 2.00, 4.00) * 1.0000 * 0.0300 * 1.0000 @ 2.00)) + " * +"(P(EUR:OIS, 2.00, 5.00) * 1.0000 * 0.0300 * 1.0000 @ 2.00)) + " * +"(P(EUR:OIS, 2.00, 3.00) * -1.0000 * L(EURIBOR12M, 2.00; 2.00, 3.00) * 1.0000 @ 2.00)) + " * +"(P(EUR:OIS, 2.00, 4.00) * -1.0000 * L(EURIBOR12M, 2.00; 3.00, 4.00) * 1.0000 @ 2.00)) + " * +"(P(EUR:OIS, 2.00, 5.00) * -1.0000 * L(EURIBOR12M, 2.00; 4.00, 5.00) * 1.0000 @ 2.00))}], " * +"[L(EURIBOR12M, 2.00; 2.00, 5.00)])}], [{(((((((" * +"(P(EUR:OIS, 1.00, 2.00) * 1.0000 * 0.0300 * 1.0000 @ 1.00) + " * +"(P(EUR:OIS, 1.00, 3.00) * 1.0000 * 0.0300 * 1.0000 @ 1.00)) + " * +"(P(EUR:OIS, 1.00, 4.00) * 1.0000 * 0.0300 * 1.0000 @ 1.00)) + " * +"(P(EUR:OIS, 1.00, 5.00) * 1.0000 * 0.0300 * 1.0000 @ 1.00)) + " * +"(P(EUR:OIS, 1.00, 2.00) * -1.0000 * L(EURIBOR12M, 1.00; 1.00, 2.00) * 1.0000 @ 1.00)) + " * +"(P(EUR:OIS, 1.00, 3.00) * -1.0000 * L(EURIBOR12M, 1.00; 2.00, 3.00) * 1.0000 @ 1.00)) + " * +"(P(EUR:OIS, 1.00, 4.00) * -1.0000 * L(EURIBOR12M, 1.00; 3.00, 4.00) * 1.0000 @ 1.00)) + " * +"(P(EUR:OIS, 1.00, 5.00) * -1.0000 * L(EURIBOR12M, 1.00; 4.00, 5.00) * 1.0000 @ 1.00))}], " * +"[L(EURIBOR12M, 1.00; 1.00, 5.00)])}], [], [L(EURIBOR12M, 0.00; 0.00, 5.00)])" + + @testset "Test discounted_cashflows" begin berm = DiffFusion.bermudan_swaption_leg( "berm", @@ -270,6 +294,7 @@ berm_45 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} make_regression_variables, nothing, # path nothing, # make_regression + false, # regression_on_exercise_trigger ) @test isa(berm, DiffFusion.BermudanSwaptionLeg) cfs = DiffFusion.discounted_cashflows(berm, 0.0) @@ -287,6 +312,18 @@ berm_45 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} @test length(DiffFusion.discounted_cashflows(berm, 5.0)) == 0 @test length(DiffFusion.discounted_cashflows(berm, 5.5)) == 0 + + berm_2 = DiffFusion.bermudan_swaption_leg( + "berm", + [ exercise_1, exercise_2, exercise_3,], + 1.0, # long option + "OIS", # default discounting (curve key) + make_regression_variables, + nothing, # path + nothing, # make_regression + true, # regression_on_exercise_trigger + ) + @test string(DiffFusion.discounted_cashflows(berm_2, 0.0)[1]) == berm_2_00 end @testset "Test regression details setup" begin @@ -302,6 +339,7 @@ berm_45 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} make_regression_variables, NoPath(), # path no_make_regression, # make_regression + false, # regression_on_exercise_trigger ) @test isa(berm, DiffFusion.BermudanSwaptionLeg) @test berm.regression_data.path == NoPath() @@ -319,6 +357,7 @@ berm_45 = "((1.0000 - {({"*option_10*"} > {"*underl_10*"})} * {({"*option_20*"} make_regression_variables, nothing, # path nothing, # make_regression + false, # regression_on_exercise_trigger ) @test isa(berm, DiffFusion.BermudanSwaptionLeg) @test isnothing(berm.regression_data.path)