edgekit

edgekit.options

Black-Scholes — European option pricing, the full first-order Greeks, and an implied-volatility solver, with a continuous dividend / carry yield q throughout. Enough to price, hedge, and back out vol without pulling in a derivatives library.

What's inside. The two normal-distribution primitives (norm_cdf, norm_pdf) the formulas are built on, the price itself (bs_price), the Greek bundle (bs_greeks), and a Newton/bisection implied-vol root-finder (implied_vol). Calls and puts are selected with kind="call" / "put"; the dividend yield q also serves as the foreign rate for FX or the convenience-adjusted carry for a future.

!Know your Greek units before you size a hedge

The Greeks are reported in raw Black-Scholes units, not the rescaled forms desks often quote:

  • delta — change in option value per $1 move in spot.
  • gamma — change in delta per $1 move in spot (i.e. per $1²).
  • vega — per 1.00 (100 vol-points) change in sigma; divide by 100 for the per-1%-vol figure traders usually quote.
  • theta — per year; divide by 365 for per-calendar-day decay.
  • rho — per 1.00 (100 bp × 100) change in the rate.

Distribution primitives#

norm_cdf#

Standard-normal cumulative distribution function — the N(·) in the Black-Scholes price. Vectorised over array input.

norm_cdf(x) -> float | np.ndarray
  • x — a scalar or array of standardised values.

Returns: the cumulative probability P(Z ≤ x), same shape as the input.

norm_pdf#

Standard-normal probability density function — the n(·) that appears in gamma and vega.

norm_pdf(x) -> float | np.ndarray
  • x — a scalar or array of standardised values.

Returns: the density at x, same shape as the input.

Pricing#

bs_price#

The Black-Scholes-Merton price of a European call or put with continuous yield q. The core valuation everything else in the module supports.

bs_price(S, K, t, r, sigma, kind="call", q=0.0) -> float
ParamTypeDefaultMeaning
SfloatSpot price of the underlying.
KfloatStrike price.
tfloatTime to expiry in years.
rfloatContinuously-compounded risk-free rate.
sigmafloatVolatility (annualised, e.g. 0.20 for 20%).
kindstr"call""call" or "put".
qfloat0.0Continuous dividend / carry yield.

Returns: a float option premium in the underlying's currency.

import edgekit as ek
ek.options.bs_price(S=100, K=105, t=0.5, r=0.03, sigma=0.20, kind="call")

Greeks & implied vol#

bs_greeks#

All five first-order Greeks in one call — delta, gamma, vega, theta, rho — for the same option as bs_price. Read the units callout above before you size a hedge off these.

bs_greeks(S, K, t, r, sigma, kind="call", q=0.0) -> dict
ParamTypeDefaultMeaning
SfloatSpot price.
KfloatStrike.
tfloatTime to expiry (years).
rfloatRisk-free rate.
sigmafloatVolatility.
kindstr"call""call" or "put".
qfloat0.0Dividend / carry yield.

Returns: a dict with keys "delta" (per $1 spot), "gamma" (per $1² spot), "vega" (per 1.00 vol — ÷100 for per-1%), "theta" (per year — ÷365 for per-day), and "rho" (per 1.00 rate).

g = ek.options.bs_greeks(S=100, K=100, t=0.25, r=0.03, sigma=0.20)
g["delta"]
g["vega"] / 100     # per 1% vol move
g["theta"] / 365    # per calendar day

implied_vol#

Back out the volatility that reprices a European option to an observed price — a Newton root-find with a bisection fallback. The inverse of bs_price: feed it a market premium, get the market's vol view.

implied_vol(price, S, K, t, r, kind="call", q=0.0, tol=1e-6, max_iter=100) -> float
ParamTypeDefaultMeaning
pricefloatObserved option premium to match.
SfloatSpot price.
KfloatStrike.
tfloatTime to expiry (years).
rfloatRisk-free rate.
kindstr"call""call" or "put".
qfloat0.0Dividend / carry yield.
tolfloat1e-6Price convergence tolerance.
max_iterint100Maximum solver iterations.

Returns: a float implied volatility. Returns nan when price is below intrinsic value (no real vol reproduces it).

iv = ek.options.implied_vol(price=6.20, S=100, K=100, t=0.25, r=0.03, kind="call")
# nan if price < max(S - K, 0) discounted (below intrinsic)

Trees, jumps & the surface#

Beyond flat-vol Black-Scholes: binomial_tree prices American exercise, merton_price adds jumps, svi_fit / svi_iv parameterise a smile, delta_hedge_sim shows what hedging at the wrong vol actually costs, and variance_swap_strike turns a strip of quotes into the fair variance level.

binomial_tree#

A Cox-Ross-Rubinstein binomial tree — the workhorse for American options, where early exercise makes the closed form unavailable. With american=False it converges to bs_price as n grows, which is also the standard sanity check on the tree itself.

binomial_tree(s, k, t, r, sigma, n=500, kind="call", american=True) -> float
ParamTypeDefaultMeaning
sfloatSpot price.
kfloatStrike.
tfloatTime to expiry (years).
rfloatRisk-free rate.
sigmafloatVolatility.
nint500Tree steps (error shrinks ~1/n).
kindstr"call""call" or "put".
americanboolTrueAllow early exercise at every node.

