Verification¶
A test suite tells you whether the code still does what it did yesterday.
riskpy.verify answers a different question: whether what it does is right.
It computes a value with the library and compares it to something the library
had no hand in — a closed form, a published table, a mathematical identity, or
the same quantity by an unrelated route.
from riskpy import verify
report = verify.run()
print(report.summary())
assert report # False if any check failed
python -m riskpy.verify # the same from the command line
riskpy-verify --bench # with timings, for speed regressions
riskpy-verify --json report.json # for a dashboard
riskpy-verify -m life -m reserving # a subset
What a check is¶
Every check is a name, the value the library produced, the reference it should match, and an absolute tolerance. The tolerance is chosen per check and stated: machine precision for exact identities, the printed precision for values transcribed from a book, and a few standard errors for anything Monte Carlo. Every stochastic check runs on a fixed seed, so it is reproducible, and is loose enough never to fail by chance.
Some of what is checked, by module:
| module | examples |
|---|---|
core |
FactorModel against hand arithmetic; the hand-checked chain ladder; the compound Poisson FFT with unit severity is Poisson(λ); the C++ aggregate-loss mean is λ·E[X] |
special |
Φ(0) = ½; Φ⁻¹(Φ(x)) = x; P(1, x) = 1 − e⁻ˣ; I_x(a, b) + I_{1−x}(b, a) = 1; the t with one degree of freedom is Cauchy; ψ(1) = −γ; ψ₁(1) = π²/6; and, when SciPy is installed, agreement with it to 1e-13 (1e-8 for the Student-t quantile) |
mc |
cdf(ppf(q)) = q for every family; sample means within four standard errors of the analytic mean; Latin hypercube within one; Iman–Conover recovers the target Spearman and leaves the marginals untouched; a truncated density integrates to one |
quant |
the canonical Black–Scholes value; put–call parity; delta as a central difference; implied vol round trip; Heston → Black–Scholes as vol-of-vol → 0; Heston put–call parity; the GBM terminal mean |
life |
the SULT's l_100, ä_40 and A_40 against AMLCR Appendix D; A_x = 1 − d·ä_x; A_{x:n} = A¹_{x:n} + ₙE_x; Makeham's closed form against numerical integration; M_x / D_x = A_x; ä_xy + ä_x̄ȳ = ä_x + ä_y |
reserving |
GenIns factors, total reserve and Mack standard errors against Mack (1993); se² = process² + parameter²; agreement with the compiled LossTriangle; Bornhuetter–Ferguson collapsing to the chain ladder |
rates |
par at par; ytm(price(y)) = y; the bootstrap round trip; Nelson–Siegel limits; both short-rate bonds at σ = 0; Vasicek against Monte Carlo |
credit |
equity + debt = assets; equity as a Black–Scholes call; the ASRF at ρ = 0; the credit triangle; the Basel correlation limits; the BCBS risk-weight table |
viz |
every palette colour clears 3:1 contrast on its surface; the sequential ramps are monotone in lightness; eight categorical slots and no more |
Without SciPy the suite runs 111 checks; with it, the 14 oracle checks bring it
to 125. NumPy is required for the mc, quant and reserving checks and
Matplotlib for viz. On an install without them those modules are reported as
skipped, with the extra that enables them (pip install "open-riskpy[sim]"),
and the run still passes: a missing extra is a fact about the environment, not a
wrong number. Asking for such a module explicitly (riskpy-verify -m mc) fails,
because nothing you asked for could run.
Where it runs¶
- CI, on every push and pull request — a wrong number fails the build.
- Nightly, with the SciPy oracle switched on and the benchmarks included, keeping the JSON report as an artifact so a slow drift in accuracy or speed leaves a trail even on days nobody pushes.
- Here, regenerated on every docs build. The table below is the report from the commit this site was built from.
Adding a check¶
life, reserving, rates and credit contribute checks through a private
hook in their own module. Checks for the core, special, mc, quant and
viz are written directly in riskpy/verify.py.
def _verification_checks():
"""Return a list of (name, value, reference, tolerance) tuples."""
return [
("whole life A_x = 1 - d * ä_x", A, 1 - d * a, 1e-12),
]
The rule for what belongs here: pick things that would catch a class of error, not a single typo. Put–call parity catches every sign error in a pricing formula; one reference price catches one.
Latest report¶
RiskPY 0.4.0 — 111 of 111 checks pass (1.4s)
| module | check | value | reference | error | tolerance | |
|---|---|---|---|---|---|---|
core |
FactorModel: 1000 × 3.0 (FL) × 2.0 (age 19) | 6000 | 6000 | 0.0e+00 | 1e-09 | ✅ |
core |
FactorModel: unmatched inputs leave the base rate alone | 1000 | 1000 | 0.0e+00 | 1e-09 | ✅ |
core |
ActuarialMath: PV of 20-year annuity-immediate at 5% | 623111 | 623111 | 0.0e+00 | 1e-06 | ✅ |
core |
ActuarialMath: FV of 30 payments at 6% | 395291 | 395291 | 0.0e+00 | 1e-06 | ✅ |
core |
LossTriangle: hand-checked chain ladder IBNR | 2540 | 2540 | 0.0e+00 | 1e-06 | ✅ |
core |
FourierTransform: compound Poisson with unit severity = Poisson(2) | 1.66533e-16 | 0 | 1.7e-16 | 1e-12 | ✅ |
core |
FourierTransform: PMF has unit mass | 1 | 1 | 0.0e+00 | 1e-12 | ✅ |
core |
FourierTransform: convolution [1,2,3]*[4,5] | 0 | 0 | 0.0e+00 | 1e-10 | ✅ |
core |
ExperienceRating: full credibility at k claims | 1 | 1 | 0.0e+00 | 1e-12 | ✅ |
core |
ExperienceRating: √(n/k) below the standard | 0.499538 | 0.499538 | 0.0e+00 | 1e-12 | ✅ |
core |
ExposureRating: ILF at the base limit is 1 | 1 | 1 | 0.0e+00 | 1e-12 | ✅ |
core |
ExposureRating: burning cost of a 100 xs 100 layer | 62.5 | 62.5 | 0.0e+00 | 1e-09 | ✅ |
core |
RateAnalyzer: indicated change at a 77% loss ratio | 0.1 | 0.1 | 0.0e+00 | 1e-12 | ✅ |
core |
RateAnalyzer: on-level factor compounds rate changes | 1.1319 | 1.1319 | 0.0e+00 | 1e-12 | ✅ |
core |
MonteCarloSimulator: aggregate mean = λ·E[severity] (200k trials, ±3 s.e.) | 16918.222331 | 16889.339658 | 2.9e+01 | 6e+01 | ✅ |
special |
norm_cdf(0) = 1/2 | 0.5 | 0.5 | 0.0e+00 | 1e-16 | ✅ |
special |
norm_cdf(-37) keeps its tail (erf-based forms give 0) | 1 | 1 | 0.0e+00 | 0e+00 | ✅ |
special |
norm_ppf(norm_cdf(x)) = x at x = -3.7 | -3.7 | -3.7 | 4.4e-16 | 1e-13 | ✅ |
special |
norm_ppf(0.975) = 1.959964 | 1.959964 | 1.959964 | 0.0e+00 | 1e-14 | ✅ |
special |
gammainc(1, x) = 1 - e^-x | 0.899741 | 0.899741 | 1.1e-16 | 1e-14 | ✅ |
special |
gammainc + gammaincc = 1 | 1 | 1 | 0.0e+00 | 1e-14 | ✅ |
special |
gammainc(n, x) = Poisson survival (n=4, x=2.5) | 0.242424 | 0.242424 | 5.6e-17 | 1e-14 | ✅ |
special |
betainc(1, 1, x) = x | 0.37 | 0.37 | 0.0e+00 | 1e-14 | ✅ |
special |
betainc symmetry I_x(a,b) = 1 - I_{1-x}(b,a) | 1 | 1 | 1.1e-16 | 1e-14 | ✅ |
special |
betainc(a, 1, x) = x^a | 0.216 | 0.216 | 2.2e-16 | 1e-14 | ✅ |
special |
t_cdf with 1 d.o.f. is Cauchy: ½ + atan(x)/π | 0.791286 | 0.791286 | 1.1e-16 | 1e-13 | ✅ |
special |
t_ppf(t_cdf(x)) = x, 4 d.o.f. | 2.1 | 2.1 | 4.4e-16 | 1e-09 | ✅ |
special |
chi2_cdf with 2 d.o.f. = 1 - e^{-x/2} | 0.77687 | 0.77687 | 2.2e-16 | 1e-14 | ✅ |
special |
digamma(1) = -Euler–Mascheroni | -0.577216 | -0.577216 | 1.9e-14 | 1e-13 | ✅ |
special |
trigamma(1) = π²/6 | 1.644934 | 1.644934 | 2.4e-14 | 1e-13 | ✅ |
special |
brent solves x³ = 2 | 1.259921 | 1.259921 | 0.0e+00 | 1e-12 | ✅ |
special |
simpson ∫₀^π sin = 2 | 2 | 2 | 6.8e-10 | 1e-09 | ✅ |
special |
nelder_mead finds (1, -2) | 7.15219e-11 | 0 | 7.2e-11 | 1e-05 | ✅ |
special |
kolmogorov_sf(1) = 0.26999967 | 0.27 | 0.27 | 0.0e+00 | 1e-12 | ✅ |
mc |
Normal: cdf(ppf(q)) = q | 1.38778e-17 | 0 | 1.4e-17 | 1e-09 | ✅ |
mc |
LogNormal: cdf(ppf(q)) = q | 1.38778e-17 | 0 | 1.4e-17 | 1e-09 | ✅ |
mc |
Gamma: cdf(ppf(q)) = q | 1.11022e-16 | 0 | 1.1e-16 | 1e-09 | ✅ |
mc |
Beta: cdf(ppf(q)) = q | 3.88578e-16 | 0 | 3.9e-16 | 1e-09 | ✅ |
mc |
Weibull: cdf(ppf(q)) = q | 1.38778e-17 | 0 | 1.4e-17 | 1e-09 | ✅ |
mc |
StudentT: cdf(ppf(q)) = q | 4.16334e-17 | 0 | 4.2e-17 | 1e-09 | ✅ |
mc |
Pareto: cdf(ppf(q)) = q | 2.91434e-16 | 0 | 2.9e-16 | 1e-09 | ✅ |
mc |
PERT: cdf(ppf(q)) = q | 1.66533e-16 | 0 | 1.7e-16 | 1e-09 | ✅ |
mc |
Triangular: cdf(ppf(q)) = q | 1.11022e-16 | 0 | 1.1e-16 | 1e-09 | ✅ |
mc |
Exponential: cdf(ppf(q)) = q | 0 | 0 | 0.0e+00 | 1e-09 | ✅ |
mc |
Uniform: cdf(ppf(q)) = q | 8.67362e-19 | 0 | 8.7e-19 | 1e-09 | ✅ |
mc |
Gamma: sample mean = analytic mean (200k, ±4 s.e.) | 4.25047 | 4.25 | 4.7e-04 | 2e-02 | ✅ |
mc |
Gamma: LHS mean within 1 s.e. (variance reduction) | 4.249999 | 4.25 | 8.7e-07 | 6e-03 | ✅ |
mc |
LogNormal: sample mean = analytic mean (200k, ±4 s.e.) | 4.242976 | 4.241852 | 1.1e-03 | 3e-02 | ✅ |
mc |
LogNormal: LHS mean within 1 s.e. (variance reduction) | 4.241807 | 4.241852 | 4.5e-05 | 8e-03 | ✅ |
mc |
Poisson: sample mean = analytic mean (200k, ±4 s.e.) | 140.000835 | 140 | 8.3e-04 | 1e-01 | ✅ |
mc |
Poisson: LHS mean within 1 s.e. (variance reduction) | 139.99998 | 140 | 2.0e-05 | 3e-02 | ✅ |
mc |
NegativeBinomial: sample mean = analytic mean (200k, ±4 s.e.) | 139.973535 | 140 | 2.6e-02 | 2e-01 | ✅ |
mc |
NegativeBinomial: LHS mean within 1 s.e. (variance reduction) | 139.999945 | 140 | 5.5e-05 | 4e-02 | ✅ |
mc |
Weibull: sample mean = analytic mean (200k, ±4 s.e.) | 1.804603 | 1.805491 | 8.9e-04 | 1e-02 | ✅ |
mc |
Weibull: LHS mean within 1 s.e. (variance reduction) | 1.805488 | 1.805491 | 2.4e-06 | 3e-03 | ✅ |
mc |
Iman–Conover: Spearman(a, b) = 0.7 | 0.700447 | 0.7 | 4.5e-04 | 1e-02 | ✅ |
mc |
Iman–Conover: Spearman(a, c) = -0.4 | -0.400389 | -0.4 | 3.9e-04 | 1e-02 | ✅ |
mc |
Iman–Conover: unrelated pair stays at 0 | -0.000462066 | 0 | 4.6e-04 | 1e-02 | ✅ |
mc |
Iman–Conover: marginal untouched (same draws, only re-paired) | 0 | 0 | 0.0e+00 | 1e-09 | ✅ |
mc |
Truncated: density integrates to 1 | 1 | 1 | 8.0e-10 | 1e-06 | ✅ |
mc |
Mixture: mean is the weighted mean | 12756.62529 | 12756.62529 | 0.0e+00 | 1e-09 | ✅ |
mc |
Result.var(0.99) = np.percentile(99) | 2.324256 | 2.324256 | 0.0e+00 | 1e-12 | ✅ |
mc |
Result.tvar ≥ Result.var | 1 | 1 | 0.0e+00 | 0e+00 | ✅ |
quant |
Black–Scholes ATM call, S=K=100, T=1, r=5%, σ=20% | 10.450584 | 10.450584 | 0.0e+00 | 1e-09 | ✅ |
quant |
put–call parity | 9.26043 | 9.26043 | 3.6e-15 | 1e-10 | ✅ |
quant |
delta = ∂price/∂S (central difference) | 0.693464 | 0.693464 | 2.3e-10 | 1e-06 | ✅ |
quant |
implied_vol(price(σ)) = σ | 0.27 | 0.27 | 2.0e-10 | 1e-07 | ✅ |
quant |
Heston → Black–Scholes as vol-of-vol → 0 | 9.413432 | 9.413403 | 2.9e-05 | 1e-03 | ✅ |
quant |
Heston put–call parity | 12.002389 | 12.002389 | 0.0e+00 | 1e-06 | ✅ |
quant |
parametric_var at 99% = 2.326348 σ | 2.326348 | 2.326348 | 0.0e+00 | 1e-09 | ✅ |
quant |
GBM terminal mean = S₀e^{μT} (100k paths, ±4 s.e.) | 107.140709 | 107.250818 | 1.1e-01 | 3e-01 | ✅ |
life |
SULT l_100 vs AMLCR Table D.1 | 6248.174333 | 6248.17 | 4.3e-03 | 1e-02 | ✅ |
life |
SULT ä_40 at 5% vs AMLCR Table D.3 | 18.457757 | 18.4578 | 4.3e-05 | 5e-05 | ✅ |
life |
SULT A_40 at 5% vs AMLCR Table D.3 | 0.121059 | 0.12106 | 7.9e-07 | 5e-06 | ✅ |
life |
whole life A_x = 1 - d * ä_x | 0.121059 | 0.121059 | 1.2e-15 | 1e-12 | ✅ |
life |
endowment A_{x:n} = A¹_{x:n} + nE_x | 0.381263 | 0.381263 | 0.0e+00 | 1e-12 | ✅ |
life |
Makeham closed form 30_p_40 vs Simpson-integrated force | 0.916892 | 0.916892 | 4.8e-15 | 1e-09 | ✅ |
life |
commutation M_60 / D_60 = A_60 | 0.290282 | 0.290282 | 0.0e+00 | 1e-12 | ✅ |
life |
joint life ä_xy + ä_x̄ȳ = ä_x + ä_y | 30.963941 | 30.963941 | 0.0e+00 | 1e-12 | ✅ |
reserving |
GenIns first volume-weighted factor (Mack 1993) | 3.490607 | 3.4906 | 6.5e-06 | 5e-05 | ✅ |
reserving |
GenIns chain ladder total reserve (Mack 1993) | 1.86809e+07 | 1.86809e+07 | 3.9e-01 | 1e+00 | ✅ |
reserving |
GenIns Mack total s.e. (Mack 1993 Table) | 2.44709e+06 | 2.4471e+06 | 1.4e-01 | 2e+03 | ✅ |
reserving |
GenIns Mack s.e. of the youngest origin | 1.36315e+06 | 1.36316e+06 | 8.8e-02 | 1e+03 | ✅ |
reserving |
Mack se² = process² + parameter² (origin 9) | 1.85819e+12 | 1.85819e+12 | 2.4e-04 | 2e+06 | ✅ |
reserving |
chain ladder vs compiled LossTriangle (max relative gap) | 4.44089e-16 | 0 | 4.4e-16 | 1e-06 | ✅ |
reserving |
Bornhuetter–Ferguson at ELR = CL ultimate / premium equals CL | 1.86809e+07 | 1.86809e+07 | 3.7e-09 | 2e+01 | ✅ |
reserving |
Cape Cod: Σ latest = ELR · Σ premium · %developed | 3.43581e+07 | 3.43581e+07 | 0.0e+00 | 3e+01 | ✅ |
rates |
par bond prices at face when ytm = coupon | 100 | 100 | 1.4e-13 | 1e-10 | ✅ |
rates |
ytm(price(y)) = y | 0.043 | 0.043 | 1.5e-15 | 1e-10 | ✅ |
rates |
zero-coupon Macaulay duration = maturity | 7 | 7 | 0.0e+00 | 1e-12 | ✅ |
rates |
bootstrap round trip: 10y par rate | 0.04 | 0.04 | 4.9e-17 | 1e-10 | ✅ |
rates |
flat curve df(5) = exp(-0.15) | 0.860708 | 0.860708 | 0.0e+00 | 1e-14 | ✅ |
rates |
forward consistency df(5) = df(2)·exp(-3f) | 0.83733 | 0.83733 | 0.0e+00 | 1e-12 | ✅ |
rates |
Nelson–Siegel z(0) = β0 + β1 | 0.02 | 0.02 | 0.0e+00 | 1e-15 | ✅ |
rates |
CIR bond at sigma = 0 = exp(-∫r dt) | 0.807927 | 0.807927 | 0.0e+00 | 1e-12 | ✅ |
rates |
Vasicek bond at sigma = 0 = exp(-∫r dt) | 0.807927 | 0.807927 | 0.0e+00 | 1e-12 | ✅ |
rates |
Vasicek bond vs Monte Carlo exp(-∫r dt) | 0.928147 | 0.928257 | 1.1e-04 | 3e-03 | ✅ |
credit |
Merton: equity + debt value = assets | 120 | 120 | 0.0e+00 | 1e-12 | ✅ |
credit |
Merton: equity = Black-Scholes call on the assets | 31.279803 | 31.279803 | 0.0e+00 | 1e-14 | ✅ |
credit |
Merton: credit spread = 0 at zero asset volatility | 5.55112e-17 | 0 | 5.6e-17 | 1e-14 | ✅ |
credit |
ASRF: conditional PD at rho = 0 is the unconditional PD | 0.02 | 0.02 | 0.0e+00 | 1e-15 | ✅ |
credit |
Vasicek loss quantile = ASRF conditional PD | 0.206066 | 0.206066 | 0.0e+00 | 1e-15 | ✅ |
credit |
CDS par spread = credit triangle h(1-R) | 0.006 | 0.006 | 3.5e-10 | 1e-08 | ✅ |
credit |
Basel corporate correlation -> 0.12 as PD -> 1 | 0.12 | 0.12 | 0.0e+00 | 1e-15 | ✅ |
credit |
Basel IRB risk weight, PD 1% / LGD 45% / M 2.5 | 0.923168 | 0.9232 | 3.2e-05 | 1e-03 | ✅ |
viz |
dark palette: every series colour ≥ 3:1 on its surface (worst) | 1 | 1 | 0.0e+00 | 0e+00 | ✅ |
viz |
dark sequential ramp: lightness monotone | 1 | 1 | 0.0e+00 | 0e+00 | ✅ |
viz |
dark palette: eight categorical slots, no more | 8 | 8 | 0.0e+00 | 0e+00 | ✅ |
viz |
light palette: every series colour ≥ 3:1 on its surface (worst) | 1 | 1 | 0.0e+00 | 0e+00 | ✅ |
viz |
light sequential ramp: lightness monotone | 1 | 1 | 0.0e+00 | 0e+00 | ✅ |
viz |
light palette: eight categorical slots, no more | 8 | 8 | 0.0e+00 | 0e+00 | ✅ |