Skip to content

Options

See the Options guide for a narrative walkthrough.

Pricing

pricing

Option pricing in pure JAX.

The Black-Scholes-Merton price is a plain differentiable function, so the Greeks module gets every sensitivity from jax.grad without duplicating formulas. A jitted Newton solver recovers implied volatility, and a binomial (Cox-Ross- Rubinstein) lattice prices American-style options.

Convention: +1 for calls, -1 for puts (is_call boolean helpers are provided for readability at call sites).

black_scholes_price

black_scholes_price(
    spot: Array,
    strike: Array,
    ttm: Array,
    vol: Array,
    rate: Array = 0.0,
    div: Array = 0.0,
    is_call: bool = True,
) -> Array

Black-Scholes-Merton price of a European option.

All array arguments broadcast, so this prices whole option chains at once.

Parameters:

Name Type Description Default
spot Array

Underlying price and strike.

required
strike Array

Underlying price and strike.

required
ttm Array

Time to maturity in years.

required
vol Array

Annualized volatility.

required
rate Array

Continuous risk-free rate and dividend yield.

0.0
div Array

Continuous risk-free rate and dividend yield.

0.0
is_call bool

True for a call, False for a put.

True
Source code in src/jaxfolio/options/pricing.py
def black_scholes_price(
    spot: Array,
    strike: Array,
    ttm: Array,
    vol: Array,
    rate: Array = 0.0,
    div: Array = 0.0,
    is_call: bool = True,
) -> Array:
    """Black-Scholes-Merton price of a European option.

    All array arguments broadcast, so this prices whole option chains at once.

    Parameters
    ----------
    spot, strike:
        Underlying price and strike.
    ttm:
        Time to maturity in years.
    vol:
        Annualized volatility.
    rate, div:
        Continuous risk-free rate and dividend yield.
    is_call:
        ``True`` for a call, ``False`` for a put.
    """
    d1, d2 = _d1_d2(spot, strike, ttm, vol, rate, div)
    disc = jnp.exp(-rate * ttm)
    carry = jnp.exp(-div * ttm)
    if is_call:
        return spot * carry * norm.cdf(d1) - strike * disc * norm.cdf(d2)
    return strike * disc * norm.cdf(-d2) - spot * carry * norm.cdf(-d1)

price_call_chain

price_call_chain(
    spot: float,
    strikes: Array,
    ttms: Array,
    vols: Array,
    rate: float = 0.0,
    div: float = 0.0,
) -> Array

Vectorized call prices across a chain of (strike, ttm, vol) triples.

ttms and vols may be scalars (broadcast to every strike) or arrays aligned with strikes.

Source code in src/jaxfolio/options/pricing.py
def price_call_chain(
    spot: float, strikes: Array, ttms: Array, vols: Array, rate: float = 0.0, div: float = 0.0
) -> Array:
    """Vectorized call prices across a chain of (strike, ttm, vol) triples.

    ``ttms`` and ``vols`` may be scalars (broadcast to every strike) or arrays
    aligned with ``strikes``.
    """
    strikes = jnp.asarray(strikes, dtype=float)
    ttms = jnp.broadcast_to(jnp.asarray(ttms, dtype=float), strikes.shape)
    vols = jnp.broadcast_to(jnp.asarray(vols, dtype=float), strikes.shape)
    return _price_over_chain(spot, strikes, ttms, vols, rate, div)

implied_volatility

implied_volatility(
    price: float,
    spot: float,
    strike: float,
    ttm: float,
    rate: float = 0.0,
    div: float = 0.0,
    is_call: bool = True,
    *,
    initial_vol: float = 0.2,
    max_iter: int = 50,
    tol: float = 1e-08,
) -> Array

Recover implied volatility via jitted Newton iterations.

Vega (the derivative of price w.r.t. vol) comes from jax.grad on the pricer, so the Newton step needs no hand-coded derivative. Returns NaN when no volatility reproduces price to within tol (e.g. a price below intrinsic value or a deep-OTM option with vanishing vega), so callers can distinguish "no solution" from a genuinely tiny implied vol.

