Write your own payoff
Build a custom Java option payoff with NablaTensor, price it, and switch between call and put behavior without changing the engine.
Every payoff on this site so far came from Products or ExoticProducts.
What does it actually cost to price something neither catalogue has?
The whole story
Product<M> really is this small — @FunctionalInterface, one method,
record(rec, in, grid). The doc comment above it says exactly what this
page sets out to prove: "changing a payoff is a three-line diff, not a
fork." Products and ExoticProducts wrap their lambdas in a private
Named/Labelled record purely so toString() prints something nicer
than Products$$Lambda@1a2b3c in a report — that wrapper is cosmetic.
SwapThePayoff's inline payoff skips it entirely and hands MonteCarlo.of(...)
a bare lambda, and nothing about the driver, the tape, or the reverse sweep
notices the difference.
A payoff neither catalogue has
A capped call — min(max(S_T − K, 0), cap), discounted — isn't in
Products or ExoticProducts. Here it is, written once, with nothing
around it but a market and a loop you've already seen twice:
static Product<EquityMarket> cappedCall(double cap) {
return (rec, in, grid) -> {
// Market inputs stay differentiable throughout the recorded payoff.
ADouble spot = in.of(EquityMarket::spot);
ADouble strike = in.of(EquityMarket::strike);
ADouble rate = in.of(EquityMarket::rate);
ADouble vol = in.of(EquityMarket::vol);
ADouble maturity = in.of(EquityMarket::maturity);
// Build the path model once; each step appends nodes to the same tape.
GbmPath model = GbmPath.of(rec, rate, vol, grid, maturity);
ADouble terminal = spot;
for (int t = 0; t < grid.steps(); t++) {
// One random shock advances the symbolic terminal price by one step.
terminal = model.step(terminal, rec.randn(), t);
}
// Apply the call payoff, floor it at zero, then cap it at the limit.
ADouble payoff = terminal.sub(strike).max(0.0).min(rec.constant(cap));
// Discount the capped payoff back to today.
rec.output(payoff.mul(rate.neg().mul(maturity).exp()));
};
}
Every line is something 1.3 or 3.1 already showed you: five named inputs,
a GbmPath stepped forward, a max/min intrinsic, a discount factor.
The only genuinely new idea is the .min(rec.constant(cap)) at the end —
one extra ADouble method call.
Running cappedCall and Products.europeanCall() side by side (128 steps,
2,000,000 scenarios, seed 42) puts real numbers on "three-line diff":
European call records to 661 nodes; Capped call (inline) records
to 663 — exactly two more. One is rec.constant(cap), the other is
the min op itself. Writing a payoff feature that isn't in the catalogue
cost this tape precisely two nodes, not a new engine feature, a new
backend, or a line changed anywhere outside the lambda.
Swap the payoff, literally
Turning this call into a put is the chapter's namesake move — one line, inside the lambda, nothing else:
// call: ADouble payoff = s.sub(strike).max(0.0).min(rec.constant(cap));
// put:
ADouble payoff = strike.sub(s).max(0.0).min(rec.constant(cap));
Three edits get you there: copy cappedCall to a cappedPut, swap that
one line, and add it to SwapThePayoff's book map. A real run of the
result:
Capped call (inline) 663 5.556285 0.272231 2.6029
Capped put (inline) 663 4.838204 -0.257065 15.9729
Same node count either way — sub() costs one node whichever operand
comes first. Delta flips sign, as a put's should; nothing about
MonteCarlo, Nabla, or the reverse sweep needed to know a put even
exists.
Try it yourself
cappedCall(market.spot() * 0.15) caps the payoff at 15.0. Change that
to market.spot() * 0.30 (30.0) and predict the direction before you
run it: a looser cap binds less often, so the price should move up,
toward the uncapped European call's 9.413115 — never past it, since a
capped call can never be worth more than the option it caps.
▶️ Run it
The same three payoffs, live — European call, capped call, and its one-line
put twin. The example below runs here;
the capped product is the upstream SwapThePayoff typed
Nabla.Inputs<EquityMarket> pattern on the portable cpu engine. Watch
delta's chart flip sides between the capped call and the capped put:
Or run the real thing:
mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
-Dexec.mainClass=com.nablatensor.examples.SwapThePayoff \
-Dscenarios=2000000 -Dsteps=128
Prints the four-row table above. Add your cappedPut entry to the book
map yourself to see the fifth row.
⚠️ What this doesn't do
Product<M> says right in its own doc comment that "any double-only
record works" as the market type M — this page only ever used
EquityMarket. A rates or FX payoff recording against its own market
record through the same interface is a real capability, just not one any
Learn page has exercised yet. This page also doesn't cover the label()
default method or the Named/Labelled wrapper pattern — cosmetic, and
optional, as the first sidenote already said.
What's next
→ Deeper: Swap the payoff — the seam in three lines
is the technical version of this page; note its code sample predates the
current GbmPath/TimeGrid constructor shape this page uses.
→ Next: Six backends, one tape — this
whole book of payoffs can already run on all six, unchanged.