Monte Carlo Greeks for discontinuous payoffs: likelihood-ratio, smoothing and conditional Monte Carlo, benchmarked against closed-form references.
Project description
RadonLab
Monte Carlo Greeks for discontinuous payoffs, done correctly and benchmarked against closed-form references.
When an option's payoff is discontinuous (a digital that pays a fixed amount if
the underlying finishes above a strike, a barrier that knocks out on touch), the
textbook pathwise (infinitesimal perturbation) estimator for its sensitivities
quietly breaks: the derivative of the payoff is zero almost everywhere, so the
estimate collapses toward zero. radonlab implements the established remedies,
tests each one against a known analytic answer, and benchmarks them side by side
so you can see which method to use and what it costs.
Pure NumPy/SciPy. Apache-2.0. No copyleft dependencies.
Companion C++ implementation
A header-only C++20 port lives at github.com/quantsingularity/radonlab-cpp. The two libraries share the same methods and the same closed-form references, and the C++ analytics are cross-checked against this library's values (which are verified to machine precision by complex-step differentiation), so the two agree to 1e-9 across languages. The C++ side additionally provides adjoint algorithmic differentiation (a full Greek vector from one reverse sweep) and exact second-order Greeks. This Python library is the reference and carries the research extensions (Merton jump-diffusion and a path-dependent Malliavin delta).
What's inside
Estimators (all with per-path standard errors):
- Likelihood-ratio method (LRM): differentiates the density, never the payoff, so it is unbiased for digitals and other discontinuous payoffs. Delta, vega and gamma.
- Smoothing: replaces the payoff with a
C¹approximation of bandwidthhand applies the pathwise method to it. A tunable bias/variance trade-off. - Conditional Monte Carlo: for barriers, integrates out the crossing event with the Brownian-bridge non-crossing probability, giving an estimator that is unbiased for the continuously-monitored price at any number of steps and Lipschitz in spot (so its delta comes out by the pathwise method).
- Pathwise and finite-difference (bump) baselines, kept so their failure modes on discontinuous payoffs are visible and measurable.
Closed-form Black-Scholes analytics (the ground truth) for European calls/puts, cash-or-nothing and asset-or-nothing digitals, and continuously-monitored down-and-out / down-and-in calls. Every explicit Greek formula is checked against complex-step differentiation of the price to machine precision.
Install
pip install -e ".[benchmark,test]"
Runtime dependencies are just numpy and scipy. pandas/matplotlib are only
needed for the benchmark table and plots.
Quick start
from radonlab import GeometricBrownianMotion, CashOrNothingCall, Greek
from radonlab import likelihood_ratio, pathwise, analytics
model = GeometricBrownianMotion(S0=100, r=0.03, sigma=0.20, T=1.0)
payoff = CashOrNothingCall(strike=100)
# Ground truth
ref = analytics.cash_or_nothing_call(100, 100, 0.03, 0.20, 1.0, greek="delta")
# Likelihood ratio: unbiased for the digital
lrm = likelihood_ratio(model, payoff, Greek.DELTA, n_paths=1_000_000)
print(lrm) # delta=... +/- ... [likelihood-ratio]
print(lrm.confidence_interval(0.95))
# Naive pathwise: collapses to ~0, badly biased
pw = pathwise(model, payoff, Greek.DELTA, n_paths=1_000_000)
print(pw.value, "vs analytic", ref)
The math, briefly
Under geometric Brownian motion S_T = S₀·exp((r − q − ½σ²)T + σ√T·Z), with
Z ~ N(0,1), a price is P = e^{−rT} E[f(S_T)].
- LRM writes
∂P/∂θ = e^{−rT} E[f(S_T)·∂_θ log p(S_T;θ)]. The score functions areZ/(S₀σ√T)for delta,(Z²−1)/σ − √T·Zfor vega, and(Z² − σ√T·Z − 1)/(S₀²σ²T)for gamma.fis never differentiated, so a jump infis no problem. - Pathwise writes
∂P/∂θ = e^{−rT} E[f′(S_T)·∂_θ S_T]. Fine whenfis Lipschitz (vanilla), degenerate whenfjumps (digital). - Conditional MC for a down-and-out call multiplies the terminal payoff by
∏ᵢ(1 − exp(−2(Xᵢ−b)(Xᵢ₊₁−b)/(σ²Δt))), the exact bridge probability of not breachingb = ln Bbetween simulated points.
Full references are in each module's docstring (Broadie-Glasserman 1996, Glasserman 2003, Reiner-Rubinstein 1991, and the smoothed-perturbation and Malliavin literature).
Benchmark
Reproduce with python examples/benchmark_digital_delta.py (1,000,000 paths,
seed 0). Delta of an at-the-money cash-or-nothing call, S₀=K=100, r=3%,
σ=20%, T=1. Analytic reference delta = 0.019333. Ranked by standard
error, the metric that actually reflects an estimator's accuracy:
| method | value | std error | |error| vs analytic | biased? |
|---|---|---|---|---|
| likelihood-ratio | 0.01939 | 2.84e-05 | 5.9e-05 | no |
| smoothing (h=default) | 0.01921 | 4.56e-05 | 1.2e-04 | no |
| finite-difference (1% bump) | 0.01934 | 9.49e-05 | 8.5e-06 | no |
| pathwise | 0.00000 | 0.00e+00 | 1.9e-02 | yes |
Two things to read off it. First, pathwise is 100% wrong: it returns exactly zero, because the digital's derivative is zero almost everywhere. Second, among the unbiased methods the likelihood-ratio estimator has the lowest variance (about 3.3× smaller standard error than the 1% finite difference) and needs no bump to tune.
And finite difference forces a bad choice: the same run sweeping the bump size shows the standard error exploding as the bump shrinks, while a large bump introduces its own bias:
| rel. bump | value | std error | |error| |
|---|---|---|---|
| 1e-01 | 0.018620 | 2.36e-05 | 7.1e-04 (bias) |
| 1e-02 | 0.019311 | 9.49e-05 | 2.2e-05 |
| 1e-03 | 0.019161 | 3.04e-04 | 1.7e-04 |
| 1e-04 | 0.017274 | 9.15e-04 | 2.1e-03 |
The likelihood-ratio method sidesteps that trade-off entirely. This is the whole reason the library exists.
Testing
pytest
The suite validates the analytics by complex-step differentiation, confirms each Monte Carlo estimator agrees with the analytic Greek within a few standard errors, and documents the pathwise failure on digitals as an explicit test.
Scope and honesty
This release covers the Black-Scholes / GBM world and, in radonlab.merton, the
Merton jump-diffusion model with a closed-form digital reference: it shows the
likelihood-ratio delta weight is unchanged by jumps and validates the estimator
against the closed form. It is a rigorous, tested core rather than a kitchen
sink: the estimators and their validation are real, and where a method is
expected to be biased (pathwise on a digital), the library shows it rather than
hiding it. It also includes a genuinely path-dependent frontier estimator in
radonlab.asian: the likelihood-ratio / Malliavin delta of a geometric
Asian digital, validated against the closed form, with a weight built from
the whole path. Stochastic volatility, arithmetic-Asian and lookback
payoffs, and a JIT/vectorized backend are natural next steps.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file radonlab-0.1.0.tar.gz.
File metadata
- Download URL: radonlab-0.1.0.tar.gz
- Upload date:
- Size: 31.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6afb9bff2e51fdc0fffeb3a567eadd09e8905d25f20bcfabf32d9a80932b3a0c
|
|
| MD5 |
9100bf79d37392f218db2fa8edfbbebf
|
|
| BLAKE2b-256 |
eeb5f930d1e67c791d4d9c3bdda793a78d6d09890f6a4271c041213bc0441f86
|
File details
Details for the file radonlab-0.1.0-py3-none-any.whl.
File metadata
- Download URL: radonlab-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d35ba2faa16fd7f181133575634daeadaf3e915497a4990a8538642f2fb9e2c3
|
|
| MD5 |
9eed7800c1419a3bf5974ae93b833859
|
|
| BLAKE2b-256 |
d0c7c213abb7721a2bed73b042815737fdbf6d285a8a22ff8029c12181608832
|