Source code in src/jaxfolio/options/pricing.py
def implied_volatility(
    price: float,
    spot: float,
    strike: float,
    ttm: float,
    rate: float = 0.0,
    div: float = 0.0,
    is_call: bool = True,
    *,
    initial_vol: float = 0.2,
    max_iter: int = 50,
    tol: float = 1e-8,
) -> Array:
    """Recover implied volatility via jitted Newton iterations.

    Vega (the derivative of price w.r.t. vol) comes from ``jax.grad`` on the
    pricer, so the Newton step needs no hand-coded derivative. Returns ``NaN``
    when no volatility reproduces ``price`` to within ``tol`` (e.g. a price below
    intrinsic value or a deep-OTM option with vanishing vega), so callers can
    distinguish "no solution" from a genuinely tiny implied vol.
    """
    price_of_vol = lambda v: black_scholes_price(spot, strike, ttm, v, rate, div, is_call)
    vega_of_vol = jax.grad(price_of_vol)

    def cond(state):
        i, v, diff = state
        return jnp.logical_and(i < max_iter, jnp.abs(diff) > tol)

    def body(state):
        i, v, _ = state
        diff = price_of_vol(v) - price
        vega = vega_of_vol(v)
        vega = jnp.where(jnp.abs(vega) < 1e-8, 1e-8, vega)
        v_new = jnp.clip(v - diff / vega, 1e-4, 10.0)
        return (i + 1, v_new, v_new - v)

    _, vol, _ = jax.lax.while_loop(cond, body, (0, jnp.asarray(initial_vol), jnp.asarray(jnp.inf)))
    # Flag non-convergence: the recovered vol must actually reprice the option.
    residual = jnp.abs(price_of_vol(vol) - price)
    return jnp.where(residual <= jnp.sqrt(tol) + 1e-6, vol, jnp.nan)

binomial_american

binomial_american(
    spot: float,
    strike: float,
    ttm: float,
    vol: float,
    rate: float = 0.0,
    div: float = 0.0,
    is_call: bool = True,
    *,
    steps: int = 256,
) -> Array

Cox-Ross-Rubinstein binomial price for an American-style option.

Backward induction with early-exercise checks at every node. steps controls the lattice resolution.

Source code in src/jaxfolio/options/pricing.py
def binomial_american(
    spot: float,
    strike: float,
    ttm: float,
    vol: float,
    rate: float = 0.0,
    div: float = 0.0,
    is_call: bool = True,
    *,
    steps: int = 256,
) -> Array:
    """Cox-Ross-Rubinstein binomial price for an American-style option.

    Backward induction with early-exercise checks at every node. ``steps``
    controls the lattice resolution.
    """
    dt = ttm / steps
    u = jnp.exp(vol * jnp.sqrt(dt))
    d = 1.0 / u
    disc = jnp.exp(-rate * dt)
    p = (jnp.exp((rate - div) * dt) - d) / (u - d)
    p = jnp.clip(p, 0.0, 1.0)
    sign = 1.0 if is_call else -1.0

    j = jnp.arange(steps + 1)
    # Terminal underlying prices and payoffs at maturity (fixed-length buffer).
    terminal_prices = spot * u**j * d ** (steps - j)
    values = jnp.maximum(sign * (terminal_prices - strike), 0.0)

    def body(i, values):
        # Rolling back to layer m = steps - i, which has m + 1 live nodes (0..m).
        m = steps - i
        node_prices = spot * u**j * d ** (m - j)  # valid for j <= m; tail is unused
        # cont[j] = disc * (p * values[j+1] + (1-p) * values[j]); roll gives values[j+1].
        up = jnp.roll(values, -1)
        cont = disc * (p * up + (1.0 - p) * values)
        exercise = jnp.maximum(sign * (node_prices - strike), 0.0)
        return jnp.maximum(cont, exercise)

    values = jax.lax.fori_loop(1, steps + 1, body, values)
    return values[0]

Greeks

greeks

Option Greeks via JAX automatic differentiation.

Rather than re-deriving closed-form Greek formulas (and risking them drifting out of sync with the pricer), we differentiate :func:black_scholes_price directly:

  • delta = d price / d spot
  • gamma = d^2 price / d spot^2
  • vega = d price / d vol
  • theta = -d price / d ttm (per-year; divide by 365 for per-calendar-day)
  • rho = d price / d rate

