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.

487 lines 31 kB
/** * # adapters/broker/ibkr/feed — the FEED FACE (kestrel-7o2.7) * * The {@link FeedSource} reference implementation over the ONE shared IB session: it opens * `reqMktData` subscriptions on the contracts the 7o2.6 layer resolved (the spot/equity underlier + * an option-chain slice) and turns the venue's tick stream into the **EXISTING** {@link BusEvent} * union — a `META` header, `TICK/SPOT` (`px`/`bid`/`ask`), `TICK/BOOK` (real `OptionQuote` legs + * `underlier_px`), and `TICK/HEARTBEAT` proof-of-life. No new event kind is invented, so * `SessionCore.step` folds this feed with zero changes and the deterministic core downstream is * untouched. * * It is MARKET DATA ONLY. There is no order path here — not in the module, and not even reachable * through the client: the narrow {@link IbFeedClient} surface declares nothing but `reqMktData` / * `cancelMktData` / `reqMarketDataType` and the event bus. Orders are 7o2.8 (paper) / 7o2.10 (live), * behind the human-signed arm envelope (7o2.9). * * ## The price doctrine (ARCHITECTURE §4, RUNTIME §4) — the heart of this face * * **The observed MID is a HEALTH SIGNAL, never a value and never a price this feed publishes.** An * option's book can be fictional: a maker quoting 1.00 × 3.00 on a contract with 5.10 of intrinsic is * not offering you 2.00, and a feed that carried that 2.00 forward as a number would have authored a * naked sale nobody wrote. So: * * - `TICK/SPOT.px` is the **observed last trade** the venue actually printed. When no trade has * printed, the feed emits **no SPOT at all** (fail-closed) — it never synthesizes a spot out of a * quote mid. * - `TICK/BOOK` legs carry the **observed sides verbatim** (`bid`/`ask`/`bsz`/`asz`), with a dark * side as `null` — never `0`, never omitted — so a downstream reducer can tell a genuinely * one-sided book (the market-maker-pull fingerprint of a real move) from a two-sided one. * - `@fair` ({@link feedFair}) is the ENGINE's `executionFair` — underlying-anchored Black-76 backed * out of the LIQUID two-sided quotes and **floored at intrinsic always** — and it carries a * {@link FairReceipt}: the model VERSION, the FIT QUALITY (how many liquid strikes backed the * surface, and the IV it priced at), and the FRESHNESS (`asof`). Belief is the author's; fair is * the engine's, and it says how it was earned. * - A **one-sided or dark book** yields `fair === null` and FAILS CLOSED to an **annotated** book * value — a SELL degrades to its intrinsic floor (`fair=fallback(intrinsic;mid-dark)`, never * naked), a BUY with no two-sided mid is genuinely unresolvable. The annotation ALWAYS names the * fallback, so a silent mid can never slip through (RUNTIME §4). * - The mid survives ONLY inside {@link QuoteHealth}: a spread WIDTH and a dimensionless spread * REGIME (`tight | wide | one-sided | dark`). There is deliberately no `mid` field anywhere on * this module's surface, so a caller cannot read one off it as a price even by accident. * * ## Staleness — the watermark (the rs4/8kc bug class, first-class here) * * A **frozen, re-printed quote does NOT advance `asOfSeq`.** IB will happily re-send an identical * bid/ask forever while the market underneath it is gone; a feed that treated each re-print as news * would keep a dead tape looking alive. So the watermark advances on a **value CHANGE**, never on a * print: a re-print increments a `reprints` counter (the frozen fingerprint) and refreshes only * `lastPrintTs` (the LINE is talking), while `asOfSeq`/`asOfTs` (the VALUE) stay pinned. Past the * staleness threshold the line is STALE and the feed's health goes **DEGRADED** with a logged reason * — never a silent one. * * That degradation has TEETH, and this is the part that matters: canonical state cannot un-know a * price it has already folded, so the frozen 445.10 sits in {@link CanonicalState} looking perfectly * alive and would happily fire an armed plan. {@link stalenessGatedProvider} wraps the real * {@link SeriesProvider} and, while the underlier line is dead, resolves **every market-side series to * UNKNOWN** rather than to its last-known value. Only a definite `true` fires (RUNTIME §3), so an * UNKNOWN operand taints every dependent trigger and the affected plans **cannot fire off a dead * feed**. A dead feed must never read as live. * * ## Determinism at the edge * * The IB client, the clock, the request-id minter and the deadline scheduler are all INJECTED (as the * transport and the contract layer inject them), so unit tests drive an in-memory double with no * socket and no timer. Events carry a monotone `seq` from 0 and the INJECTED `ts` — so the BusEvents * this edge produces are causally ordered and replay-stable, which is exactly what the deterministic * core downstream requires of them. */ import { EventName } from "@stoqey/ib"; import type { Contract } from "@stoqey/ib"; import { IbkrRequestError } from "./requests.ts"; import type { AtmCoverage, AtmPolicy } from "./surface-window.ts"; import type { IbkrContractDeps, IbkrOptionContract, IbkrQuotedContract, IbkrUnderlierContract, UnderlierSecType } from "./contract.ts"; import type { IbkrTransport, NowFn, Scheduler } from "./transport.ts"; import type { BusEvent, Mode, OptionQuote, Right, SessionPhase } from "../../../bus/index.ts"; import { EXEC_FAIR_MODEL } from "../../../fair/index.ts"; import type { FairReceipt } from "../../../fair/index.ts"; import type { FeedSource } from "../../broker.ts"; import { type SeriesProvider, type SeriesRegistry } from "../../../series/index.ts"; /** How long a line's VALUE may go unchanged before it is STALE. A quote that has not MOVED in this * long is not evidence of a calm market — it is a line we can no longer vouch for. */ export declare const DEFAULT_STALE_AFTER_MS = 15000; /** How long one `reqMktData` may go completely unanswered before the subscription FAILS. A gateway * that never answers is a FAILED request, never an infinite wait (fail-closed). */ export declare const DEFAULT_SUBSCRIBE_TIMEOUT_MS = 10000; /** Spread-to-mid ratio above which a two-sided book is a WIDE one — the dimensionless regime * boundary. A RATIO, never a price: the mid appears here only as a denominator. */ export declare const WIDE_SPREAD_RATIO = 0.15; /** A listener over the IB event bus (the transport's convention). */ type IbListener = (...args: never[]) => void; /** * The NARROW structural surface of the shared IB client the feed speaks through: the streaming * market-data requests and the event bus. It declares NO order method of any kind — the feed cannot * reach one even through the client. A test injects a double implementing exactly this; production * passes {@link IbkrTransport}'s ONE shared client via {@link feedClientOf} — never a second socket, * and never a second market-data connection. */ export interface IbFeedClient { /** Open a streaming market-data subscription for one contract. */ reqMktData(reqId: number, contract: Contract, genericTickList: string | null, snapshot: boolean, regulatorySnapshot: boolean): unknown; /** Close a streaming market-data subscription. */ cancelMktData(reqId: number): unknown; /** Choose the market-data flavour (1 real-time, 2 frozen, 3 delayed, 4 delayed-frozen). OPTIONAL: * a client that cannot switch simply omits it, and the feed never pretends it did. */ reqMarketDataType?(marketDataType: number): unknown; on(event: EventName, listener: IbListener): unknown; removeListener(event: EventName, listener: IbListener): unknown; } /** * The ONE shared IB session, viewed through the READ-ONLY market-data surface (kestrel-7o2.7). The * transport hands out its single guarded client (throwing the typed connection error if the session * is not connected or has gone degraded, so a quote can never be read over a dead socket); this * re-views it as an {@link IbFeedClient}. The cast NARROWS toward the market-data requests — the real * `IBApi` is a structural superset, and this view can place nothing. There is never a second socket. */ export declare function feedClientOf(transport: IbkrTransport): IbFeedClient; /** Why the FEED itself failed (kestrel-7o2.7) — distinct from a single refused subscription * ({@link IbkrRequestError}) and from a dead socket (`IbkrConnectionError`). A feed with no underlier * has no tape at all: there is no `underlier_px` to quote a book against and no spot to drive * canonical state, so it fails CLOSED rather than emitting a chain floating in a vacuum. */ export declare class IbkrFeedError extends Error { readonly name = "IbkrFeedError"; readonly reason: string; constructor(reason: string, options?: { cause?: unknown; }); } /** The spread REGIME of an observed book — the mid's ONLY legitimate role (a fingerprint, never a * price). `dark` = both sides gone; `one-sided` = a maker pulled one side (the real-move fingerprint); * `wide` = two-sided but the spread is a large fraction of the book (a book you cannot trust to a * cent); `tight` = a book worth reading. */ export type SpreadRegime = "tight" | "wide" | "one-sided" | "dark"; /** * The health of one observed book (kestrel-7o2.7). This is where — and ONLY where — the observed mid * is allowed to matter, and even here it never escapes as a number: it appears solely as the * denominator of a dimensionless {@link SpreadRegime}. There is deliberately NO `mid` field: a caller * cannot lift a mid off this surface and price against it, because the surface does not carry one. */ export interface QuoteHealth { /** Both sides present ⇒ a real, two-sided book. */ readonly twoSided: boolean; /** Both sides gone ⇒ dark. */ readonly dark: boolean; /** The spread WIDTH in dollars (`ask − bid`) — an observed distance, not a price. `null` unless * the book is two-sided. */ readonly spread: number | null; /** The dimensionless spread regime — the fingerprint the mid is reduced to. */ readonly spreadRegime: SpreadRegime; } /** * The fail-closed ANNOTATED book value a caller falls back to when `@fair` is unbuildable (RUNTIME * §4). The `annotation` ALWAYS names the fallback — that is the whole point: **a silent mid is * forbidden**, so a number that did not come from the model arrives wearing a label that says so. * `px` is `null` when there is no honest number at all (a BUY into a dark book) — an annotated * refusal, never a guess. */ export interface FairFallback { readonly px: number | null; readonly annotation: string; } /** * `@fair` for one option leg (kestrel-7o2.7) — the engine's value, with its receipt, plus the honest * fail-closed alternative when it cannot be built. Exactly one of `value` / `fallback` is present. * There is no `mid` on this object, and there never will be. */ export interface FeedFair { /** The ExecutionFair value — Black-76 at the interpolated IV, FLOORED AT INTRINSIC. `null` when * unbuildable (no usable underlier, or a book with zero liquid strikes to back a vol out of). */ readonly value: number | null; /** The trust envelope: model VERSION + FIT QUALITY (`nLiquid`, `ivAtStrike`) + FRESHNESS (`asof`). * `null` exactly when `value` is — a receipt is never fabricated for a number that does not exist. */ readonly receipt: FairReceipt | null; /** The annotated book value when `value` is `null`; `null` when fair resolved. */ readonly fallback: FairFallback | null; /** The observed book's health — the mid's only role. */ readonly health: QuoteHealth; /** * THE FIT QUALITY THE RECEIPT COULD NOT EXPRESS (kestrel-7o2.19): does the surface this value came * off actually KNOW WHERE THE MONEY IS? `nLiquid` is a COUNT, not a LOCATION — five deep-ITM strikes * are five liquid strikes and zero evidence about an at-the-money price. Always present, whether or * not a value resolved. */ readonly coverage: AtmCoverage; /** Non-`null` exactly when a value is returned off a surface that does NOT cover the money (the * `taint` {@link AtmPolicy}) — the number then travels WEARING the reason it cannot be trusted. * `null` when the surface covers the money, and `null` when the feed failed closed instead (there is * then no number to taint, and the fallback's own annotation carries the reason). */ readonly taint: string | null; } /** The inputs to one `@fair` resolution off a recorded/observed book. Pure: no clock, no socket. */ export interface FeedFairInput { /** The OBSERVED underlier price (a real print). `null` ⇒ no usable underlying ⇒ fair is unbuildable * (never a spot synthesized out of a quote). */ readonly underlierPx: number | null; readonly strike: number; readonly right: Right; /** The side decides the fallback SHAPE: a SELL degrades to its intrinsic floor (never naked); a BUY * with no two-sided mid is genuinely unresolvable. */ readonly side: "buy" | "sell"; /** Time to expiry in YEARS — injected, never read off a clock (RUNTIME §0). */ readonly tauYears: number; /** The chain slice the vol surface is backed out of. One-sided/dark legs contribute nothing (their * mid is a health signal, never a price) — that is `buildSurface`'s own rule, reused verbatim. */ readonly legs: readonly OptionQuote[]; /** The observation time of the quotes being valued (epoch ms) — stamped onto the receipt. */ readonly asof: number; /** What "NEAR the money" means, as a fraction of spot (default {@link DEFAULT_ATM_BAND_FRACTION}) — * a named, injectable parameter, never a magic number at a call site. */ readonly bandFraction?: number | undefined; /** What to do when the surface does NOT know the money (default `fail-closed`). */ readonly atmPolicy?: AtmPolicy | undefined; } /** * Resolve `@fair` for one option leg off an observed two-sided book (kestrel-7o2.7). * * Delegates the VALUE to the engine's own {@link executionFair} — this module does not own a second * pricing model, and it does not re-price. It owns exactly the fail-closed half the price doctrine * demands: when fair is unbuildable, produce the **annotated** book value (mirroring * `engine/pricing.ts` `resolveFair` field-for-field, including its annotation strings — those strings * ARE the audit trail RUNTIME §4 requires), and reduce the observed mid to a health signal so it can * never be mistaken for a price. * * **AND IT REFUSES TO VOUCH FOR A SURFACE THAT DOES NOT KNOW THE MONEY (kestrel-7o2.19).** `fair` is * receipt-gated — it CARRIES WHETHER IT CAN BE TRUSTED (ARCHITECTURE §4) — and a receipt reporting * `nLiquid=5` off five deep-ITM strikes while the money sits eleven points higher is a receipt that * does not know it is lying. So {@link atmCoverage} is measured on EVERY resolution, off the same legs * and the same liquidity rule the surface itself uses; a surface with no liquid strike NEAR the money * (or none BRACKETING it, which makes an at-the-money read a flat extrapolation of a wing's IV) either * FAILS CLOSED to the annotated fallback — the default — or returns its value TAINTED. Never a * confident number off a surface that does not know the money. */ export declare function feedFair(input: FeedFairInput): FeedFair; /** The feed's overall verdict on itself. `DEGRADED` is never silent — it always carries a reason. */ export type FeedHealth = "LIVE" | "DEGRADED"; /** * The staleness watermark of ONE subscribed line (kestrel-7o2.7). The distinction the whole bead * turns on lives here: `lastPrintTs` is when the LINE last spoke (IB re-sent something, anything), * while `asOfSeq`/`asOfTs` are when the VALUE last CHANGED. A frozen line re-printing the same * bid/ask forever keeps `lastPrintTs` moving and `asOfSeq` PINNED — which is exactly how a dead tape * is caught rendering as a live one. */ export interface FeedSeriesWatermark { /** `SPY` for the underlier, `SPY|445C` for an option leg. */ readonly key: string; readonly kind: "spot" | "option"; /** The emitted `seq` at which this line's VALUE last CHANGED. A frozen re-print does NOT advance * it. `null` before the line has ever carried a value. */ readonly asOfSeq: number | null; /** The injected-clock `ts` at which this line's VALUE last CHANGED. */ readonly asOfTs: number | null; /** The injected-clock `ts` at which IB last PRINTED anything on this line, changed or not — proof * the line is talking, which is NOT proof the market is moving. */ readonly lastPrintTs: number | null; /** Identical re-prints since the last value CHANGE — the frozen-quote fingerprint. */ readonly reprints: number; /** `now − asOfTs` — how old the VALUE is. `null` before the first value. */ readonly ageMs: number | null; /** `ageMs > staleAfterMs`, or no value has ever landed ⇒ STALE (fail-closed on absence). */ readonly stale: boolean; /** Both sides gone. */ readonly dark: boolean; /** A refused subscription (a request-scoped IB error / a deadline lapse) — the line is dead by * refusal rather than by silence. */ readonly failed: boolean; } /** The feed's staleness watermark (kestrel-7o2.7) — the canonical freshness truth a consumer gates * on. Read it, do not infer it: canonical state cannot tell you a price is stale, only what it last * saw. */ export interface FeedWatermark { /** The injected clock at the moment the watermark was read. */ readonly now: number; /** The last `seq` the feed emitted. */ readonly seq: number; readonly staleAfterMs: number; readonly health: FeedHealth; /** The logged reason for DEGRADED, or `null` while LIVE. Never a silent degrade. */ readonly reason: string | null; readonly series: readonly FeedSeriesWatermark[]; /** Is the UNDERLIER line stale/dead? It is the canonical coordinate every market series hangs off, * so its death taints all of them. */ readonly underlierStale: boolean; } export interface IbkrFeedConfig { /** The underlier symbol (generic tickers only in anything shipped — ARCHITECTURE §7). */ readonly instrument: string; /** The session header's opaque calendar token (`YYYY-MM-DD`). */ readonly sessionDate: string; /** The Kestrel mode this tape is stamped with. */ readonly mode: Mode; /** The chain's expiry, carried onto every `TICK/BOOK` (the venue's own `YYYYMMDD`, or a tag). */ readonly expiry?: string | undefined; /** Time to expiry in YEARS for `@fair` — INJECTED, never derived from a clock (RUNTIME §0). */ readonly tauYears?: number | undefined; /** How long a line's VALUE may go unchanged before it is STALE (default {@link DEFAULT_STALE_AFTER_MS}). */ readonly staleAfterMs?: number | undefined; /** How long one subscription may go unanswered before it FAILS (default {@link DEFAULT_SUBSCRIBE_TIMEOUT_MS}). */ readonly subscribeTimeoutMs?: number | undefined; /** What "NEAR the money" means for the fit-quality receipt, as a fraction of spot (default * {@link DEFAULT_ATM_BAND_FRACTION}) — a named, injectable parameter (kestrel-7o2.19). */ readonly atmBandFraction?: number | undefined; /** What `@fair` does when the surface does NOT know the money: FAIL CLOSED (`fair => null` + the * annotated fallback — the default) or return the value TAINTED. Never a silent confident number. */ readonly atmPolicy?: AtmPolicy | undefined; /** The session phase stamped on the proof-of-life heartbeats, when the caller pins one. */ readonly phase?: SessionPhase | undefined; /** IB market-data flavour (1 real-time, 3 delayed). Absent ⇒ the gateway's own default is left * alone — the feed never silently downgrades a caller to delayed data. */ readonly marketDataType?: number | undefined; /** The session account — used ONLY to stamp a REDACTED marker on diagnostics. */ readonly account?: string | undefined; } export interface IbkrFeedDeps { /** The shared IB client — the transport's ONE session in production ({@link feedClientOf}). */ readonly client: IbFeedClient; /** The resolved UNDERLIER contract (kestrel-7o2.6) — an equity/ETF (`STK`) or an INDEX (`IND`, * the underlier of a cash-settled index option, kestrel-7o2.24). The gateway's OWN definition, * never hand-rolled: the feed does not resolve identities, it subscribes to resolved ones. Typed as * the UNDERLIER union, not the TRADABLE one — a feed quotes an index, it never orders one. */ readonly underlier: IbkrUnderlierContract; /** The resolved OPTION legs (kestrel-7o2.6). Empty/absent ⇒ an equity-only tape: no `TICK/BOOK` is * ever emitted, which is a first-class shape on this bus. */ readonly legs?: readonly IbkrOptionContract[] | undefined; /** The injected clock (epoch ms). */ readonly now: NowFn; /** Mints a unique IB request id per subscription — a counter, never an RNG. */ readonly nextReqId?: (() => number) | undefined; /** The deadline scheduler — injected so tests trip it by hand (no real timer, no flake). */ readonly scheduler?: Scheduler | undefined; /** Redacted-diagnostic sink (default no-op). NOTE: failures never depend on this — that was the * 7o2.16 gap. A refused subscription reaches the CALLER through {@link FeedSubscription.ready} and * {@link FeedOpenResult.failed}, whether or not anyone is listening to the log. */ readonly log?: ((line: string) => void) | undefined; } /** The lifecycle of one `reqMktData` subscription. */ export type SubscriptionState = "pending" | "live" | "failed"; /** * One market-data subscription on the shared socket (kestrel-7o2.7). {@link ready} is the * caller-visible channel the 7o2.16 follow-up demanded: it RESOLVES on the line's first print and * REJECTS with a typed {@link IbkrRequestError} when the gateway refuses the request (IB 200 / 321 / * 354 / 10197 …) or when the deadline lapses. It never simply hangs. */ export interface FeedSubscription { readonly reqId: number; /** `SPY` for the underlier, `SPY|445C` for an option leg. */ readonly key: string; readonly kind: "spot" | "option"; readonly contract: IbkrQuotedContract; readonly state: SubscriptionState; /** The typed refusal, once this subscription has failed. `null` otherwise. */ readonly error: IbkrRequestError | null; /** Resolves on the first print; REJECTS on a request-scoped refusal or a deadline lapse. */ readonly ready: Promise<void>; } /** What {@link IbkrFeed.open} reports. Failures are SURFACED, never swallowed — but a refused LEG is * not a refused feed: the leg is dropped (it will never be quoted as a phantom) and the tape carries * on, while a refused UNDERLIER throws {@link IbkrFeedError} because there is then no tape at all. */ export interface FeedOpenResult { readonly underlier: IbkrUnderlierContract; /** The legs that actually subscribed. */ readonly legs: readonly IbkrOptionContract[]; /** Every per-subscription refusal, surfaced to the caller. */ readonly failed: readonly IbkrRequestError[]; } /** What a `@fair` is asked for. */ export interface FeedFairQuery { readonly strike: number; readonly right: Right; readonly side: "buy" | "sell"; } /** * The IBKR feed face (kestrel-7o2.7) — a {@link FeedSource} producing the EXISTING {@link BusEvent} * union off the ONE shared IB session. Market data only; it exposes no order path. */ export interface IbkrFeed extends FeedSource { /** Emit `META`, open every subscription, and settle. Never hangs: each subscription carries its own * deadline, and a refusal surfaces as a typed error rather than silence. */ open(): Promise<FeedOpenResult>; /** Fold whatever IB has said since the last pump into BusEvents. A value CHANGE produces a * `TICK/SPOT` / `TICK/BOOK`; a quiet interval produces a `TICK/HEARTBEAT` (proof of life on a quiet * tape) — and a frozen re-print produces the heartbeat too, because a re-print is not news. */ pump(): void; /** The bus events produced so far, in order — the {@link FeedSource} face. Re-iterable and * byte-stable (`META` first, then causally-ordered ticks). */ events(): Iterable<BusEvent>; /** The events produced since the previous drain (the live loop's incremental read). */ drain(): readonly BusEvent[]; /** The staleness watermark — read it rather than inferring freshness from canonical state. */ watermark(): FeedWatermark; /** `@fair` for one leg off the CURRENT observed book, with its receipt (or the annotated * fail-closed fallback). */ fair(query: FeedFairQuery): FeedFair; /** Every subscription, live and failed. */ subscriptions(): readonly FeedSubscription[]; /** Close every subscription and detach every listener from the shared socket. Idempotent. */ close(): void; } /** * Build the IBKR feed face (kestrel-7o2.7) — a {@link FeedSource} over the ONE shared IB session, * producing the EXISTING {@link BusEvent} union. Market data only: no order path exists here, and the * narrow client surface it speaks through declares none. */ export declare function ibkrFeed(cfg: IbkrFeedConfig, deps: IbkrFeedDeps): IbkrFeed; /** What the gate needs of a feed — just its watermark. (Injected as an interface so the gate is * testable against any freshness source, and so nothing here depends on a socket.) */ export interface WatermarkSource { watermark(): FeedWatermark; } /** * Wrap a {@link SeriesProvider} so that a DEAD FEED can never read as a live one (kestrel-7o2.7). * * The problem this exists to solve is not hypothetical, and it cannot be fixed inside canonical * state: `CanonicalState` folds every SPOT it is given and then *knows* that price forever. When the * line freezes — IB re-printing an identical quote while the market underneath it is gone — the last * value sits there looking perfectly healthy. An armed plan reading `spot` gets a number, the trigger * evaluates `true`, and the engine fires off a tape that died minutes ago. The frozen-feed forensics * (kestrel-rs4/8kc) are exactly this. * * So freshness is applied at the READ, from the one place that actually knows it — the feed's * {@link FeedWatermark}. While the feed is DEGRADED, every **market-side** series (the registry's own * market-vs-org routing decides which: `spot`, `vwap`, `hod`/`lod`, the opening range, every windowed * metric) resolves to **UNKNOWN** instead of to its last-known value. Only a definite `true` fires * (RUNTIME §3), so an UNKNOWN operand propagates through the trigger algebra and the affected plans * simply cannot fire. The taint is precise: **org** facts (the session's own bookkeeping — `pnl`, a * plan's state) are not market observations and are passed through untouched; the calendar is not a * market observation either. * * Every refusal is LOGGED with its reason (once per series while degraded — a reason, never a storm). */ export declare function stalenessGatedProvider(inner: SeriesProvider, feed: WatermarkSource, opts?: { registry?: SeriesRegistry | undefined; log?: ((line: string) => void) | undefined; }): SeriesProvider; /** Which underlier + chain slice a feed wants to watch. */ export interface FeedContractQuery { readonly symbol: string; /** The chain expiry (`YYYYMMDD`). Absent ⇒ an equity-only tape (no legs are resolved at all — an * expiry is NEVER guessed, because "the nearest one" is a chain nobody asked for). */ readonly expiry?: string | undefined; /** * The OBSERVED underlier price the strike window is CENTRED on — WHERE THE MONEY IS. **Required * whenever `expiry` is present** (kestrel-7o2.19): a chain slice picked without it is not a slice * of the market, it is a slice of the grid, and a grid whose middle is not the money hands the agent * five deep-ITM calls and five worthless puts (the live defect: 741–745 at a spot of 751.94). */ readonly spot?: number | undefined; /** How many LISTED strikes to take EACH SIDE of the ATM one (default * {@link DEFAULT_SURFACE_HALF_WIDTH} = 4 ⇒ 9 strikes ⇒ 18 two-sided legs — the order path's own * window). A named, injectable parameter; never a magic number. */ readonly halfWidth?: number | undefined; /** Pin a trading class (the only sanctioned way to break a two-class ambiguity). */ readonly tradingClass?: string | undefined; /** * What the UNDERLIER is (kestrel-7o2.24): an equity/ETF (`STK`, the default — what every caller has * always meant) or an INDEX (`IND`, e.g. the NDX/XND class of cash-settled index options, whose * underlier is a published number and not an ETF). * * NEVER inferred from the symbol. An adapter that sniffed "this looks index-y" would be guessing an * identity, which is the one thing this layer exists not to do. Stating `STK` for an index symbol * does not silently mis-resolve: the gateway simply has no `STK` definition for it and the lookup * refuses `unresolvable`, loudly. */ readonly underlierSecType?: UnderlierSecType | undefined; } /** * Resolve the contracts a feed subscribes to, by DRIVING the 7o2.6 contract layer (kestrel-7o2.7). * Nothing about identity resolution is re-implemented here: the equity comes from * {@link resolveUnderlier} (an equity/ETF or, since kestrel-7o2.24, an INDEX), the listed strikes * from {@link listOptionStrikes} (the venue's truth for THAT expiry, not the union across expiries), * and each leg from {@link resolveContract}. An * ambiguous or unresolvable identity raises the 7o2.6 typed refusal and reaches STAND_DOWN — the feed * never subscribes to a guessed contract. * * The WINDOW is {@link surfaceWindow} — the ONE rule (kestrel-7o2.19), the same one the order path * selects its vol surface with. It is centred on the ATM strike **the venue LISTS for this expiry**, * read from {@link listOptionStrikes} rather than from the chain's UNION grid: `reqSecDefOptParams` * returns the union of strikes across every expiry, and a strike in the union may simply not be listed * on the expiry asked for — selecting off it invents an identity the venue does not list. * * @throws {IbkrFeedError} when an option chain is asked for with no observed spot (fail-closed: no * money, no window). */ export declare function resolveFeedContracts(query: FeedContractQuery, deps: IbkrContractDeps): Promise<{ underlier: IbkrUnderlierContract; legs: readonly IbkrOptionContract[]; }>; export { EXEC_FAIR_MODEL }; //# sourceMappingURL=feed.d.ts.map