Skip to content

Validation lab

Simulating paths is the easy part. The validation lab checks whether the numbers can be trusted.

Convergence

convergence_study prices a payoff with an increasing number of paths and compares each price with a reference. An unbiased estimator stays within a few standard errors (z_score), and its error falls like 1/sqrt(N).

Discretisation bias

discretisation_study reprices on the same random stream with more and more time steps. The bias shows whether a scheme is accurate on a coarse grid, and the log-log slope of |bias| against dt estimates its weak order. For Heston, the full-truncation Euler scheme gives an order close to 1, while Andersen's QE scheme is unbiased within noise even at a handful of steps.

Model comparison

compare_models simulates several models and tabulates terminal moments and quantiles, with an overlaid density chart.

Reports

ValidationReport combines all of the above, plus risk-factor backtests (PIT, Anderson-Darling, Kupiec, Christoffersen), into one self-contained HTML file with pass/fail badges:

"""Build a full, self-contained HTML validation report for the Heston model.

The report contains:
- Monte Carlo convergence of the QE scheme against the semi-analytic Heston price;
- the discretisation bias of the QE and Euler schemes, with their estimated weak order;
- a comparison of GBM, Heston and Merton terminal distributions;
- a backtest of 10-day returns against realised data generated from a Heston world.

Open examples/output/heston_validation_report.html in a browser; it works offline.
"""

from pathlib import Path

import numpy as np

import stochPathSim as sps
from stochPathSim.analytics import heston_price
from stochPathSim.pricing import EuropeanCall
from stochPathSim.validation import ValidationReport

S0, K, T, r = 100.0, 100.0, 1.0, 0.03
params = dict(v0=0.04, kappa=1.5, theta=0.04, xi=0.6, rho=-0.7)
reference = heston_price(S0, K, T, r, 0.0, **params)
qe = sps.Heston(S0, r, **params)

report = ValidationReport("Heston model validation")

report.add_convergence(
    qe,
    EuropeanCall(K),
    T,
    reference,
    label="Convergence of the QE scheme",
    n_paths_list=(2_000, 8_000, 32_000, 128_000),
    n_steps=25,
    seed=1,
    discount=r,
)

report.add_discretisation(
    qe,
    EuropeanCall(K),
    T,
    reference,
    label="Discretisation: QE scheme",
    n_steps_list=(2, 4, 8, 16, 32),
    n_paths=100_000,
    seed=2,
    discount=r,
)

report.add_discretisation(
    lambda n: sps.Heston(S0, r, **params, scheme="euler"),
    EuropeanCall(K),
    T,
    reference,
    label="Discretisation: full-truncation Euler",
    n_steps_list=(2, 4, 8, 16, 32),
    n_paths=100_000,
    seed=2,
    discount=r,
)

report.add_comparison(
    {
        "GBM (20% vol)": sps.GBM(S0, r, 0.2),
        "Heston": qe,
        "Merton jumps": sps.MertonJumpDiffusion(S0, r, 0.15, 1.0, -0.1, 0.15),
    },
    T=T,
    n_steps=50,
    n_paths=50_000,
    seed=3,
    label="GBM vs Heston vs Merton",
)

# backtest: 10-day spot moves from a Heston "real world" against the Heston model's distribution
horizon, n_dates = 10 / 252, 250
world = qe.simulate(T=horizon, n_steps=10, n_paths=n_dates, seed=99)
realised = world.terminal()
model_dist = qe.simulate(T=horizon, n_steps=10, n_paths=5_000, seed=4).terminal()
report.add_backtest(
    np.tile(model_dist, (n_dates, 1)), realised, level=0.99, label="Backtest of 10-day spot moves"
)

out = report.to_html(Path(__file__).resolve().parent / "output" / "heston_validation_report.html")
print(report.summary().to_string(index=False))
print(f"\nall checks passed: {report.passed}")
print(f"wrote {out} ({out.stat().st_size / 1e6:.1f} MB)")

A validation report