Loss Model
Gaussian Loss Model
Project.toml
name = "GaussianFactorModel"
version = "0.1.0"
uuid = "b4865c2b-8080-47c0-a052-c54cf23146cd"
authors = ["Patrick Haener <contact@haenerconsulting.com>"]
[deps]
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
GaussianLossModel.jl
module GaussianFactorModel
using Random
using Distributions
using Statistics
using LinearAlgebra
using Optim
# $x_n = \sum_i a_{n,i} \xi_i + b_n \xi_n\qquad \xi_i$: Gaussian variable
#
# $a_i$: factor loading
#
# $b_n$: idiosyncratic loading
#
# $b_n^2 = 1-\sum_i a_i^2$
#
# $\mathbb{E}[\xi_{i}\xi_j]=\delta_{ij}$
# `factorloadings`: 2 dimensional vector $a_{n,i}$
#
# `idiosyncraticloadings`: vector $b_n$
struct GaussianFactorModelParameters
factorloadings::Matrix{Float64}
idiosyncraticloadings::Vector{Float64}
end
function gaussianfactormodelparameters(factorloadings::Matrix{Float64})
@assert all(x->(x>=-1.0 && x<=1.0), factorloadings) "Illegal value for factor loadings: all loadings must be between -1 and 1"
i = 1.0 .- mapslices(x->sum(x .* x), factorloadings, dims=[2])
@assert all(x->(x>=0.0), i) "Illegal value for idiosyncratic loadings: implied imaginary value of idiosyncratic loading"
GaussianFactorModelParameters(factorloadings, vec(sqrt.(i)))
end
# Single factor model:
function gaussianfactormodelparameters(factorloadings::Vector{Float64})
gaussianfactormodelparameters(reshape(factorloadings, length(factorloadings), 1))
end
# Single factor model with common driver loading ρ
function gaussianfactormodelparameters(ρ::Float64, numbervariables::Int)
gaussianfactormodelparameters(fill(ρ, (numbervariables, 1)))
end
# Correlation matrix from `GaussianFactorModelParameter`s
function cor(p::GaussianFactorModelParameters)
c = p.factorloadings * transpose(p.factorloadings)
for i in 1:size(p.factorloadings)[1]
c[i, i] = c[i, i] + p.idiosyncraticloadings[i] * p.idiosyncraticloadings[i]
end
c
end
# See https://github.com/JuliaLang/julia/issues/22279
function Base.IteratorSize(p::GaussianFactorModelParameters)
Base.IsInfinite()
end
# +
function Base.iterate(p::GaussianFactorModelParameters)
iterate(p, (Random.seed!(), Normal()))
end
function Base.iterate(p::GaussianFactorModelParameters, state::Tuple{Random.AbstractRNG, UnivariateDistribution})
numberidiosyncratic, numberfactors = size(p.factorloadings)
factors = rand(state[1], state[2], numberfactors)
idiosyncratic = rand(state[1], state[2], numberidiosyncratic)
r = p.factorloadings * factors .+ p.idiosyncraticloadings .* idiosyncratic
(r, state)
end
end
LossModel
Project.toml
name = "LossModel"
uuid = "11c3f125-d377-4068-8121-5488f83f8688"
authors = ["Patrick Haener <contact@haenerconsulting.com>"]
version = "0.1.0"
[deps]
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
GaussianFactorModel = "b4865c2b-8080-47c0-a052-c54cf23146cd"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
LossModel.jl
module LossModel
using Random
using Statistics
using Distributions
using LinearAlgebra
using GaussianFactorModel
# ## Model Parameters
abstract type LossModelParameters end
# ### Simple Model
# For uncorrelated losses with deterministic PD and loss at default
struct SimpleLossModelParameters <: LossModelParameters
# probability of default
pds::Vector{Float64}
# loss when default
losses::Vector{Float64}
end
function simplelossmodelparameters(pdsandls::Vector{Tuple{Float64, Float64}})
pds = [x[1] for x in pdsandls]
ls = [x[2] for x in pdsandls]
SimpleLossModelParameters(pds, ls)
end
function simplelossmodelparameters(pds::Vector{Float64}, losses::Vector{Float64})
@assert length(pds)==length(losses)
SimpleLossModelParameters(pds, losses)
end
# `SimpleLlosParameters` for `n` entities, each having the same PD and loss
function simplelossmodelparameters(pd::Float64, loss::Float64, n)
SimpleLossModelParameters(fill(pd, n), fill(loss, n))
end
# ### Parameters for Gaussian Factor Loss Model
# Deterministic PDs and losses; losses are dependent given by a Gaussian factor model
struct GaussianFactorLossModelParameters <: LossModelParameters
pds::Vector{Float64}
losses::Vector{Float64}
factormodelparameters::GaussianFactorModel.GaussianFactorModelParameters
end
function gaussianfactorlossmodelparameters(pdslosses::Vector{Tuple{Float64, Float64}}, factormodelparameters::GaussianFactorModel.GaussianFactorModelParameters)
@assert length(pdslosses)==length(factormodelparameters.idiosyncraticloadings) "Parameters imply inconsistent number of variables"
GaussianFactorLossModelParameters([x[1] for x in pdslosses], [x[2] for x in pdslosses], factormodelparameters)
end
function gaussianfactorlossmodelparameters(pds::Vector{Float64}, losses::Vector{Float64}, factormodelparameters::GaussianFactorModel.GaussianFactorModelParameters)
@assert length(pds)==length(losses) "Vectors for PDs and for losses must have same length"
gaussianfactorlossmodelparameters(collect(zip(pds, losses)), factormodelparameters)
end
function gaussianfactorlossmodelparameters(lossmodelparameters::SimpleLossModelParameters, factormodelparameters::GaussianFactorModel.GaussianFactorModelParameters)
gaussianfactorlossmodelparameters(lossmodelparameters.pds, lossmodelparameters.losses, factormodelparameters)
end
# ## Loss Models
# Returns a vector where the elements of the vector are the losses for a given counterparty for sample 1 to sample `numbersamples`:
#
# $v = [\ell_1,\ldots \ell_n,\ldots,\ell_N]$, $N$ number of counteparties and $\ell_n$ vector containing losses for counterparty $n$ for all samples
function lossespercounterparty(p::M, numbersamples::Int64) where { M<: LossModelParameters }
numberitems = length(p.pds)
result = [zeros(Float64, numbersamples) for n in 1:numberitems]
r = Iterators.take(p, numbersamples)
for (n, l) in Iterators.enumerate(r)
for i in 1:numberitems
result[i][n] = l[i]
end
end
result
end
# Returns a vector where the elements of the vector are the losses for a given sample for each counterparty:
#
# $v = [\ell_1,\ldots \ell_i,\ldots,\ell_{\text{numbersamples}}]$, $\ell_i$ vector containing losses for sample $i$ for all counterparties
function lossespersample(p::M, numbersamples::Int64) where { M<: LossModelParameters }
collect(Vector{Float64}, Iterators.take(p, numbersamples))
end
# ### Simple
# +
# See https://github.com/JuliaLang/julia/issues/22279
function Base.IteratorSize(p::SimpleLossModelParameters)
Base.IsInfinite()
end
function Base.iterate(p::SimpleLossModelParameters)
iterate(p, Random.seed!())
end
function Base.iterate(p::SimpleLossModelParameters, state::Random.AbstractRNG)
q = rand(length(p.pds))
r = map(x -> x[1] <= x[2] ? x[3] : 0.0, zip(q, p.pds, p.losses))
(r, state)
end
# Mean of losses
function Statistics.mean(p::SimpleLossModelParameters)
p.pds .* p.losses
end
# Variance of losses
function var(p::SimpleLossModelParameters)
p.pds .* p.losses .* p.losses - mean(p) .* mean(p)
end
# Covariance of losses
function cov(p::SimpleLossModelParameters)
diagm(var(p))
end
# ### Gaussian
# +
# See https://github.com/JuliaLang/julia/issues/22279
function Base.IteratorSize(p::GaussianFactorLossModelParameters)
Base.IsInfinite()
end
function Base.iterate(p::GaussianFactorLossModelParameters)
iterate(p, (Random.seed!(), Normal()))
end
function Base.iterate(p::GaussianFactorLossModelParameters, state::Tuple{Random.AbstractRNG, UnivariateDistribution})
q = Base.iterate(p.factormodelparameters, state)
z = [quantile(Distributions.Normal(), x) for x in p.pds]
r = map(x -> x[1] <= x[2] ? x[3] : 0.0, zip(q[1], z, p.losses))
(r, state)
end
# -
SimpleLossModelParameters
function Statistics.mean(p::GaussianFactorLossModelParameters)
p.pds .* p.losses
end
function var(p::GaussianFactorLossModelParameters)
p.pds .* p.losses .* p.losses - mean(p) .* mean(p)
end
function cov(p::GaussianFactorLossModelParameters)
throw("Not implemented yet. Need to calcualte CDF(z1, z2)")
end
# ## Loss Aggregation Functions
# $f([L_1,\ldots,L_i,\ldots, L_{\text{numbersamples}}])$ where $L_i$ is the total loss for sample $i$
function eulerdecomposition(p::P, f, numbersamples::Int64, ϵ::Float64=0.0001) where { P<: LossModelParameters }
x = lossespercounterparty(p, numbersamples)
value = f(sum(x))
decomposition = contributions(x, value, f, numbersamples, ϵ)
(value, decomposition)
end
# Losses is a vector $[\ell_1,\ldots \ell_n,\ldots,\ell_N]$, $N$ number of counteparties and $\ell_n$ vector containing losses for counterparty $n$ for all samples
#
# The output of function `lossespercounterparty` yield such a vector.
#
# `i` is the index of the counterparty: i=1,..., (number of counterparties)
#
function contribution(losses::Vector{Vector{Float64}}, value::Float64, i::Int64, f, numbersamples::Int64, ϵ::Float64=0.0001) where { P<: LossModelParameters }
x = copy(losses)
x_old = x[i]
x_new = x_old .* (1.0 + ϵ)
x[i] = x_new
b = f(sum(x))
(b-value) / ϵ
end
function contributions(losses::Vector{Vector{Float64}}, value::Float64, f, numbersamples::Int64, ϵ::Float64=0.0001) where { P<: LossModelParameters }
[contribution(losses, value, i , f, numbersamples, ϵ) for i in 1:length(losses)]
end
end
Vasicek Loss Model
Project.toml
name = "VasicekLossModel"
uuid = "0f8c25db-2d65-45d2-a5d9-c1b381ca065a"
authors = ["Patrick Haener <contact@haenerconsulting.com>"]
version = "0.1.0"
[deps]
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
VasicekLossModel.jl
module VasicekLossModel
using Distributions
const normal = Normal()
function vasiceklossquantile(p::Float64, ρ::Float64, q::Float64)
cdf(normal, (quantile(normal, p) + sqrt(ρ)*quantile(normal, q))/sqrt(1-ρ))
end
# Fraction of defaulted entities for finite number of entities $n>>1$ and $\rho=0$
function fractiondefaulteduncorrelatedapprox(p::Float64, n::Int, q::Float64)
quantile(Normal(p, sqrt(p*(1-p)/n)), q)
end
end
LossData
Project.toml
name = "LossData"
uuid = "011f8915-1e18-4f05-bb6b-684427991dcf"
authors = ["Patrick Haener <contact@haenerconsulting.com>"]
version = "0.1.0"
[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
GaussianFactorModel = "b4865c2b-8080-47c0-a052-c54cf23146cd"
LossModel = "11c3f125-d377-4068-8121-5488f83f8688"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
VasicekLossModel = "0f8c25db-2d65-45d2-a5d9-c1b381ca065a"
LossData.jl
module LossData
using Random
using Distributions
using DataFrames
using GaussianFactorModel
using LossModel
abstract type PartitioningIndex end
struct NettingSetIdentifier{T} <: PartitioningIndex
id::T
end
function string(id::NettingSetIdentifier{T}) where {T}
id.id
end
@enum Region EW EAP EECA UK NA SALAC MENA SSA SA
const regionnames = Dict([
(EW, "Europe (west) area"),
(EAP, "East Asia & Pacific"),
(EECA, "Eastern Europe & Central Asia"),
(UK, "United Kingdom"),
(NA, "North America"),
(SALAC, "South America, Latin America & Carribean"),
(MENA, "Middle East & North Africa"),
(SSA, "Sub-Saharan Africa"),
(SA, "South Asia")
])
function string(r::Region)
get(regionnames, r, Nothing)
end
function region(s::String)
d = Dict([(get(regionnames, r, Nothing), r) for r in instances(Region)])
get(d, s, Nothing)
end
@enum Sector FI SR M MQ RW BSO TUS RE
const sectornames = Dict([
(FI, "Finance Industry"),
(SR, "Sovereigns/Regionals"),
(M, "Manufacturing"),
(MQ, "Mining & Quarrying"),
(RW, "Retail / Wholesale trade"),
(BSO, "Buisiness Service & Other"),
(TUS, "Transport, Utilities & Storage"),
(RE, "Real Estate")
])
function string(r::Sector)
get(sectornames, r, Nothing)
end
function sector(s::String)
d = Dict([(get(sectornames, q, Nothing), q) for q in instances(Sector)])
get(d, s, Nothing)
end
struct RegionSectorPartitioningIndex <: PartitioningIndex
region::Region
sector::Sector
end
function instances(::Type{RegionSectorPartitioningIndex})
[RegionSectorPartitioningIndex(r, s) for r in instances(Region) for s in instances(Sector)]
end
function nettingsetidentifiers(n::I) where {I<:Integer}
[NettingSetIdentifier(i) for i in 1:n]
end
function randomnotionals(nettingsetidentifiers::Vector{NettingSetIdentifier{T}}, maxnotional::Float64) where {T}
n = rand(Uniform(0.0, maxnotional), length(nettingsetidentifiers))
collect(zip(nettingsetidentifiers, n))
end
function randompds(nettingsetidentifiers::Vector{NettingSetIdentifier{T}}, maxpd::Float64) where {T}
p = rand(Uniform(0.0, maxpd), length(nettingsetidentifiers))
collect(zip(nettingsetidentifiers, p))
end
function randomsimplelossmodelparameters(nettingsetidentifiers::Vector{NettingSetIdentifier{T}}, maxpd::Float64, maxloss::Float64) where {T}
l = rand(Uniform(0.0, maxloss), length(nettingsetidentifiers))
p = rand(Uniform(0.0, maxpd), length(nettingsetidentifiers))
(nettingsetidentifiers, LossModel.SimpleLossModelParameters(l,p))
end
function randomsimplelossmodelparameters(n::Int64, minmaxpd::Tuple{Float64,Float64}, minmaxloss::Tuple{Float64, Float64})
p = rand(Uniform(minmaxpd[1], minmaxpd[2]), n)
l = rand(Uniform(minmaxloss[1], minmaxloss[2]), n)
LossModel.SimpleLossModelParameters(p, l)
end
function randomsimplelossmodelparameters(n::Int64, maxpd::Float64, maxloss::Float64)
randomsimplelossmodelparameters(n, (0.0, maxpd), (0.0, maxloss))
end
function randomlossmodelparameters(nettingsetidentifiers::Vector{NettingSetIdentifier{T}}, maxnotional::Float64, maxpd::Float64) where {T}
n = rand(Uniform(0.0, maxnotional), length(nettingsetidentifiers))
p = rand(Uniform(0.0, maxpd), length(nettingsetidentifiers))
collect(zip(nettingsetidentifiers, n, p))
end
function randomgaussianfactorlossmodelparameters(lossmodelparameters::LossModel.SimpleLossModelParameters, numberdrivers::Int64, minmaxloading::Tuple{Float64,Float64})
a = rand(Uniform(minmaxloading[1], minmaxloading[2]), (length(lossmodelparameters.pds), numberdrivers))
g = GaussianFactorModel.gaussianfactormodelparameters(a)
LossModel.GaussianFactorLossModelParameters(lossmodelparameters.pds, lossmodelparameters.losses, g)
end
function convert(::Type{DataFrame}, nettingsetidentifiers::Vector{NettingSetIdentifier{T}}, p::LossModel.GaussianFactorLossModelParameters) where {T}
@assert length(nettingsetidentifiers) == length(p.losses) "Number of netting set identifiers not identical to the number of risk losses in the loss model"
ndrivers = size(p.factormodelparameters.factorloadings)[2]
drivernames = map(x->"mean$(x-1)", 1:ndrivers)
df = hcat(DataFrame(GID = map(string, nettingsetidentifiers), EAD = p.losses, PD = p.pds), DataFrame(p.factormodelparameters.factorloadings, drivernames))
df
end
function convert(::Type{LossModel.GaussianFactorLossModelParameters}, df::DataFrame)
nettingsets = df[:, "GID"]
eads = df[:, "EAD"]
pds = df[:, "PD"]
numberdrivers = 5
drivernames = map(x->"mean$(x-1)", 1:numberdrivers)
loadings = Matrix{Float64}(df[:, drivernames])
g = GaussianFactorModel.gaussianfactormodelparameters(loadings)
LossModel.GaussianFactorLossModelParameters(pds, eads, g)
end
end