Jump-diffusion: the Greek the smoothing breaks
Merton jump-diffusion records an actual random event — did a jump happen this step — onto a tape. Most of the resulting Greeks come out clean. One of them, verified by hand, comes out NaN.
A diffusion alone can't fit a short-dated volatility smile — Black-Scholes has no mechanism for a sudden gap, and the market prices one in. Merton's 1976 fix adds a compound-Poisson jump to geometric Brownian motion: sometimes, with some intensity, the spot jumps by a random lognormal amount. Recording that on a tape means putting an actual coin flip — did a jump happen this step, yes or no — through the same reverse-mode machinery that differentiates a smooth diffusion. Most of what comes out the other side is exactly the clean Greek you'd expect. One of them isn't.
The whole story
MertonJumpModel.step's own comment states a precise, easy-to-get-wrong
fact: the martingale-preserving drift compensator for this discretization
is -ln(1 + λκdt), not the textbook continuous-time -λκdt. The smoothed
"at-most-one-jump" factor has expectation exactly 1 + λκdt, not
e^{λκdt}, so only the logarithmic form makes the discounted spot an exact
per-step martingale under this specific scheme — the two forms agree only
to O(dt²). Smooth.lt(rec, u, λ·dt, width) is what makes "at most one
jump this step" differentiable at all: the underlying event is discrete
and stochastic, but P(≥2 jumps per step) = O((λdt)²), so the smoothed
at-most-one-jump approximation becomes exact as the step count grows —
the same fine-grid argument 3.1 used to justify treating a discretely-
monitored Asian option as continuous.
The step, verified against an exact reference
MertonJumpModel.step is validated against a completely independent
oracle: MertonJumpDiffusion.price, the closed-form Poisson-weighted
average of Black-Scholes prices, one term per possible jump count to
expiry. JumpDiffusionShowcase runs both side by side on the same market:
double mc = price(merton, MertonJumpModel.european(OptionType.CALL, t, steps), paths, 42L);
double exact = MertonJumpDiffusion.price(OptionType.CALL, s0, 100, t, r, sigma,
merton.jumpIntensity(), merton.jumpMean(), merton.jumpVol());
At 1.5 million paths, 128 steps: Monte-Carlo 7.4181 against the exact
series 7.5152 — a 1.3% gap, inside the deeper doc's own claimed 1.5%
convergence bound. That's the whole-price check. The interesting part is
what happens when you ask the same recorded tape for a Greek instead of a
price.
Three Greeks, one of them NaN
Wrote and ran a standalone probe (javac+java against the built
classes, same approach as every prior module) checking three adjoint
Greeks from one built pricer against independent central bumps: spot
delta, d(price)/d(jumpMean), and d(price)/d(jumpIntensity). The first
two are clean — spot delta matches its bump to a relative error of
0.01%, jumpMean's matches to 0.00%, both far inside the doc's own
claimed 5×10⁻³ and 8% tolerances. The third:
spot delta: adjoint=0.619564 bump=0.619515 relErr=0.0001
jumpMean: adjoint=-8.245744 bump=-8.245598 relErr=0.0000
jumpIntensity: adjoint=NaN bump=1.902657 relErr=NaN
Not a one-off: it's NaN at the model's own default indicator width
(2×10⁻⁴) on every run tried. MertonJumpMarket's own doc comment says
jumpIntensity "enters both the drift compensator and — via the smoothed
per-step jump indicator — the jump frequency," which reads as a claim that
its Greek should exist. In practice, at the width the model actually
ships with, it doesn't.
Traced the NaN to Smooth.step: e = exp(-x/width), where x = λdt − u
inside the jump indicator. On the roughly 99.5% of steps where no jump
occurs, u (uniform on [0,1]) sits far above λdt ≈ 0.0047, so x is
strongly negative and -x/width — dividing by a width of 2×10⁻⁴ — blows
up to several thousand, overflowing exp to Infinity in the forward
pass (harmless there; 1/(Infinity+1) still correctly rounds to 0). The
reverse sweep needs Infinity's own local derivative, which meets a
near-zero factor from the surrounding quotient rule and produces
Infinity × 0 → NaN, even though the forward value was exactly right.
Sweeping the indicator width from 2×10⁻⁴ to 10⁻¹ confirmed it isn't a
one-time fluke: the NaN persists through 10⁻³, and by 10⁻² — wide
enough to finally return a finite gradient — the price itself has already
drifted from 7.42 to 4.77, continuing down to 0.21 at 10⁻¹. There
is no width in that range where both the price and this one Greek are
simultaneously trustworthy.
The real run
JumpDiffusionShowcase also prints the implied-volatility smile both
jump models carve into an otherwise flat Black-Scholes surface, same
strikes, same 6-month maturity:
| strike | Black ivol | Merton ivol | Kou ivol |
|---|---|---|---|
| 80 | 15.00% | 28.98% | 27.08% |
| 100 | 15.00% | 23.77% | 21.34% |
| 120 | 15.00% | 22.50% | 20.48% |
Black-Scholes is flat by construction — one input volatility, one output
volatility, every strike. Both jump models produce a real downward slope
instead, from the same negative jumpMean (−0.09) biasing the jump
distribution toward crashes: a diffusion literally cannot produce this
shape no matter how its single volatility parameter is tuned, which is the
entire reason this model exists.
Try it yourself
Change merton.jumpMean() from -0.09 to +0.09 (a market that fears
melt-ups, not crashes) and predict which side of the smile gets steeper
before you rerun the showcase. (The slope flips: 80-strike ivol falls
from 28.98% to 22.97% and 120-strike ivol rises from 22.50% to
30.19% — a positive jump mean fattens the right tail instead of the
left, the mirror image of the negative-mean smile around the ATM strike.)
▶️ Run it
mvn -o -q -pl nablatensor-examples exec:java \
-Dexec.mainClass=com.nablatensor.examples.JumpDiffusionShowcase -Dpaths=1500000
cpu-jit throughout — every pricer in this showcase is built with
.on("cpu-jit") explicitly, and nothing here approaches 7.6's classfile
ceiling.
⚠️ What this doesn't do
d(price)/d(jumpIntensity) is unreliable at this model's shipped
smoothing width — read the price and the jumpMean/jumpVol/spot/vol/rate
Greeks, but don't trust an intensity Greek pulled from this exact step
block without checking it against a bump first, this page's own finding
above notwithstanding the class doc's claim. Separately, and by design:
only Merton's lognormal jump size is covered in depth here, though Kou's
asymmetric double-exponential jump appears in the same showcase; neither
Variance-Gamma nor Bates (Heston plus jumps) exist in this codebase yet —
the deeper doc's own "Deferred" section names both directly, needing a
recordable Gamma subordinator and a Heston-composed step respectively.
What's next
→ Deeper: Jump-diffusion step blocks: Merton and Kou
has the full KouJumpModel API and the pinned test tolerances this page's
numbers are checked against.
→ Module 8 (Credit) is complete. Next: VaR and Expected Shortfall: three routes to one number —
Module 9 opens with risk aggregation, computed rather than looked up from
a table.