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
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 form | 11.925386 |
| Monte-Carlo tape | 11.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:
| Greek | value |
|---|---|
| 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.
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:
| adjustment | value |
|---|---|
| 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.