Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Monotone Convex #171

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
OptimizationMetaheuristics = "3aafef2f-86ae-4776-b337-85a36adf0b55"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Expand All @@ -30,17 +31,17 @@ FinanceModelsMakieCoreExt = "MakieCore"
AccessibleOptimization = "^0.1.1"
Accessors = "^0.1"
BSplineKit = "^0.16"
Dates = "^1.6"
FinanceCore = "^2.1"
IntervalSets = "^0.7"
LinearAlgebra = "^1.6"
Dates = "^1.6"
MakieCore = "0.6"
Optimization = "^3.15"
OptimizationMetaheuristics = "^0.1.2"
PrecompileTools = "^1.1"
Reexport = "^1.2"
StaticArrays = "^1.6"
SpecialFunctions = "2"
StaticArrays = "^1.6"
Transducers = "^0.4"
UnicodePlots = "^3.6"
julia = "1.9"
Expand Down
1 change: 1 addition & 0 deletions src/FinanceModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import BSplineKit
import UnicodePlots
using Transducers: @next, complete, __foldl__, asfoldable
import SpecialFunctions
import QuadGK



Expand Down
26 changes: 22 additions & 4 deletions src/fit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ __default_optic(m::MyModel) = OptArgs([

"""
__default_optic(m::Yield.Constant) = OptArgs(@optic(_.rate.value) => -1.0 .. 1.0)
__default_optic(m::Yield.MonotoneConvexUnInit) = OptArgs(@optic(_.rates) .=> -1.0 .. 1.0)
__default_optic(m::Yield.MonotoneConvex) = OptArgs(@optic(_.rates) .=> -1.0 .. 1.0)
__default_optic(m::Yield.IntermediateYieldCurve) = OptArgs(@optic(_.ys[end]) => 0.0 .. 1.0)
__default_optic(m::Yield.NelsonSiegel) = OptArgs([
@optic(_.τ₁) => 0.0 .. 100.0
Expand Down Expand Up @@ -222,7 +224,7 @@ The default solver is `ECA()` from Metahueristics.jl. This is a stochastic globa
## Defining the variables

An arbitrarily complex model may be the object we intend to fit - how does `fit` know what free variables are able to be solved for within the given model?
`variables` is a singlular or vector optic argument. What does this mean?
`variables` is a singular or vector optic argument. What does this mean?
- An optic (or "lens") is a way to define an accessor to a given object. Example:

```julia-repl
Expand All @@ -237,7 +239,7 @@ julia> lens(obj)
"AA"
```
An optic argument is a singular or vector of lenses with an optional range of acceptable parameters. For example, we might have a model as follows where we want
`fit` to optize parameters `a` and `b`:
`fit` to optimize parameters `a` and `b`:

```julia
struct MyModel <:FinanceModels.AbstractModel
Expand All @@ -252,7 +254,7 @@ __default_optic(m::MyModel) = OptArgs([
```
In this way, fit know which arbitrary parameters in a given object may be modified. Technically, we are not modifying the immutable `MyModel`, but instead efficiently creating a new instance. This is enabled by [AccessibleOptimization.jl](https://gitlab.com/aplavin/AccessibleOptimization.jl).

Note that not all opitmization algorithms want a bounded interval. In that case, simply leave off the paired range. The prior example would then become:
Note that not all optimization algorithms want a bounded interval. In that case, simply leave off the paired range. The prior example would then become:

```julia
__default_optic(m::MyModel) = OptArgs([
Expand All @@ -265,7 +267,7 @@ __default_optic(m::MyModel) = OptArgs([

## Additional Examples

See the tutorials in the package documentation for FinanceModels.jl or the docstrings of FinanceModels.jl's avaiable model types.
See the tutorials in the package documentation for FinanceModels.jl or the docstrings of FinanceModels.jl's available model types.
"""
function fit(mod0, quotes, method::F=Fit.Loss(x -> x^2);
variables=__default_optic(mod0),
Expand All @@ -281,6 +283,22 @@ function fit(mod0, quotes, method::F=Fit.Loss(x -> x^2);

end

function fit(mod0::Yield.MonotoneConvexUnInit, quotes, method::F=Fit.Loss(x -> x^2);
variables=__default_optic(mod0),
optimizer=__default_optim(mod0)
) where
{F<:Fit.Loss}
# use maturities as the times
# fit a vector of rates to the times
times = [maturity(q.instrument) for q in quotes]
if !iszero(first(times))
pushfirst!(times, zero(eltype(times)))
end
mc = mod0(times)
rates = fit(mc, quotes, method; variables, optimizer)
end


function fit(mod0::Spline.BSpline, quotes, method::Fit.Bootstrap)
discount_vector = [0.0]
times = [maturity(quotes[1])]
Expand Down
1 change: 1 addition & 0 deletions src/model/Yield.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ end

include("Yield/SmithWilson.jl")
include("Yield/NelsonSiegelSvensson.jl")
include("Yield/MonotoneConvex.jl")

## Generic and Fallbacks
"""
Expand Down
263 changes: 263 additions & 0 deletions src/model/Yield/MonotoneConvex.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
"""
An unfit Monotone Convex Yield Curve Model will simply have


"""
struct MonotoneConvex{T,U} <: AbstractYieldModel
f::Vector{T}
fᵈ::Vector{T}
rates::Vector{T}
times::Vector{U}
# inner constructor ensures f consistency with rates at construction
function MonotoneConvex(rates::Vector{T}, times::Vector{U}) where {T,U}
f, fᵈ = __monotone_convex_fs(rates, times)
new{T,U}(f, fᵈ, rates, times)
end
end


struct MonotoneConvexUnInit
end

struct MonotoneConvexUnoptimized{T,U}
rates::Vector{T}
times::Vector{U}
end

MonotoneConvex() = MonotoneConvexUnInit()
function (m::MonotoneConvexUnInit)(times)
rates = zeros(length(times))
MonotoneConvex(rates, times)
end



function __issector1(g0, g1)
a = (g0 > 0) && (g1 >= -2 * g0) && (-0.5 * g0 >= g1)
b = (g0 < 0) && (g1 <= -2 * g0) && (-0.5 * g0 <= g1)
a || b
end

function __issector2(g0, g1)
a = (g0 > 0) && (g1 < -2 * g0)
b = (g0 < 0) && (g1 > -2 * g0)
a || b
end


# Hagan West - WILMOTT magazine pgs 75-81
function g(f⁻, f, fᵈ)
@show f⁻, f, fᵈ
@show g0 = f⁻ - fᵈ
@show g1 = f - fᵈ
if sign(g0) == sign(g1)
# sector (iv)
η = g1 / (g1 + g0)
α = -g0 * g1 / (g1 + g0)

if x < η
return x -> α + (g0 - α) * ((η - x) / η)^2
else
return x -> α + (g1 - α) * ((x - η) / (1 - η))^2
end


elseif __issector1(g0, g1)
# sector (i)
x -> g0 * (1 - 4 * x + 3 * x^2) + g1 * (-2 * x + 3 * x^2)
elseif __issector2(g0, g1)
# sector (ii)
η = (g1 + 2 * g0) / (g1 - g0)
if x < η
return x -> g0
else
return x -> g0 + (g1 - g0) * ((x - η) / (1 - η))^2
end
else
# sector (iii)
η = 3 * g1 / (g1 - g0)
if x > η
return x -> g1
else
return x -> g1 + (g0 - g1) * ((η - x) / η)^2
end

end
end

function g_rate(x, f⁻, f, fᵈ)
@show x, f⁻, f, fᵈ
@show g0 = f⁻ - fᵈ
@show g1 = f - fᵈ
if sign(g0) == sign(g1)
# sector (iv)
η = g1 / (g1 + g0)
α = -g0 * g1 / (g1 + g0)

if x < η
return α * x - (g0 - α) * ((η - x)^3 / η^2 - η) / 3
else
return (2 * α + g0) / 3 * η + α * (x - η) + (g1 - α) / 3 * (x - η)^3 / (1 - η)^2
end


elseif __issector1(g0, g1)
# sector (i)
@show "(i)"
g0 * (x - 2 * x^2 + x^3) + g1 * (-x^2 + x^3)
elseif __issector2(g0, g1)
# sector (ii)
η = (g1 + 2 * g0) / (g1 - g0)
if x < η
return g0 * x
else
return g0 * x + ((g1 - g0) * (x - η)^3 / (1 - η)^2) / 3
end
else
# sector (iii)
η = 3 * g1 / (g1 - g0)
if x > η
return (2 * g1 + g0) / 3 * η + g1 * (x - η)
else
return g1 * x - (g0 - g1) * ((η - x)^3 / η^2 - η) / 3
end

end
end

function forward(t, rates, times)

t, i_time, rates, times = __monotone_convex_init(t, rates, times)
f, fᵈ = __monotone_convex_fs(rates, times)

x = (t - times[i_time]) / (times[i_time+1] - times[i_time])
return fᵈ[i_time+1] + g(x, f[i_time], f[i_time+1], fᵈ[i_time+1])
end

"""
returns the index associated with the time t, an initial rate vector, and a time vector
"""
function __i_time(t, times)
i_time = findfirst(x -> x > t, times)
if i_time == nothing
i_time = lastindex(times)
end
return i_time
end

"""
returns a pair of vectors (f and fᵈ) used in Monotone Convex Yield Curve fitting
"""
function __monotone_convex_fs(rates, times)
# step 1
fᵈ = deepcopy(rates)
for i in 2:length(times)
fᵈ[i] = (times[i] * rates[i] - times[i-1] * rates[i-1]) / (times[i] - times[i-1])
end
# step 2
f = similar(rates, length(rates) + 1)
# fill in middle elements first, then do 1st and last
for i in 1:length(rates)-1
t_prior = if i == 1
0
else
times[i-1]
end

weight1 = (times[i] - t_prior) / (times[i+1] - t_prior)
weight2 = (times[i+1] - times[i]) / (times[i+1] - t_prior)
f[i+1] = weight1 * fᵈ[i+1] + weight2 * fᵈ[i]
end
# step 3
# collar(a,b,c) = clamp(b, a, c)
f[1] = fᵈ[1] - 0.5 * (f[2] - fᵈ[1])

f[end] = fᵈ[end] - 0.5 * (f[end-1] - fᵈ[end])
f[1] = clamp(f[1], 0, 2 * fᵈ[2])
f[end] = clamp(f[end], 0, 2 * fᵈ[end])

for j in 2:(length(times)-1)
f[j] = clamp(f[j], 0, 2 * min(fᵈ[j], fᵈ[j+1]))
end

return f, fᵈ
end


function Base.zero(mc::MonotoneConvex, t)
lt = last(mc.times)
f, fᵈ = mc.f, mc.fᵈ
if t > lt
r = Base.zero(mc, lt)
i_time = __i_time(t, mc.times)
return r * lt / t + forward(lt, mc.rates, mc.times) * (1 - lt / t)
end
@show i_time = __i_time(t, mc.times)
# if the time is greater than the last input time then extrapolate using the forwards

x = if i_time == 1
x = t / times[i_time]
else
x = (t - times[i_time-1]) / (times[i_time] - times[i_time-1])
end
G = g(f[i_time], f[i_time+1], fᵈ[i_time])
return Continuous(1 / t * (times[i_time] * rates[i_time] + (t - times[i_time]) * fᵈ[i_time] + (times[i_time] - times[i_time-1]) * G))

# STATUS:
# intermediate G/other results OK
# need to get the right rate. Instead of last formula, trying the approach on pg 39 of
# http://uu.diva-portal.org/smash/get/diva2:1477828/FULLTEXT01.pdf
# and have added QuadGK and converted the G function to return a function of x instead of a calculated value
# (also need to change the signature of associated test cases)
# QuadGK.quadgk(G,t)

end

function FinanceCore.discount(mc::MonotoneConvex, t)
r = zero(mc, t)
return discount(r, t)
end

function Base.zero(mc::MonotoneConvexUnoptimized, t)
lt = last(times)
# if the time is greater than the last input time then extrapolate using the forwards
if t > lt
r = myzero(lt, rates, times, f, fᵈ)
return r * lt / t + forward(lt, rates, times) * (1 - lt / t)
end

t, i_time, rates, times = __monotone_convex_init(t, rates, times)
f, fᵈ = __monotone_convex_fs(mc.rates, mc.times)
x = (t - times[i_time]) / (times[i_time+1] - times[i_time])
G = g_rate(x, f[i_time], f[i_time+1], fᵈ[i_time+1])
return 1 / t * (times[i_time] * rates[i_time] + (t - times[i_time]) * fᵈ[i_time+1] + (times[i_time+1] - times[i_time]) * G)

end

function FinanceCore.discount(mc::MonotoneConvexUnoptimized, t)
r = zero(mc, t)
return exp(-r * t)
end

times = 1:5
rates = [0.03, 0.04, 0.047, 0.06, 0.06]
# forward(5.19, rates, times)
# myzero(1, rates, times)


# using Test
# @test forward(0.5, rates, times) ≈ 0.02875
# @test forward(1, rates, times) ≈ 0.04
# @test forward(2, rates, times) ≈ 0.0555
# @test forward(2.5, rates, times) ≈ 0.0571254591368226
# @test forward(5, rates, times) ≈ 0.05025
# @test forward(5.2, rates, times) ≈ 0.05025

# @test myzero(0.5, rates, times) ≈ 0.02625
# @test myzero(1, rates, times) ≈ 0.03
# @test myzero(2, rates, times) ≈ 0.04
# @test myzero(2.5, rates, times) ≈ 0.0431375956535047
# @test myzero(5, rates, times) ≈ 0.06
# @test myzero(5.2, rates, times) ≈ 0.059625


Loading
Loading