UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

188 lines 13.3 kB
/** * # fair/surface — the liquid-strike implied-vol surface * * The vol read behind `@fair` is backed out of the **liquid** quotes only: a quote counts * iff it is **two-sided** (both `bid` and `ask` present) **and both sides are real**. A * one-sided or dark leg is the MM-pull fingerprint of a real move (ARCHITECTURE §4) — its * "mid" is a health signal, never a price, so it contributes no vol point. From the liquid * legs we build a sparse (strike → iv) curve and read it by **linear interpolation with flat * extrapolation** (a single point ⇒ a flat surface). * * ## The input-side quote-realness gate (ven.3) * A quote *side* can be **FABRICATED** — a venue/exchange placeholder, not a real willingness * to trade. `src/fair` enforces honesty on the **output** side (its `@fair` is intrinsic- * floored, receipt-gated, and a dark leg's mid is "a health signal, never a price"). This is * the mirror on the **input** side: {@link realSides} **DEMOTES a fabricated side to DARK** * (`null` — the model's existing unusable-side marker, bus/types.ts) **BEFORE** its price can * reach the mid the vol read is backed out of. A demoted side then behaves like any dark side * already does — one-sided ⇒ no two-sided mid ⇒ no vol point; **both** sides fabricated ⇒ the * whole leg is dark ⇒ no point (fail-closed to UNKNOWN, never a fabricated anchor). No second * dark concept is forked: demotion *is* `null`, and the exclusion is the "one-sided/dark legs * contribute no point" rule this surface already ships. * * ## Per strike: parity AGREEMENT, not a blind average (kestrel-ku99) * Back out an IV from each liquid leg's mid (call and/or put). At the PARITY-IMPLIED forward, * put-call parity forces a liquid call and put at the same strike to the same IV — so a call-IV * and put-IV that DISAGREE are evidence the inputs are inconsistent, not noise to be smoothed. * This surface therefore retains the call and put IVs separately and requires them to agree * within {@link PARITY_TOTAL_VOL_TOLERANCE} — a band in TOTAL-vol (`sigma*sqrt(tau)`), the only * quantity a price identifies at r = 0, so the guard's verdict is invariant to the tenor rather * than detonating as tau -> 0. On violation the strike contributes **NO point** (fail-closed) * rather than laundering the inconsistency into an authoritative-looking mean. Each point carries * its {@link IvPoint.dispersion} so a receipt can no longer read healthy while lying. With a * correct (parity-read) forward the two IVs agree to bisection precision, so this guard is the * fail-closed NET, not the magnitude fix — that is the forward's job. * * ## The forward is DERIVED, never assumed (ADR-0037 §2, kestrel-ukwz) * The vol read and the Black-76 price are both taken at a **forward**, and the forward is NOT the * underlying spot: dividends, cost-of-carry, borrow, and any clock-desync between the spot feed * and the option quotes all drive a wedge between them. Assuming `forward = spot` was the * DOMINANT error behind the live SPY-752 incident (`@fair` 28% above the ask): an ATM option has * delta ~0.5, so a 0.37 spot/forward gap is ~0.18 of price error on a 0.72 option. * * {@link impliedForward} solves the forward out of the option market itself — `F* = K* + (Cmid − * Pmid)` at the ATM-most strike where BOTH a call and a put are liquid (put-call parity, taken at * the strike whose parity read is least leveraged by vol error). This captures carry and dividends * for free (they are already priced into the quotes) and is immune to the spot/quote clock-desync * (the forward comes from the SAME quotes being priced, on their OWN clock). * * Pure functions only — no I/O, no clock (RUNTIME §0). */ import type { OptionQuote } from "../bus/types.ts"; /** One point of the surface: an implied vol attributed to a strike. */ export interface IvPoint { readonly strike: number; readonly iv: number; /** `|ivCall − ivPut|` at this strike — the parity disagreement the point was accepted under, in * IV units (0 when the two agree exactly). The GUARD is applied to this scaled into total-vol * space (`dispersion * sqrt(tau) <= ` {@link PARITY_TOTAL_VOL_TOLERANCE}); the receipt reports * the IV-space number because that is the human-legible one, and the tenor needed to convert it * is already on the receipt. **Absent** when only one side was liquid: with a single leg there * is no disagreement to observe, and reporting `0` would claim an agreement that was never * checked. */ readonly dispersion?: number; } /** * The parity-agreement band, in **TOTAL-VOL** units: `v = sigma * sqrt(tau)` — the standard * deviation of log-forward to expiry (kestrel-ku99 acceptance decision, settled on the integrated * T1+T2 base in Phase-2; see `docs/plans/2026-07-16-fair-redliff-artifact.md` for the calibration * evidence). * * ## Why total-vol and not an absolute IV band * Black-76 at r = 0 identifies **only** `v = sigma*sqrt(tau)` from a price — `sigma` is merely * `v / sqrt(tau)` (this is the same identity that makes tau cancel out of `@fair`'s VALUE: * `buildSurface` inverts sigma from the same book at the same tau it then prices with). Two * consequences follow, and together they rule out an IV-space band: * * 1. **An IV band is a tenor-dependent guard on a tenor-INVARIANT quantity.** Since * `|sigmaC - sigmaP| = |vC - vP| / sqrt(tau)`, the SAME price inconsistency reads as an * arbitrarily large IV disagreement as tau -> 0. Measured on the real corpus: one identical * book at 15:59 shows IV-dispersion p50 = 2.7e-2 at a 4-day tau and p50 = 2.0e+0 at a * session-close tau — a 76x swing from the tenor alone. An IV band of 0.02 therefore admits a * normal chain at 4DTE and rejects EVERY strike of the same chain near a 0DTE close, collapsing * the surface to empty exactly when the benchmark's 0DTE tapes need it most. * 2. **The harm the guard exists to bound is measured in v, not sigma.** Averaging a dispersed * pair injects `~dSigma/2` of surface error, which reaches `@fair` multiplied by vega * (`= F*sqrt(tau)*phi(d1)`), i.e. the resulting price error is proportional to * `dSigma*sqrt(tau) = dV`. A v band bounds the actual @fair damage uniformly across tenor; an * IV band does not. * * The v-space read is EXACTLY tau-invariant, not approximately: measured across the corpus at tau * ratios up to 5761x, `max |vDisp(tauTrue) - vDisp(tauBroken)| = 5.6e-14` (float epsilon). This is * why the band survives the wcnd tau fix without recalibration — an IV band would not. * * ## The value: 1e-3 * Two INDEPENDENT lines of evidence land on the same place. * * (1) SEPARATION QUALITY, measured on the real corpus. A well-shaped band should admit the pairs * whose model-free price-space parity residual (`|Cmid - Pmid - (F - K)|`) is small and reject the * large ones — i.e. `worstResidualAdmitted < bestResidualRejected`. An INVERSION of that ordering * means the guard is anti-correlated with the inconsistency it exists to detect. Measured on the * 4DTE SPY tape, `1e-3` separates monotonically at both sampled instants (12:00: admits <= $0.180, * rejects >= $0.245; 15:59: admits <= $0.160, rejects >= $0.215) — while `2e-3` INVERTS at 12:00 * (admits $0.340, rejects $0.310), and an IV band of `0.02` inverts badly on the real 0DTE tape at * 10:59: it REJECTS a pair whose price residual is $0.005 (pristine — its only sin is a vega of * 0.18) while ADMITTING one at $0.085. * * The cost is disclosed: `1e-3` admits FEWER strikes than a `0.02` IV band on the 4DTE tape (5/23 * vs 9/23 at 12:00). Those 18 rejected strikes are not "legitimate points spuriously dropped" — * their residuals run $0.245..$0.90 and are the American early-exercise structure described below. * The surviving 4-5 are the near-ATM window with the cleanest parity, which is where armed * documents price and from which the surface interpolates. `@fair` still builds on every corpus * tape. * * An absolute band (not a book-width-scaled one) keeps the guard independent of quote width — a * wide book is a freshness signal, not a licence to disagree about parity. * * NOTE (open, for the fleet): a legitimate structured parity residual DOES exist on this corpus — * SPY/QQQ options are AMERICAN, and an ITM call ahead of an ex-dividend carries an early-exercise * premium that no European put-call parity can absorb. It is visible as a smooth, monotone * residual reaching $0.90 at 10 strikes ITM on the 2024-06-17 tape (whose 2024-06-21 expiry IS a * SPY ex-div date), where `F(K) = K + C - P` converges on SPOT exactly — the signature of a call * priced for immediate exercise. This band drops those strikes. That is arguably CORRECT (a * European inversion of an American early-exercise premium is not a vol point), but it is a * disclosed property, not an accident. */ export declare const PARITY_TOTAL_VOL_TOLERANCE = 0.001; /** Where the forward `@fair` priced at came from. `"parity"` is the derived, trusted read; * `"spot"` is the KNOWN-DEGRADED fallback ADR-0037 §2 mandates when no strike qualifies — * never null (that would kill `@fair` on ~92% of real strikes), never a novel guess, and * never silent: it always rides with a machine-readable {@link ImpliedForward.degraded}. */ export type ForwardSource = "parity" | "spot"; /** The widest band a parity-implied forward may sit from spot before it is rejected as * not-a-forward (ADR-0037 §5: a violent disagreement is a SIGNAL, not a price — refuse, never * project). Deliberately loose: real carry/dividend/desync wedges are basis points, so anything * past ±10% is a broken quote, not a forward. */ export declare const FORWARD_NO_ARB_BAND = 0.1; /** The forward `@fair` prices at, with the receipt material proving where it came from. */ export interface ImpliedForward { /** The forward used for BOTH the IV inversion and the Black-76 price. */ readonly forward: number; /** `"parity"` (derived) or `"spot"` (known-degraded fallback). */ readonly source: ForwardSource; /** The strike parity was read at. Present iff `source === "parity"`. */ readonly strike?: number; /** `forward − spot`: how far the OPTION market's own forward sits from the spot feed — the * carry + dividend + clock-desync wedge, made observable. Present iff `source === "parity"`. */ readonly residual?: number; /** The machine-readable reason the derivation failed. Present iff `source === "spot"` — this * is what makes the degraded fallback loud instead of silent. */ readonly degraded?: string; } /** * Derive the forward from the option market by put-call parity: `F* = K* + (Cmid − Pmid)` at the * ATM-most strike where a call AND a put are both liquid (ADR-0037 §2). `spot` is used only as * the provisional anchor for the input-side realness gate ({@link realSides}, whose no-arb bounds * need *an* underlier context) and as the ATM-ness yardstick — never as the answer. * * **Fail-closed, per ADR-0037 §2/§5:** when no strike carries both sides liquid, or the solved * forward is non-finite/non-positive or outside the wide no-arb band {@link FORWARD_NO_ARB_BAND}, * the result is the KNOWN-DEGRADED `{ forward: spot, source: "spot", degraded: <reason> }` — the * caller must carry that reason onto the receipt and annotate any price built on it. It is never * a silent spot substitution, and never a guess. * * Deterministic and leg-order-invariant: the ATM-most strike is chosen by `|K − spot|` with ties * broken to the LOWER strike, so permuting `quotes` cannot change the answer. */ export declare function impliedForward(quotes: readonly OptionQuote[], spot: number): ImpliedForward; /** * Build the sparse implied-vol surface from a chain slice. Only two-sided quotes that yield a * positive-vol back-out contribute; strikes are returned **sorted ascending**, each carrying * the average IV of its usable legs. `forward` is the DERIVED forward ({@link impliedForward}) — * not the spot; `tauYears` the shared time-to-expiry of the slice. * * A strike carrying BOTH a liquid call and put must satisfy put-call parity within * {@link PARITY_TOTAL_VOL_TOLERANCE} (compared in total-vol space, so the verdict does not move * with `tauYears`) or it contributes NO point (fail-closed, kestrel-ku99). The accepted point's * IV is the mean of the two, which — because the two are keyed by `right` and combined in a fixed * (call, put) order — is a function of the SET of legs, never of the order they arrived in (the * old running sum/count was order-dependent; RUNTIME §0). */ export declare function buildSurface(quotes: readonly OptionQuote[], forward: number, tauYears: number): IvPoint[]; /** * Read the surface at `strike` by linear interpolation, flat-extrapolating past both ends. * A single point ⇒ that point's IV everywhere. Assumes `points` is non-empty and sorted * ascending (as {@link buildSurface} returns); returns `null` only for an empty surface. */ export declare function interpIv(points: readonly IvPoint[], strike: number): number | null; //# sourceMappingURL=surface.d.ts.map