cpm-approx
Analytical Value at Risk and Expected Shortfall of a credit portfolio in the multi-factor Merton model — no simulation, deterministic, fast for portfolios with thousands of facilities. It implements the multi-factor adjustment of M. Pykhtin, "Multi-factor adjustment", Risk, March 2004, in Rust with Python bindings. It is not affiliated with the author of the paper or its publisher.
All figures are loss rates relative to total exposure; the loss quantile includes the expected loss.
Install
Python 3.12 or newer, no dependencies (NumPy arrays are accepted as input).
pip install cpm-approx # once released on PyPI
pip install --index-url https://test.pypi.org/simple/ cpm-approx # current test release
Usage
The example portfolio is the one of the paper (Table C: 11 factors, ten buckets, 750 facilities):
from cpm_approx import paper_portfolio, portfolio_risk
r = portfolio_risk(**paper_portfolio(rho=0.5), confidence=0.999)
print(f"VaR {r.var:.2%} ES {r.es:.2%} one-factor limit {r.var_limit:.2%}") # VaR 2.33% ES 2.76%
paper_portfolio() returns a dictionary with the keys pd, lgd, exposure, asset_correlation, lgd_std and
loadings. The two stars unpack it into keyword arguments, so the call is the same as
portfolio_risk(pd=..., lgd=..., exposure=..., asset_correlation=..., lgd_std=..., loadings=..., confidence=0.999).
Your own portfolio, one entry per facility:
r = portfolio_risk(
pd, lgd, exposure, asset_correlation, 0.999,
factor_weights=weights, # sector weights per facility ...
factor_correlation=corr, # ... and the correlation matrix of the factors
lgd_std=lgd_std, # optional: standard deviation of the LGD
)
asset_correlationis the squared loading R² on the systematic factors.- Instead of weights and correlation matrix you can pass
loadings(rows of unit norm on independent factors). Without any factor input a single factor is used. - The result
RiskResulthasexpected_loss,var,esand, for comparison,var_limitandes_limitof the comparable one-factor portfolio before the multi-factor and granularity corrections. analyze(dict)takes and returns JSON-like dictionaries. Invalid input raisesValueError. The GIL is released during the computation.
The cost grows quadratically with the number of facilities (3000 facilities ≈ 1 s on two cores). The method is a second-order expansion; for very concentrated portfolios with few facilities, check against a simulation.
Theory
Portfolio of M facilities with exposure weights w_i, default probabilities PD_i, random LGD λ_i with mean μ_i and standard deviation σ_i (independent of everything else). The asset value depends on K independent standard normal systematic factors η and an idiosyncratic factor ξ_i; a facility defaults when its asset value falls below the threshold. r_i² is the asset correlation (asset_correlation), β_i the unit-norm loading on the factors.
Step 1 — comparable one-factor portfolio. The factors are collapsed to the one direction b that matters most in the tail; a_i is the effective loading of facility i on it.
Given the single factor Y = b·η the conditional default probability and expected loss are
and for infinitely many small facilities VaR and expected shortfall follow directly (Φ₂ is the bivariate normal CDF). These are var_limit and es_limit.
Step 2 — second-order correction. The real loss is ℓ(Y) plus a residual with conditional variance v(y). A Taylor expansion of the quantile in v gives
The variance has two parts, the multi-factor adjustment (the factors beyond Y still move the loss) and the granularity adjustment (finite number of facilities, LGD risk), with h_i = Φ⁻¹(p_i(y)):
var_rate is t_0 + Δt with v = v_∞ + v_GA. The bivariate normal CDF is evaluated with Plackett's identity by Gauss–Legendre quadrature (5 to 24 nodes chosen by |ρ|, error below 2·10⁻¹⁵; 40 nodes with the substitution r = sin t for |ρ| ≥ 0.9):
Development
uv sync && uv run pytest # builds the Rust extension (Rust toolchain required)
cargo test --release # Rust tests: Table C of the paper, parity with a Python reference implementation
The Rust library (cpm_approx) and a JSON command line tool (cpm-approx) are built from the same sources.
The formulas above are PNG images rendered with typst; their typst source is kept in HTML
comments in this README (invisible on GitHub). Edit the comment and run python tools/formulas.py to regenerate
docs/img/*.png.
Reference
M. Pykhtin, Multi-factor adjustment, Risk 17(3), 2004, 85–90.
License
MIT
Metadata
Release files for cpm-approx 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cpm_approx-0.1.0.tar.gz | 210.2 kB | Without provenance | Details for cpm_approx-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cpm_approx-0.1.0-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl | CPython 3.12 | abi3 | Linux glibc 2.17+ x86-64 | Details for cpm_approx-0.1.0-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
Total release size: 533.7 kB
Release files / cpm_approx-0.1.0.tar.gz
| Download URL | cpm_approx-0.1.0.tar.gz |
|---|---|
| Size | 210.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d699fed98b39e8601d54de224270052cbaa8cdb290d794efe510430c1a99188
|
|
BLAKE2b-256 checksum How to use checksums |
d51994949fb46a27cba0deb118774b84cdb3496b3b39e1af7db2a2f7792a947b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / cpm_approx-0.1.0-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
| Download URL | cpm_approx-0.1.0-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
|---|---|
| Size | 323.5 kB |
| Tags | CPython 3.12 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1937d26ad0dfe86627d2fd849b260eb3d845f0d1af6264a799f76c206734ad23
|
|
BLAKE2b-256 checksum How to use checksums |
2faf98826472e79635bb5fc5ddc746661e2120bf4f8ac10b07ec3411f5695c86
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for cpm-approx 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.