Barriers and digitals — payoffs with a kink
Why a discontinuous payoff breaks both bump-and-revalue and a reverse sweep, and how nablatensor-ops' Smooth.step trades a sharp corner for a curve steep enough to matter and smooth enough to differentiate.
Every payoff so far bends smoothly. A knock-out barrier doesn't: the option is worth something, then the instant a level is touched, it's worth nothing. What does a reverse sweep even do with a slope that jumps from flat to flat with nothing sensible in between?
The whole story
Smooth.java's own doc comment states the property that makes this whole
approach work, not just plausible: "as width -> 0 the smoothed value
converges to the discontinuous limit; its derivative concentrates into a
spike, so a barrier delta computed this way is a genuine (mollified)
delta, not a bumped one." The smoothing isn't a hack that happens to give
a usable number — it's built entirely from the engine's own primitive ops
(div, neg, exp, add), so its adjoint is the exact derivative of
the smoothed function, no special-cased reverse rule required.
The smoothed indicator
nablatensor-ops' Smooth.step is a logistic curve, built from four
ADouble calls:
public static ADouble step(AadRecorder rec, ADouble x, double width) {
requireWidth(width);
ADouble e = x.div(width).neg().exp(); // exp(-x/width)
return rec.constant(1.0).div(e.add(1.0)); // 1 / (1 + exp(-x/width))
}
gt/lt/between all reduce to step on a shifted argument. A knock-out
barrier multiplies one of these per monitored step — "still alive so far,"
smoothed — instead of checking a hard boundary:
ADouble survival = rec.constant(1.0);
for (int t = 0; t < grid.steps(); t++) {
path = sim.model.step(path, rec.randn(), t);
ADouble notBreached = up
? Smooth.lt(rec, path, barrier, width) // still below an up-barrier
: Smooth.gt(rec, path, barrier, width); // still above a down-barrier
survival = survival.mul(notBreached);
}
ExoticProducts.barrier(...) records exactly this loop; digitalCash
and digitalAsset use one Smooth.gt/lt at expiry instead of a running
product across every step.
Checks that actually ran
ExoticsTest verifies this construction three separate ways; running the
same market (atmOneYear, 64 steps, 200,000 scenarios, seed 12345) gives
the same real numbers:
In/out parity. UP_OUT and UP_IN at the same level and width are
built as vanilla × survival and vanilla × (1 − survival) — they have
to sum back to the plain European, and they do, to six decimals:
vanilla=9.400362 UP_OUT=3.409376 UP_IN=5.990986 OUT+IN=9.400362
Digital vs. the closed form. A cash-or-nothing digital's fair value
is e^{-rT} N(d2) — the same d2 Black-Scholes uses:
digital mc=0.504659 closedForm=0.504572 diff=8.70e-05
The barrier's adjoint delta is checked against a central bump — but of the same smoothed payoff, not the original discontinuous contract:
barrier adjointDelta=0.006051 bumpDelta=0.005933 diff=1.18e-04
That's a deliberate, easy-to-miss choice. Bumping the actual discontinuous barrier is exactly the "notoriously noisy" estimate this page opened with — smoothing doesn't make that noise go away for free on the raw contract; it replaces the contract itself with a differentiable stand-in, and then both bump-and-revalue and adjoint AD get a clean answer on that stand-in. Adjoint AD is the cheap way to differentiate the smoothed payoff, not a way to avoid smoothing it in the first place.
Try it yourself
ExoticsTest.barrierConvergesAsSmoothingWidthShrinks runs the same
UP_OUT barrier at width = 4.0, 2.0, 1.0, 0.5, and 0.25, in sequence.
Run it yourself and watch the price move less between each successive
pair — the test's own assertion is exactly that: the gap between the last
two widths is smaller than the gap between the first two, because the
sequence is settling onto the sharp-barrier limit rather than wandering.
▶️ Run it
mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
-Dexec.mainClass=com.nablatensor.bench.ProductBench \
-Dscenarios=1000000 -Dsteps=128
Prints price, delta, and the adjoint-vs-bump timing from 2.1 for a whole
book of products at once — European, Asian, lookback, barrier, digital,
cliquet, and autocallable, all through the same .greeks()/.priceOnly()
pattern.
⚠️ What this doesn't do
This page covers exactly two of ExoticProducts' five payoffs — barrier
and digital, the two the plan calls "payoffs with a kink." It doesn't
touch cliquets or autocallables, which use the same Smooth primitives
for a different reason (a ratchet's local/global clamps, an
early-redemption trigger) rather than a barrier's knock-in/out kink. It
also doesn't say how to choose width beyond the rule of thumb already in
Smooth.java's own doc comment: too small re-introduces the variance a
discontinuity brings, too large biases the price.
What's next
→ Deeper: Barriers, digitals and the smoothed indicator
covers MultiMetric — pricing several of these payoffs off one market and
seed at once — which this page left out entirely.
→ Next: Write your own payoff, the "swap
the payoff" exercise, turned into three lines you change yourself.