Skip to content

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:

\[r_t = \ln\!\left(\frac{S_t}{S_{t-1}}\right) \sim \mathcal{N}(\mu, \sigma^2)\]

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.

Log-normal distribution

Left: simulated stock price paths over one year. Center: the distribution of final prices is log-normal (skewed right). Right: daily log-returns are normally distributed (symmetric). 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:

\[N(x) = \frac{1}{\sqrt{2\pi}} \int_{-\infty}^{x} e^{-u^2/2}\, du \qquad \text{(CDF — cumulative probability)}\]
\[n(x) = \frac{1}{\sqrt{2\pi}}\, e^{-x^2/2} \qquad \text{(PDF — the bell curve)}\]

Quick reference: \(N(0) = 0.5\), \(N(1.96) \approx 0.975\), \(N(-\infty) = 0\), \(N(\infty) = 1\).

from scipy.stats import norm
N = norm.cdf   # N(0) = 0.5
n = norm.pdf   # n(0) ≈ 0.399
N <- pnorm   # N(0) = 0.5
n <- dnorm   # n(0) ≈ 0.399
N(x)  =NORM.S.DIST(x, TRUE)
n(x)  =NORM.S.DIST(x, FALSE)

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:

\[C = e^{-rT} \cdot \mathbb{E}[\max(S_T - K, 0)]\]

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.

http://localhost:2112/l1/greeks?root=SPX&dte=90&center=atf&greeks=core,chain&format=html

These two numbers appear in every option pricing formula. They measure how far in-the-money the forward is, normalized by volatility:

\[d_1 = \frac{\ln(F/K) + \frac{1}{2}\sigma^2 T}{\sigma\sqrt{T}}\]
\[d_2 = d_1 - \sigma\sqrt{T}\]

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}}\).

import numpy as np

d1 = (np.log(F / K) + 0.5 * vol**2 * T) / (vol * np.sqrt(T))
d2 = d1 - vol * np.sqrt(T)
d1 <- (log(F / K) + 0.5 * vol^2 * T) / (vol * sqrt(T))
d2 <- d1 - vol * sqrt(T)
d1 = (LN(F/K) + 0.5*vol^2*T) / (vol*SQRT(T))
d2 = d1 - vol*SQRT(T)

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:

http://localhost:2112/l1/greeks?root=SPX&dte=90&center=atf&greeks=core,chain&format=html

With \(d_1\) and \(d_2\) in hand, the Black-76 price formula is:

\[C = DF \cdot \bigl[F \cdot N(d_1) - K \cdot N(d_2)\bigr]\]
\[P = DF \cdot \bigl[K \cdot N(-d_2) - F \cdot N(-d_1)\bigr]\]

Or compactly, with \(\phi = +1\) for calls and \(\phi = -1\) for puts:

\[V = \phi \cdot DF \cdot \bigl[F \cdot N(\phi \cdot d_1) - K \cdot N(\phi \cdot d_2)\bigr]\]

where \(DF = e^{-r \cdot t_\text{disc}}\) is the discount factor, computed from the L1 fields rate and t_disc.

DF = np.exp(-rate * t_disc)
cp = 1  # +1 for call, -1 for put
price = cp * DF * (F * N(cp * d1) - K * N(cp * d2))
DF <- exp(-rate * t_disc)
cp <- 1  # +1 for call, -1 for put
price <- cp * DF * (F * N(cp * d1) - K * N(cp * d2))
DF    = EXP(-rate * t_disc)
Price = DF * (F*NORM.S.DIST(d1,TRUE) - K*NORM.S.DIST(d2,TRUE))    [call]

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.

Dividend impact on forward

Left: the forward price under continuous yield (BSM) vs discrete dividends. The discrete forward drops at each ex-date. Right: the resulting call pricing error across strikes.

To see this concretely, here's what happens inside a binomial tree when a dividend falls mid-life:

Binomial tree with dividend

Left: a clean recombining tree. Right: a $3 dividend at step 2 shifts the entire tree downward — every node after the ex-date is $3 lower. The tree still recombines, but at shifted levels. This discrete drop is what continuous-yield BSM misses.

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:

\[\Delta = \phi \cdot DF \cdot N(\phi \cdot d_1) \cdot \frac{F}{S}\]

The \(F/S\) factor converts from "per dollar of forward" to "per dollar of spot" — this is Lavender's spot-adjusted delta convention.

