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 Cashflows datastructure #95

Closed
wants to merge 1 commit into from
Closed
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
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
FinanceCore = "b9b1ffdd-6612-4b69-8227-7663be06e089"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
Lazy = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0"
MuladdMacro = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Expand All @@ -21,9 +22,10 @@ FinanceCore = "^1"
ForwardDiff = "^0.10"
MuladdMacro = "^0.2"
QuadGK = "^2"
Reexport = "^1.2"
Lazy = "^0.15"
SnoopPrecompile = "^1"
StatsBase = "^0.33"
Reexport = "^1.2"
Yields = "^2,^3"
julia = "^1.6"

Expand Down
5 changes: 4 additions & 1 deletion src/ActuaryUtilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ using Dates
import FinanceCore
@reexport using FinanceCore: internal_rate_of_return, irr
using ForwardDiff
import Lazy
using QuadGK
using MuladdMacro
using Yields
import StatsBase
using SnoopPrecompile

include("cashflow.jl")
include("financial_math.jl")
include("risk_measures.jl")
include("derivatives.jl")
Expand Down Expand Up @@ -155,6 +157,7 @@ export years_between, duration,
accum_offset,
Macaulay,Modified,DV01,KeyRatePar,KeyRateZero,KeyRate,duration, convexity,
VaR,ValueAtRisk,CTE,ConditionalTailExpectation,ExpectedShortfall,
eurocall, europut
eurocall, europut,
Cashflow

end # module
32 changes: 32 additions & 0 deletions src/cashflow.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
struct Cashflow{A,T}
amount::A
time::T
end

#TODO Define Cashflow on a vector/iterable?

@inline function present_value(yield,cf::Cashflow)
return discount(yield,cf.time) * cf.amount
end

# this method ignores the time argument and instead uses
# the time within the cashflow. This is to allow for `present_value(yield,cfs)`
# to dispatch on the values of `cfs`. Otherwise, if `cfs` is a generator or otherwise
# an iterable of ambiguous types, we lose the ability to dispatch on the right method
# for Cashflow or just a real type where we infer the timepoint
function present_value(yield,cf::Cashflow,time::T) where {T<:Real}
return discount(yield,cf.time) * cf.amount
end


# There are places where we want to infer a 1:n range in a lazy way if not Cashflows; if Cashflows, we often want to ignore
# the times given in the function and stay true to the times embedded in the Cashflows, and these utility functions accomplish this
__times(cfs;start=1) = (__time(cf,t) for (t,cf) in zip(Lazy.range(start),cfs))
__times(cfs,times) = (__time(cf,t) for (t,cf) in zip(times,cfs))
__time(cf::Cashflow,t) = cf.time
__time(cf,t) = t

#look through to the cashflow and grab the amount
__cashflows(cfs) = (__cashflow(cf) for cf in cfs)
__cashflow(cf::Cashflow) = cf.amount
__cashflow(cf) = cf
114 changes: 50 additions & 64 deletions src/financial_math.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""
present_value(interest, cashflows::Vector, timepoints)
present_value(interest, cashflows::Vector)
present_value(yield, cashflow, timepoints)
present_value(yield, cashflow)

Discount the `cashflows` vector at the given `interest_interestrate`, with the cashflows occurring
Discount the `cashflow` at the given `yield`, with the cashflows occurring
at the times specified in `timepoints`. If no `timepoints` given, assumes that cashflows happen at times 1,2,...,n.

The `interest` can be an `InterestCurve`, a single scalar, or a vector wrapped in an `InterestCurve`.
# Arguments
- `yield` can be any valid Yields.jl yield rate or object
- `cashflow` can be a `Real`-valued scalar, a `Cashflow`, or a vector of scalars or `Cashflow`
- `timepoints` is a scalar or vector of time-points that the corresponding cashflows occur. If `cashflow` is a `Cashflow` object, then the time used in determining the discount factor will be the time in the `Cashflow`` data, and the `timepoints` will be ignored.

# Examples

```julia-repl
julia> present_value(0.1, [10,20],[0,1])
28.18181818181818
Expand All @@ -16,22 +20,22 @@ julia> present_value(Yields.Forward([0.1,0.2]), [10,20],[0,1])
```

Example on how to use real dates using the [DayCounts.jl](https://github.com/JuliaFinance/DayCounts.jl) package
```jldoctest

