Write your own payoff
Product<M> is a plain functional interface with one method. This page writes a payoff that isn't in either catalogue, runs it for real, and swaps call for put in one line to prove the seam holds.
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) -> {
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);
GbmPath model = new GbmPath(rec, rate, vol, grid, maturity);
ADouble s = spot;
for (int t = 0; t < grid.steps(); t++) {
s = model.step(s, rec.randn(), t);
}
ADouble payoff = s.sub(strike).max(0.0).min(rec.constant(cap));
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 module'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
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.