All are vmap-friendly, so :func:chain_greeks returns every Greek across a whole option chain in one call.

delta

delta(
    spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True
) -> Array

Delta: sensitivity of price to the underlying spot.

Source code in src/jaxfolio/options/greeks.py
def delta(spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True) -> Array:
    """Delta: sensitivity of price to the underlying spot."""
    f = lambda s: _price(s, strike, ttm, vol, rate, div, is_call)
    return jax.grad(f)(jnp.asarray(float(spot)))

gamma

gamma(
    spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True
) -> Array

Gamma: second derivative of price w.r.t. spot (same for calls and puts).

Source code in src/jaxfolio/options/greeks.py
def gamma(spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True) -> Array:
    """Gamma: second derivative of price w.r.t. spot (same for calls and puts)."""
    f = lambda s: _price(s, strike, ttm, vol, rate, div, is_call)
    return jax.grad(jax.grad(f))(jnp.asarray(float(spot)))

vega

vega(
    spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True
) -> Array

Vega: sensitivity of price to volatility (per 1.00 change in vol).

Source code in src/jaxfolio/options/greeks.py
def vega(spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True) -> Array:
    """Vega: sensitivity of price to volatility (per 1.00 change in vol)."""
    f = lambda v: _price(spot, strike, ttm, v, rate, div, is_call)
    return jax.grad(f)(jnp.asarray(float(vol)))

theta

theta(
    spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True
) -> Array

Theta: time decay (per year). Negative for most long options.

Source code in src/jaxfolio/options/greeks.py
def theta(spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True) -> Array:
    """Theta: time decay (per year). Negative for most long options."""
    f = lambda t: _price(spot, strike, t, vol, rate, div, is_call)
    return -jax.grad(f)(jnp.asarray(float(ttm)))

rho

rho(
    spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True
) -> Array

Rho: sensitivity of price to the risk-free rate.

Source code in src/jaxfolio/options/greeks.py
def rho(spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True) -> Array:
    """Rho: sensitivity of price to the risk-free rate."""
    f = lambda r: _price(spot, strike, ttm, vol, r, div, is_call)
    return jax.grad(f)(jnp.asarray(float(rate)))

all_greeks

all_greeks(
    spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True
) -> dict[str, float]

Return every first/second-order Greek for a single option as a dict.

Source code in src/jaxfolio/options/greeks.py
def all_greeks(spot, strike, ttm, vol, rate=0.0, div=0.0, is_call=True) -> dict[str, float]:
    """Return every first/second-order Greek for a single option as a dict."""
    return {
        "price": float(_price(spot, strike, ttm, vol, rate, div, is_call)),
        "delta": float(delta(spot, strike, ttm, vol, rate, div, is_call)),
        "gamma": float(gamma(spot, strike, ttm, vol, rate, div, is_call)),
        "vega": float(vega(spot, strike, ttm, vol, rate, div, is_call)),
        "theta": float(theta(spot, strike, ttm, vol, rate, div, is_call)),
        "rho": float(rho(spot, strike, ttm, vol, rate, div, is_call)),
    }

chain_greeks

chain_greeks(
    spot: float,
    strikes: Array,
    ttms: Array,
    vols: Array,
    rate: float = 0.0,
    div: float = 0.0,
    is_call: bool = True,
) -> dict[str, Array]

Vectorized Greeks across a chain of (strike, ttm, vol) triples.

Returns a dict of arrays (one entry per Greek), each aligned with the input chain. Built from vmap over the single-option autodiff Greeks.

