Monte Carlo Greeks for discontinuous payoffs, in header-only C++20, validated against closed-form references and cross-checked to 1e-9 against the companion Python library.
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 estimator for its sensitivities breaks: the payoff's derivative is zero almost everywhere, so the estimate collapses toward zero. This library implements the established remedies (likelihood-ratio, smoothing, Brownian-bridge conditional Monte Carlo), tests each against a known analytic answer, and benchmarks them so you can see which method to use and what it costs.
Header-only. C++20. Standard library only, no third-party dependencies. Apache-2.0.
This is the native counterpart to the Python radonlab package
(github.com/quantsingularity/radonlab,
on PyPI as radonlab). The two share the same methods and the same closed-form
references, and the C++ analytics are checked against the Python library's
machine-precision-validated values to 1e-9 in the test suite.
Python (radonlab) |
C++ (radonlab-cpp) |
|
|---|---|---|
| Role | Reference implementation; carries the research extensions | Header-only production port |
| Build | pip install, pure NumPy/SciPy | Header-only; C++20 concepts constrain payoff types |
| Extras | Merton jump-diffusion, path-dependent Malliavin delta | Adjoint algorithmic differentiation, exact second-order Greeks, threaded estimator |
The C++ build keeps the hot loops in plain vectorizable form.
Estimators (each returns a value with its Monte Carlo standard error):
| Estimator | Identity / mechanism | Notes |
|---|---|---|
| Likelihood-ratio | Differentiates the density, never the payoff, so it is unbiased for digitals. Delta, vega, gamma. | |
| Smoothing | differentiate a |
A tunable bias/variance trade-off. |
| Conditional Monte Carlo (barriers) | multiplies the terminal payoff by the Brownian-bridge non-crossing probability |
Unbiased for the continuously-monitored value at any number of steps, and Lipschitz in spot, so its delta comes out pathwise. |
| Pathwise / finite-difference | baselines | Kept so their failure modes are visible. |
Closed-form Black-Scholes analytics are provided for European call/put, cash-or-nothing and asset-or-nothing digital calls, and continuously-monitored down-and-out / down-and-in calls.
A compact reverse-mode automatic differentiation tape (autodiff.hpp) computes a
full gradient from a single reverse sweep. bs_greeks_aad differentiates the
closed-form price and reproduces the analytic delta, vega and rho to 1e-9 (this
also makes the analytics test suite self-contained: the Greeks are checked two
independent ways, explicit formula and reverse-mode differentiation, with no
reliance on external constants). mc_greeks_aad differentiates a Monte Carlo
pricer path by path for pathwise or smoothed Greeks.
bs_second_order adds exact second-order Greeks (gamma, vanna, volga) via
hyper-dual numbers, forward-mode second-order AD with no truncation error,
validated against the closed form. A hyper-dual number carries a value and
three infinitesimal parts:
Evaluating
so the
AAD's value is the whole gradient at a cost independent of the number of inputs.
Pricing a book of examples/perf_aad.cpp):
| N | AAD (1 sweep) | Bump (2N revaluations) | Speedup | Max grad diff |
|---|---|---|---|---|
| 1 | 0.17 s | 0.12 s | 0.72x | 7.7e-10 |
| 4 | 0.23 s | 0.50 s | 2.17x | 6.7e-09 |
| 16 | 0.27 s | 2.14 s | 8.05x | 7.5e-09 |
| 64 | 0.30 s | 8.83 s | 29.9x | 7.6e-09 |
| 256 | 0.30 s | 35.6 s | 120x | 7.7e-09 |
| 1024 | 0.30 s | 141 s | 468x | 7.7e-09 |
Read honestly: for a single sensitivity the tape overhead makes AAD slower than a bump (0.72x at N = 1). The win is asymptotic in the number of sensitivities, which is exactly the regime of a real risk book. The gradient agrees with bump to the bump's own truncation error; AAD itself is machine-precision.
parallel_likelihood_ratio splits paths across threads, each with an independent
spawned RNG, and matches the single-threaded estimate within Monte Carlo noise
(verified in the test suite). Scaling numbers are not reported here because the
build host has a single core; the code is written to scale on multicore hardware.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failureRequires CMake 3.20+ and a C++20 compiler (tested with GCC 13). To consume it
from another CMake project after cmake --install build:
find_package(radonlab CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE radonlab::radonlab)Because it is header-only you can also just add include/ to your include path.
Via Conan:
conan create . --build=missing# your conanfile.py
def requirements(self):
self.requires("radonlab/0.1.1")Via vcpkg as an overlay port (until the port is published in the curated registry):
vcpkg install radonlab --overlay-ports=<path-to-this-repo>/portsBoth install the headers plus a CMake package config, so find_package(radonlab CONFIG REQUIRED)
works the same way regardless of how the library was installed.
#include "radonlab/radonlab.hpp"
#include <cstdio>
int main() {
using namespace radonlab;
const GeometricBrownianMotion model(100.0, 0.03, 0.20, 1.0);
const CashOrNothingCall payoff{100.0};
NormalSampler rng(0);
const GreekResult d = likelihood_ratio(model, payoff, Greek::Delta, 1'000'000, rng);
const auto [lo, hi] = d.confidence_interval(0.95);
std::printf("digital delta = %.6f 95%% CI [%.6f, %.6f]\n", d.value, lo, hi);
}Adding a payoff is just a struct satisfying the TerminalPayoff concept
(operator(), pathwise_derivative, smoothed, smoothed_derivative).
From examples/benchmark_digital_delta.cpp (1,000,000 paths). Delta of an
at-the-money cash-or-nothing digital call,
| Method | Value | Std. error | Biased? |
|---|---|---|---|
| Likelihood-ratio | 0.019373 | 2.838e-05 | No |
| Smoothing | 0.019263 | 4.568e-05 | No |
| Finite-difference (1% bump) | 0.019331 | 9.490e-05 | No |
| Pathwise | 0.000000 | 0.000e+00 | Yes |
Naive pathwise returns exactly zero, a 100% error. Among the unbiased methods the likelihood-ratio estimator has the lowest standard error (about 3.3 times lower than a 1% finite difference) and needs no bump to tune. Sweeping the bump size shows the finite-difference standard error diverging as the bump shrinks:
| Rel. bump | Value | Std. error | Abs. error |
|---|---|---|---|
| 1e-01 | 0.018608 | 2.359e-05 | 7.25e-04 (bias) |
| 1e-02 | 0.019296 | 9.482e-05 | 3.76e-05 |
| 1e-03 | 0.019409 | 3.063e-04 | 7.55e-05 |
| 1e-04 | 0.018778 | 9.544e-04 | 5.55e-04 |
These numbers track the Python library's to within Monte Carlo noise, as they should: same estimators, same math.
This release covers the Black-Scholes / geometric Brownian motion world and the payoffs above, now with reverse-mode AAD and a threaded estimator. It is a rigorous, tested core, not a kitchen sink. Remaining natural next steps: stochastic volatility and jump models, Asian and lookback payoffs, forward-over- reverse AAD for large Hessians (exact second-order Greeks for a single option are already provided via hyper-dual numbers), and a SIMD-vectorized kernel. Where a method is expected to be biased (pathwise on a digital) the library shows it rather than hiding it.
Apache-2.0. See LICENSE.