Skip to content

Application

Service

wayfault.application.service

The :class:WrongWayRiskService orchestration.

WrongWayRiskService

Orchestrates the WWR estimation through the outbound ports.

The service contains no I/O and no numpy-free leakage of external concerns: everything outside the domain is reached through the request's ports.

estimate

estimate(request: WWRRequest) -> WWRResult

Run the full WWR pipeline and return a :class:WWRResult.

Source code in src/wayfault/application/service.py
def estimate(self, request: WWRRequest) -> WWRResult:
    """Run the full WWR pipeline and return a :class:`WWRResult`."""
    cube = request.exposure.load()
    curve = request.credit.load()
    grid = cube.grid

    model = request.model
    model.calibrate_to_curve(curve, grid)

    # Baseline (independent) metrics.
    epe_profile = metrics.epe(cube)
    pfe_profile = metrics.pfe(cube, request.pfe_quantile)
    eepe_value = metrics.eepe(cube)
    baseline_cva = cva.discounted_cva(epe_profile, curve, grid, request.discount)

    # Conditional exposure and WWR-CVA.
    conditional = model.conditional_ee(cube, curve, grid)
    wwr_cva = cva.discounted_cva(conditional, curve, grid, request.discount)

    alpha = wwr.alpha_multiplier(wwr_cva, baseline_cva)
    params = _model_params(model)
    dep_param = _dependence_param(model)
    classification = wwr.classify(dep_param, alpha)
    ead_value = wwr.ead(alpha, eepe_value)

    diag = wwr.diagnostics(
        unconditional=epe_profile,
        conditional=conditional,
        hazard=curve.hazard(grid.times),
        baseline_cva=baseline_cva,
        wwr_cva=wwr_cva,
    )

    return WWRResult(
        baseline_cva=baseline_cva,
        wwr_cva=wwr_cva,
        alpha=alpha,
        classification=classification,
        tenors=grid.times,
        epe=epe_profile.values,
        conditional_ee=conditional.values,
        pfe=pfe_profile,
        eepe=eepe_value,
        ead=ead_value,
        uplift_pct=diag.uplift_pct,
        ee_ratio=diag.ee_ratio,
        ee_hazard_corr=diag.ee_hazard_corr,
        model=type(model).__name__,
        params=params,
    )

DTOs

wayfault.application.dto

Data-transfer objects for the WWR use case.

WWRRequest dataclass

WWRRequest(exposure: ExposureSource, credit: CreditCurveSource, model: DependenceModel, discount: ndarray | None = None, pfe_quantile: float = 0.95)

Inputs for a WWR estimation.

Parameters:

Name Type Description Default
exposure ExposureSource

Port supplying the exposure cube.

required
credit CreditCurveSource

Port supplying the credit curve.

required
model DependenceModel

The dependence model to apply.

required
discount ndarray | None

Optional discount factors on the grid (defaults to 1.0).

None
pfe_quantile float

Quantile used for the PFE profile.

0.95

WWRResult dataclass

WWRResult(baseline_cva: float, wwr_cva: float, alpha: float, classification: WWRClass, tenors: ndarray, epe: ndarray, conditional_ee: ndarray, pfe: ndarray, eepe: float, ead: float, uplift_pct: float, ee_ratio: ndarray, ee_hazard_corr: float, model: str, params: dict[str, float] = dict())

Outputs of a WWR estimation.

Attributes:

Name Type Description
baseline_cva, wwr_cva, alpha

The independent CVA, the WWR-adjusted CVA, and their ratio.

classification WWRClass

WWR / RWR / NEUTRAL label.

tenors ndarray

The tenor grid year-fractions (x-axis for the per-tenor profiles).

epe, conditional_ee, pfe

Per-tenor profiles (numpy arrays).

eepe float

Effective EPE scalar.

ead float

alpha * eepe.

uplift_pct, ee_ratio, ee_hazard_corr

Diagnostics.

model, params

Metadata about the dependence model used.

to_dict

to_dict() -> dict[str, Any]

Return a JSON-serialisable dictionary representation.

