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.
167 lines • 10.6 kB
TypeScript
/**
* # adapters/broker/ibkr/surface-window — THE ONE SURFACE-WINDOW RULE (kestrel-7o2.19)
*
* Which strikes back a vol surface, and whether that surface may be trusted to price the money.
* There is exactly ONE definition of each here, because **a rule that exists twice is a rule that
* will diverge** — and it had already diverged: the ORDER path centred its window on the ATM strike
* the venue lists, while the FEED took whatever the listed grid happened to start with. Rendered
* against the live paper gateway with SPY at 751.94, the feed resolved:
*
* 741C 741P 742C 742P 743C 743P 744C 744P 745C 745P
*
* Every call 7–11 points IN THE MONEY (nearly pure intrinsic — its price carries almost no vol
* information) and every put a 1-to-4-cent lottery ticket (one tick is a huge relative move). The
* agent's Frame — its screen, its scarce token budget — was spent on strikes it cannot trade, and the
* vol surface built from those legs was then interpolated to price an AT-THE-MONEY leg while its
* receipt reported `nLiquid=5` and looked perfectly healthy. A receipt that says `nLiquid=5` while all
* five strikes are deep ITM is a receipt that does not know it is lying.
*
* So this module owns two things and nothing else:
*
* 1. {@link surfaceWindow} — the ATM-CENTRED window of the strikes the venue **LISTS FOR THAT
* EXPIRY**. Centre on the observed spot; take {@link SurfaceWindowInput.halfWidth} listed strikes
* each side. Never the chain's UNION grid (`reqSecDefOptParams` returns the union across every
* expiry, and a strike in the union may simply not be listed on the expiry you want — selecting
* off it invents an identity the venue does not list). Never a centre-less pick: without the
* money there is no window, only whatever the grid happens to start with.
*
* 2. {@link atmCoverage} — the HONEST fit-quality question `nLiquid` cannot answer: *does this
* surface know where the money is?* It is measured off the SAME liquidity rule the surface itself
* uses ({@link buildSurface} — imported, never re-implemented), and it is deliberately TWO facts,
* because either one alone can be gamed by a pathological chain:
* - **bracketing** — are there liquid strikes on BOTH sides of the money? `interpIv` FLAT-
* EXTRAPOLATES past the ends of the surface, so an ATM read off a surface whose liquid strikes
* all lie below the money is not an interpolation at all: it is the deepest wing's IV, wearing
* an at-the-money label. (This is the live case exactly.)
* - **distance** — is the nearest liquid strike actually NEAR the money (within
* {@link DEFAULT_ATM_BAND_FRACTION} of spot)? A surface that brackets the money from ±40
* points away brackets it in name only.
*
* Pure functions: no clock, no RNG, no socket (RUNTIME §0). Deterministic on the same inputs.
*
* ## PLACES NO ORDERS
* This is selection and fit-quality arithmetic. It reaches no client, opens no request, and names no
* order path — market data only, like every other module in the feed face.
*/
import type { OptionQuote } from "../../../bus/index.ts";
/**
* How many LISTED strikes to take EACH SIDE of the at-the-money one. The default is the order path's
* own (kestrel-7o2.8's dry run): 4 each side ⇒ **9 strikes ⇒ 18 two-sided legs**. A `@fair` with a
* receipt needs real liquid neighbours on both wings — one leg cannot imply its own surface, and a
* surface that stops at the money can only extrapolate across it.
*/
export declare const DEFAULT_SURFACE_HALF_WIDTH = 4;
/**
* What "NEAR the money" means, as a fraction of spot — the band a liquid strike must fall inside to
* count as covering an at-the-money valuation. Dimensionless on purpose: half a percent of spot is
* the same statement about a 40-dollar name and a 750-dollar one. (At SPY 751.94 that is ±3.76 — so
* the live case's nearest liquid strike, 745, sits 6.94 out and does NOT cover the money.)
*/
export declare const DEFAULT_ATM_BAND_FRACTION = 0.005;
/**
* What to do with a valuation whose surface does not know the money:
* - `fail-closed` (the default) — `fair => null`, and the caller gets the ANNOTATED book fallback
* (a SELL still floored at intrinsic, never naked). No number, no receipt.
* - `taint` — the value and its receipt survive, but they travel WEARING the reason they cannot be
* trusted. Useful for a percept that wants to show the number and refuse to vouch for it.
* There is no third option, and there is no silent one.
*/
export type AtmPolicy = "fail-closed" | "taint";
/** Why a surface window could not be built. Fail-closed: a window is never invented, and a window
* with no money at its centre is not a window — it is the kestrel-7o2.19 defect. */
export declare class IbkrSurfaceWindowError extends Error {
readonly name = "IbkrSurfaceWindowError";
readonly reason: string;
constructor(reason: string);
}
export interface SurfaceWindowInput {
/** The strikes the venue **LISTS FOR THIS EXPIRY** (`listOptionStrikes`) — never the chain's UNION
* grid across expiries (`resolveOptionChain().strikes`), which can contain strikes this expiry does
* not list at all. */
readonly listed: readonly number[];
/** The OBSERVED underlier price — where the money actually is. */
readonly spot: number;
/** How many listed strikes each side of the ATM one (default {@link DEFAULT_SURFACE_HALF_WIDTH}). */
readonly halfWidth?: number | undefined;
}
/** An ATM-centred slice of the venue's LISTED grid — the one thing both the feed and the order path
* select their surface with. */
export interface SurfaceWindow {
/** The observed spot the window was centred on. */
readonly spot: number;
/** The LISTED strike nearest the money — the venue's own, never a rounded guess. */
readonly atm: number;
readonly halfWidth: number;
/** The window, ascending. Always a subset of `listed`: a strike the venue does not list can never
* appear here, however close to the money it would have been. */
readonly strikes: readonly number[];
}
/**
* The LISTED strike nearest the money. Ties break LOW (deterministic — no clock, no RNG). `null` only
* for an empty/unusable grid, which the caller fails closed on.
*/
export declare function atmListedStrike(listed: readonly number[], spot: number): number | null;
/**
* THE ONE RULE (kestrel-7o2.19). Centre on the ATM strike the venue LISTS for this expiry, take
* `halfWidth` listed strikes each side. Both the feed's chain slice and the order path's vol surface
* are selected with this and nothing else.
*
* @throws {IbkrSurfaceWindowError} on an empty listed grid (a grid is never invented) or on a spot
* that is not a usable price (a window with no money at its centre is exactly the defect this exists
* to kill: it silently becomes "whatever the grid happens to start with").
*/
export declare function surfaceWindow(input: SurfaceWindowInput): SurfaceWindow;
export interface AtmCoverageInput {
/** The chain slice the surface is actually backed out of (the SAME legs handed to `executionFair`). */
readonly legs: readonly OptionQuote[];
/** The observed underlier price. `null` ⇒ there is no money to cover, and nothing may vouch for one. */
readonly spot: number | null;
/** Time to expiry in YEARS — injected, never read off a clock (RUNTIME §0). */
readonly tauYears: number;
/** The near-the-money band, as a fraction of spot (default {@link DEFAULT_ATM_BAND_FRACTION}). */
readonly bandFraction?: number | undefined;
}
/**
* The fit-quality fact `nLiquid` cannot express: **does the surface know where the money is?**
* `nLiquid=5` is a count, not a location — five deep-ITM strikes are five liquid strikes and zero
* evidence about an at-the-money price.
*/
export interface AtmCoverage {
readonly spot: number | null;
/** The near-the-money band in dollars (`bandFraction × spot`). `null` with no usable spot. */
readonly bandUsd: number | null;
readonly bandFraction: number;
/** How many strikes actually backed the surface — the same count the receipt reports. */
readonly nLiquid: number;
/** How many of those lie WITHIN the band around the money. This is the number that mattered. */
readonly nNearMoney: number;
/** The liquid strike closest to the money, and its distance. `null` on an empty surface. */
readonly nearestLiquidStrike: number | null;
readonly distanceUsd: number | null;
/** Are there liquid strikes at-or-below AND at-or-above the money? If not, an ATM read is a FLAT
* EXTRAPOLATION off a wing (`interpIv` flat-extrapolates past both ends), not an interpolation. */
readonly bracketsSpot: boolean;
/** The verdict: may a receipt vouch for an AT-THE-MONEY valuation off this surface? */
readonly supportsAtm: boolean;
/** Why not — a logged reason, never a silent `false`. `null` exactly when {@link supportsAtm}. */
readonly reason: string | null;
}
/**
* Measure a surface's coverage OF THE MONEY (kestrel-7o2.19). The liquidity rule is not re-invented
* here: {@link buildSurface} is asked which strikes actually produced a vol point, so a leg this
* module calls "liquid" is exactly a leg the surface was built from — one-sided, dark, crossed and
* no-arb-violating books contribute nothing, as they contribute nothing there.
*
* Which is exactly why the surface must be built at the SAME FORWARD the model prices at — the
* parity read, not spot (kestrel-ukwz). The forward decides which legs are liquid at all (it sets
* `realSides`' no-arb bounds) and whether a strike's call and put agree about vol, so measuring
* coverage at spot while `executionFair` prices at the parity forward would describe a DIFFERENT
* surface than the one the number came off — a receipt vouching for a surface that was never
* built. `spot` still defines the MONEY (the band, the bracketing, the distances): where the
* underlier is, is a fact about spot; what the options are worth, is a fact about the forward.
*/
export declare function atmCoverage(input: AtmCoverageInput): AtmCoverage;
/** The taint a valuation carries when its surface does not know the money — the sentence a receipt
* reporting `nLiquid` alone would never say about itself. */
export declare function atmTaint(coverage: AtmCoverage): string;
//# sourceMappingURL=surface-window.d.ts.map