Source code in src/jaxfolio/options/greeks.py
def chain_greeks(
    spot: float,
    strikes: Array,
    ttms: Array,
    vols: Array,
    rate: float = 0.0,
    div: float = 0.0,
    is_call: bool = True,
) -> dict[str, Array]:
    """Vectorized Greeks across a chain of (strike, ttm, vol) triples.

    Returns a dict of arrays (one entry per Greek), each aligned with the input
    chain. Built from ``vmap`` over the single-option autodiff Greeks.
    """
    strikes = jnp.asarray(strikes, dtype=float)
    ttms = jnp.broadcast_to(jnp.asarray(ttms, dtype=float), strikes.shape)
    vols = jnp.broadcast_to(jnp.asarray(vols, dtype=float), strikes.shape)

    # argnums: 0=spot, 2=ttm, 3=vol, 4=rate.
    price_fn = lambda s, k, t, v, r: _price(s, k, t, v, r, div, is_call)

    d_delta = jax.vmap(lambda k, t, v: jax.grad(price_fn, argnums=0)(spot, k, t, v, rate))
    d_gamma = jax.vmap(
        lambda k, t, v: jax.grad(jax.grad(price_fn, argnums=0), argnums=0)(spot, k, t, v, rate)
    )
    d_vega = jax.vmap(lambda k, t, v: jax.grad(price_fn, argnums=3)(spot, k, t, v, rate))
    d_theta = jax.vmap(lambda k, t, v: -jax.grad(price_fn, argnums=2)(spot, k, t, v, rate))
    d_rho = jax.vmap(lambda k, t, v: jax.grad(price_fn, argnums=4)(spot, k, t, v, rate))
    d_price = jax.vmap(lambda k, t, v: price_fn(spot, k, t, v, rate))

    return {
        "price": d_price(strikes, ttms, vols),
        "delta": d_delta(strikes, ttms, vols),
        "gamma": d_gamma(strikes, ttms, vols),
        "vega": d_vega(strikes, ttms, vols),
        "theta": d_theta(strikes, ttms, vols),
        "rho": d_rho(strikes, ttms, vols),
    }

Strategies

strategies

Multi-leg options strategies: payoffs, P&L, and Greeks.

An :class:OptionLeg describes a single position (call/put, long/short, strike, expiry, quantity). An :class:OptionStrategy is a collection of legs (optionally plus a stock leg) with helpers to evaluate payoff at expiry, mark-to-model P&L at a horizon, net Greeks, and the break-even points. A library of preset constructors builds the common structures (covered call, collar, spreads, straddle, iron condor, butterfly, calendar).

OptionLeg dataclass

OptionLeg(
    kind: str,
    strike: float,
    quantity: float = 1.0,
    expiry: float = 0.25,
    premium: float | None = None,
)

A single option position.

Attributes:

Name Type Description
kind str

"call" or "put".

strike float

Strike price.

quantity float

Signed contract count — positive = long, negative = short.

expiry float

Time to expiry in years (used for mark-to-model and Greeks).

premium float | None

Premium paid (long) or received (short) per contract. If None it is filled from the Black-Scholes price at construction time by the presets.

intrinsic
intrinsic(spot: ndarray) -> ndarray

Intrinsic value of the option at spot (per contract, unsigned).

Source code in src/jaxfolio/options/strategies.py
def intrinsic(self, spot: np.ndarray) -> np.ndarray:
    """Intrinsic value of the option at ``spot`` (per contract, unsigned)."""
    if self.is_call:
        return np.maximum(spot - self.strike, 0.0)
    return np.maximum(self.strike - spot, 0.0)
payoff_at_expiry
payoff_at_expiry(spot: ndarray) -> ndarray

Signed P&L of this leg at expiry, net of premium.

Source code in src/jaxfolio/options/strategies.py
def payoff_at_expiry(self, spot: np.ndarray) -> np.ndarray:
    """Signed P&L of this leg at expiry, net of premium."""
    prem = self.premium if self.premium is not None else 0.0
    return self.quantity * (self.intrinsic(spot) - prem)

StockLeg dataclass

StockLeg(quantity: float = 1.0, entry_price: float = 100.0)

A linear underlying position held alongside options (e.g. covered call).

OptionStrategy dataclass

OptionStrategy(
    name: str,
    legs: list[OptionLeg] = list(),
    stock: StockLeg | None = None,
)

A named collection of option (and optional stock) legs.

payoff_at_expiry
payoff_at_expiry(spot: ndarray) -> ndarray

Total strategy P&L at expiry across a grid of terminal spot prices.

