Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Automatic Differentiation

All sensitivities in quantsupport are computed by algorithmic differentiation, not bumping. The implementation lives in src/ad/.

Scalar types

TypeModeUse
f64nonefastest pricing, no risk
Fwd1..Fwd4 (Fwd<N>)forward, N-th order tangentssecond-order Greeks, tests
Dual<T>reverse (tape) over an inner scalar Tfull curve sensitivities
DualFwd = Dual<Fwd2>reverse over forwardthe default AD type: exact first derivatives to every quote and second-order information for IFT
ADForward = Fwd2alias used by curve code

Every pricer, curve and instrument is generic over T: Scalar; DualFwd::scalar(x), DualFwd::zero(), DualFwd::one(), DualFwd::from(x) create constants.

Tape

Tape is a thread-local recorder. Operations on Dual values push nodes only while recording:

Tape::start_recording_fwd();
let x = DualFwd::new(0.04);          // leaf (recorded)
let c = DualFwd::scalar(2.0);        // constant (not recorded)
let y = (x * c).exp();
y.backward();                        // reverse sweep from y
let dy_dx = x.adjoint()?;            // 2·exp(0.08)
Tape::stop_recording_fwd();
APIPurpose
Tape::start_recording_fwd() / stop_recording_fwd() / is_active()control recording (start_recording etc. for Dual<f64>)
Tape::set_mark_fwd() / rewind_to_mark_fwd()keep the market-data part of the tape and discard trade-level nodes between evaluations
Tape::rewind_to_init_fwd(), propagate_mark_to_start_fwd(), reset_mark_fwd()full reset / propagate adjoints from mark to start
Dual::new(f64)leaf variable; constant(f64) non-differentiable
value(), inner(), adjoint() -> Result<T>read primal / inner forward value / gradient
backward(), backward_to_mark(), backward_mark_to_start()reverse sweeps over different tape ranges
put_on_tape(), ensure_on_tape(), is_on_tape()register a value created off-tape

PricingContext::initialize() starts recording, bootstraps curves and surfaces (quotes become leaves), then sets a mark. Each evaluate call records the pricing nodes after the mark, runs backward_to_mark() and propagate_mark_to_start_fwd() to reach the quote leaves, reads their adjoints, and rewinds to the mark so the next trade starts from a clean tape. This is what makes portfolio-wide sensitivities cost roughly one extra pricing per trade.

Curves and pillars

curve.put_pillars_on_tape() marks pillar discount factors as leaves; curve.pillars() -> Option<Vec<(String, DualFwd)>> returns them labelled with the quote identifier. The bootstrapper uses the implicit function theorem to convert pillar adjoints into quote adjoints (see Curve Bootstrapping), so the labels in SensitivityMap are the original quotes (OIS_USD_SOFR_5Y), not internal pillars.

Forward mode

Fwd<N> carries the value and up to N tangents:

let x = Fwd2::var(1.5);               // seed tangent 1
let y = x * x;
y.value();              // 2.25
y.first_derivative();   // 3.0
y.second_derivative();  // 2.0

Fwd::constant(x) has zero tangents. Inside DualFwd, the forward component propagates through the reverse sweep, which is how the bootstrapper obtains the Jacobian needed for the IFT without a second pass.

Costs and caveats

  • Recording allocates: keep Tape::start_recording_fwd() scoped and rewind between trades.
  • Functions with branches (max, if) are differentiated along the taken branch; digital payoffs need smoothing (see the scripting FuzzyEvaluator).
  • Sensitivities are exact derivatives of the implemented formulas, so bisection solvers in Hull-White pricers are differentiated via IFT at the converged root.