Pricing code is full of decisions: a payoff that pays if the spot ends above the strike, a barrier check, a floor on volatility, a solver that stops when it is close enough. AAD differentiates the code that ran. At a decision, that means the side that was taken.
This is exact, and it is also the source of most surprises with AAD on real pricers. The derivative of a step is zero on both sides of it. The derivative of a knock-out is the derivative of whichever branch you were on. The jump itself, which is what a risk manager usually cares about near a barrier, is not in the number.
What AAD returns at a decision
A step. A digital call pays 1 if the final spot is above the strike. Priced by Monte Carlo with the comparison kept on the record (aadc.iif), the pathwise AAD delta is 0.0: on every path the payoff is flat in the spot. The closed-form delta is 0.018996903871755226. Bumping does not rescue it: with the same random numbers, the central bump gave 0.018681076520808804 at h = 1, 0.023048081421777056 at h = 0.1 and 0.0 at h = 0.001, where no path crossed the strike inside the bump.
A kink. For max(S − K, 0), AAD returned a derivative of 0.0 just below the strike, 1.0 just above it, and 1.0 exactly at it. At the kink the derivative does not exist; the engine reports the slope of the side the comparison chose.
A branch. Here is a contract that knocks out to a rebate of 1 if the spot is at or above 102 today, written with an ordinary Python if:
def knock_out(S):
if S >= B: # collapses to a Python bool while recording
return aadc.idouble(rebate)
return bs_call(S)
f = aadc.Functions(); f.start_recording(trace=True, trace_inputs_as_random=False)
s = aadc.idouble(101.7); a = s.mark_as_input()
o = knock_out(s).mark_as_output()
f.stop_recording()
At spot 101.7 the price is 8.010806439387522 and the AAD delta 0.5359894024953883: the delta of the call. Nothing in that number says the price falls to 1 a short way up.
How far was the input from flipping it?
The useful question at a decision is not only “which way did it go” but “how close was it”. The engine keeps every decision it froze during recording, with its signed margin: the left side minus the right side of the comparison at record time. Asked for the decisions closest to flipping, it answers:
branches order=margin
entry count kind op margin(first) where
#2 1 FROZEN >= -0.29999999999999716 p2_branch.py:45 in knock_out ibool->bool x1
The spot was 0.3 below the barrier, at a named line. The same record can be replayed at new inputs, and it re-checks every recorded decision as it goes. At spot 101.9 the decision survives and the replay returns 8.118394621766484, the same digits as pricing again. At 102.5 it does not:
FLIPPED #2 frozen >= margin -0.29999999999999716 -> 0.5 p2_branch.py:45 in knock_out
The value is withheld rather than returned: past the flipped decision, the recorded arithmetic is the call’s, not the price at that spot.
Writing the comparison with aadc.iif (or aadc.where_arr for arrays, aadc.branching_function for whole functions) keeps both sides on the record, so a replay across the threshold follows the right branch. It does not change the derivative at the input point: that is still the slope of the side you are on.
What to do about it
Report the margin. For a production Greek, the derivative of the branch taken is often the right answer, as long as you also know how far the input is from the nearest decision. A delta reported with “0.3 from the barrier at line 45” is more useful than a delta alone, and the margin is free: it is on the record.
Smooth the decision. Replace the step with a smooth transition over a band, and AAD sees the jump as a steep slope. With a logistic band of width 0.5 around the strike, the digital’s AAD delta was 0.018939830301448957; with width 2.0, 0.01892119508062362, against the closed form 0.018996903871755226. The price moves too, from 0.4185046363427941 unsmoothed to 0.4190136026007339 and 0.4202531408453784: smoothing is a model change, and its width is a choice with a cost in price bias that should be stated alongside the Greek.
Re-record across the decision. When a scenario crosses a threshold, record again at the new point. A replay that names the flipped decision tells you when this is necessary.
Your agent can check this directly
aadc-mcp is a local MCP server that lets a coding agent ask the AADC engine instead of reading the code. pip install aadc-mcp, register it with your MCP client, and ask the agent to trace your pricer. inspect_tape(tape_id, "branches order=margin") lists the decisions closest to flipping, each with its margin and file:line; replay re-evaluates at new inputs and either returns the exact value or withholds it, naming the decision that flipped or could not be re-checked. For a script that records with marked inputs, open_session, ask_session and replay_session answer the same questions from the live recording.
AADC runs here as Community Edition: free for non-commercial and academic use; commercial use, including evaluation inside a company, needs a licence from MatLogica.
Implemented using AADC, a commercial adjoint AD compiler (matlogica.com).