← learnModule 2 · Every Greek from one sweep4 min read

Reading a Greek you didn't ask for

How .greek(EquityMarket::spot) finds the right number without ever seeing the string "spot", why every market field becomes an input whether the payoff reads it or not, and the Greek that was already sitting in the tape.

p.greek(EquityMarket::spot) is delta. p.greek(EquityMarket::vol) is vega. Nowhere in that call does the string "spot" or "vol" appear. How does a method reference on a record component turn into the right entry of a reverse sweep's gradient?

The whole story

EquityMarket's five fields each become a named input node before the payoff records anything. One reverse sweep produces one adjoint per node, so all five sensitivities — including dV/dT, never printed on earlier pages — come out of the same run. A method reference like EquityMarket::spot resolves to the right one by running on a probe record whose components hold their own index.

Did you know?

MarketShape.indexOf resolves EquityMarket::spot to an index with no reflection on the lambda and no bytecode inspection. It builds one "probe" instance where component i holds the value i + 0.25, then just calls your accessor on it: EquityMarket::spot applied to EquityMarket(0.25, 1.25, 2.25, 3.25, 4.25) returns 0.25, subtract the offset and round, and you have index 0. The number the accessor returns is the answer to which field it reads. It's also why every market-record component is required to be a double — the trick only works if there's nothing else the accessor could plausibly do.

Every component becomes an input, whether the payoff reads it or not

in.of(EquityMarket::spot) in 1.3's Products.Sim looks like it's just fetching a value. What actually happens is eager, and happens once, before your lambda runs at all:

private Inputs(AadRecorder recorder, MarketShape<M> shape, M defaults) {
  this.shape = shape;
  this.inputs = new ADouble[shape.size()];
  for (int i = 0; i < inputs.length; i++) {
    inputs[i] = recorder.input(shape.name(i), shape.value(defaults, i));
  }
}

That constructor runs before your payoff lambda ever sees a single ADouble — it walks every component of EquityMarketspot, strike, vol, rate, maturity, all five — and records a named INPUT node for each, using the record's own component names. in.of(EquityMarket::spot) afterward just hands back the ADouble that was already made; it doesn't record anything new.

One reverse sweep, five names, five numbers

AadResult — what a replay actually returns — keeps one adjoint per named input and looks it up by name, not by node index:

/** Sensitivity of the price to a named input. */
public double gradient(String inputName) {
  return outputGradient[0][inputIndexOf(inputName)];
}

Run 1.3's exact europeanCall() example again and read off every name at once instead of stopping at the four the earlier pages printed:

spot      strike        vol       rate    maturity
0.598570  -0.504569   38.591312  50.456860   5.372837
  = delta   = dV/dK      = vega     = rho      = dV/dT

dV/dT — the option's sensitivity to time to expiry — was sitting in that same 1,000,000-scenario run all along. VanillaEuropeanGreeks (1.3) and Benchmarks (2.1) both compute it — GREEKS = 5 counts it — neither one prints it.

Did you know?

Every one of those five inputs is unconditionally active, per 1.2's markActive(): an INPUT node is active by definition, regardless of whether anything downstream ever uses it. Products.Sim happens to read all five EquityMarket fields, so none of these five sensitivities are actually zero — but nothing in the mechanism requires that. Swap in a market record with a field no payoff touches, and .greek(...) on it would still work, still cost its slot in the reverse sweep, and just come back as 0.0 instead of throwing.

Try it yourself

Add one line to 1.3's example, right after the four line(...) calls:

p.greek(EquityMarket::maturity);   // dV/dT — already computed, never printed

No rebuild of the tape, no extra scenario batch — it's the same run, just reading one more name out of a gradient that was already there.

▶️ Run it

Same command as 1.3, since this page changes what you print, not what you build:

mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
  -Dexec.mainClass=com.nablatensor.examples.VanillaEuropeanGreeks \
  -Dscenarios=1000000

Add the greek(EquityMarket::maturity) line above to VanillaEuropean Greeks.java yourself to see it printed; the number above came from exactly this command.

⚠️ What this doesn't do

This page is about retrieving a Greek that already exists on the tape, not computing one you couldn't get before — dV/dT was always there, because steps and maturity were always inputs to the model. It doesn't explain how the reverse sweep actually derives each adjoint arithmetically (that's adjoint-for-dummies.md's job, linked from 1.1), and it doesn't cover a Greek that isn't a market component — a second-order Greek like gamma needs a different mechanism entirely, out of scope here.

What's next

→ Deeper: AadResult.java covers the multi-output case this page skipped — a tape with several named rec.output(...) calls carries one full gradient row per output. → Next: The Asian option, where a payoff needs the whole path instead of one terminal value — the same Asian call this benchmark already used, properly explained.


Questions or corrections? open an issue