diff --git a/Project.toml b/Project.toml index 878df9e..dcfba16 100644 --- a/Project.toml +++ b/Project.toml @@ -30,7 +30,7 @@ FinanceModelsMakieCoreExt = "MakieCore" AccessibleOptimization = "^0.1.1" Accessors = "^0.1" BSplineKit = "^0.16" -FinanceCore = "^2" +FinanceCore = "^2.1" IntervalSets = "^0.7" MakieCore = "0.6" Optimization = "^3.15" diff --git a/docs/src/contracts.md b/docs/src/contracts.md index bc0e024..440c7e2 100644 --- a/docs/src/contracts.md +++ b/docs/src/contracts.md @@ -88,7 +88,48 @@ Note that all contracts in FinanceModels.jl are currently *unit* contracts in th #### More complex Contracts -**When the cashflow depends on a model**. An example of this is a floating bond where the coupon paid depends on a view of forward rates. See [this section in the overview](@ref Contracts-that-depend-on-the-model-(or-multiple-models)) on projections for how this is handled. +##### Transformations + +Contracts (`<:AbstractContract`) and [`Projection`](@ref)s can be modified to be scaled or transformed using the transformations in [Transducers.jl](https://juliafolds2.github.io/Transducers.jl/stable/#List-of-transducers) after importing that package. + +Most commonly, this is likely simply chaining `Map(...)` calls. Two use-cases of this may be to (1) scale the contract by a factor or (2) change the sign of the contract to indicate an obligation/liability instead of an asset. Examples of this: + +```julia-repl +julia> using Transducers, FinanceModels + +julia> Bond.Fixed(0.05,Periodic(1),3) |> collect +3-element Vector{Cashflow{Float64, Float64}}: + Cashflow{Float64, Float64}(0.05, 1.0) + Cashflow{Float64, Float64}(0.05, 2.0) + Cashflow{Float64, Float64}(1.05, 3.0) + +julia> Bond.Fixed(0.05,Periodic(1),3) |> Map(-) |> collect +3-element Vector{Cashflow{Float64, Float64}}: + Cashflow{Float64, Float64}(-0.05, 1.0) + Cashflow{Float64, Float64}(-0.05, 2.0) + Cashflow{Float64, Float64}(-1.05, 3.0) + +julia> Bond.Fixed(0.05,Periodic(1),3) |> Map(-) |> Map(x->x*2) |> collect +3-element Vector{Cashflow{Float64, Float64}}: + Cashflow{Float64, Float64}(-0.1, 1.0) + Cashflow{Float64, Float64}(-0.1, 2.0) + Cashflow{Float64, Float64}(-2.1, 3.0) +``` + +Another example of this is how `InterestRateSwap`[@ref] is implemented. It's simply a `Composite` contract of a positive fixed rate bond and a negative floating rate bond: + +```julia +function InterestRateSwap(curve, tenor; model_key="OIS") + fixed_rate = par(curve, tenor; frequency=4) + fixed_leg = Bond.Fixed(rate(fixed_rate), Periodic(4), tenor) + float_leg = Bond.Floating(0.0, Periodic(4), tenor, model_key) |> Map(-) + return Composite(fixed_leg, float_leg) +end +``` + +##### Cashflows are model dependent + +An example of this is a floating bond where the coupon paid depends on a view of forward rates. See [this section in the overview](@ref Contracts-that-depend-on-the-model-(or-multiple-models)) on projections for how this is handled. ## Available Contracts & Modules diff --git a/src/Contract.jl b/src/Contract.jl index 9ed3a1a..ffad590 100644 --- a/src/Contract.jl +++ b/src/Contract.jl @@ -424,4 +424,40 @@ end function cashflows_timepoints(qs::Vector{Q}) where {Q<:Quote} cashflows_timepoints([q.instrument for q in qs]) -end \ No newline at end of file +end + +""" + InterestRateSwap(curve, tenor; model_key="OIS") + +A convenience method for creating an interest rate swap given a curve and a tenor via a `Composite` contract consisting of receiving a [fixed bond](@ref Bond.Fixed) and paying (i.e. the negative of) a [floating bond](@ref Bond.Floating). + +The notional is a unit (1.0) amount and assumed to settle four times per period. + + +A [`Projection`](@ref), with an indexable `model_key` is still needed to project a swap. See examples below for what this looks like. + +# Examples + +```julia-repl + +julia> curve = Yield.Constant(0.05); + +julia> swap = InterestRateSwap(curve,10); + +julia> Projection(swap,Dict("OIS" => curve),CashflowProjection()) |> collect +80-element Vector{Cashflow{Float64, Float64}}: +Cashflow{Float64, Float64}(0.012272234429039353, 0.25) +Cashflow{Float64, Float64}(0.012272234429039353, 0.5) +⋮ +Cashflow{Float64, Float64}(-0.012272234429039353, 9.75) +Cashflow{Float64, Float64}(-1.0122722344290391, 10.0) + +``` + +""" +function InterestRateSwap(curve, tenor; model_key="OIS") + fixed_rate = par(curve, tenor; frequency=4) + fixed_leg = Bond.Fixed(rate(fixed_rate), Periodic(4), tenor) + float_leg = Bond.Floating(0.0, Periodic(4), tenor, model_key) |> Map(-) + return Composite(fixed_leg, float_leg) +end diff --git a/src/FinanceModels.jl b/src/FinanceModels.jl index 56f509f..812f5d9 100644 --- a/src/FinanceModels.jl +++ b/src/FinanceModels.jl @@ -24,7 +24,7 @@ include("model/Model.jl") include("Projection.jl") include("fit.jl") -export Cashflow, Quote, Forward, CommonEquity, Option +export Cashflow, Quote, Forward, CommonEquity, Option, InterestRateSwap using .Bond: ZCBYield, ZCBPrice, ParSwapYield, ParYield, CMTYield, ForwardYields, OISYield export Bond, ZCBYield, ZCBPrice, ParSwapYield, ParYield, CMTYield, ForwardYields, OISYield diff --git a/src/Projection.jl b/src/Projection.jl index a3dd323..ab20805 100644 --- a/src/Projection.jl +++ b/src/Projection.jl @@ -60,7 +60,7 @@ struct CashflowProjection <: ProjectionKind end # collecting a Projection gives your the reducible defined below with __foldl__ Base.collect(p::P) where {P<:AbstractProjection} = p |> Map(identity) |> collect # collecting a contract wraps the contract in with the default Projection, defined next -Base.collect(c::C) where {C<:FinanceCore.AbstractContract} = Projection(c) |> Map(identity) |> collect +Base.collect(c::C) where {C<:FinanceCore.AbstractContract} = Projection(c) |> collect # Default Projections ########################## @@ -73,7 +73,7 @@ Projection(c, m) = Projection(c, m, CashflowProjection()) # Reducibles ################################### -# a more composible, efficient way to create a collection of things that you can apply subsequent transformations to +# a more composable, efficient way to create a collection of things that you can apply subsequent transformations to # (and those transformations can be Transducers). # https://juliafolds2.github.io/Transducers.jl/stable/howto/reducibles/ # https://www.youtube.com/watch?v=6mTbuzafcII @@ -84,7 +84,7 @@ Projection(c, m) = Projection(c, m, CashflowProjection()) # `__foldl__` where you can define the collection using a `for` loop # and `foldl__` you can also define state that is used within the loop -# this wraps a contract in a default proejction and makes a contract a reducible collection of cashflows +# this wraps a contract in a default projection and makes a contract a reducible collection of cashflows function Transducers.asfoldable(c::C) where {C<:FinanceCore.AbstractContract} Projection(c) |> Map(identity) end @@ -97,6 +97,14 @@ end return complete(rf, val) end +# If a Transducer has been combined with a contract into an Eduction +# then unwrap the contract and apply the transducer to the projection +@inline function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:Transducers.Eduction,M,K} + rf = __rewrap(p.contract.rf, rf) # compose the xform with any othe existing transducers + p_alt = @set p.contract = p.contract.coll # reset the contract to the underlying contract without transducers + Transducers.__foldl__(rf, val, p_alt) # project with a newly combined reduction +end + # @inline function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:Bond.Fixed,M,K} b = p.contract @@ -159,4 +167,44 @@ end @inline function Transducers.asfoldable(p::Projection{C,M,K}) where {C<:Cashflow,M,K<:CashflowProjection} Ref(p.contract) |> Map(identity) -end \ No newline at end of file +end + +""" + __rewrap(from::Transducers.Reduction, to) + __rewrap(from, to) + +Used to unwrap a Reduction which is a composition of contracts and a transducer and apply the transducers to the associated projection instead of the transducer. + +For example, on its own a contract is not project-able, but wrapped in a (default) [`Projection`](@ref) it can be. But it may also be a lot more convienent +to construct contracts which have scaling or negated modifications and let that flow into a projection. + +# Examples + +```julia-repl +julia> Bond.Fixed(0.05,Periodic(1),3) |> collect +3-element Vector{Cashflow{Float64, Float64}}: + Cashflow{Float64, Float64}(0.05, 1.0) + Cashflow{Float64, Float64}(0.05, 2.0) + Cashflow{Float64, Float64}(1.05, 3.0) + +julia> Bond.Fixed(0.05,Periodic(1),3) |> Map(-) |> collect +3-element Vector{Cashflow{Float64, Float64}}: + Cashflow{Float64, Float64}(-0.05, 1.0) + Cashflow{Float64, Float64}(-0.05, 2.0) + Cashflow{Float64, Float64}(-1.05, 3.0) + +julia> Bond.Fixed(0.05,Periodic(1),3) |> Map(-) |> Map(x->x*2) |> collect +3-element Vector{Cashflow{Float64, Float64}}: + Cashflow{Float64, Float64}(-0.1, 1.0) + Cashflow{Float64, Float64}(-0.1, 2.0) + Cashflow{Float64, Float64}(-2.1, 3.0) +``` +""" +function __rewrap(from::Transducers.Reduction, to) + rfx = from.xform # get the transducer's "xform" from the projection's contract + __rewrap(from.inner, Transducers.Reduction(rfx, to)) # compose the xform with any othe existing transducers +end +function __rewrap(from, to) + # we've hit bottom, so return `to` + return to +end diff --git a/test/Project.toml b/test/Project.toml index d09aec2..9acef95 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,3 +5,4 @@ CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" DecFP = "55939f99-70c6-5e9b-8bb0-5071ed7d61fd" FinanceCore = "b9b1ffdd-6612-4b69-8227-7663be06e089" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Transducers = "28d57a85-8fef-5791-bfe6-a80928e7c999" diff --git a/test/runtests.jl b/test/runtests.jl index e0b5d2e..241e159 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,6 +7,7 @@ using Accessors using FinanceModels using Test +using Transducers include("generic.jl") include("sp.jl") diff --git a/test/sp.jl b/test/sp.jl index e13bf65..0755814 100644 --- a/test/sp.jl +++ b/test/sp.jl @@ -39,6 +39,34 @@ end p = Projection(c) @test collect(p) == [collect(a); collect(b)] + + + # a swap value with the same curve used to parameterize should have + # zero value at inception + curve = Yield.Constant(0.05) + swap = InterestRateSwap(curve, 10) + proj = Projection(swap, Dict("OIS" => curve), CashflowProjection()) + @test pv(0.04, proj |> collect) ≈ 0.0 atol = 1e-12 +end + +@testset "Transduced Contracts" begin + + @test (Bond.Fixed(0.05, Periodic(1), 3) |> Map(-) |> collect) == [ + Cashflow{Float64,Float64}(-0.05, 1.0), + Cashflow{Float64,Float64}(-0.05, 2.0), + Cashflow{Float64,Float64}(-1.05, 3.0) + ] + + @test (Bond.Fixed(0.05, Periodic(1), 3) |> Map(-) |> Map(x -> x * 2) |> collect) == [ + Cashflow{Float64,Float64}(-0.1, 1.0), + Cashflow{Float64,Float64}(-0.1, 2.0), + Cashflow{Float64,Float64}(-2.1, 3.0) + ] + + + + + end @testset "Fit Models" begin