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
TypeScript
/**
* # 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