Delta visualization

Left: call price vs spot with tangent line at ATM — the slope is delta. Right: delta ranges from 0 (deep OTM) to 1 (deep ITM), crossing 0.50 at the money.
delta = cp * DF * N(cp * d1) * F / S
delta <- cp * DF * N(cp * d1) * F / S
Delta = DF * NORM.S.DIST(d1,TRUE) * F/S    [call]

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.

Early exercise

Left: the European put (blue) can fall below intrinsic value — the American put (purple) never does. The gold region is the early exercise premium. Right: the early-exercise boundary B(tau) vs time-to-expiry. Below the gold curve, immediate exercise dominates holding.

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:

Binomial tree exercise

A 6-step CRR tree for an American put. Red squares: exercise immediately (intrinsic > continuation). Green circles: hold (continuation value is higher). The exercise frontier cuts diagonally across the tree — below it, early exercise is always optimal.

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 = DF \cdot \frac{n(d_1)}{F \cdot \sigma \cdot \sqrt{T}} \cdot \left(\frac{F}{S}\right)^2\]

Gamma is always positive (for both calls and puts) and highest for at-the-money options near expiry.

gamma = DF * n(d1) / (F * vol * np.sqrt(T)) * (F / S)**2
gamma <- DF * n(d1) / (F * vol * sqrt(T)) * (F / S)^2
Gamma = DF * NORM.S.DIST(d1,FALSE) / (F*vol*SQRT(T)) * (F/S)^2

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:

\[\nu = \frac{DF \cdot F \cdot n(d_1) \cdot \sqrt{T}}{100}\]

The division by 100 converts from "per unit vol" to "per 1% vol" — Lavender's convention.

vega = DF * F * n(d1) * np.sqrt(T) / 100
vega <- DF * F * n(d1) * sqrt(T) / 100
Vega = DF * F * NORM.S.DIST(d1,FALSE) * SQRT(T) / 100

Example:

vega = 0.97403 × 6711.04 × 0.3924 × 0.8422 / 100
     = 21.59

Lavender vega = 21.60  ✓

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:

\[\theta = -\frac{1}{365}\left[\frac{DF \cdot F \cdot n(d_1) \cdot \sigma}{2\sqrt{T}} - r \cdot V\right]\]

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.

theta = -(DF * F * n(d1) * vol / (2 * np.sqrt(T)) - rate * price) / 365
theta <- -(DF * F * n(d1) * vol / (2 * sqrt(T)) - rate * price) / 365
Theta = -(DF*F*NORM.S.DIST(d1,FALSE)*vol/(2*SQRT(T)) - rate*Price) / 365

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 real world

Left: ATM theta accelerates dramatically near expiry — the "gamma-theta tradeoff" in action. Right: on Friday, the discrete decay spans the entire weekend (3 calendar days), while analytical theta is the same every day.

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:

\[\rho = \frac{\phi \cdot K \cdot t_\text{disc} \cdot DF \cdot N(\phi \cdot d_2)}{100}\]
rho = cp * K * t_disc * DF * N(cp * d2) / 100
rho <- cp * K * t_disc * DF * N(cp * d2) / 100
Rho = K * t_disc * DF * NORM.S.DIST(d2,TRUE) / 100    [call]

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:

http://localhost:2112/l1/greeks?root=SPX&dte=90&center=atf&greeks=core,chain&format=html

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&center=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&center=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:

Theo   = 508.3      (Lavender: 508.7)
Delta  =  0.5681    (Lavender: 0.5684)
Gamma  =  0.000338  (Lavender: 0.000338)
Vega   = 21.59      (Lavender: 21.60)
Theta  = -0.8164    (Lavender: -0.816)
Rho    = 23.01      (Lavender: 23.02)

12. What About BSM?

BSM inputs from L1

You need und_price (spot), rate, and borrow — plus the derived dividend yield \(q\):

http://localhost:2112/l1/greeks?root=AAPL&dte=90&center=atf&greeks=core,chain&format=html

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:

\[F = S \cdot e^{(r - q) \cdot t_\text{disc}}\]

You can derive \(q\) from Lavender's forward:

\[q = r - \frac{\ln(F/S)}{t_\text{disc}}\]

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:

http://localhost:2112/l1/greeks?root=AAPL&dte=90&center=atf&right=call&greeks=core,chain&format=csv

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.