Source code in src/wayfault/application/dto.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-serialisable dictionary representation."""
    return {
        "baseline_cva": self.baseline_cva,
        "wwr_cva": self.wwr_cva,
        "alpha": self.alpha,
        "classification": self.classification.value,
        "tenors": self.tenors.tolist(),
        "epe": self.epe.tolist(),
        "conditional_ee": self.conditional_ee.tolist(),
        "pfe": self.pfe.tolist(),
        "eepe": self.eepe,
        "ead": self.ead,
        "uplift_pct": self.uplift_pct,
        "ee_ratio": self.ee_ratio.tolist(),
        "ee_hazard_corr": self.ee_hazard_corr,
        "model": self.model,
        "params": self.params,
    }

Inverse solvers

wayfault.application.inverse

Inverse WWR solvers (prescriptive analytics).

Where the rest of the library answers "given this dependence, what is the WWR?", this module inverts the question:

  • :func:calibrate_to_alpha / :func:calibrate_to_cva — find the dependence parameter that reproduces a target alpha multiplier or WWR-CVA (e.g. a desk/regulator target, or a historically observed CVA).
  • :func:find_breakpoint — find the dependence level at which a chosen metric (alpha, WWR-CVA, EAD) crosses a threshold — a reverse-stress / capital breach diagnostic: "how much wrong-way correlation until we breach?".

All solvers root-find on the monotone metric-vs-parameter curve with a robust bracketed bisection (deterministic, numpy-only). The caller supplies a model_factory mapping a scalar parameter to a dependence model, so the same solver works for the Hull-White b, the Gaussian rho, or a copula theta.

SolveResult dataclass

SolveResult(param: float, achieved: float, target: float, iterations: int, converged: bool, result: WWRResult)

Outcome of an inverse solve.

Attributes:

Name Type Description
param float

The solved dependence parameter.

achieved float

The metric value attained at param.

target float

The requested target / threshold.

iterations int

Number of bisection steps taken.

converged bool

Whether the solver reached tol within max_iter.

result WWRResult

The full :class:WWRResult evaluated at param.

alpha_metric

alpha_metric(result: WWRResult) -> float

Metric selector: the alpha multiplier.

Source code in src/wayfault/application/inverse.py
def alpha_metric(result: WWRResult) -> float:
    """Metric selector: the alpha multiplier."""
    return result.alpha

wwr_cva_metric

wwr_cva_metric(result: WWRResult) -> float

Metric selector: the WWR-adjusted CVA.

Source code in src/wayfault/application/inverse.py
def wwr_cva_metric(result: WWRResult) -> float:
    """Metric selector: the WWR-adjusted CVA."""
    return result.wwr_cva

ead_metric

ead_metric(result: WWRResult) -> float

Metric selector: the exposure-at-default view.

Source code in src/wayfault/application/inverse.py
def ead_metric(result: WWRResult) -> float:
    """Metric selector: the exposure-at-default view."""
    return result.ead

solve_for_target

solve_for_target(exposure: ExposureSource, credit: CreditCurveSource, model_factory: ModelFactory, target: float, *, metric: Metric = alpha_metric, lo: float, hi: float, tol: float = 0.0001, max_iter: int = 60, discount: ndarray | None = None, pfe_quantile: float = 0.95) -> SolveResult

Find the parameter in [lo, hi] whose metric equals target.

The metric must be monotone in the parameter over the bracket (true for the built-in models). Raises :class:ValidationError if [lo, hi] does not bracket the target.

Source code in src/wayfault/application/inverse.py
def solve_for_target(
    exposure: ExposureSource,
    credit: CreditCurveSource,
    model_factory: ModelFactory,
    target: float,
    *,
    metric: Metric = alpha_metric,
    lo: float,
    hi: float,
    tol: float = 1e-4,
    max_iter: int = 60,
    discount: np.ndarray | None = None,
    pfe_quantile: float = 0.95,
) -> SolveResult:
    """Find the parameter in ``[lo, hi]`` whose ``metric`` equals ``target``.

    The metric must be monotone in the parameter over the bracket (true for the
    built-in models). Raises :class:`ValidationError` if ``[lo, hi]`` does not
    bracket the target.
    """
    if hi <= lo:
        raise ValidationError("Require lo < hi for the solver bracket.")
    service = WrongWayRiskService()

    def evaluate(param: float) -> WWRResult:
        request = WWRRequest(
            exposure=exposure,
            credit=credit,
            model=model_factory(param),
            discount=discount,
            pfe_quantile=pfe_quantile,
        )
        return service.estimate(request)

    res_lo = evaluate(lo)
    res_hi = evaluate(hi)
    f_lo = metric(res_lo) - target
    f_hi = metric(res_hi) - target

    if f_lo * f_hi > 0.0:
        raise ValidationError(
            f"Target {target!r} not bracketed: metric spans "
            f"[{metric(res_lo):.6g}, {metric(res_hi):.6g}] over [{lo}, {hi}]."
        )

    a, b = lo, hi
    mid, res_mid, f_mid = lo, res_lo, f_lo
    for i in range(1, max_iter + 1):
        mid = 0.5 * (a + b)
        res_mid = evaluate(mid)
        f_mid = metric(res_mid) - target
        if abs(f_mid) <= tol:
            return SolveResult(mid, metric(res_mid), target, i, True, res_mid)
        if (f_mid > 0.0) == (f_lo > 0.0):
            a, f_lo = mid, f_mid
        else:
            b = mid
    return SolveResult(mid, metric(res_mid), target, max_iter, False, res_mid)

calibrate_to_alpha

calibrate_to_alpha(exposure: ExposureSource, credit: CreditCurveSource, model_factory: ModelFactory, target_alpha: float, *, lo: float, hi: float, tol: float = 0.0001, max_iter: int = 60, discount: ndarray | None = None, pfe_quantile: float = 0.95) -> SolveResult

Find the dependence parameter that reproduces target_alpha.

Source code in src/wayfault/application/inverse.py
def calibrate_to_alpha(
    exposure: ExposureSource,
    credit: CreditCurveSource,
    model_factory: ModelFactory,
    target_alpha: float,
    *,
    lo: float,
    hi: float,
    tol: float = 1e-4,
    max_iter: int = 60,
    discount: np.ndarray | None = None,
    pfe_quantile: float = 0.95,
) -> SolveResult:
    """Find the dependence parameter that reproduces ``target_alpha``."""
    return solve_for_target(
        exposure, credit, model_factory, target_alpha, metric=alpha_metric,
        lo=lo, hi=hi, tol=tol, max_iter=max_iter, discount=discount,
        pfe_quantile=pfe_quantile,
    )

calibrate_to_cva

calibrate_to_cva(exposure: ExposureSource, credit: CreditCurveSource, model_factory: ModelFactory, target_cva: float, *, lo: float, hi: float, tol: float = 1e-08, max_iter: int = 80, discount: ndarray | None = None, pfe_quantile: float = 0.95) -> SolveResult

Find the dependence parameter that reproduces target_cva.

Source code in src/wayfault/application/inverse.py
def calibrate_to_cva(
    exposure: ExposureSource,
    credit: CreditCurveSource,
    model_factory: ModelFactory,
    target_cva: float,
    *,
    lo: float,
    hi: float,
    tol: float = 1e-8,
    max_iter: int = 80,
    discount: np.ndarray | None = None,
    pfe_quantile: float = 0.95,
) -> SolveResult:
    """Find the dependence parameter that reproduces ``target_cva``."""
    return solve_for_target(
        exposure, credit, model_factory, target_cva, metric=wwr_cva_metric,
        lo=lo, hi=hi, tol=tol, max_iter=max_iter, discount=discount,
        pfe_quantile=pfe_quantile,
    )

find_breakpoint

find_breakpoint(exposure: ExposureSource, credit: CreditCurveSource, model_factory: ModelFactory, threshold: float, *, metric: Metric = alpha_metric, lo: float, hi: float, tol: float = 0.0001, max_iter: int = 60, discount: ndarray | None = None, pfe_quantile: float = 0.95) -> SolveResult

Find the dependence level at which metric crosses threshold.

A reverse-stress diagnostic: the returned param is the breakpoint where the chosen metric (default alpha) equals threshold.

Source code in src/wayfault/application/inverse.py
def find_breakpoint(
    exposure: ExposureSource,
    credit: CreditCurveSource,
    model_factory: ModelFactory,
    threshold: float,
    *,
    metric: Metric = alpha_metric,
    lo: float,
    hi: float,
    tol: float = 1e-4,
    max_iter: int = 60,
    discount: np.ndarray | None = None,
    pfe_quantile: float = 0.95,
) -> SolveResult:
    """Find the dependence level at which ``metric`` crosses ``threshold``.

    A reverse-stress diagnostic: the returned ``param`` is the breakpoint where
    the chosen metric (default alpha) equals ``threshold``.
    """
    return solve_for_target(
        exposure, credit, model_factory, threshold, metric=metric,
        lo=lo, hi=hi, tol=tol, max_iter=max_iter, discount=discount,
        pfe_quantile=pfe_quantile,
    )