```jldoctest
using DayCounts
dates = Date(2012,12,31):Year(1):Date(2013,12,31)
times = map(d -> yearfrac(dates[1], d, DayCounts.Actual365Fixed()),dates) # [0.0,1.0]
present_value(0.1, [10,20],times)

# output
28.18181818181818

```

"""
function present_value(yc::T, cashflows, timepoints) where {T <: Yields.AbstractYield}
s = 0.0
for (cf,t) in zip(cashflows,timepoints)
cfs = __cashflows(cashflows)
times = __times(cfs,timepoints)
for (cf,t) in zip(cfs,times)
v = discount(yc,t)
@muladd s = s + v * cf
end
Expand All @@ -40,37 +44,18 @@ function present_value(yc::T, cashflows, timepoints) where {T <: Yields.Abstract
end

function present_value(yc::T, cashflows) where {T <: Yields.AbstractYield}
present_value(yc,cashflows,1:length(cashflows))
end

function present_value(i, x)

v = 1.0
v_factor = discount(i,0,1)
pv = 0.0

for (t,cf) in enumerate(x)
v = v * v_factor
@muladd pv = pv + v * cf
end
return pv
times = __times(cashflows)
present_value(yc,cashflows,times)
end

function present_value(i, v, times)
return present_value(Yields.Constant(i), v, times)
end

# Interest Given is an array, assume forwards.
function present_value(i::AbstractArray, v)
yc = Yields.Forward(i)
return sum(discount(yc, t) * cf for (t,cf) in enumerate(v))
function present_value(i, v)
return present_value(Yields.Constant(i), v)
end

# Interest Given is an array, assume forwards.
function present_value(i::AbstractArray, v, times)
yc = Yields.Forward(i, times)
return sum(discount(yc, t) * cf for (cf, t) in zip(v,times))
end

"""
pv()
Expand Down Expand Up @@ -156,7 +141,7 @@ Calculate the time when the accumulated cashflows breakeven given the yield.
Assumptions:

- cashflows occur at the end of the period
- cashflows evenly spaced with the first one occuring at time zero if `times` not given
- cashflows evenly spaced with the first one occurring at time zero if `times` not given

Returns `nothing` if cashflow stream never breaks even.

Expand All @@ -172,22 +157,18 @@ julia> breakeven(0.10, [-10,-15,2,3,4,8]) # returns the `nothing` value

