Skip to content

Performance

wayfault is built for production CCR/XVA workloads. Two complementary acceleration techniques keep it fast while staying numpy-only.

1. Vectorised re-weighting

Every built-in dependence model re-weights the entire exposure cube at once. Instead of looping over tenors in Python, the shared re-weighting core computes column-wise softmax / copula weights and the conditional EE in single numpy expressions:

# conceptually, for the whole (n_scenarios x n_tenors) cube:
W   = softmax_columns(b * V)            # column-wise, numerically stable
ee  = conditional_ee_columns(V, W)      # per-tenor conditional EE

This is both faster and numerically identical to a per-tenor loop, and it lets numpy push the work down to vectorised BLAS-backed kernels.

2. Parallel batch & sweep

Pricing many counterparties, or sweeping the wrong-way knob to trace an alpha curve, is embarrassingly parallel — each estimate is independent. The wayfault.application.parallel module fans the work out across workers while preserving deterministic, input-order results (same output regardless of worker count).

Sweep a parameter grid

import numpy as np
from wayfault.application.parallel import sweep_models
from wayfault.adapters.outbound.exposure_inmemory import InMemoryExposureSource
from wayfault.adapters.outbound.credit_flat import FlatHazardCreditCurveSource
from wayfault.adapters.outbound.dependence_hullwhite import HullWhiteHazardModel

tenors = [i / 4 for i in range(1, 13)]
cube = np.asarray(tenors) + np.random.default_rng(0).normal(scale=0.6, size=(20_000, 12))
exposure = InMemoryExposureSource(cube, tenors)
credit = FlatHazardCreditCurveSource(hazard=0.02, recovery=0.4)

bs = np.linspace(-1.2, 1.2, 25)
results = sweep_models(
    exposure, credit,
    [HullWhiteHazardModel(b=b) for b in bs],
    max_workers=8,                       # default thread pool
)
alphas = [r.alpha for r in results]      # aligned with bs

Batch many counterparties

from wayfault.application.parallel import batch_estimate
from wayfault.application.dto import WWRRequest

requests = [WWRRequest(exp_i, credit_i, model_i) for exp_i, credit_i, model_i in book]
results = batch_estimate(requests, max_workers=16)

Threads vs processes

The default backend is a thread pool. Because numpy releases the GIL during array operations, threads already overlap the heavy numeric work with low overhead. For CPU-bound, pure-Python-heavy paths, pass your own ProcessPoolExecutor:

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=8) as pool:
    results = batch_estimate(requests, executor=pool)

Determinism guarantee

For a fixed seed and inputs, batch_estimate / sweep_models return identical results to the sequential path, independent of max_workers or backend (NFR-4). This is enforced by tests.

API

wayfault.application.parallel

Parallel orchestration helpers (acceleration).

Batch and parameter-sweep workloads — pricing many counterparties, or sweeping the wrong-way knob to trace the alpha curve — are embarrassingly parallel: each estimate is independent. These helpers fan the work out across workers while preserving deterministic, input-order results (NFR-4): the output for index i depends only on request i, never on the worker count.

Only the standard library and numpy are used. The default backend is a thread pool, which accelerates the numpy-heavy core because numpy releases the GIL during array operations. For CPU-bound pure-Python paths, pass your own concurrent.futures.ProcessPoolExecutor via executor.

batch_estimate

batch_estimate(requests: Iterable[WWRRequest], *, max_workers: int | None = None, executor: Executor | None = None) -> list[WWRResult]

Estimate a batch of requests in parallel, preserving input order.

Parameters:

Name Type Description Default
requests Iterable[WWRRequest]

The requests to evaluate.

required
max_workers int | None

Worker count for the default thread pool (ignored if executor is given). None lets the pool choose.

None
executor Executor | None

An optional pre-built concurrent.futures.Executor (e.g. a ProcessPoolExecutor). The caller owns its lifecycle.

None

Returns:

Type Description
list[WWRResult]

Results aligned with the input order.

Source code in src/wayfault/application/parallel.py
def batch_estimate(
    requests: Iterable[WWRRequest],
    *,
    max_workers: int | None = None,
    executor: Executor | None = None,
) -> list[WWRResult]:
    """Estimate a batch of requests in parallel, preserving input order.

    Parameters
    ----------
    requests:
        The requests to evaluate.
    max_workers:
        Worker count for the default thread pool (ignored if ``executor`` is
        given). ``None`` lets the pool choose.
    executor:
        An optional pre-built ``concurrent.futures.Executor`` (e.g. a
        ``ProcessPoolExecutor``). The caller owns its lifecycle.

    Returns
    -------
    list[WWRResult]
        Results aligned with the input order.
    """
    service = WrongWayRiskService()
    reqs = list(requests)
    if executor is not None:
        return list(executor.map(service.estimate, reqs))
    with ThreadPoolExecutor(max_workers=max_workers) as pool:
        return list(pool.map(service.estimate, reqs))

sweep_models

sweep_models(exposure: ExposureSource, credit: CreditCurveSource, models: Sequence[DependenceModel], *, discount: ndarray | None = None, pfe_quantile: float = 0.95, max_workers: int | None = None, executor: Executor | None = None) -> list[WWRResult]

Sweep several dependence models over a shared exposure and credit curve.

Builds one request per model (re-using the same exposure/credit ports) and evaluates them in parallel via :func:batch_estimate. Useful for tracing an alpha curve across a grid of b / rho / theta values, or for comparing model families side by side.

Source code in src/wayfault/application/parallel.py
def sweep_models(
    exposure: ExposureSource,
    credit: CreditCurveSource,
    models: Sequence[DependenceModel],
    *,
    discount: np.ndarray | None = None,
    pfe_quantile: float = 0.95,
    max_workers: int | None = None,
    executor: Executor | None = None,
) -> list[WWRResult]:
    """Sweep several dependence models over a shared exposure and credit curve.

    Builds one request per model (re-using the same exposure/credit ports) and
    evaluates them in parallel via :func:`batch_estimate`. Useful for tracing an
    alpha curve across a grid of ``b`` / ``rho`` / ``theta`` values, or for
    comparing model families side by side.
    """
    requests = [
        WWRRequest(
            exposure=exposure,
            credit=credit,
            model=model,
            discount=discount,
            pfe_quantile=pfe_quantile,
        )
        for model in models
    ]
    return batch_estimate(requests, max_workers=max_workers, executor=executor)