Returns: a float option premium.

import edgekit as ek
am = ek.options.binomial_tree(s=100, k=105, t=0.5, r=0.03, sigma=0.20, kind="put")
eu = ek.options.binomial_tree(s=100, k=105, t=0.5, r=0.03, sigma=0.20, kind="put", american=False)
am - eu   # the early-exercise premium

merton_price#

Merton's jump-diffusion price — a Poisson-weighted series of Black-Scholes prices, each conditioned on a number of jumps. Jumps are what a diffusion cannot fake: they produce the short-dated smile and price the crash risk flat-vol BS ignores. Truncated at n_terms terms (the Poisson weights die fast).

merton_price(s, k, t, r, sigma, lam, jump_mu, jump_sigma, kind="call", n_terms=40) -> float
ParamTypeDefaultMeaning
sfloatSpot price.
kfloatStrike.
tfloatTime to expiry (years).
rfloatRisk-free rate.
sigmafloatDiffusive volatility (between jumps).
lamfloatJump intensity (expected jumps per year).
jump_mufloatMean log-jump size (negative = crash-shaped).
jump_sigmafloatLog-jump size volatility.
kindstr"call""call" or "put".
n_termsint40Poisson series truncation.

Returns: a float option premium (→ bs_price as lam → 0).

ek.options.merton_price(s=100, k=90, t=0.25, r=0.03, sigma=0.15,
                        lam=0.5, jump_mu=-0.10, jump_sigma=0.15, kind="put")
# downside puts price well above flat-vol BS — that's the jump premium

svi_fit#

Fit the raw SVI (stochastic volatility inspired) parameterisation to an implied-vol smile — total variance w(k) = a + b·(rho·(k−m) + sqrt((k−m)² + sig²)) — via a built-in Nelder-Mead (pure numpy, no scipy). Five parameters give you a smooth, arbitrage-resistant curve through noisy strike quotes.

svi_fit(log_moneyness, iv, t) -> dict
ParamTypeDefaultMeaning
log_moneynessarraylog(K/F) per quote.
ivarrayImplied vol per quote (e.g. from implied_vol).
tfloatTime to expiry of the slice (years).

Returns: a dict of the raw-SVI parameters "a", "b", "rho", "m", "sig" — feed it straight to svi_iv.

params = ek.options.svi_fit(k_log, ivs, t=0.25)

svi_iv#

Evaluate a fitted SVI slice at any log-moneyness — the interpolator/extrapolator you quote off once svi_fit has parameterised the smile.

svi_iv(params, log_moneyness, t) -> np.ndarray
  • params — the dict returned by svi_fit.
  • log_moneyness — the log(K/F) grid to evaluate at.
  • t — the slice expiry (years).

Returns: a numpy array of implied vols on the grid.

import numpy as np
grid = np.linspace(-0.3, 0.3, 61)
smile = ek.options.svi_iv(params, grid, t=0.25)   # the smooth fitted smile

delta_hedge_sim#

Simulate the P&L of a short option delta-hedged at sigma_hedge while the world actually moves at sigma_real — the cleanest demonstration that an option position is a volatilitybet: hedge at the true vol and the mean P&L is ~0 with residual discrete-hedging noise; sell at an implied above realized and the gap is your expected profit. Deterministic given a seeded rng.

delta_hedge_sim(s0, k, t, r, sigma_real, sigma_hedge, steps=52, n_paths=2000, kind="call", rng=None) -> dict
ParamTypeDefaultMeaning
s0floatInitial spot.
kfloatStrike.
tfloatExpiry (years).
rfloatRisk-free rate.
sigma_realfloatThe vol the paths are generated with (realized).
sigma_hedgefloatThe vol the BS delta is computed at (implied).
stepsint52Rehedge count over the option's life.
n_pathsint2000Monte-Carlo paths.
kindstr"call""call" or "put".
rngGenerator | NoneNonenumpy Generator; defaults to bootstrap_rng().

Returns: a dict with keys "pnl"(per-path P&L array), "mean", and "std".

sim = ek.options.delta_hedge_sim(s0=100, k=100, t=0.25, r=0.03,
                                 sigma_real=0.18, sigma_hedge=0.22)
sim["mean"]   # > 0: sold at 22 vol, world delivered 18 — the vol premium captured

variance_swap_strike#

The fair strike of a variance swap by discrete static replication — a weighted strip of OTM options across strikes replicates the log contract, whose price is the risk-neutral expected variance. This is the machinery behind a VIX-style index, in one call.

variance_swap_strike(strikes, ivs, s, t, r=0.0) -> float
ParamTypeDefaultMeaning
strikesarrayThe strike grid of the quote strip.
ivsarrayImplied vol at each strike.
sfloatSpot price.
tfloatExpiry (years).
rfloat0.0Risk-free rate.

Returns: a float — the fair variance strike K_var in vol² units (take sqrt for the vol-swap-style number).

kvar = ek.options.variance_swap_strike(strikes, ivs, s=100, t=30/365)
kvar ** 0.5   # the VIX-style vol level implied by the strip

See also#