```
"""
function breakeven(y::T, cashflows::Vector, timepoints::Vector) where {T <: Yields.AbstractYield}
accum = zero(eltype(cashflows))
function breakeven(y::T, cashflows, timepoints) where {T <: Yields.AbstractYield}
accum = 0.0
last_neg = nothing

accum += cashflows[1]
if accum >= 0 && isnothing(last_neg)
last_neg = timepoints[1]
end

for i in 2:length(cashflows)
times = __times(cfs,timepoints)
prior_time = first(times)
for (cf, t) in zip(cashflows,times)
# accumulate the flow from each timepoint to the next
accum *= Yields.accumulation(y, timepoints[i - 1], timepoints[i])
accum += cashflows[i]

if accum >= 0 && isnothing(last_neg)
last_neg = timepoints[i]
accum *= Yields.accumulation(y, prior_time, t)
accum += __cashflow(cf)
prior_time = t
if accum >= 0 && isnothing(last_neg)
last_neg = t
elseif accum < 0
last_neg = nothing
end
Expand All @@ -197,16 +178,13 @@ function breakeven(y::T, cashflows::Vector, timepoints::Vector) where {T <: Yiel

end

function breakeven(y::T, cfs, times) where {T <: Real}
function breakeven(y, cfs, times)
return breakeven(Yields.Constant(y), cfs, times)
end

function breakeven(y::Vector{T}, cfs, times) where {T <: Real}
return breakeven(Yields.Forward(y), cfs, times)
end

function breakeven(i, cashflows::Vector)
return breakeven(i, cashflows, [t for t in 0:length(cashflows) - 1])
function breakeven(i, cashflows)
times = __times(cashflows;start=0)
return breakeven(i, cashflows, times)
end

abstract type Duration end
Expand Down Expand Up @@ -307,9 +285,14 @@ julia> convexity(0.03,my_lump_sum_value)
```
"""
function duration(::Macaulay, yield, cfs, times)
return sum(times .* price.(yield, cfs, times) / price(yield, cfs, times))
a= sum(pv(yield, __multiply_time(cf,t), t) for (t,cf) in zip(times,cfs))
b = sum(pv(yield, cf, t) for (t,cf) in zip(times,cfs))
a / b
end

__multiply_time(cf::Cashflow,time) = (cf.amount * cf.time,cf.time)
__multiply_time(cf,time) = cf * time

function duration(::Modified, yield, cfs, times)
D(i) = price(i, cfs, times)
return duration(yield, D)
Expand All @@ -321,10 +304,10 @@ function duration(yield::Y, valuation_function::T) where {Y<:Yields.AbstractYiel
end

function duration(yield, cfs, times)
return duration(Modified(), yield, vec(cfs), times)
return duration(Modified(), yield, cfs, times)
end
function duration(yield::Y, cfs) where {Y <: Yields.AbstractYield}
times = 1:length(cfs)
times = __times(cfs)
return duration(Modified(), yield, cfs, times)
end

Expand All @@ -336,8 +319,8 @@ function duration(::DV01, yield, cfs, times)
return duration(DV01(), yield, i -> price(i, vec(cfs), times))
end
function duration(d::Duration, yield, cfs)
times = 1:length(cfs)
return duration(d, yield, vec(cfs), times)
times = __times(cfs)
return duration(d, yield, cfs, times)
end

function duration(::DV01, yield, valuation_function::Y) where {Y<:Function}
Expand Down Expand Up @@ -383,11 +366,12 @@ julia> convexity(0.03,my_lump_sum_value)

"""
function convexity(yield, cfs, times)
return convexity(yield, i -> price(i, cfs, times))
ts = __times(cfs,times)
return convexity(yield, i -> price(i, cfs, ts))
end

function convexity(yield,cfs)
times = 1:length(cfs)
times = __times(cfs)
return convexity(yield, i -> price(i, cfs, times))
end

Expand Down Expand Up @@ -496,7 +480,7 @@ function duration(keyrate::KeyRateDuration, curve, cashflows, timepoints)
end

function duration(keyrate::KeyRateDuration, curve, cashflows)
timepoints = eachindex(cashflows)
timepoints = __times(cashflows)
krd_points = 1:maximum(timepoints)
return duration(keyrate, curve, cashflows, timepoints, krd_points)

Expand All @@ -507,7 +491,8 @@ end

Return the solved-for constant spread to add to `curve1` in order to equate the discounted `cashflows` with `curve2`
"""
function spread(curve1,curve2,cashflows,times=eachindex(cashflows))
function spread(curve1,curve2,cashflows,times=__times(cashflows))
ts = [0.; Lazy.take(times,length(cashflows))]
pv1 = pv(curve1,cashflows,times)
pv2 = pv(curve2,cashflows,times)
irr1 = irr([-pv1;cashflows], [0.;times])
Expand All @@ -518,7 +503,7 @@ function spread(curve1,curve2,cashflows,times=eachindex(cashflows))
end

"""
moic(cashflows<:AbstractArray)
moic(cashflows)

The multiple on invested capital ("moic") is the un-discounted sum of distributions divided by the sum of the contributions. The function assumes that negative numbers in the array represent contributions and positive numbers represent distributions.

Expand All @@ -530,7 +515,8 @@ julia> moic([-10,20,30])
```

"""
function moic(cfs::T) where {T<:AbstractArray}
function moic(cashflows)
cfs = __cashflows(cashflows)
returned = sum(cf for cf in cfs if cf > 0)
invested = -sum(cf for cf in cfs if cf < 0)
return returned / invested
Expand Down
Loading