Start Here — Price, Then Differentiate

Prove Your Adjoint Greeks Match Bump-and-Revalue

Reconcile a Java adjoint sweep against ten independent central revaluations, factor by factor, with common random numbers.

Product
Market data
SimulationOptional
Java source
AdjointVsBump.java

This exact source runs in TeaVM. Form changes update its Java literals and reset manual edits.

import com.nablatensor.engine.Nabla;
import com.nablatensor.quant.EquityMarket;
import com.nablatensor.quant.GreekFactorEnum;
import com.nablatensor.quant.MonteCarlo;
import com.nablatensor.quant.Product;
import com.nablatensor.quant.Products;

public final class AdjointVsBump {
  private AdjointVsBump() {}

  public static void main(String[] args) {
    EquityMarket market = EquityMarket.of()
        .spot(100)
        .strike(100)
        .vol(20 / 100.0)
        .rate(3 / 100.0)
        .maturity(1)
        .build();
    Product<EquityMarket> product = Products.asianCall();
    int steps = 64;
    long scenarios = 20000L;
    long seed = 42L;
    double relativeBump = 0.5 / 100.0;
    try (MonteCarlo<EquityMarket> adjoint = MonteCarlo.of(product)
        .market(market)
        .steps(steps)
        .fp64()
        .greeks()
        .on("cpu")
        .build(); MonteCarlo<EquityMarket> priceOnly = MonteCarlo.of(product)
        .market(market)
        .steps(steps)
        .fp64()
        .priceOnly()
        .on("cpu")
        .build()) {
      Nabla.TypedValuation<EquityMarket> value = adjoint.run(scenarios, seed);
      GreekFactorEnum[] factors = GreekFactorEnum.values();
      double bumpSeconds = 0.0;
      double maxRelativeError = 0.0;
      for (GreekFactorEnum factor : factors) {
        double gradient = factor.greek(value);
        double base = factor.value(market);
        double h = relativeBump * Math.max(1.0, Math.abs(base));
        long start = System.nanoTime();
        double up = priceOnly.run(factor.bump(market, h), scenarios, seed)
            .price();
        double down = priceOnly.run(factor.bump(market, -h), scenarios, seed)
            .price();
        bumpSeconds += (System.nanoTime() - start) / 1.0e9;
        double bumped = (up - down) / (2.0 * h);
        double relativeError = Math.abs(gradient - bumped) / Math.max(Math.abs(gradient), 1.0e-9);
        maxRelativeError = Math.max(maxRelativeError, relativeError);
        System.out.println("ROW|" + factor.label + "|" + gradient + "|" + bumped + "|" + relativeError);
      }
      System.out.println("META|" + value.price() + "|" + value.seconds() + "|" + bumpSeconds + "|"
          + maxRelativeError + "|" + (2 * factors.length));
    }
  }
}
TeaVM compiles and runs the Java source above in this browser.
Implementation guide

Reconcile a gradient with independent repricing

Bump-and-revalue remains a valuable validation oracle even when adjoint AD is the production sensitivity engine.

Core mechanism

A central finite difference reprices the trade above and below a factor value and estimates the slope. The adjoint obtains the derivative of the recorded computation directly; common random numbers reduce the simulation noise in their comparison.

Practical workflow

Choose a bump consistent with factor units, hold random draws and all unrelated inputs fixed, and compare absolute as well as relative differences. Investigate material disagreements by first checking payoff discontinuities and unit scaling.

Key details

Bump-and-revalue gets a Greek by repricing under a shifted input: (V(x+h) - V(x-h)) / 2h. For N risk factors that is 1 + 2N price-only Monte-Carlo runs (one-sided: 1 + N). Adjoint AD gets all N from one reverse sweep that costs a small constant on top of the price.

The ratio is ~ (1 + 2N) / (1 + adjoint overhead). It grows linearly with the number of risk factors: at N = 20 the bump grid is 41 revaluations against the same single adjoint sweep. The two agree on every number —

price + delta + vega + rho + dV/dK + dV/dT from one adjoint sweep vs the 1 + 2×5 central-bump grid, per payoff. 1,000,000 scenarios, 128 steps, seed 42, cpu-jit. Harness: .../bench/ProductBench.java.

The smoothed path-dependent payoffs (barrier, digital, cliquet, autocallable) get a *usable* adjoint delta this way — a raw-discontinuity bump of those is dominated by variance, not just slower.

Scope and review point

Finite differences themselves have truncation and Monte-Carlo error. Agreement is evidence, not a blanket proof, and a regulated validation standard must set its own tolerances.