Wrong-Way Risk (WWR) estimation for counterparty credit risk.
📖 Documentation · 🎮 Live playground · 📊 Worked example · 📄 Paper (PDF)
wayfault quantifies the adverse dependence between exposure and counterparty
credit quality — the risk that exposure rises precisely when the counterparty
deteriorates (WWR), and its favourable mirror, Right-Way Risk (RWR). It takes a
Monte-Carlo exposure cube and a credit curve as inputs and produces:
- baseline (independence-assumption) exposure metrics and CVA,
- a conditional expected exposure given default under a pluggable dependence model,
- a WWR-adjusted CVA and the empirical alpha multiplier
α = WWR-CVA / independent-CVA, - ML-based calibration of the dependence parameter,
- WWR/RWR classification and diagnostics.
The library does not generate exposures or bootstrap curves — those are inputs.
An interactive browser playground
runs the real wayfault wheel via WebAssembly (Pyodide) — no install, no server.
Adjust the dependence model and parameters and watch the CVA, alpha multiplier,
and exposure charts recompute live.
wayfault follows a strict hexagonal (ports & adapters) design. The
dependency rule points inward: adapters → application → ports → domain. The
domain, application, and ports layers import only the standard library
and numpy. Optional adapters import their heavy dependencies lazily and
raise a clear MissingDependencyError if the extra is not installed.
pip install wayfault # core (numpy only)
pip install 'wayfault[io,ml,viz]' # with optional extrasFor local development (editable install with the dev tooling):
pip install -e '.[io,ml,viz,dev]'Optional extras:
| Extra | Enables | Pulls in |
|---|---|---|
[io] |
CSV/Parquet source adapters | pandas, pyarrow |
[ml] |
scikit-learn survival calibrator | scikit-learn |
[viz] |
diagnostic plots | matplotlib |
[dev] |
tests, type-checking, linting | pytest, mypy, … |
import numpy as np
from wayfault import estimate_wwr
from wayfault.adapters.outbound.exposure_inmemory import InMemoryExposureSource
from wayfault.adapters.outbound.credit_flat import FlatHazardCreditCurveSource
from wayfault.adapters.outbound.dependence_hullwhite import HullWhiteHazardModel
cube = np.random.default_rng(0).normal(size=(10_000, 12)) + 1.0 # scenarios x tenors
tenors = [i / 4 for i in range(1, 13)] # quarterly to 3y
result = estimate_wwr(
exposure=InMemoryExposureSource(cube, tenors),
credit=FlatHazardCreditCurveSource(hazard=0.02, recovery=0.4),
model=HullWhiteHazardModel(b=0.5), # b > 0 -> wrong-way
)
print(result.baseline_cva, result.wwr_cva, result.alpha, result.classification)A full runnable example lives in
examples/quickstart.py
and uses only the in-memory adapters (zero extras).
python -m wayfault estimate \
--exposure cube.csv --credit curve.csv \
--model hullwhite --b 0.5 --out result.json(--exposure/--credit CSV ingestion requires the [io] extra.)
| Model | Knob | WWR when | Notes |
|---|---|---|---|
IndependentModel |
— | — | conditional EE ≡ unconditional EE |
HullWhiteHazardModel |
b |
b > 0 |
λ(t) = exp(a(t) + b·V(t)) (Hull–White) |
GaussianCopulaModel |
ρ |
ρ > 0 |
one-factor Gaussian copula |
ClaytonCopulaModel |
θ |
θ > 0 |
Archimedean, lower-tail dependence |
FrankCopulaModel |
θ |
θ > 0 |
Archimedean, symmetric (sign sets direction) |
All models are numpy-only and fully vectorised across tenors.
RegressionCalibrator— numpy-only OLS estimate of the Hull–Whiteb.SklearnSurvivalCalibrator—[ml]covariate-hazard surrogate.
Invert the relationship — target-driven calibration and reverse-stress:
from wayfault import calibrate_to_alpha, find_breakpoint
hw = lambda b: HullWhiteHazardModel(b=b)
calibrate_to_alpha(exposure, credit, hw, target_alpha=1.25, lo=-1.5, hi=1.5) # which b -> alpha 1.25?
find_breakpoint(exposure, credit, hw, threshold=1.40, lo=0.0, hi=3.0) # b where alpha breaches 1.40The [viz] extra adds a beautiful matplotlib plotting module
(wayfault.adapters.outbound.viz) — lazily imported, so the core stays
numpy-only. Regenerate the gallery with pip install 'wayfault[viz]' then
python examples/gallery.py.
| Exposure profiles — EPE vs conditional EE, shaded WWR adjustment | EE ratio — per-tenor conditional/unconditional |
![]() |
![]() |
| Alpha sweep — alpha & CVA vs the dependence knob | Dashboard — everything at a glance |
![]() |
![]() |
from wayfault.adapters.outbound import viz
fig = viz.plot_dashboard(result, bs=bs, alphas=alphas, wwr_cvas=wwr_cvas)
viz.save(fig, "dashboard.png")Two acceleration techniques, both numpy-only:
- Vectorised re-weighting — every model re-weights the whole exposure cube in a single numpy expression (no per-tenor Python loop).
- Parallel batch & sweep —
wayfault.application.parallelfans independent estimates across workers with deterministic, input-order results:
from wayfault.application.parallel import sweep_models
from wayfault.adapters.outbound.dependence_hullwhite import HullWhiteHazardModel
results = sweep_models(
exposure, credit,
[HullWhiteHazardModel(b=b) for b in np.linspace(-1.2, 1.2, 25)],
max_workers=8, # threads (numpy releases the GIL); or pass a ProcessPoolExecutor
)
alphas = [r.alpha for r in results]See the Performance docs.
pytest # tests
mypy --strict src # type checks
ruff check # lintReference: Hull & White, CVA and Wrong-Way Risk (2012).
MIT.



