← learnModule 8 · Credit6 min read

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

Each step draws a diffusion normal, a uniform for whether a jump occurs, smoothed into an at-most-one-jump indicator via Smooth.lt, and a lognormal jump size. Spot delta and the jumpMean Greek both match an independent central bump to well inside their documented tolerances. But d(price)/d(jumpIntensity) at the model's own default smoothing width comes back NaN every time — traced to Smooth.step's exp(-x/width) overflowing to Infinity on the vast majority of no-jump steps, meeting a near-zero local factor in the reverse pass. Widening the indicator clears the NaN but visibly distorts the price, from 7.42 down to 0.21 by the width where the gradient finally becomes finite — no single width makes both trustworthy together. The real run: a Merton ATM call at 1.5 million paths matches the exact Poisson-series price to 1.3%, and the jump models carve a real downward-sloping implied-vol smile a pure diffusion structurally cannot produce.

Did you know?

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.

Did you know?

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:

strikeBlack ivolMerton ivolKou ivol
8015.00%28.98%27.08%
10015.00%23.77%21.34%
12015.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.


Questions or corrections? open an issue