← learnModule 5 · Multi-asset and exotic payoffs5 min read

Quanto and convexity adjustments

The correction a foreign payoff settled at a fixed FX rate needs, why it's one extra term in a drift instead of a separate calculation on the tape, and the two Greeks a closed form can't name that fall out of the same adjoint sweep for free.

A quanto option pays a foreign stock's payoff in domestic currency, at an FX rate fixed today rather than whatever it turns out to be at expiry. The closed form for that needs a named correction — the quanto adjustment. What does the same correction look like inside a tape that was never told it needed a special case?

The whole story

A quanto payoff correlates a foreign asset with an FX rate through one extra drift term, not a separate formula. The closed form and a 64-step Monte-Carlo tape agree to within noise. The tape's single greeks() call also returns two sensitivities — to correlation and to FX vol — that the closed form's AnalyticGreeks record has no field for at all.

Did you know?

AnalyticGreeks — the record every closed form in nablatensor-quant.analytic returns — has exactly six fields: price, delta, gamma, vega, theta, and rho. There is no field for a sensitivity to correlation, and none for a second asset's volatility, because most of those closed forms only ever have one underlying. A quanto option has two risk factors an ordinary vanilla doesn't — the asset/FX correlation and the FX volatility — and QuantoAdjustment.quantoOption simply has nowhere to put their Greeks even if someone wanted to derive them by hand.

One extra term in a drift

FxProducts.quantoOption doesn't call anything named "quanto adjustment." It records a plain correlated-GBM step, the same shape as 5.1's basket and 5.2's spread, with the correction folded directly into the drift line:

// domestic-measure drift of the foreign asset: r_f - corr volS volX - 0.5 volS^2
ADouble drift = rf.sub(corr.mul(volS).mul(volX)).sub(volS.mul(volS).mul(0.5)).mul(dt);

Compare that to the closed form, which needs its own named function for exactly this one number:

public static double driftAdjustment(double corr, double volAsset, double volFx) {
  return -corr * volAsset * volFx;
}

Same arithmetic, same sign, same three inputs — QuantoAdjustment.driftAdjustment isolates it as its own method because the closed-form Black76 pricer needs a single adjusted forward handed to it in one piece. The tape doesn't need that isolation; corr.mul(volS).mul(volX) is just two more ADouble multiplications recorded next to the ones already computing the ordinary GBM drift.

Checked against the closed form

QuantoMarket's base case (foreign spot 100, strike 100, 22%/9% asset/FX vols, ρ=−0.35, domestic 3%, foreign 1.2%, 1 year, fixed FX 1.25) priced by QuantoAdjustment.quantoOption and by FxProducts.quantoOption at 64 steps, 2,000,000 scenarios, seed 42, cpu-jit, fp64:

price
closed form11.925386
Monte-Carlo tape11.925416

A difference of 0.00003 against a run whose own standard error is ±0.0133 — indistinguishable. Setting ρ=0 collapses both to the same 11.437, the plain foreign option converted at the fixed FX with no correlation correction at all — exactly what QuantoAdjustment's own doc comment claims, and exactly what the tape does too, since a zero corr input just zeroes out that one drift term.

Greeks the closed form never named

The same .greeks() call that gives the price also gives every sensitivity QuantoMarket has a component for — including the two AnalyticGreeks has nowhere to put:

Greekvalue
d(price)/d(ρ)−1.4149
d(price)/d(volFx)+5.5025
d(price)/d(assetSpot)+0.7146
d(price)/d(volAsset)+50.613

The first two are real risk numbers — how much a quanto book's value moves if the asset/FX correlation shifts, or if FX volatility alone jumps — that a desk pricing off the closed form would otherwise have to derive a new formula for, or bump corr and reprice by hand. Here they cost nothing beyond the four the closed form already gives, because corr and volFx were never anything other than ordinary QuantoMarket components on the tape.

Did you know?

ConvexityAdjustment.eurodollarFutures, in the same adjust package, gets its own two sensitivities (dSigma, dMeanReversion) a completely different way: by bumping its Hull-White parameters up and down by 1e-6 and central-differencing, right there inside the method. It's plain double arithmetic with no ADouble or AadRecorder in sight — the exact bump-and-revalue technique 2.1 built a whole page around adjoint AD making unnecessary, quietly still in use for the one closed form in this codebase that never got a tape-based Monte-Carlo counterpart to check itself against.

The rest of the package

QuantoAdjustment shares its adjust package with three more corrections for a rate observed in one measure but paid in another — different problem, same theme, out of scope for a page about quanto specifically. ConvexityQuantoShowcase's own real output for its own worked examples:

adjustmentvalue
Eurodollar futures convexity (2y expiry, 3M rate)1.90 bp
LIBOR in arrears (3y fixing, semi-annual, 3% forward)0.57 bp
CMS (10y swap rate observed at 5y)11.25 bp

Each is its own closed form, in basis points instead of a price, correcting a rate rather than an option — a big enough topic on its own that cramming it into this page would cost the quanto story its focus. The deeper link below has all three, plus the timing adjustment that interpolates between paying a rate at its natural date and paying it in arrears.

Try it yourself

The measured d(price)/d(ρ) = −1.4149 is a local slope. Use it to predict the closed-form price at ρ=+0.35 (flip the sign of the base case's −0.35) before computing it directly: linear extrapolation over Δρ=0.70 predicts a drop of about 0.99, landing near 10.93. The actual closed-form move is a little different, because driftAdjustment is linear in ρ but Black76's price isn't — the Greek gets you close, not exact, over a step this large.

▶️ Run it

mvn -o -q -pl nablatensor-examples exec:java \
  -Dexec.mainClass=com.nablatensor.examples.ConvexityQuantoShowcase

Runs on cpu-jit only. Prints all four convexity/timing corrections alongside the quanto forward and option this page focused on.

⚠️ What this doesn't do

This page picked one adjustment — quanto — to show the "falls out of the tape for free" idea concretely, and named the other three (Eurodollar-futures, in-arrears, CMS) rather than deriving them; each has its own closed form and its own convexity intuition that deserves more room than a shared page would give it. It also doesn't cover a quanto forward or swap — only the option — and doesn't touch the timing adjustment that interpolates between paying a rate at fixing versus one period later.

What's next

→ Deeper: Convexity, timing and quanto adjustments has all five closed forms, their pinned test tolerances, and the full TimingAdjustment.liborPaymentShift derivation this page skipped. → Next: The binomial lattice, from scratch — where a payoff finally gets to decide when to exercise, instead of only reading a terminal or path-dependent value.


Questions or corrections? open an issue