XVA
Module: Shoals.Xva.
This module computes valuation-adjustment building blocks: survival and
default probabilities, expected positive and negative exposure, two-deal
netting, CVA and DVA on a time grid, and funding and capital adjustments.
It also has a hazard-curve CVA and a sampled wrong-way-risk estimator.
Shoals.Csa, at the end of this page, reduces exposure by posted
collateral. Times are years from the valuation date; exposures, CVA and the
other adjustments are in the currency of the exposures.
Survival and default
Section titled “Survival and default”def survival_probability_constant_hazard(hazard: f32, t: f32) -> f32def default_probability_in_interval(hazard: f32, t_start: f32, t_end: f32) -> f32def discount_factor_constant_rate(r: f32, t: f32) -> f32survival_probability_constant_hazard is exp(-hazard * t), the
probability of surviving to time t under a constant nonnegative hazard rate.
It is one at time zero and does not increase with time. default_probability_in_interval is
the probability of defaulting in [t_start, t_end], the difference of the
survival probabilities at the two endpoints. discount_factor_constant_rate
is exp(-r * t). For example:
s = survival_probability_constant_hazard(cast(0.02, f32), cast(1.0, f32)) -- exp(-0.02)p = default_probability_in_interval(cast(0.03, f32), cast(0.0, f32), cast(5.0, f32))-- p == 1 - survival(5.0)Exposure and netting
Section titled “Exposure and netting”def expected_positive_exposure[n](exposures: tensor[n, f32]) -> f32def expected_negative_exposure[n](exposures: tensor[n, f32]) -> f32def netted_exposure_2_deals[n](deal_a: tensor[n, f32], deal_b: tensor[n, f32]) -> tensor[n, f32]expected_positive_exposure is the mean over the sample of the positive
part of each exposure, and expected_negative_exposure is the mean of the
negative part. netted_exposure_2_deals adds two exposure tensors
pointwise, the netting of two deals under a single agreement. For example:
exposures = to_tensor([cast(-10.0, f32), cast(5.0, f32), cast(-3.0, f32), cast(20.0, f32)])epe = expected_positive_exposure(exposures) -- (5 + 20) / 4 == 6.25ene = expected_negative_exposure(exposures) -- (-10 - 3) / 4CVA and DVA
Section titled “CVA and DVA”def cva_constant_hazard[n](time_grid: tensor[n, f32], epe: tensor[n, f32], hazard: f32, recovery: f32, discount_rate: f32) -> f32def dva_constant_hazard[n](time_grid: tensor[n, f32], ene: tensor[n, f32], hazard_own: f32, recovery_own: f32, discount_rate: f32) -> f32cva_constant_hazard sums one term per grid time t_i:
CVA = (1 - recovery) * sum_i (S(t_{i-1}) - S(t_i)) * epe[i] * exp(-r * t_i)
with S(t) = exp(-hazard * t) and t_{-1} = 0. The first interval
therefore starts at time zero, and each interval uses the exposure and the
discount factor at its end point t_i; nothing is interpolated or averaged
inside an interval. epe[i] is the expected positive exposure at t_i
and r is discount_rate. dva_constant_hazard is the symmetric debit
valuation adjustment, the same sum with -ene[i] in place of epe[i] and
the institution's own hazard and recovery.
With finite intermediate values, CVA is zero when hazard is zero, recovery is one, or every exposure is zero. The example below uses exposures of 10, 15, and 12 at years 1, 2, and 3:
time_grid = to_tensor([cast(1.0, f32), cast(2.0, f32), cast(3.0, f32)])epe = to_tensor([cast(10.0, f32), cast(15.0, f32), cast(12.0, f32)])cva = cva_constant_hazard(time_grid, epe, cast(0.05, f32), cast(0.4, f32), cast(0.03, f32))-- 0.96757334CVA is not generally increasing in hazard for a varying exposure profile. Raising hazard moves default probability toward earlier intervals; a profile with exposure concentrated later can therefore produce a smaller CVA.
A negative expected negative exposure yields a positive DVA, since the institution gains on its own default.
Funding, capital, and varying hazard
Section titled “Funding, capital, and varying hazard”def fva[n](time_grid: tensor[n, f32], epe: tensor[n, f32], funding_spread: f32, discount_rate: f32) -> f32def kva[n](time_grid: tensor[n, f32], ead: tensor[n, f32], cost_of_capital: f32, regulatory_capital_weight: f32, discount_rate: f32) -> f32def xva_cva_stochastic_hazard[n, m](time_grid: tensor[m, f32], epe: tensor[m, f32], hazards: HazardCurve[n], recovery: f32, discount_rate: f32) -> f32def xva_cva_wwr_constant_hazard[n](rng_key: key, time_grid: tensor[n, f32], epe: tensor[n, f32], hazard: f32, recovery: f32, discount_rate: f32, rho: f32, n_paths: i64) -> f32fva is funding_spread times the trapezoidal integral of
exp(-r * t) * epe(t) over the grid. kva is
cost_of_capital * regulatory_capital_weight times the same integral of
ead. Both integrate from the first grid point to the last, with no
contribution before the first point; start the grid at 0 to include it. On
the example grid, fva with a 1% spread is 0.24462284 and kva with
cost 0.1 and weight 0.08 is 0.19569828.
xva_cva_stochastic_hazard is cva_constant_hazard with the survival
curve of a Shoals.Cds.HazardCurve in place of the constant hazard;
despite its name it draws no random paths. A one-pillar curve at hazard
0.05 reproduces the constant-hazard CVA above, 0.96757334.
Wrong-way risk
Section titled “Wrong-way risk”xva_cva_wwr_constant_hazard estimates CVA by simulation with a Gaussian
copula between exposure and default. For each of n_paths draws it takes
an exposure factor z_e and an independent z_d, sets the default time
tau = -ln(1 - N(-rho * z_e + sqrt(1 - rho^2) * z_d)) / hazard, and, if
tau falls before the last grid time, adds
(1 - recovery) * epe(tau) * exp(0.5 * z_e - 0.125) * exp(-r * tau). Here
epe(tau) interpolates epe linearly in time (flat before the first grid
point), and exp(0.5 * z_e - 0.125) is a mean-one lognormal exposure shock
with a fixed volatility of 0.5. The estimate is the sum divided by
n_paths, which is independent of the grid length.
rho is the correlation in [-1, 1] between the exposure factor and early
default. Positive rho is wrong-way risk: high exposure comes with early
default. On the example above with 20,000 paths and seed 3, rho = 0
gives 0.95674473, rho = 0.5 gives 1.3768874, and rho = -0.5
gives 0.6184209.
Input checks
Section titled “Input checks”xva_cva_wwr_constant_hazard requires n_paths >= 1 and fails with a
diagnostic naming the received count otherwise. The remaining conditions
are caller obligations. Supply nonnegative, strictly
increasing times (a grid point at 0 adds nothing to CVA or DVA and starts
the FVA and KVA integrals at 0), time_grid and exposure tensors of the same length
(the type requires it for each call), 0 <= recovery <= 1, a nonnegative
hazard. An invalid recovery has no diagnostic: a recovery above 1 gives
a negative CVA. These functions do not
model a general portfolio netting agreement; see the next section for
collateral.
Collateralized exposure
Section titled “Collateralized exposure”def csa_collateralized_exposure(exposure: f32, threshold: f32, mta: f32, independent_amount: f32, haircut: f32) -> f32def csa_collateralized_exposure_path[n](exposures: tensor[n, f32], threshold: f32, mta: f32, independent_amount: f32, haircut: f32) -> tensor[n, f32]Module: Shoals.Csa. Under a credit support annex, the exposure left after
collateral is computed from the positive part E of the exposure:
E <= threshold: no collateral is called; the result isE.E - threshold < mta: the call is below the minimum transfer amount; the result isE.- otherwise collateral
(E - threshold) * (1 - haircut)is posted and the independent amount is subtracted: the result ismax(E - (E - threshold) * (1 - haircut) - independent_amount, 0).
The independent amount reduces exposure only in that third case. The
_path form applies the same rule to every entry:
collateralized = csa_collateralized_exposure_path(to_tensor([cast(-3.0, f32), cast(1.5, f32), cast(2.3, f32), cast(10.0, f32)]), cast(2.0, f32), cast(0.5, f32), cast(1.0, f32), cast(0.02, f32))-- [0.0, 1.5, 2.3, 1.1599998]