Skip to content

Commit

Permalink
Refactor sensitivity calculation for AMC payoffs
Browse files Browse the repository at this point in the history
  - Amend AMC payoffs for smooth step function
  - Add has_amc_function to identify AMC payoffs
  - Add regression re-calibration to sensitivity calculation
  - Add swaption sensitivity component test
  - Add gradient calculation via FiniteDifferences
  • Loading branch information
FrameConsult committed Dec 10, 2023
1 parent 4e94b6f commit 0c22a1f
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 18 deletions.
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ version = "0.3.0"
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand Down
1 change: 1 addition & 0 deletions src/DiffFusion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module DiffFusion
using ChainRulesCore
using DelimitedFiles
using Distributions
using FiniteDifferences
using ForwardDiff
using Interpolations
using LinearAlgebra
Expand Down
45 changes: 38 additions & 7 deletions src/analytics/Valuations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ function model_price_and_deltas(
payoffs::AbstractVector,
path_obj::Path,
pay_time::Union{ModelTime, Nothing} = nothing,
discount_curve_key::Union{String,Nothing} = nothing
discount_curve_key::Union{String,Nothing} = nothing,
)
if has_amc_payoff(payoffs)
@warn "Zygote cannot properly handle AMC payoffs."
end
#
pay_time = _effective_pay_time(payoffs, pay_time, discount_curve_key)
#
Expand All @@ -97,7 +100,7 @@ function model_price_and_deltas(
return mean( X )
end
# res = obj_function(path_obj.ts_dict)
(v, g) = _function_value_and_gradient(obj_function, path_obj.ts_dict)
(v, g) = _function_value_and_gradient(obj_function, path_obj.ts_dict, Zygote)
return (v, g)
end

Expand All @@ -108,7 +111,7 @@ end
path_obj::Path,
pay_time::Union{ModelTime, Nothing} = nothing,
discount_curve_key::Union{String,Nothing} = nothing,
ad_module::Module = Zygote,
ad_module::Module = ForwardDiff,
)
Calculate model price and curve sensitivities. Sensitivities are
Expand All @@ -120,22 +123,35 @@ Here, payoffs is a vector of `Payoff` objects and `path_obj` is a simulated `Pat
numeraire calculation.
`ad_module` can be `Zygote` or `ForwardDiff`.
For AMC payoffs we need to update the regression path and trigger a
recalibration. For sensitivity calculation, we impose the constraint that
regression calibration uses the same paths as valuation.
"""
function model_price_and_deltas_vector(
payoffs::AbstractVector,
path_obj::Path,
pay_time::Union{ModelTime, Nothing} = nothing,
discount_curve_key::Union{String,Nothing} = nothing,
ad_module::Module = Zygote,
ad_module::Module = ForwardDiff,
)
#
if has_amc_payoff(payoffs) && (ad_module == Zygote)
@warn "Zygote cannot properly handle AMC payoffs."
end
#
pay_time = _effective_pay_time(payoffs, pay_time, discount_curve_key)
ts_dict = deepcopy(path_obj.ts_dict) # maybe we don't want (or need) this
(ts_labels, ts_values) = termstructure_values(ts_dict)
#
obj_function(ts_values_) = begin
termstructure_dictionary!(ts_dict, ts_labels, ts_values_)
path_ = path(path_obj.sim, ts_dict, path_obj.context, path_obj.interpolation)
# we need to update regression paths for AMC payoffs
for p in payoffs
reset_regression!(p, path_)
end
#
X = zeros(length(path_))
if length(payoffs) > 0
X += sum(( p(path_) for p in payoffs ))
Expand Down Expand Up @@ -185,6 +201,9 @@ function model_price_and_vegas(
pay_time::Union{ModelTime, Nothing} = nothing,
discount_curve_key::Union{String,Nothing} = nothing
)
if has_amc_payoff(payoffs)
@warn "Zygote cannot properly handle AMC payoffs."
end
#
pay_time = _effective_pay_time(payoffs, pay_time, discount_curve_key)
#
Expand Down Expand Up @@ -220,7 +239,7 @@ function model_price_and_vegas(
return price
end
# res = obj_function(param_dict)
(v, g) = _function_value_and_gradient(obj_function, param_dict)
(v, g) = _function_value_and_gradient(obj_function, param_dict, Zygote)
return (v, g)
end

Expand All @@ -234,7 +253,7 @@ end
context::Context,
pay_time::Union{ModelTime, Nothing} = nothing,
discount_curve_key::Union{String,Nothing} = nothing,
ad_module::Module = Zygote,
ad_module::Module = ForwardDiff,
)
Calculate model price and model sensitivities. Sensitivities are
Expand All @@ -252,6 +271,10 @@ signature `simulation(model::Model, ch::CorrelationHolder)`.
numeraire calculation.
`ad_module` can be `Zygote` or `ForwardDiff`.
For AMC payoffs we need to update the regression path and trigger a
recalibration. For sensitivity calculation, we impose the constraint that
regression calibration uses the same paths as valuation.
"""
function model_price_and_vegas_vector(
payoffs::AbstractVector,
Expand All @@ -261,9 +284,13 @@ function model_price_and_vegas_vector(
context::Context,
pay_time::Union{ModelTime, Nothing} = nothing,
discount_curve_key::Union{String,Nothing} = nothing,
ad_module::Module = Zygote,
ad_module::Module = ForwardDiff,
)
#
if has_amc_payoff(payoffs) && (ad_module == Zygote)
@warn "Zygote cannot properly handle AMC payoffs."
end
#
pay_time = _effective_pay_time(payoffs, pay_time, discount_curve_key)
#
param_dict = model_parameters(model)
Expand All @@ -287,6 +314,10 @@ function model_price_and_vegas_vector(
end
sim = simulation(mdl, ch)
path_ = path(sim, ts_list, context, LinearPathInterpolation)
# we need to update regression paths for AMC payoffs
for p in payoffs
reset_regression!(p, path_)
end
#
X = zeros(length(path_))
if length(payoffs) > 0
Expand Down
75 changes: 69 additions & 6 deletions src/payoffs/AmcPayoffs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,58 @@ function obs_times(p::AmcPayoff)
return times
end


"""
has_amc_payoff(p::AmcPayoff)
Determine whether a payoff is or contains an AMC payoff.
AMC payoffs require special treatment e.g. for sensitivity calculation.
"""
has_amc_payoff(p::AmcPayoff) = true


"""
has_amc_payoff(p::UnaryNode)
Determine whether a payoff is or contains an AMC payoff.
"""
has_amc_payoff(p::UnaryNode) = has_amc_payoff(p.x)

"""
has_amc_payoff(p::BinaryNode)
Determine whether a payoff is or contains an AMC payoff.
"""
has_amc_payoff(p::BinaryNode) = has_amc_payoff(p.x) || has_amc_payoff(p.y)

"""
has_amc_payoff(p::Union{Leaf, CompoundedRate, Optionlet, Swaption})
Determine whether a payoff is or contains an AMC payoff.
"""
has_amc_payoff(p::Union{Leaf, CompoundedRate, Optionlet, Swaption}) = false

"""
has_amc_payoff(p::Payoff)
Determine whether a payoff is or contains an AMC payoff.
"""
has_amc_payoff(p::Payoff) = begin
error("Payoff " * string(typeof(p)) * " needs to implement has_amc_payoff method.")
return false
end

"""
has_amc_payoff(payoffs::AbstractVector)
Determine whether any payoff is or contains an AMC payoff.
"""
has_amc_payoff(payoffs::AbstractVector) = begin
return any([ has_amc_payoff(p) for p in payoffs ])
end


"""
reset_regression!(
p::AmcPayoff,
Expand Down Expand Up @@ -269,7 +321,7 @@ end

"""
reset_regression!(
p::Leaf,
p::Union{Leaf, CompoundedRate, Optionlet, Swaption},
path::Union{AbstractPath, Nothing} = nothing,
make_regression::Union{Function, Nothing} = nothing,
)
Expand Down Expand Up @@ -304,6 +356,17 @@ function reset_regression!(
end


"""
_is_larger_zero(T::AbstractArray)
Implement a differentiable version of the indicator (T>0).
"""
function _is_larger_zero(T::AbstractArray)
# return 1.0 * (T .> 0.0)
scaling = 1.0e+8
return 0.5 .+ 0.5 .* tanh.(scaling .* T)
end

"""
struct AmcMax <: AmcPayoff
links::AmcPayoffLinks
Expand Down Expand Up @@ -370,8 +433,8 @@ function at(p::AmcMax, path::AbstractPath)
end
Y = Y .* N
end
use_X = (T .> 0.0)
return use_X .* X + (.!use_X) .* Y
use_X = _is_larger_zero(T)
return use_X .* X .+ (1.0 .- use_X) .* Y
end

"""
Expand Down Expand Up @@ -450,8 +513,8 @@ function at(p::AmcMin, path::AbstractPath)
end
Y = Y .* N
end
use_X = (T .<= 0.0)
return use_X .* X + (.!use_X) .* Y
use_Y = _is_larger_zero(T)
return (1.0 .- use_Y) .* X .+ use_Y .* Y
end

"""
Expand Down Expand Up @@ -516,7 +579,7 @@ Evaluate an AmcOne payoff at a given path.
"""
function at(p::AmcOne, path::AbstractPath)
(X, Y, T) = at(p.links, p.regr, path)
return (T .> 0.0)
return _is_larger_zero(T)
end

"""
Expand Down
16 changes: 11 additions & 5 deletions src/utils/Gradients.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,32 @@ tuning AD routines.
_function_value_and_gradient(
f::Function,
x::Any,
m::Module = Zygote,
m::Module = ForwardDiff,
)
Calculate the function value and gradient of a function.
"""
function _function_value_and_gradient(
f::Function,
x::Any,
m::Module = Zygote,
m::Module = ForwardDiff,
)
#
if m == ForwardDiff
v = f(x)
g = ForwardDiff.gradient(f, x)
return (v, g)
end
if m == Zygote
(v, g) = withgradient(f, x)
@assert length(g) == 1
return (v, g[1])
end
if m == ForwardDiff
if m == FiniteDifferences
v = f(x)
g = ForwardDiff.gradient(f, x)
return (v, g)
m = central_fdm(3, 1)
g = grad(m, f, x)
return (v, g[1])
end
error("Unknown module " * string(m) * ".")
end
1 change: 1 addition & 0 deletions test/componenttests/componenttests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ using Test
include("sensitivities/option_deltas.jl")
include("sensitivities/swap_deltas.jl")
include("sensitivities/option_vegas.jl")
include("sensitivities/swaptions_delta_vega.jl")


end
Expand Down
Loading

0 comments on commit 0c22a1f

Please sign in to comment.