Deep Hedging in Options Trading

Deep hedging replaces the Black-Scholes delta formula with a neural network that learns the hedge directly. This page goes deep on the two pieces that matter: the hedge curve (the math of delta, and how it bends with price and volatility) and the tail risk (how to compute CVaR). Pure numpy and matplotlib, step by step.


1. What deep hedging is

Classic hedging is a formula. You sell an option, compute its Black-Scholes delta, and trade the underlying to stay delta-neutral. Deep hedging throws out the formula and lets a neural network learn the hedge directly: at each step it reads the state (price, volatility, and its own current position) and outputs how much to trade. It is trained on simulated paths to minimise a risk measure of the terminal profit and loss, including trading costs:

$$\min_{\theta}\;\; \mathrm{CVaR}_{\alpha}\!\left(-Z + \sum_t \delta_t\,\Delta S_t - C_T(\delta)\right), \qquad \delta_t = f_\theta(\log S_t,\, v_t,\, \delta_{t-1})$$

Based on the paper. This walkthrough reproduces, in a self-contained and illustrative way, the ideas from Deep Hedging by Bühler, Gonon, Teichmann and Wood (2018). For the visual intuition see the companion reel on Instagram.

Before any network, you have to understand the object it is learning. So this page goes deep on two things: the hedge curve (the math of delta, and how it moves with price and volatility) and the tail risk (how to compute CVaR).


2. The hedge curve: where the math lives

The hedge ratio is a derivative. If $V(S)$ is the option value and $S$ the underlying price, the amount of underlying you hold to neutralise small price moves is delta:

$$\delta \;=\; \frac{\partial V}{\partial S}$$

For a European call under Black-Scholes (take the rate $r = 0$ for clarity) this derivative has a clean closed form. It is just the normal CDF evaluated at $d_1$:

$$\delta \;=\; N(d_1), \qquad d_1 \;=\; \frac{\ln(S/K) + \tfrac12\,\sigma^2\tau}{\sigma\sqrt{\tau}}$$

where $N(\cdot)$ is the standard normal CDF, $K$ the strike, $\sigma$ the volatility, and $\tau$ the time to expiry. Notice that exactly two inputs move $d_1$, and therefore the hedge: the price $S$ and the volatility $\sigma$. The whole curve is those two knobs. In code it is four lines:

import numpy as np, math

def ncdf(x):                         # standard normal CDF, N(.)
    return 0.5 * (1 + np.vectorize(math.erf)(np.asarray(x) / math.sqrt(2)))

def call_delta(S, vol, tau, K=100.0):
    d1 = (np.log(S / K) + 0.5 * vol**2 * tau) / (vol * np.sqrt(tau))
    return ncdf(d1)                  # delta = N(d1)

2a. Hedge vs price

Fix the volatility and sweep the price. The hedge traces the classic S-curve: deep out of the money you hold almost nothing ($\delta \to 0$), deep in the money you hold one full unit ($\delta \to 1$), and at the money you sit near $\tfrac12$. The slope of this curve is gamma, the option’s second derivative:

$$\Gamma \;=\; \frac{\partial \delta}{\partial S} \;=\; \frac{N'(d_1)}{S\,\sigma\sqrt{\tau}} \;>\; 0$$

Gamma is largest at the money and near expiry, which is exactly where the curve is steepest. High gamma means the hedge changes fast for small moves in $S$, so you have to re-trade often, and re-trading is what costs money. That tension is the whole reason deep hedging exists.

import matplotlib.pyplot as plt

K, TAU = 100.0, 15 / 250          # 15 trading days to expiry
S = np.linspace(88, 112, 240)

for vol in (0.15, 0.25, 0.35):
    plt.plot(S, call_delta(S, vol, TAU), label=f"sigma = {vol:.0%}")
plt.axvline(K, ls=":", color="grey")
plt.xlabel("Price S"); plt.ylabel("hedge delta = N(d1)")
plt.legend(); plt.show()
Hedge delta vs price for three volatility levels

The delta curve across price, at three volatility levels. Each is an S-curve from 0 to 1, crossing near $\delta = 0.5$ at the strike. The slope is gamma. Lower volatility (blue) makes the curve steeper, that is more gamma, more re-trading, more cost.

2b. Hedge vs volatility

Now fix the price and sweep the volatility. This is the part people forget: the hedge moves even when the price does not. Higher volatility pulls every delta toward $\tfrac12$, because a wider distribution makes it more uncertain which side of the strike you will finish. The sensitivity of delta to volatility is vanna:

$$\text{vanna} \;=\; \frac{\partial \delta}{\partial \sigma} \;=\; -\,N'(d_1)\,\frac{d_2}{\sigma}, \qquad d_2 = d_1 - \sigma\sqrt{\tau}$$

The sign tells the story. Out of the money ($S < K$) we have $d_2 < 0$, so vanna is positive: more volatility raises the hedge. In the money ($S > K$) vanna is negative: more volatility lowers it. At the money $d_2 \approx 0$ and the hedge barely moves. A network that only saw price would miss this entire axis.

V = np.linspace(0.05, 0.55, 240)

for S0, label in [(96, "OTM (S = 96)"), (100, "ATM (S = 100)"), (104, "ITM (S = 104)")]:
    plt.plot(V, call_delta(S0, V, TAU), label=label)
plt.axhline(0.5, ls=":", color="grey")
plt.xlabel("Volatility sigma"); plt.ylabel("hedge delta = N(d1)")
plt.legend(); plt.show()
Hedge delta vs volatility for OTM, ATM and ITM

The hedge across volatility, at three price points. The out-of-the-money hedge (blue) rises toward $\tfrac12$, the in-the-money hedge (red) falls toward $\tfrac12$, and the at-the-money hedge (purple) stays flat. Volatility is a real hedging input, not just a pricing one.

2c. Putting it together: the surface

The two curves above are just slices. Plot $\delta(S, \sigma)$ over both axes at once and you get the full hedge surface. This is the object the deep-hedging network learns, and it is exactly where it earns its keep: once trading costs are real, the optimal policy stops sitting on this surface at all times and instead carves a no-trade band around it, only re-hedging when it drifts too far. The formula gives you the surface; deep hedging gives you when to actually move.

from matplotlib import cm

S_ax = np.linspace(88, 112, 60); V_ax = np.linspace(0.08, 0.50, 60)
SS, VV = np.meshgrid(S_ax, V_ax)
DELTA = call_delta(SS, VV, TAU)

ax = plt.figure(figsize=(8, 5)).add_subplot(projection="3d")
ax.plot_surface(SS, VV, DELTA, cmap=cm.coolwarm)
ax.set_xlabel("Price S"); ax.set_ylabel("Volatility")
ax.set_zlabel("hedge delta"); plt.show()
Hedge delta as a 3D surface of price and volatility

The hedge as a surface of price and volatility. The price axis is the S-curve from section 2a; the volatility axis is the pull-toward-one-half from section 2b. The deep-hedging network learns this whole map, and how it should bend once costs are real.

2d. The same hedge for a position: a call spread

A real book is rarely one option. Subtract two call deltas and you get the hedge for a call spread (here, long the 98 call, short the 102 call). Its hedge surface is no longer a 0-to-1 ramp, it is a ridge that peaks between the strikes, the shape you saw rotating in the reel. Same $N(d_1)$ building block, just combined:

from matplotlib import cm

def spread_delta(S, vol, tau, KA=98.0, KB=102.0):
    return call_delta(S, vol, tau, KA) - call_delta(S, vol, tau, KB)

SPREAD = spread_delta(SS, VV, TAU)
ax = plt.figure(figsize=(8, 5)).add_subplot(projection="3d")
ax.plot_surface(SS, VV, SPREAD, cmap=cm.coolwarm)
ax.set_xlabel("Price S"); ax.set_ylabel("Volatility")
ax.set_zlabel("hedge delta"); plt.show()
Call spread hedge surface, a ridge between the strikes

The 98/102 call-spread hedge. Instead of a 0-to-1 ramp it is a ridge that peaks between the strikes and fades outside them. The hedge ratio across price (the cross-section) is the bump you saw spinning in the video.


3. Deep hedging: let a network learn to hedge

Everything above used the Black-Scholes formula. Deep hedging does the opposite, it never sees a formula. We give a small neural network only simulated price paths and a single instruction, minimise the risk of your final P&L, and let it discover the hedge by gradient descent.

Simulate many GBM paths of the stock. At each step the network reads the state (log-moneyness and time to expiry) and outputs a hedge ratio $\delta_t$. Tally the hedged P&L on every path and train to minimise its variance. Because the hedge enters the P&L linearly, the gradient is clean:

$$\text{P\&L} = \text{premium} - Z + \sum_t \delta_t\,\Delta S_t,\qquad \mathcal{L} = \mathrm{Var}(\text{P\&L}),\qquad \frac{\partial \mathcal{L}}{\partial \delta_t} = \frac{\partial \mathcal{L}}{\partial \text{P\&L}}\,\Delta S_t$$

(MLP below is a standard small numpy network: two hidden layers, a sigmoid output so the hedge sits in $[0,1]$, and Adam. The novel part is the objective, not the net.)

rng = np.random.default_rng(0)
M, N, S0, K, SIG, T = 6000, 20, 100.0, 100.0, 0.20, 30/250
dt = T / N

# simulate GBM stock paths
Z = rng.standard_normal((M, N))
S = np.empty((M, N + 1)); S[:, 0] = S0
S[:, 1:] = S0 * np.exp(np.cumsum((-0.5*SIG**2)*dt + SIG*np.sqrt(dt)*Z, axis=1))
dS = S[:, 1:] - S[:, :-1]
payoff = np.maximum(S[:, -1] - K, 0.0)

# state the network sees at each step: log-moneyness and time-to-maturity
tau  = T - np.arange(N) * dt
logm = np.log(S[:, :-1] / K)
X = np.stack([logm.ravel(), np.broadcast_to(tau, (M, N)).ravel()], axis=1)

net = MLP([2, 24, 24, 1])                 # small net, sigmoid output in (0,1)
for epoch in range(400):
    d    = net.forward(X).reshape(M, N)   # hedge ratio at every (path, step)
    pnl  = premium - payoff + (d * dS).sum(axis=1)
    loss = pnl.var()                      # the ONLY objective. no delta formula.
    grad = (2 * (pnl - pnl.mean()) / M)[:, None] * dS      # dLoss/dd
    net.backprop(grad.reshape(-1, 1))     # Adam step through the network
The network drives its P&L risk down to the delta-hedge floor

Training from random weights, the network drives its P&L risk from about $2.6 down to $0.53, right onto the dashed line, which is the risk of a textbook delta hedge on the same paths. It taught itself to hedge from paths and an objective alone.

And what did it actually learn? Plot the trained network’s hedge across price and lay the Black-Scholes delta on top. They sit on each other. The network rediscovered the delta with no formula, just paths and “minimise risk.”

The learned hedge matches the Black-Scholes delta curve

The learned hedge (green) versus the Black-Scholes delta (dashed). They match. This is the point of deep hedging: it recovers the textbook answer when the textbook is right. Add transaction costs to the objective and it stops matching, it carves a no-trade band and only re-hedges when the drift is worth the fee. That deviation is the whole payoff, and it is what the reel animates.


4. Tail risk: how to compute CVaR

Deep hedging does not minimise variance, it minimises the tail. Two risk numbers describe that tail. Value at Risk is a quantile of the loss: the level the loss will not exceed with probability $\alpha$. Conditional Value at Risk (also called Expected Shortfall) goes one step further and averages the losses beyond that point, the mean of the worst $1-\alpha$ of outcomes:

$$\mathrm{CVaR}_\alpha(X) \;=\; -\,\mathbb{E}\!\left[\,X \mid X \le q_{1-\alpha}\,\right], \qquad q_{1-\alpha} = \text{the } (1-\alpha)\text{ quantile of P\&L } X$$

CVaR is what a desk actually loses on a bad day, and unlike VaR it is sensitive to how bad the tail gets, not just where it starts. Computing it from a sample of P&L outcomes is three lines: find the quantile, keep everything at or below it, average it.

rng = np.random.default_rng(5)
N = 24_000

# Terminal P&L of the book across scenarios ($k): deep hedge ON vs OFF
pnl_on  = rng.normal(0.4, 1.05, N)
pnl_off = rng.normal(0.15, 1.45, N) + rng.binomial(1, 0.06, N) * rng.normal(-3.3, 1.2, N)

def var_cvar(x, a=0.99):
    q    = np.percentile(x, (1 - a) * 100)   # the (1 - a) quantile of P&L
    var  = -q                                # Value at Risk
    cvar = -x[x <= q].mean()                  # average loss beyond it
    return round(var, 1), round(cvar, 1)

print("deep hedge ON :  VaR/CVaR =", var_cvar(pnl_on))
print("deep hedge OFF:  VaR/CVaR =", var_cvar(pnl_off))

plt.hist(pnl_off, 60, alpha=0.55, label="deep hedge OFF")
plt.hist(pnl_on,  60, alpha=0.80, label="deep hedge ON")
plt.xlabel("Terminal P&L ($k)"); plt.legend(); plt.show()
Terminal P&L distribution with 99% VaR and CVaR

The terminal P&L distribution over thousands of scenarios. With the deep hedge on (green) the 99% CVaR is about -2.4; with it off (red) the tail fattens and the CVaR blows out to about -6.0. The dashed lines mark each CVaR. Minimising that green tail is literally the network’s training objective.


5. Recap

Pair this with the neural network vol surface notebook to see the same idea, a network learning a financial object directly from data, applied to the volatility smile instead of the hedge.

Free download

Get the full Deep Hedging notebook + a backtest simulator and a costs model

Everything on this page as a runnable Jupyter notebook, plus a full backtest of the learned hedge and a transaction-cost version that learns a no-trade band. Pure numpy, runs offline. Join the newsletter (free) and the download is yours.

  • The full notebook — every formula, plot and Python cell from this page: the hedge curve, the call and call-spread surfaces, the deep-hedging trainer that rediscovers the delta, and the CVaR tail, ready to run.
  • Backtesting simulator — a $100k short-options book run over a year, the neural hedge against the Black-Scholes delta, with both equity curves, Sharpe, and max drawdown side by side.
  • Deep hedging with transaction costs — the same network retrained with a cost penalty, so it learns a no-trade band and cuts turnover instead of rehedging every step (the version that actually saves money).
  • Pure numpy — the network, the training loop and the backtest are all readable numpy and matplotlib. No PyTorch, no data files, nothing to install beyond the basics.

Write your name and a valid email to unlock the download.

deep_hedging_extras.ipynb

Enter your name & email above to unlock

Download

Free. One short email when there's something new. No spam, unsubscribe in one click.

David Arias, CFA
Written and Modelled by

David Arias, CFA

Licensed portfolio manager with 4+ years of experience, specializing in emerging markets private debt, derivatives, and quantitative finance.

Let's Connect