The call as a function of two variables
Hold the strike , the rate and the time to expiry fixed. Then the call's value is a function of two variables: the stock price and the volatility , a decimal per year. Case study 4.1.4 took its partial derivatives one at a time. Its gradient is : delta and vega , or 0.2777 per vol point, for the six-month call at the money.
Its Hessian (lesson 4.3.1) has three different entries:
- Gamma , the change in delta per dollar (case study 2.7.4).
- Vanna . Mixed partials do not depend on the order (lesson 4.1.2), so it is two things at once: the change in delta per unit of , and the change in vega per dollar.
- Volga , the change in vega per unit of .
The Derivatives Modelling course derives the formulas; here, take them as given and check them by bumping. They use and from case study 2.4.6, and the standard normal density .
Rule: Vanna and volga of a call
With , vega and (case studies 2.7.4 and 2.4.6), these give the gradient and the Hessian of .
| strike 100 | strike 120 | |
|---|---|---|
| price | 6.3710 | 0.8914 |
| , | 0.1768, 0.0354 | , |
| delta | 0.5702 | 0.1330 |
| vega | 27.7721 | 15.1941 |
| gamma | 0.027772 | 0.015194 |
| vanna | 1.3471 | |
| volga | 0.8679 | 105.9655 |
At the money, is close to 0, so vanna and volga are both small. At strike 120, and are both near . Vanna is positive there: a rise in volatility makes the call more likely to finish in the money, so its delta rises. Volga is large: vega itself grows with volatility.
Per vol point. Desks measure volatility moves in vol points, 0.01 of (case study 2.4.6). For strike 120, vanna is the rise in delta for one vol point; the delta formula at 21% gives 0.1463, up from 0.1330. Volga is the rise in vega per vol point, for one vol point; at 21% it rises by 0.0102.
Try it
call is case study 2.4.6's formula with and first. greeks returns delta, vega, gamma, vanna and volga, with and .
The loop prints each row of the table for the two strikes. The last two lines compare vanna and volga per vol point with repricing at 21%.
Checking by bumping
Lesson 4.1.2's central differences estimate all three entries from prices alone. With case study 2.8.4's bumps, $1 to the stock (, or 1%) and one vol point to the volatility (), it takes nine prices: today's, the stock bumped up and down, the volatility bumped up and down, and the four corners for vanna.
| gamma | vanna | volga | |
|---|---|---|---|
| strike 100, formula | 0.027772 | 0.867879 | |
| strike 100, bumped | 0.027762 | 0.871773 | |
| strike 120, formula | 0.015194 | 1.347119 | 105.965466 |
| strike 120, bumped | 0.015193 | 1.344299 | 105.995559 |
Every bumped value is within about 0.5% of the formula. At the money the relative errors look largest only because vanna and volga are so small there.
Try it
bumped applies the second differences and the four-corner formula of lesson 4.1.2 to the price. Each line prints a Greek from the formula, bumped, and the relative error.
The second-order estimate
Write for the change in the stock price and for the change in volatility, as a decimal. The step is , and lesson 4.3.1's quadratic copy of gives the change .
Rule: Second-order P&L estimate
The first line is , the first-order estimate of case study 4.2.4. The rest is ; as in lesson 4.3.1, the cross term has no . With it is case study 2.7.4's delta–gamma estimate; without vanna and volga it is case study 4.1.4's.
Time is held fixed: this is an instant move, as in a stress test. Case studies 4.1.4 and 4.2.4 add theta for the passage of a day.
The letter still has two jobs, as in case study 2.7.4: alone it is delta, and in it means "change in". Here is the sell-off of the hook, and , term by term:
| term | strike 100 | strike 120 |
|---|---|---|
| vega | 1.3886 | 0.7597 |
| 0.8887 | 0.4862 | |
| vanna | 0.0278 | |
| volga | 0.0011 | 0.1325 |
| delta, gamma and vega | 0.1821 | |
| all five terms | ||
| repriced |
For strike 120, delta, gamma and vega add up to a gain of 0.1821. The vanna term, , turns it into a loss: the rise in volatility lifted delta, so the fall in the stock cost more than delta alone says. With all five terms the estimate is within 2 cents of repricing.
At the money the two cross terms add only 0.0289, and the estimate stays 17 cents off. The section on failures explains that gap.
Try it
The loop prints the five terms for each strike, then the sum of the first three, the sum of all five, and the repriced change. The second line for each strike adds the two cross terms.
A grid of scenarios
A risk system does not look at one scenario. It reprices a grid: here the stock moves by , , , 0, 2, 5 or 10 dollars and the volatility by , , 0, 2 or 5 vol points, 35 scenarios in all. Each estimate is scored by its largest error and by its root-mean-square error: square each error, average, and take the square root.
| estimate | strike 100: RMS | largest | strike 120: RMS | largest |
|---|---|---|---|---|
| delta–gamma (2.7.4) | 0.8692 | 1.3892 | 0.5916 | 1.6049 |
| first order, delta and vega (4.2.4) | 0.7701 | 1.8127 | 0.5176 | 1.6049 |
| delta, gamma and vega (4.1.4) | 0.1320 | 0.4241 | 0.2710 | 0.8452 |
| full second order | 0.1324 | 0.4578 | 0.0944 | 0.3501 |
Neither first-order nor stock-only estimates are good enough once both inputs move: their RMS errors are 52 to 87 cents. At the money, gamma and vega do the rest of the work, and vanna and volga change almost nothing. At strike 120 they cut the RMS error almost three times, from 0.2710 to 0.0944.
The pattern holds away from the money on either side:
| strike | vanna | volga | RMS error without vanna and volga | RMS error with them |
|---|---|---|---|---|
| 90 | 66.3417 | 0.2149 | 0.1155 | |
| 100 | 0.8679 | 0.1320 | 0.1324 | |
| 110 | 1.1257 | 39.5746 | 0.2245 | 0.0944 |
| 120 | 1.3471 | 105.9655 | 0.2710 | 0.0944 |
The error of the estimated change in the strike-120 call over a grid of stock and volatility moves, shaded where it is under 2 cents. Without the cross terms the estimate is good only when one input stays still; with vanna and volga it holds along the sell-off diagonal too.
Try it
errors scores the four estimates on the 35 scenarios for one strike and expiry. The first loop prints the first table, one line per estimate.
The second prints the rows of the strike table: vanna, volga and the two RMS errors.
Where it fails
At the money: the terms of degree 3
Case study 4.1.4's estimate missed its worst night, the stock down $10 and volatility up 5 points, by . It traced the miss to vega changing with the stock: 26.34 at $95, against 27.77 at $100. Vanna is that change per dollar, but measured at $100 itself.
There vanna is only : vega peaks at $99.50, so at $100 it is almost flat. Moved by vanna alone, vega would be 28.12 at $95, higher rather than lower. What brings it down to 26.34 is the bend of vega in , about per dollar squared, and that is a term of degree 3 in the expansion of (lesson 4.3.1). With the bend, the estimate of vega at $95 is 26.40.
So at the money the second-order estimate still misses the $8 sell-off of the term table by . Its largest term of degree 3 is . Since the order of differentiation does not matter, is the same bend of vega, by a central difference.
That term is , and the next one, , is . Together they make , the whole miss. The second-order estimate cannot see them, however carefully its five Greeks are computed.
Large moves
The error of degree 3 grows like the cube of the move. In a crash, with the stock down $20 and volatility up 10 points, the at-the-money call loses 4.7508. The second-order estimate says 2.9283, off by 1.8225, or 38% of the loss. For strike 120 and a $20 rally with volatility down 5 points, the estimate is off by 1.3657.
Near expiry
Case study 2.7.4 found gamma growing like at the money, as the price curve closes in on the payoff's corner. The same happens to the whole expansion. On the same 35-scenario grid, for the at-the-money call:
| time to expiry | price | RMS error, full second order | largest error | |
|---|---|---|---|---|
| 6 months | 6.3710 | 0.0278 | 0.1324 | 0.4578 |
| 1 month | 2.4271 | 0.0689 | 0.3891 | 1.1656 |
| 1 week | 1.1352 | 0.1438 | 1.7765 | 3.67 |
With a week left the typical error is larger than the price of the option. For short-dated options near the money, desks reprice every scenario.
Try it
The first line is vega at $95 and $100, where vega peaks, and vega at $95 moved by vanna alone and with its bend. The second is the at-the-money sell-off: the miss, by a central difference of the gamma formula, and the two terms of degree 3.
The next two lines are the large moves: true change, estimate, error and error as a share of the true change. The loop scores the grid at three expiries.
Try it
The first line is the short position's vanna term in the sell-off. The second is for strike 120, and the bend term for , .
The last line measures in vol points instead, which multiplies vanna by 0.01 and volga by : the eigenvalues change, the sign of the determinant does not.
The second-order P&L estimate
Write two functions for a Black–Scholes call, using norm.cdf and norm.pdf from scipy.stats.
call_second_order(S, K, T, r, sigma)returns the tuple(gamma, vanna, volga), the entries of the Hessian of , from the formulas above.second_order_change(S, K, T, r, sigma, dS, dsigma)returns the second-order estimate of the change in the call's value when the stock moves bydSand the volatility bydsigma(a decimal: 0.05 is 5 vol points).
If or , both raise ValueError: the formulas divide by them.
The tests check the two strikes of this case study, and compare vanna and volga with central differences for another option. They try the sell-off, a stock move alone (case study 2.7.4's estimate) and a volatility move alone, then pass and .
See the tests
import math
from scipy.stats import norm
def _call(S, K, T, r, sigma):
d1 = (math.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * math.sqrt(T))
return S * norm.cdf(d1) - K * math.exp(-r * T) * norm.cdf(d1 - sigma * math.sqrt(T))
def _triple(result):
assert isinstance(result, tuple) and len(result) == 3, f"expected a tuple (gamma, vanna, volga), got {result!r}"
return result
def test_at_the_money():
"""The 6-month call at strike 100: gamma 0.027772, vanna −0.069430, volga 0.867879"""
gamma, vanna, volga = _triple(call_second_order(100, 100, 0.5, 0.03, 0.2))
expect_close(gamma, 0.027772131739916564, what="gamma", rel=1e-9)
expect_close(vanna, -0.06943032934979133, what="vanna", rel=1e-9)
expect_close(volga, 0.8678791168723916, what="volga", rel=1e-9)
def test_out_of_the_money():
"""Strike 120: vanna 1.347119 and volga 105.9655, far from zero"""
_, vanna, volga = _triple(call_second_order(100, 120, 0.5, 0.03, 0.2))
expect_close(vanna, 1.3471194695733808, what="vanna at strike 120", rel=1e-9)
expect_close(volga, 105.9654660703653, what="volga at strike 120", rel=1e-9)
def test_by_bumping():
"""Vanna and volga match central differences of the price for S = 50, K = 55, T = 0.25, σ = 0.35"""
S, K, T, r, s, h, k = 50, 55, 0.25, 0.01, 0.35, 1e-2, 1e-3
vanna_b = (_call(S + h, K, T, r, s + k) - _call(S + h, K, T, r, s - k) - _call(S - h, K, T, r, s + k) + _call(S - h, K, T, r, s - k)) / (4 * h * k)
volga_b = (_call(S, K, T, r, s + k) - 2 * _call(S, K, T, r, s) + _call(S, K, T, r, s - k)) / k**2
_, vanna, volga = _triple(call_second_order(S, K, T, r, s))
expect_close(vanna, vanna_b, what="vanna against the four-corner difference", rel=1e-4)
expect_close(volga, volga_b, what="volga against the second difference in sigma", rel=1e-4)
def test_sell_off():
"""Strike 120, stock −$8 and volatility +5 points: −0.2243, within 3 cents of repricing (−0.2438)"""
est = second_order_change(100, 120, 0.5, 0.03, 0.2, -8, 0.05)
expect_close(est, -0.22428677057242452, what="second_order_change(..., dS=-8, dsigma=0.05)", rel=1e-8)
true = _call(92, 120, 0.5, 0.03, 0.25) - _call(100, 120, 0.5, 0.03, 0.2)
assert abs(est - true) < 0.03, f"expected within 0.03 of the repriced change {true:.4f}, got {est:.4f}"
def test_stock_only():
"""With no volatility move it is case study 2.7.4's delta–gamma estimate: −2.5036 for a $5 fall"""
expect_close(second_order_change(100, 100, 0.5, 0.03, 0.2, -5, 0), -2.503638865254377, what="second_order_change(..., dS=-5, dsigma=0)", rel=1e-8)
def test_volatility_only():
"""With no stock move it is vega·Δσ + ½·volga·Δσ²: 1.3897 for +5 vol points"""
expect_close(second_order_change(100, 100, 0.5, 0.03, 0.2, 0, 0.05), 1.3896914358919188, what="second_order_change(..., dS=0, dsigma=0.05)", rel=1e-8)
def test_at_expiry():
"""T = 0 raises ValueError: the formulas divide by zero"""
try:
call_second_order(100, 100, 0, 0.03, 0.2)
except ValueError:
return
raise AssertionError("expected ValueError for T = 0")
def test_zero_volatility():
"""σ = 0 raises ValueError in second_order_change too"""
try:
second_order_change(100, 100, 0.5, 0.03, 0, 1, 0.01)
except ValueError:
return
raise AssertionError("expected ValueError for sigma = 0")
Summary card
- With , and fixed, a call's value has gradient and Hessian . Vanna and volga ; nine bumped prices confirm all three entries.
- Second-order estimate: .
- At the money vanna and volga nearly vanish, and gamma and vega do the work. Away from the money they matter: in a sell-off the vanna term can flip the sign of the estimate.
- What the expansion leaves out is of degree 3, mostly the change of gamma with volatility, which is also the bend of vega in behind case study 4.1.4's leftover. It grows with large moves and near expiry, where desks reprice instead.
- Mind the units (per vol point), the signs of short positions, and that stock and volatility tend to move together.