Crack the Sky

pricing

Calculators, plus two derived objects: the ATM term structure and the SVI vol surface. Pass S, K, T, r, q (or F) and sigma explicitly to the calculators.

GreeksConventions
Defaults: vega per 1.00 vol, theta per year of calendar time, spot delta/gamma. Dual (∂/∂K) is always also populated. Tests that read a Greek must name the convention.
Named catalog
price; 1st delta, dual_delta, vega, theta, rho, rho_dividend; 2nd gamma, dual_gamma, vanna, volga, charm, veta, vera; 3rd speed, zomma, color, ultima; elasticity.
European vs American
Engine protocol: EuropeanBSM closed-form; AmericanCRR tree with bump-and-revalue Greeks. Style comes from the contract (SPY American, SPX/SPXW European), but the engine is what an IV invert follows: a chain downgrades far-from-the-money SPY strikes to EuropeanBSM, and a row inverted on one engine and repriced on the other carries the early-exercise premium as residual.
IV invert
One inverter per exercise style, matching the two engines. implied_vol inverts European BSM: Newton with bounds, Brent fallback, closed-form vega for the Newton step. implied_vol_american inverts the American CRR tree with Brent alone — the tree has no closed-form vega, and a central-difference one costs two extra trees per Newton step, more evaluations than the bracketed search it was meant to accelerate. Bounds differ with the style, so each has its own: discounted_bounds and american_bounds (undiscounted intrinsic below, and K rather than Ke-rT above, for a put). A price outside them → ValueError. Neither returns NaN.
The tree is part of the answer
An American sigma is the value that reprices the contract on a given tree, so the inverter takes the engine's own n_steps; invert an 801-step price on a 401-step tree and sigma moves by more than the solver tolerance. Accuracy is bounded by discretisation, not by xtol. The search also starts above |r-q|√dt, below which the CRR tree is not arbitrage-free and refuses to price at all. implied_vol_american returns 0.0 for a price with no time value — a deep ITM American put is exercised now and is worth K-S at every sigma, which is a property of the contract, not a solver failure.
ATM term structure
pricing.term_structure reduces option_day_bars to one row per (date, root, expiry): parity forward, ATM strike, both leg IVs. Same chain the surface fits. Vol time is the hybrid (DEFAULT_DAYCOUNT): sessions/252 inside the calendar horizon, ACT/365 past it; each row stamps daycount as bus/252 or act/365. Money time (the rate tenor) stays ACT/365. Scheduled Tue–Sat 12:00 ET; archive rebuild via scripts/build_term_structure.py. A stale or crossed ATM print is written null, never as zero vol.
SVI surface
pricing.surface fits raw SVI (Gatheral), w(k) = a + b(ρ(k−m) + √((k−m)² + σ²)) over k = ln(K/F), per (date, root, expiry) on own-IV OTM strikes from option_day_bars — calls above the parity forward, puts below, same chain and same F as the ATM curve. SPX/SPXW only: SPY strikes invert on the American tree (implied_vol_american) and this fit path is European end to end — an SPY smile is follow-up, not a flag. OTM closes at or below MIN_TIME_VALUE_FRAC of the forward are dropped before inversion (an OTM close is time value; a near-worthless one is a tick-dominated stale print, and day bars carry no trade-age signal — that filter lives on snapshots via last_trade_asof_ns). Box bounds plus the non-negative-total-variance domain constraint; deterministic data-derived seed; MAX_NFEV raised past scipy's default because real chains exhaust it long before the 1e-12 tolerances are met. The arbitrage guards are enforced inside the fit: a slice whose unconstrained optimum violates butterfly (g(k) ≥ 0 on the padded grid) or dips below its predecessor's total variance (calendar, chained in expiry order on the guard's union grid) is refit by SLSQP under both as hard constraints, from feasibility-projected seeds, and the repair is kept only while it still explains the slice (REPAIR_MAX_REL_RMS) — past that the arbitrage is in the data and SurfaceArbitrageError raises from the unchanged post-fit guards. Surface.vol(K, T) interpolates linearly in total variance between bracketing expiries, flat outside, exact on a fitted expiry. Scheduled Tue–Sat 12:15 ET as massive-surface (after the ATM curve at 12:00, before coverage_audit at 12:30). CLI: python -m pricing.surface --date … --underlying SPXW; archive: scripts/build_surface.py → vol_surface. Read back with load_surface / Surface.from_rows — do not refit from day bars. No VIX surface: those options are on the VX future, a different modelling question. BLAS threads are pinned to 1 by scripts/cronjob.sh and scripts/build_surface.py: the fit is a constrained least-squares solve, and at 8 threads instead of 1 it lands elsewhere in an equally good basin — 397 of 720 values moved on 2026-09-04, svi_rho by up to 2.24e-03 relative, with rms_error moving less than 1e-09. Parameters that depend on how busy the box was cannot be diffed across a rebuild or reproduced by a backtest. The ATM curve is unaffected (scalar Brent, no BLAS).