Source code in src/jaxfolio/options/strategies.py
def payoff_at_expiry(self, spot: np.ndarray) -> np.ndarray:
    """Total strategy P&L at expiry across a grid of terminal spot prices."""
    spot = np.asarray(spot, dtype=float)
    total = np.zeros_like(spot)
    for leg in self.legs:
        total = total + leg.payoff_at_expiry(spot)
    if self.stock is not None:
        total = total + self.stock.payoff_at_expiry(spot)
    return total
net_premium
net_premium() -> float

Net premium: negative = net debit (paid), positive = net credit.

Source code in src/jaxfolio/options/strategies.py
def net_premium(self) -> float:
    """Net premium: negative = net debit (paid), positive = net credit."""
    return float(-sum(leg.quantity * (leg.premium or 0.0) for leg in self.legs))
value_at
value_at(
    spot: float,
    vol: float,
    rate: float = 0.0,
    div: float = 0.0,
    ttm_shift: float = 0.0,
) -> float

Mark-to-model value of all option legs at a horizon.

ttm_shift is subtracted from each leg's expiry to advance time (e.g. ttm_shift=1/12 marks the book one month forward).

Source code in src/jaxfolio/options/strategies.py
def value_at(
    self, spot: float, vol: float, rate: float = 0.0, div: float = 0.0, ttm_shift: float = 0.0
) -> float:
    """Mark-to-model value of all option legs at a horizon.

    ``ttm_shift`` is subtracted from each leg's expiry to advance time (e.g.
    ``ttm_shift=1/12`` marks the book one month forward).
    """
    total = 0.0
    for leg in self.legs:
        t = max(leg.expiry - ttm_shift, 1e-6)
        price = float(black_scholes_price(spot, leg.strike, t, vol, rate, div, leg.is_call))
        total += leg.quantity * price
    if self.stock is not None:
        total += self.stock.quantity * spot
    return total
pnl_at
pnl_at(
    spot: float,
    vol: float,
    rate: float = 0.0,
    div: float = 0.0,
    ttm_shift: float = 0.0,
) -> float

Mark-to-model P&L relative to the entry cost of the option legs.

Source code in src/jaxfolio/options/strategies.py
def pnl_at(
    self, spot: float, vol: float, rate: float = 0.0, div: float = 0.0, ttm_shift: float = 0.0
) -> float:
    """Mark-to-model P&L relative to the entry cost of the option legs."""
    entry = sum(leg.quantity * (leg.premium or 0.0) for leg in self.legs)
    if self.stock is not None:
        entry += self.stock.quantity * self.stock.entry_price
    return self.value_at(spot, vol, rate, div, ttm_shift) - entry
greeks
greeks(
    spot: float,
    vol: float,
    rate: float = 0.0,
    div: float = 0.0,
) -> dict

Net position Greeks (sum of per-leg Greeks weighted by quantity).

Source code in src/jaxfolio/options/strategies.py
def greeks(self, spot: float, vol: float, rate: float = 0.0, div: float = 0.0) -> dict:
    """Net position Greeks (sum of per-leg Greeks weighted by quantity)."""
    agg = {"delta": 0.0, "gamma": 0.0, "vega": 0.0, "theta": 0.0, "rho": 0.0}
    for leg in self.legs:
        g = all_greeks(spot, leg.strike, leg.expiry, vol, rate, div, leg.is_call)
        for k in agg:
            agg[k] += leg.quantity * g[k]
    if self.stock is not None:
        agg["delta"] += self.stock.quantity  # stock has unit delta
    return agg
break_evens
break_evens(
    spot_grid: ndarray | None = None,
) -> list[float]

Approximate break-even spot prices (payoff sign changes).

Source code in src/jaxfolio/options/strategies.py
def break_evens(self, spot_grid: np.ndarray | None = None) -> list[float]:
    """Approximate break-even spot prices (payoff sign changes)."""
    if spot_grid is None:
        strikes = [leg.strike for leg in self.legs]
        lo, hi = 0.5 * min(strikes), 1.5 * max(strikes)
        spot_grid = np.linspace(lo, hi, 2000)
    pnl = self.payoff_at_expiry(spot_grid)
    sign = np.sign(pnl)
    crossings = np.where(np.diff(sign) != 0)[0]
    # Linear interpolation at each zero crossing.
    bes = []
    for i in crossings:
        x0, x1 = spot_grid[i], spot_grid[i + 1]
        y0, y1 = pnl[i], pnl[i + 1]
        if y1 != y0:
            bes.append(float(x0 - y0 * (x1 - x0) / (y1 - y0)))
    return bes

