Replicating Lavender's Greeks¶
This guide builds option pricing from first principles — the same math that powers Lavender — and shows you how to independently verify every Greek we publish. Each section introduces one concept with the underlying mathematics, then the corresponding code, then a live verification against the Lavender API.
By the end you'll have a working implementation that reproduces our numbers exactly.
1. Why Log-Normal?¶
Option pricing starts with a simple observation: stock prices can't go below zero, but they can double, triple, or grow without bound. The distribution of future prices is skewed — there's a floor at zero but no ceiling.
It turns out that if you take the logarithm of daily returns, they look approximately normal:
This means the price itself follows a log-normal distribution: the log of the price is normally distributed. This is the foundational assumption behind Black-Scholes.
Why this matters
If the stock price is log-normal, then we can compute the expected value of any payoff — like \(\max(S_T - K, 0)\) for a call — using the properties of the normal distribution. That's exactly what the Black-Scholes formula does.
2. The Normal Distribution¶
Every formula below uses two functions from the standard normal distribution:
Quick reference: \(N(0) = 0.5\), \(N(1.96) \approx 0.975\), \(N(-\infty) = 0\), \(N(\infty) = 1\).
3. From Price Distribution to Option Value¶
A European call option pays \(\max(S_T - K, 0)\) at expiry. Under risk-neutral pricing, the option's value today is the discounted expected payoff:
Because \(S_T\) is log-normal, this integral has a closed-form solution. The result is the Black-76 formula when expressed in terms of the forward price.
The key insight: \(N(d_2)\) is the (risk-neutral) probability that the option finishes in the money, and \(N(d_1)\) is a delta-weighted version of that probability.
4. The Core: \(d_1\) and \(d_2\)¶
Try it now
Fetch the pricing inputs for a representative SPX call and put around 90 days out, anchored at the forward. The chain group provides forward, t, t_disc, and rate. The core group provides the Greeks you'll verify against.
These two numbers appear in every option pricing formula. They measure how far in-the-money the forward is, normalized by volatility:
where:
| Symbol | Lavender L1 field | Meaning |
|---|---|---|
| \(F\) | forward |
Calibrated forward price |
| \(K\) | strike |
Strike price |
| \(\sigma\) | vol |
Implied volatility (decimal) |
| \(T\) | t |
Variance-weighted time to expiry (years) |
Interpretation:
- \(d_2 > 0\) means the forward is above the strike — the option is in the money
- The magnitude tells you how many "vol-adjusted standard deviations" ITM you are
- \(N(d_2)\) is the risk-neutral probability of finishing ITM
Why t and t_disc differ
Lavender publishes two time fields per expiry, and they are not the same number:
t(variance time) counts only hours when the market is moving. Overnight, weekends, and holidays are excluded because realized volatility doesn't accrue when prices aren't changing.t_disc(calendar time) counts every day because interest accrues continuously regardless of whether the market is open.
For mid-dated options the two are very close. They diverge most for short-dated options spanning long holiday weekends. Use t wherever \(\sigma\sqrt{T}\) appears (d₁, d₂, gamma, vega, theta's diffusion term) and t_disc for the discount factor \(DF = e^{-r \cdot t_\text{disc}}\).
Example — SPX 6600 call, Dec 18 2026 expiry:
F = 6711.04 K = 6600 vol = 0.20805 T = 0.7094
d1 = (ln(6711.04/6600) + 0.5 × 0.20805² × 0.7094) / (0.20805 × √0.7094)
= (0.01668 + 0.01534) / 0.17521
= 0.1828
d2 = 0.1828 - 0.17521 = 0.0075
Both positive — the forward (\(6711) is above the strike (\)6600), so this call is slightly in the money.
5. Option Price (Black-76)¶
Try it now
The theo field is Lavender's model price — the number your formula should reproduce:
With \(d_1\) and \(d_2\) in hand, the Black-76 price formula is:
Or compactly, with \(\phi = +1\) for calls and \(\phi = -1\) for puts:
where \(DF = e^{-r \cdot t_\text{disc}}\) is the discount factor, computed from the L1 fields rate and t_disc.
Example (continued):
rate = 0.03699 t_disc = 0.71184
DF = exp(-0.03699 × 0.71184) = 0.97403
price = 0.97403 × (6711.04 × N(0.1828) - 6600 × N(0.0075))
= 0.97403 × (6711.04 × 0.5725 - 6600 × 0.5030)
= 0.97403 × (3841.6 - 3319.8)
= 508.3
Lavender theo = 508.7 (worked here to 4 decimal places — the §11 code
matches Lavender to < 0.001%.)
5b. Where the Model Breaks: Dividends¶
The Black-76 formula above assumes the forward price \(F\) is known and fixed. In practice, Lavender calibrates \(F\) from live option markets, accounting for discrete dividend payments. But the classic BSM approach uses a continuous dividend yield \(q\) — a smooth approximation of lumpy cash payments.
This matters. Real dividends are discrete events: on the ex-date, the stock drops by approximately the dividend amount, and the forward drops with it. A continuous yield smears this effect across all tenors.
To see this concretely, here's what happens inside a binomial tree when a dividend falls mid-life:
The impact is largest for:
- Short-dated options spanning an ex-date — a $0.75 quarterly dividend on a $200 stock is a 0.4% price drop. For a 2-week ATM call, that matters.
- Deep ITM options — the forward-spot spread depends on cumulative dividends, and continuous yield gets the compounding wrong.
- Calendar spreads — the near-term and far-term legs see different numbers of discrete dividends.
How Lavender handles this
Lavender calibrates the forward price per tenor from live put-call parity, implicitly capturing the market's dividend expectations. The forward field on the L1 API reflects this calibrated value — not a theoretical \(S \cdot e^{(r-q)T}\) calculation. This is why Black-76 with our forward matches exactly, while BSM with a continuous yield approximation introduces error.
6. Delta — Sensitivity to the Underlying¶
Delta measures how much the option price changes per $1 move in the stock:
The \(F/S\) factor converts from "per dollar of forward" to "per dollar of spot" — this is Lavender's spot-adjusted delta convention.
Example:
S = 6583.72 F/S = 6711.04 / 6583.72 = 1.01934
delta = 0.97403 × N(0.1828) × 1.01934
= 0.97403 × 0.5725 × 1.01934
= 0.5681
Lavender delta = 0.5684 ✓
6b. Where the Model Breaks: Early Exercise¶
Black-76 (and BSM) assume the option holder waits until expiry to exercise. This is true for European options (SPX, NDX, RUT), but most US equity options are American — they can be exercised at any time.
Why would you exercise early? Consider a deep in-the-money put: if you own a $100 put on a $20 stock, the put is worth $80 of intrinsic value. But Black-76 says the option is worth less than $80 because it discounts the payoff: \(DF \times 80 < 80\). The rational move is to exercise now, collect $80 in cash, and earn interest on it.
Here's what this looks like inside a binomial tree. At each node, the model asks: "Is exercising now worth more than holding?" Red squares are nodes where the answer is yes:
This has real consequences for Greeks:
- Delta for a deep ITM American put is exactly \(-1\) (you will exercise), while Black-76 gives a delta slightly above \(-1\)
- Gamma vanishes below the exercise boundary (delta is flat at \(-1\))
- Theta can flip sign near the boundary — the option is gaining value as the interest advantage of early exercise grows
How Lavender handles this
Lavender uses a binomial tree (Cox-Ross-Rubinstein) for American options. At each node, the model checks whether early exercise is optimal, producing Greeks that correctly reflect the exercise boundary. This is why our American Greeks can't be replicated with Black-76 — you need a tree. The BSM approximation is typically within 5% for delta, gamma, and vega; theta on American options with upcoming dividends can differ 10–15% because the early-exercise boundary reshapes the time-decay curve. Deep ITM puts and near-expiry options diverge most.
7. Gamma — Curvature of Delta¶
Gamma measures how fast delta changes — it's the second derivative of price with respect to spot:
Gamma is always positive (for both calls and puts) and highest for at-the-money options near expiry.
Example:
n(0.1828) = 0.3924
gamma = 0.97403 × 0.3924 / (6711.04 × 0.20805 × 0.8422) × 1.01934²
= 0.000338
Lavender gamma = 0.000338 ✓
8. Vega — Sensitivity to Volatility¶
Vega measures price sensitivity to a 1% change in implied volatility:
The division by 100 converts from "per unit vol" to "per 1% vol" — Lavender's convention.
Example:
9. Theta and Decay — Two Views of Time¶
Time erodes option value. Lavender provides two measures of this, both in the core field group:
- Theta (\(\theta\)) — the analytical partial derivative \(\partial V / \partial t\), per calendar day. This is the standard Greek that every textbook defines and every risk system expects.
- Decay — the expected dollar change from now to the same time on the next trading day. This accounts for weekends, holidays, and the discrete forward roll. Decay is what your P&L actually shows overnight.
The analytical theta formula:
The first term (\(\sigma\) decay) is always positive — volatility erodes time value. The second term (\(r \cdot V\)) represents interest accrual on the option's value. For most options, theta is negative (you lose money as time passes). Deep in-the-money options can have positive theta when the interest term dominates.
Example:
theta = -(0.97403 × 6711.04 × 0.3924 × 0.20805 / (2 × 0.8422) - 0.03699 × 508.3) / 365
= -(316.8 - 18.8) / 365
= -0.8164
Lavender theta = -0.816 ✓
Theta vs Decay
Lavender publishes two time-decay measures. Theta is the analytical partial derivative shown above — what standard models produce. Decay is the expected price change from now to the same time on the next trading day, accounting for weekends and the discrete forward roll.
Concrete scenarios for an ATM call losing ~$0.15 per calendar day:
| Scenario | Theta (per calendar day) | Decay (to next trading day) |
|---|---|---|
| Tuesday, no upcoming holiday | -$0.15 | -$0.15 (1 trading day = 1 calendar day) |
| Friday before a normal Monday | -$0.15 | -$0.45 (spans Sat + Sun) |
| Wednesday before Thanksgiving | -$0.15 | -$0.60 (spans Thu holiday + weekend) |
| Deep ITM American put near exercise boundary | < 0 (interest cost dominates) | can flip to > 0 (early exercise strictly beats holding) |
Use theta for risk reports and anywhere you want the standard partial derivative. Use decay for P&L attribution — it tells you what your position will actually gain or lose by the next time the market opens.
9b. Theta in the Real World¶
The analytical theta formula above is clean and elegant — but it misses several effects that matter in practice.
Theta acceleration near expiry. An ATM option with 30 days left loses about $0.15/day. With 5 days left, it loses $0.40/day. With 1 day left, it can lose several dollars. This non-linearity is fully captured by the formula (the \(1/\sqrt{T}\) term blows up), but it surprises traders who think of theta as roughly constant.
Weekends and holidays. The analytical formula treats time as continuous — every day is the same. But markets are closed on weekends. An option expiring Monday has two extra days of theta built into Friday's close. This is why Lavender provides decay alongside theta: decay accounts for the actual calendar, including weekends, holidays, and the discrete forward roll from one business day to the next.
Dividends and theta. An option spanning an ex-date has a theta "bump" — the forward drops by the dividend, changing the option's moneyness. Continuous-yield BSM spreads this smoothly, but in reality the effect is concentrated on a single day.
American exercise and theta. Near the exercise boundary, an American put's theta can turn positive — the option is gaining value as the interest advantage of exercise grows. Black-76's analytical theta can't capture this; only a tree model with an explicit exercise check produces correct American theta.
Which number should I use?
Use theta for risk reports, margin calculations, and any context where you want the standard partial derivative. Use decay for P&L attribution — it tells you what your position will actually gain or lose overnight, accounting for weekends and holidays.
10. Rho — Interest Rate Sensitivity¶
Rho measures price sensitivity to a 1% change in the risk-free rate:
Example:
rho = 6600 × 0.71184 × 0.97403 × N(0.0075) / 100
= 6600 × 0.71184 × 0.97403 × 0.5030 / 100
= 23.01
Lavender rho = 23.02 ✓
11. Complete Implementation¶
Everything above in one function — paste it, point it at your Lavender instance, and verify.
Try it with any option
Swap the root and tenor to verify any European option:
The chain fields (forward, t, t_disc, rate) vary by expiry — each tenor has its own calibrated forward and time parameters. Longer tenors (dte=180, dte=365) give larger absolute Greeks which can make hand-calculation easier.
import numpy as np
from scipy.stats import norm
import pandas as pd
def black76(F, K, T, t_disc, r, vol, is_call, S):
"""Replicate Lavender's European Greeks from L1 pricing inputs."""
DF = np.exp(-r * t_disc)
sqrtT = np.sqrt(T)
d1 = (np.log(F / K) + 0.5 * vol**2 * T) / (vol * sqrtT)
d2 = d1 - vol * sqrtT
cp = 1 if is_call else -1
FoS = F / S
price = cp * DF * (F * norm.cdf(cp * d1) - K * norm.cdf(cp * d2))
delta = cp * DF * norm.cdf(cp * d1) * FoS
gamma = DF * norm.pdf(d1) / (F * vol * sqrtT) * FoS**2
vega = DF * F * norm.pdf(d1) * sqrtT / 100
theta = -(DF * F * norm.pdf(d1) * vol / (2 * sqrtT) - r * price) / 365
rho = cp * K * t_disc * DF * norm.cdf(cp * d2) / 100
return dict(price=price, delta=delta, gamma=gamma,
vega=vega, theta=theta, rho=rho)
# Fetch and verify -- pull the ATF call at the nearest 90-day expiry
url = "http://localhost:2112/l1/greeks"
df = pd.read_csv(f"{url}?root=SPX&dte=90¢er=atf&right=call&greeks=core,chain&format=csv")
row = df.iloc[0]
ref = black76(row.forward, row.strike, row.t, row.t_disc,
row.rate, row.vol, True, row.und_price)
print("Greek Lavender Reference Diff")
print("─" * 50)
for g in ["theo", "delta", "gamma", "vega", "theta", "rho"]:
lav, b76 = row[g], ref[g]
pct = abs(lav - b76) / max(abs(lav), 1e-12) * 100
print(f"{g:6s} {lav:+10.6f} {b76:+10.6f} {pct:.3f}%")
black76 <- function(F, K, T, t_disc, r, vol, is_call, S) {
DF <- exp(-r * t_disc)
sqrtT <- sqrt(T)
d1 <- (log(F / K) + 0.5 * vol^2 * T) / (vol * sqrtT)
d2 <- d1 - vol * sqrtT
cp <- ifelse(is_call, 1, -1)
FoS <- F / S
price <- cp * DF * (F * pnorm(cp * d1) - K * pnorm(cp * d2))
delta <- cp * DF * pnorm(cp * d1) * FoS
gamma <- DF * dnorm(d1) / (F * vol * sqrtT) * FoS^2
vega <- DF * F * dnorm(d1) * sqrtT / 100
theta <- -(DF * F * dnorm(d1) * vol / (2 * sqrtT) - r * price) / 365
rho <- cp * K * t_disc * DF * pnorm(cp * d2) / 100
list(price=price, delta=delta, gamma=gamma,
vega=vega, theta=theta, rho=rho)
}
# Fetch and verify -- pull the ATF call at the nearest 90-day expiry
df <- read.csv("http://localhost:2112/l1/greeks?root=SPX&dte=90¢er=atf&right=call&greeks=core,chain&format=csv")
row <- df[1, ]
ref <- black76(row$forward, row$strike, row$t, row$t_disc,
row$rate, row$vol, TRUE, row$und_price)
for(g in c("theo", "delta", "gamma", "vega", "theta", "rho")) {
lav <- row[[g]]
b76 <- ref[[g]]
pct <- abs(lav - b76) / max(abs(lav), 1e-12) * 100
cat(sprintf(" %6s lav=%+.6f ref=%+.6f diff=%.3f%%\n", g, lav, b76, pct))
}
Set up cells:
F = 6711.04 (from L1 "forward")
K = 6600 (strike)
T = 0.7094 (from L1 "t")
T_disc = 0.7118 (from L1 "t_disc")
r = 0.03699 (from L1 "rate")
vol = 0.20805 (from L1 "vol")
S = 6583.72 (from L1 "und_price")
cp = 1 (1 for call, -1 for put)
Compute:
DF = EXP(-r * T_disc)
d1 = (LN(F/K) + 0.5*vol^2*T) / (vol*SQRT(T))
d2 = d1 - vol*SQRT(T)
Price = cp * DF * (F*NORM.S.DIST(cp*d1,TRUE) - K*NORM.S.DIST(cp*d2,TRUE))
Delta = cp * DF * NORM.S.DIST(cp*d1,TRUE) * F/S
Gamma = DF * NORM.S.DIST(d1,FALSE) / (F*vol*SQRT(T)) * (F/S)^2
Vega = DF * F * NORM.S.DIST(d1,FALSE) * SQRT(T) / 100
Theta = -(DF*F*NORM.S.DIST(d1,FALSE)*vol/(2*SQRT(T)) - r*Price) / 365
Rho = cp * K * T_disc * DF * NORM.S.DIST(cp*d2,TRUE) / 100
Expected output:
12. What About BSM?¶
BSM inputs from L1
You need und_price (spot), rate, and borrow — plus the derived dividend yield \(q\):
BSM works for both European and American options (as an approximation for American).
The classic Black-Scholes-Merton formula uses the spot price \(S\) and a continuous dividend yield \(q\) instead of the forward \(F\). The two are related:
You can derive \(q\) from Lavender's forward:
Using BSM with this \(q\) will give you a close approximation — typically within 5% for delta, gamma, vega, and theta. Rho and epsilon may differ by 10–20% because the parameterizations handle interest rate sensitivity differently.
For exact replication of European Greeks, use Black-76 with the forward field directly.
13. American Options¶
Black-76 and BSM assume the option can only be exercised at expiry (European exercise). American options — most US equity options — can be exercised any time before expiry. This early exercise premium means:
- American call prices ≥ European call prices (equality for non-dividend stocks)
- American put prices > European put prices (always, because you can exercise a deep ITM put early to earn interest on the strike)
Lavender prices American options using a binomial tree (Cox-Ross-Rubinstein), which models the stock price at discrete time steps and works backward from expiry, checking at each node whether early exercise is optimal.
A BSM approximation for American Greeks is typically within 5% for delta, gamma, and vega. Theta on American calls or puts with upcoming dividends can differ 10–15% because the early-exercise boundary reshapes time decay in ways BSM's closed form can't capture. For a more accurate independent check, implement a CRR tree — Hull chapters 13 and 21 provide a clear treatment of the math.
Worked example: AAPL American call¶
To make the point concrete, here is a live comparison. Pull an ATF AAPL call at the nearest 90-day expiry, then run our Black-76 reference implementation from §11 against it:
AAPL is American-style and has an upcoming dividend, so Black-76 is expected to approximate rather than reproduce Lavender's Greeks.
| Greek | Lavender (tree) | Black-76 (reference) | Diff |
|---|---|---|---|
theo |
13.564 | 13.566 | 0.013% |
delta |
0.506 | 0.506 | 0.017% |
gamma |
0.011 | 0.011 | 0.082% |
vega |
0.534 | 0.534 | 0.000% |
theta |
-0.0876 | -0.0764 | 12.77% |
rho |
0.308 | 0.308 | 0.182% |
Price, delta, gamma, vega, and rho all agree to within 0.2% — for this particular strike and tenor, the early-exercise premium is small. But theta is 12.77% off, exactly as expected: the upcoming dividend shifts the optimal exercise point for part of the option's life, reshaping the time-decay curve in a way that Black-76's analytical theta cannot capture.
Flip this over: when you run the same comparison on SPX (European, no dividends) in §11, every value matches to < 0.001%. That is the distinction in practice.
Reference CRR implementation
A working CRR tree with early-exercise check — the same code that generates the exercise-boundary chart in §6b — lives in scripts/early_exercise.py in the docs repo. It's short enough to read end-to-end (~150 lines) and covers the tree setup, backward induction, and boundary extraction.
Summary: Inputs and Expected Match¶
Every option on the Lavender L1 API includes the pricing inputs you need:
| L1 Field | Symbol | Used for |
|---|---|---|
forward |
\(F\) | Forward price (Black-76 input) |
vol |
\(\sigma\) | Implied volatility |
t |
\(T\) | Variance-weighted time (for \(\sigma\sqrt{T}\)) |
t_disc |
\(t_\text{disc}\) | Calendar time (for discounting) |
rate |
\(r\) | Risk-free rate |
und_price |
\(S\) | Spot price (for delta/gamma adjustment) |
strike |
\(K\) | Strike price |
right |
— | call or put |
Expected agreement for European options (Black-76):
| Greek | Typical difference |
|---|---|
| Delta | < 0.01% |
| Gamma | < 0.01% |
| Vega | < 0.01% |
| Theta | < 0.01% |
| Rho | < 0.01% |
These are not approximations — with the same inputs, Black-76 reproduces Lavender's European Greeks exactly.