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.
161 lines • 9.88 kB
TypeScript
/**
* # frame/options-analytics — the per-strike options surface projection (pane-library-spec §5)
*
* The **options-analytics projection**: the single context extension that turns the owner's GEX/IV
* hypothesis from unanswerable to measurable. It rides on {@link ../frame/types.ts MarketPane} as
* an optional `options` view and carries, per strike / expiry:
* - **NBBO bid / ask + mid** — OBSERVED (from the OPRA book), the two-sided liquid mid or `null`.
* - **implied vol** — a **MODEL** output, Black-Scholes (r = 0) back-out from the option mid +
* underlier spot + time-to-expiry `tau` + rate. Never observed; `null` when there is no
* positive-vol solution (a strike with no two-sided quote reads UNKNOWN, never a guessed IV).
* - **greeks** (delta, gamma, vega) — **MODEL** outputs from the same Black-Scholes read.
* - **open interest** — OBSERVED, from the OI sidecar (OPRA `statistics`), `null` when absent.
*
* ## The honesty split (CONTEXT.md — OBS vs MODEL)
* The projection delivers OBSERVED NBBO + OI and computes the MODEL (IV/greeks) at build time from
* that frozen input. IV and greeks are **computed, never sourced** — so every pane that renders one
* is a MODEL pane and MUST carry the Black-Scholes receipt: {@link OptionsAnalytics.method} names
* the method (Black-Scholes) and the rate / `tau` assumptions, the receipt *basis* every dependent
* pane cites. The construct-time honesty guard ({@link ../frame/types.ts makeField}) refuses a MODEL
* Field without its receipt + confidence.
*
* ## Purity + the T-5m guard (ADR-0009)
* The projection is a **pure function of the frozen frame input** — no wall clock, no RNG, no
* cursor. `tau` is computed from the **frozen session time at the cutoff sequence** (`cutoffTs`, the
* ts of the last event ≤ `throughSeq`), never from a wall clock and never with look-ahead: the
* surface folds only events at or before the cutoff (the T-5m no-look-ahead discipline). `asOfSeq`
* is the engine's monotonic ordinal, never a date. A stale/frozen underlier is flagged
* ({@link OptionsAnalytics.spotStale}) so a dependent pane can fail closed — a GEX read off a dead
* spot is a lie, not a number.
*/
import type { BusEvent } from "../bus/types.ts";
import type { Right } from "../bus/types.ts";
/** One strike/right leg of the options-analytics surface. NBBO + mid + OI are OBSERVED; IV +
* greeks are Black-Scholes MODEL outputs (`null` = UNKNOWN, never a guessed number). */
export interface OptionAnalyticsLeg {
readonly strike: number;
readonly right: Right;
/** Top-of-book bid; `null` ⇒ dark/absent side (OBS). */
readonly bid: number | null;
/** Top-of-book ask; `null` ⇒ dark/absent side (OBS). */
readonly ask: number | null;
/** The two-sided liquid mid (`null` unless BOTH sides are present and the mid is positive) (OBS). */
readonly mid: number | null;
/** Black-Scholes implied vol (MODEL); `null` when no positive-vol solution exists. */
readonly iv: number | null;
/** Black-Scholes delta (MODEL); `null` when IV is unknown. */
readonly delta: number | null;
/** Black-Scholes gamma per $1 of underlier (MODEL); `null` when IV is unknown. */
readonly gamma: number | null;
/** Black-Scholes vega per 1.00 vol (MODEL); `null` when IV is unknown. */
readonly vega: number | null;
/** Open interest for the leg (OBS, from the OI sidecar); `null` when the sidecar has no row. */
readonly oi: number | null;
}
/** One expiry's slice of the surface — its shared time-to-expiry `tau` and its legs (all strikes,
* both rights: the full-chain view GEX needs, not near-money-only). */
export interface OptionsExpiry {
/** The expiry label (a date `2024-03-05` or a tag). */
readonly expiry: string;
/** Year-fraction from the frozen cutoff to this expiry's close (`null` when the close is unknown);
* the shared `tau` the leg IV/greeks were computed under. Never a wall clock — a duration. */
readonly tauYears: number | null;
/** Whole days to expiry from the cutoff (a small integer, date-blind); `null` when unknown. */
readonly daysToExpiry: number | null;
/** Every leg quoted at/before the cutoff, sorted by strike then right (full chain). */
readonly legs: readonly OptionAnalyticsLeg[];
}
/**
* The options-analytics surface for one underlier at the frozen cutoff. `spot` is the OBSERVED
* underlier at `asOfSeq`; `spotStale` flags a frozen/stale underlier (a GEX read off it must fail
* closed to UNKNOWN). `method` is the MODEL receipt BASIS every dependent pane cites — it names the
* Black-Scholes method + the rate / `tau` assumptions the IV/greeks rest on.
*/
export interface OptionsAnalytics {
readonly underlier: string;
/** OBSERVED underlier spot at the cutoff; `null` when never observed. */
readonly spot: number | null;
/** The underlier is stale/frozen (age > the staleness backstop, or never observed): a dependent
* MODEL read (GEX) must taint to UNKNOWN rather than compute off a dead spot. */
readonly spotStale: boolean;
/** Age of the underlier quote at the cutoff, in whole seconds (`null` when never observed). */
readonly spotStaleSeconds: number | null;
/** The engine monotonic sequence at the cutoff (an ordinal, NEVER a wall clock / date). */
readonly asOfSeq: number;
/** The per-contract multiplier the greeks/OI are scaled by (e.g. 100 for SPY options). */
readonly multiplier: number;
/** The risk-free rate assumption (Kestrel runs the surface at r = 0). */
readonly rate: number;
/** The MODEL receipt basis: the method + the rate/`tau` assumptions IV & greeks rest on. */
readonly method: string;
/** Front-first expiries (each a full-chain slice). */
readonly expiries: readonly OptionsExpiry[];
}
/** A reduced NBBO surface + the underlier context, folded causally to the cutoff. */
export interface ReducedSurface {
readonly underlier: string;
/** Latest NBBO per `${expiry}:${strike}:${right}`, latest-wins at/before the cutoff. */
readonly legs: ReadonlyMap<string, {
readonly expiry: string;
readonly strike: number;
readonly right: Right;
readonly bid: number | null;
readonly ask: number | null;
}>;
/** The last observed underlier spot at/before the cutoff (`null` when none). */
readonly spot: number | null;
/** The ts the spot was last observed at (`null` when none) — used only to age the spot. */
readonly spotTs: number | null;
/** The cutoff sequence (the last folded seq). */
readonly cutoffSeq: number;
/** The cutoff ts (the last folded ts) — the frozen session time `tau` is measured from. */
readonly cutoffTs: number;
}
/**
* Fold the OPRA SPOT + BOOK tape to the causal surface at `throughSeq` — latest NBBO per
* `(expiry, strike, right)` and the latest underlier spot, considering ONLY events at/before the
* cutoff (no look-ahead — the T-5m guard). Pure over the events; reads only `ts` (for aging/`tau`),
* never a wall clock. `throughSeq` defaults to the last event's seq (the whole tape).
*/
export declare function reduceOptionSurface(events: Iterable<BusEvent>, throughSeq?: number): ReducedSurface;
/**
* The declared default age (ms) past which an underlier spot is STALE — the feed stopped printing,
* whatever the market did. Named ONCE (kestrel-rs4) so the two surfaces that age the SAME spot read
* the SAME backstop and can never disagree about whether it is alive: this projection (a GEX/skew
* read off a dead spot taints to UNKNOWN) and the levels pane (which tags the rendered value
* `[STALE <age>]` rather than present a dead price with live confidence).
*/
export declare const SPOT_STALE_AFTER_MS = 120000;
/** The knobs the projection is built under. `expiryCloseTs` maps an expiry label to its close
* epoch-ms (supplied by the caller — dates are external context; the projection stays date-free and
* only ever computes a duration from them). `openInterest` maps `${expiry}:${strike}:${right}` → OI. */
export interface OptionsAnalyticsConfig {
/** Expiry label → close epoch-ms (the caller computes it; the projection only diffs it vs cutoff). */
readonly expiryCloseTs: ReadonlyMap<string, number>;
/** `${expiry}:${strike}:${right}` → open interest (from the OI sidecar). Absent ⇒ leg OI `null`. */
readonly openInterest?: ReadonlyMap<string, number>;
/** Per-contract multiplier (default 100). */
readonly multiplier?: number;
/** Risk-free rate (default 0 — Kestrel runs the surface at r = 0). */
readonly rate?: number;
/** Staleness backstop in ms: a spot older than this at the cutoff is stale (default
* {@link SPOT_STALE_AFTER_MS}). */
readonly staleMs?: number;
}
/**
* Build the {@link OptionsAnalytics} projection from a frozen {@link ReducedSurface} — computing
* the MODEL IV/greeks per leg via Black-Scholes (r = 0) from the observed mid + spot + `tau`. Pure:
* a total function of `surface` + `config`. A leg with no two-sided mid, an unknown `tau`, or an
* unknown/stale spot yields `iv = null` (UNKNOWN — never a guessed IV); its greeks follow.
*/
export declare function buildOptionsAnalytics(surface: ReducedSurface, config: OptionsAnalyticsConfig): OptionsAnalytics;
/** One row of the normalized OI sidecar (`data/tape-corpus/normalized/*-oi-*.json`). */
export interface OpenInterestRow {
readonly expiry: string;
readonly strike: number;
readonly right: Right;
readonly oi: number;
}
/** Fold OI sidecar rows into the `${expiry}:${strike}:${right}` → OI map the projection reads. */
export declare function openInterestMap(rows: readonly OpenInterestRow[]): ReadonlyMap<string, number>;
//# sourceMappingURL=options-analytics.d.ts.map