covered_call

covered_call(
    spot: float,
    strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long stock + short call: income overlay that caps upside.

Source code in src/jaxfolio/options/strategies.py
def covered_call(
    spot: float, strike: float, *, expiry: float = 0.25, vol: float = 0.25, rate: float = 0.0
) -> OptionStrategy:
    """Long stock + short call: income overlay that caps upside."""
    leg = _priced_leg("call", strike, -1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Covered Call", [leg], StockLeg(1.0, spot))

protective_put

protective_put(
    spot: float,
    strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long stock + long put: downside insurance.

Source code in src/jaxfolio/options/strategies.py
def protective_put(
    spot: float, strike: float, *, expiry: float = 0.25, vol: float = 0.25, rate: float = 0.0
) -> OptionStrategy:
    """Long stock + long put: downside insurance."""
    leg = _priced_leg("put", strike, 1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Protective Put", [leg], StockLeg(1.0, spot))

collar

collar(
    spot: float,
    put_strike: float,
    call_strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long stock + long put + short call: bounded payoff, often near-zero cost.

Source code in src/jaxfolio/options/strategies.py
def collar(
    spot: float,
    put_strike: float,
    call_strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Long stock + long put + short call: bounded payoff, often near-zero cost."""
    put = _priced_leg("put", put_strike, 1.0, expiry, spot, vol, rate, 0.0)
    call = _priced_leg("call", call_strike, -1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Collar", [put, call], StockLeg(1.0, spot))

bull_call_spread

bull_call_spread(
    spot: float,
    lower: float,
    upper: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long lower-strike call + short upper-strike call: capped bullish debit.

Source code in src/jaxfolio/options/strategies.py
def bull_call_spread(
    spot: float,
    lower: float,
    upper: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Long lower-strike call + short upper-strike call: capped bullish debit."""
    long_c = _priced_leg("call", lower, 1.0, expiry, spot, vol, rate, 0.0)
    short_c = _priced_leg("call", upper, -1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Bull Call Spread", [long_c, short_c])

bear_put_spread

bear_put_spread(
    spot: float,
    upper: float,
    lower: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long upper-strike put + short lower-strike put: capped bearish debit.

Source code in src/jaxfolio/options/strategies.py
def bear_put_spread(
    spot: float,
    upper: float,
    lower: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Long upper-strike put + short lower-strike put: capped bearish debit."""
    long_p = _priced_leg("put", upper, 1.0, expiry, spot, vol, rate, 0.0)
    short_p = _priced_leg("put", lower, -1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Bear Put Spread", [long_p, short_p])

straddle

straddle(
    spot: float,
    strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long call + long put at the same strike: a bet on large moves either way.

Source code in src/jaxfolio/options/strategies.py
def straddle(
    spot: float, strike: float, *, expiry: float = 0.25, vol: float = 0.25, rate: float = 0.0
) -> OptionStrategy:
    """Long call + long put at the same strike: a bet on large moves either way."""
    call = _priced_leg("call", strike, 1.0, expiry, spot, vol, rate, 0.0)
    put = _priced_leg("put", strike, 1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Long Straddle", [call, put])

strangle

strangle(
    spot: float,
    put_strike: float,
    call_strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long OTM put + long OTM call: cheaper volatility bet than a straddle.

Source code in src/jaxfolio/options/strategies.py
def strangle(
    spot: float,
    put_strike: float,
    call_strike: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Long OTM put + long OTM call: cheaper volatility bet than a straddle."""
    put = _priced_leg("put", put_strike, 1.0, expiry, spot, vol, rate, 0.0)
    call = _priced_leg("call", call_strike, 1.0, expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Long Strangle", [put, call])

iron_condor

iron_condor(
    spot: float,
    put_long: float,
    put_short: float,
    call_short: float,
    call_long: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Short strangle wrapped in long protective wings: a range-bound credit trade.

Strikes must satisfy put_long < put_short < call_short < call_long.

Source code in src/jaxfolio/options/strategies.py
def iron_condor(
    spot: float,
    put_long: float,
    put_short: float,
    call_short: float,
    call_long: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Short strangle wrapped in long protective wings: a range-bound credit trade.

    Strikes must satisfy ``put_long < put_short < call_short < call_long``.
    """
    legs = [
        _priced_leg("put", put_long, 1.0, expiry, spot, vol, rate, 0.0),
        _priced_leg("put", put_short, -1.0, expiry, spot, vol, rate, 0.0),
        _priced_leg("call", call_short, -1.0, expiry, spot, vol, rate, 0.0),
        _priced_leg("call", call_long, 1.0, expiry, spot, vol, rate, 0.0),
    ]
    return OptionStrategy("Iron Condor", legs)

butterfly

butterfly(
    spot: float,
    lower: float,
    middle: float,
    upper: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Long butterfly with calls: peak payoff pinned at the middle strike.

Source code in src/jaxfolio/options/strategies.py
def butterfly(
    spot: float,
    lower: float,
    middle: float,
    upper: float,
    *,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Long butterfly with calls: peak payoff pinned at the middle strike."""
    legs = [
        _priced_leg("call", lower, 1.0, expiry, spot, vol, rate, 0.0),
        _priced_leg("call", middle, -2.0, expiry, spot, vol, rate, 0.0),
        _priced_leg("call", upper, 1.0, expiry, spot, vol, rate, 0.0),
    ]
    return OptionStrategy("Long Butterfly", legs)

calendar_spread

calendar_spread(
    spot: float,
    strike: float,
    *,
    near_expiry: float = 0.08,
    far_expiry: float = 0.33,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy

Short near-dated + long far-dated call at one strike: a theta/vega play.

Because the legs have different expiries, use :meth:OptionStrategy.pnl_at (mark-to-model) rather than expiry payoff to analyze this structure.

Source code in src/jaxfolio/options/strategies.py
def calendar_spread(
    spot: float,
    strike: float,
    *,
    near_expiry: float = 0.08,
    far_expiry: float = 0.33,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OptionStrategy:
    """Short near-dated + long far-dated call at one strike: a theta/vega play.

    Because the legs have different expiries, use :meth:`OptionStrategy.pnl_at`
    (mark-to-model) rather than expiry payoff to analyze this structure.
    """
    short_near = _priced_leg("call", strike, -1.0, near_expiry, spot, vol, rate, 0.0)
    long_far = _priced_leg("call", strike, 1.0, far_expiry, spot, vol, rate, 0.0)
    return OptionStrategy("Calendar Spread", [short_near, long_far])

Overlays

overlay

Options overlays on top of an optimized equity portfolio.

Bridges the optimizer output (:class:PortfolioResult) and the options layer: apply a covered-call or collar overlay to the largest holdings, aggregate the net Greeks of the resulting book, and produce a combined at-expiry payoff for the overlaid position. This is what makes the two halves of the library compose.

OverlayBook dataclass

OverlayBook(
    strategies: dict[str, OptionStrategy],
    weights: dict[str, float],
    spots: dict[str, float],
)

A basket of per-asset option strategies scaled by portfolio weights.

Attributes:

Name Type Description
strategies dict[str, OptionStrategy]

Mapping asset -> OptionStrategy for the overlaid holdings.

weights dict[str, float]

Portfolio weight of each overlaid asset (used to scale payoffs/Greeks).

spots dict[str, float]

Reference spot price per asset.

net_greeks
net_greeks(
    vol: float = 0.25, rate: float = 0.0
) -> dict[str, float]

Weighted net Greeks across the whole overlay book.

Source code in src/jaxfolio/options/overlay.py
def net_greeks(self, vol: float = 0.25, rate: float = 0.0) -> dict[str, float]:
    """Weighted net Greeks across the whole overlay book."""
    agg = {"delta": 0.0, "gamma": 0.0, "vega": 0.0, "theta": 0.0, "rho": 0.0}
    for asset, strat in self.strategies.items():
        w = self.weights[asset]
        g = strat.greeks(self.spots[asset], vol, rate)
        for k in agg:
            agg[k] += w * g[k]
    return agg
payoff_curve
payoff_curve(shock_grid: ndarray) -> ndarray

Weighted portfolio P&L across a grid of relative spot shocks.

shock_grid is a multiplier applied to every asset's spot (e.g. np.linspace(0.7, 1.3, 200) sweeps -30% to +30%). Returns the weight-scaled aggregate P&L of the overlaid holdings.

Source code in src/jaxfolio/options/overlay.py
def payoff_curve(self, shock_grid: np.ndarray) -> np.ndarray:
    """Weighted portfolio P&L across a grid of *relative* spot shocks.

    ``shock_grid`` is a multiplier applied to every asset's spot (e.g.
    ``np.linspace(0.7, 1.3, 200)`` sweeps -30% to +30%). Returns the
    weight-scaled aggregate P&L of the overlaid holdings.
    """
    total = np.zeros_like(shock_grid, dtype=float)
    for asset, strat in self.strategies.items():
        w = self.weights[asset]
        spots = self.spots[asset] * shock_grid
        total = total + w * strat.payoff_at_expiry(spots)
    return total

covered_call_overlay

covered_call_overlay(
    result: PortfolioResult,
    spots: dict[str, float],
    *,
    top_n: int = 5,
    moneyness: float = 1.05,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OverlayBook

Write covered calls on the top_n largest holdings.

moneyness sets the call strike relative to spot (1.05 = 5% OTM). Assets without a provided spot price are skipped.

Source code in src/jaxfolio/options/overlay.py
def covered_call_overlay(
    result: PortfolioResult,
    spots: dict[str, float],
    *,
    top_n: int = 5,
    moneyness: float = 1.05,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OverlayBook:
    """Write covered calls on the ``top_n`` largest holdings.

    ``moneyness`` sets the call strike relative to spot (``1.05`` = 5% OTM).
    Assets without a provided spot price are skipped.
    """
    holdings = result.top(top_n)
    strategies: dict[str, OptionStrategy] = {}
    weights: dict[str, float] = {}
    used_spots: dict[str, float] = {}
    for asset, w in holdings.items():
        if asset not in spots or w <= 0:
            continue
        s = spots[asset]
        strategies[asset] = covered_call(s, s * moneyness, expiry=expiry, vol=vol, rate=rate)
        weights[asset] = float(w)
        used_spots[asset] = s
    return OverlayBook(strategies, weights, used_spots)

collar_overlay

collar_overlay(
    result: PortfolioResult,
    spots: dict[str, float],
    *,
    top_n: int = 5,
    put_moneyness: float = 0.95,
    call_moneyness: float = 1.05,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OverlayBook

Wrap the top_n largest holdings in protective collars.

Bounds each holding's outcome between put_moneyness and call_moneyness times spot — capping both tail loss and upside.

Source code in src/jaxfolio/options/overlay.py
def collar_overlay(
    result: PortfolioResult,
    spots: dict[str, float],
    *,
    top_n: int = 5,
    put_moneyness: float = 0.95,
    call_moneyness: float = 1.05,
    expiry: float = 0.25,
    vol: float = 0.25,
    rate: float = 0.0,
) -> OverlayBook:
    """Wrap the ``top_n`` largest holdings in protective collars.

    Bounds each holding's outcome between ``put_moneyness`` and
    ``call_moneyness`` times spot — capping both tail loss and upside.
    """
    holdings = result.top(top_n)
    strategies: dict[str, OptionStrategy] = {}
    weights: dict[str, float] = {}
    used_spots: dict[str, float] = {}
    for asset, w in holdings.items():
        if asset not in spots or w <= 0:
            continue
        s = spots[asset]
        strategies[asset] = collar(
            s, s * put_moneyness, s * call_moneyness, expiry=expiry, vol=vol, rate=rate
        )
        weights[asset] = float(w)
        used_spots[asset] = s
    return OverlayBook(strategies, weights, used_spots)