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.

933 lines (922 loc) 31.2 kB
import { listOptionStrikes, resolveContract, resolveUnderlier } from "./bin-8gxcgtzn.js"; import { EXEC_FAIR_MODEL, UNKNOWN, buildSurface, defaultSeriesRegistry, executionFair, impliedForward, isUnknown } from "./bin-am3351y2.js"; import"./bin-73jr945c.js"; import { intrinsic } from "./bin-df1tn094.js"; import { BUS_SCHEMA } from "./bin-3ft0k1jq.js"; import"./bin-2ywrx58g.js"; import { require_dist } from "./bin-ah4h5nq5.js"; import { redactAccount } from "./bin-ff04wpv3.js"; import { __toESM } from "./bin-wckvcay0.js"; // src/adapters/broker/ibkr/feed.ts var import_ib2 = __toESM(require_dist(), 1); // src/adapters/broker/ibkr/requests.ts var import_ib = __toESM(require_dist(), 1); var IB_CONNECTION_REQ_ID = -1; class IbkrRequestError extends Error { name = "IbkrRequestError"; reqId; code; label; constructor(reqId, label, reason, options = {}) { super(`IBKR request refused [reqId=${reqId}${options.code === undefined ? "" : ` code=${options.code}`}]: ${label}${reason} (fail-closed)`, options.cause === undefined ? undefined : { cause: options.cause }); this.reqId = reqId; this.label = label; this.code = options.code; } } class IbRequestRouter { #bus; #log; #owners = new Map; #listener; #disposed = false; constructor(bus, opts = {}) { this.#bus = bus; this.#log = opts.log ?? (() => {}); const onError = (err, code, reqId) => { this.dispatch(err, code, reqId); }; this.#listener = onError; this.#bus.on(import_ib.EventName.error, this.#listener); } open(reqId, label, onError) { if (this.#owners.has(reqId)) { this.#log(`request-router: reqId ${reqId} claimed twice (${label}) — the id space must be unique`); } this.#owners.set(reqId, { label, onError }); } close(reqId) { this.#owners.delete(reqId); } owns(reqId) { return this.#owners.has(reqId); } dispose() { if (this.#disposed) return; this.#disposed = true; this.#owners.clear(); this.#bus.removeListener(import_ib.EventName.error, this.#listener); } dispatch(err, code, reqId) { if (import_ib.isNonFatalError(code, err)) { this.#log(`ib info code=${code}${reqId === undefined ? "" : ` reqId=${reqId}`}`); return; } const id = reqId ?? IB_CONNECTION_REQ_ID; if (id === IB_CONNECTION_REQ_ID) return; const owner = this.#owners.get(id); if (owner === undefined) { this.#log(`ib request-scoped error code=${code} reqId=${id} — not this router's request`); return; } this.#log(`ib request-scoped error code=${code} reqId=${id} (${owner.label}) — surfaced to the requester`); owner.onError(new IbkrRequestError(id, owner.label, describeIbErrorCode(code), { code, cause: err })); } } function describeIbErrorCode(code) { switch (code) { case 200: return "the gateway has NO security definition for this contract (IB 200) — the identity does not exist at the venue"; case 321: return "the gateway rejected the request as invalid (IB 321)"; case 354: return "this account is NOT subscribed to the requested market data (IB 354) — no data will ever arrive"; case 10197: return "no market data during a competing live session (IB 10197) — the gateway will send no ticks"; case 10167: return "requested real-time market data is not available; delayed data may be (IB 10167)"; default: return `the gateway refused the request (IB ${code}) — an unrecognised fatal code, refused rather than assumed harmless`; } } // src/adapters/broker/ibkr/surface-window.ts var DEFAULT_SURFACE_HALF_WIDTH = 4; var DEFAULT_ATM_BAND_FRACTION = 0.005; class IbkrSurfaceWindowError extends Error { name = "IbkrSurfaceWindowError"; reason; constructor(reason) { super(`IBKR surface window refused: ${reason} (fail-closed; STAND_DOWN)`); this.reason = reason; } } function atmListedStrike(listed, spot) { if (!Number.isFinite(spot)) return null; let best = null; for (const s of listed) { if (!Number.isFinite(s)) continue; if (best === null) { best = s; continue; } const d = Math.abs(s - spot); const db = Math.abs(best - spot); if (d < db || d === db && s < best) best = s; } return best; } function surfaceWindow(input) { const { spot } = input; const halfWidth = input.halfWidth ?? DEFAULT_SURFACE_HALF_WIDTH; if (!Number.isFinite(spot) || spot <= 0) { throw new IbkrSurfaceWindowError(`a surface window was asked for with NO usable spot (got ${String(spot)}) — a window with no money at its centre is not a window, it is whatever the grid happens to start with (kestrel-7o2.19: 741–745 while the money was at 751.94)`); } const grid = [...new Set(input.listed.filter((s) => Number.isFinite(s)))].sort((a, b) => a - b); if (grid.length === 0) { throw new IbkrSurfaceWindowError("the venue LISTS no strikes for this expiry — a strike grid is never invented, and a surface is never built off a grid nobody published"); } if (!Number.isFinite(halfWidth) || halfWidth < 0) { throw new IbkrSurfaceWindowError(`halfWidth must be a non-negative number of listed strikes each side (got ${String(halfWidth)})`); } const atm = atmListedStrike(grid, spot); const i = grid.indexOf(atm); const half = Math.floor(halfWidth); const strikes = grid.slice(Math.max(0, i - half), Math.min(grid.length, i + half + 1)); return { spot, atm, halfWidth: half, strikes }; } function atmCoverage(input) { const { legs, spot, tauYears } = input; const bandFraction = input.bandFraction ?? DEFAULT_ATM_BAND_FRACTION; if (spot === null || !Number.isFinite(spot) || spot <= 0) { return { spot: null, bandUsd: null, bandFraction, nLiquid: 0, nNearMoney: 0, nearestLiquidStrike: null, distanceUsd: null, bracketsSpot: false, supportsAtm: false, reason: "there is no usable underlier, so there is no money for the surface to cover — nothing may vouch for an at-the-money valuation" }; } const points = buildSurface(legs, impliedForward(legs, spot).forward, tauYears); const strikes = points.map((p) => p.strike); const bandUsd = Math.abs(bandFraction) * spot; const nLiquid = strikes.length; const nNearMoney = strikes.filter((s) => Math.abs(s - spot) <= bandUsd).length; const below = strikes.some((s) => s <= spot); const above = strikes.some((s) => s >= spot); const bracketsSpot = below && above; const nearestLiquidStrike = atmListedStrike(strikes, spot); const distanceUsd = nearestLiquidStrike === null ? null : Math.abs(nearestLiquidStrike - spot); const base = { spot, bandUsd, bandFraction, nLiquid, nNearMoney, nearestLiquidStrike, distanceUsd, bracketsSpot }; if (nLiquid === 0) { return { ...base, supportsAtm: false, reason: "the surface has NO liquid strikes at all — there is nothing to interpolate and nothing to vouch for" }; } if (!bracketsSpot) { const side = above ? "ABOVE" : "BELOW"; return { ...base, supportsAtm: false, reason: `every one of the ${nLiquid} liquid strike(s) lies ${side} the money (spot=${spot}, liquid=[${strikes.join(", ")}]) — ` + `the surface does not BRACKET the money, so an at-the-money read off it is a flat EXTRAPOLATION of the wing's IV, not an interpolation` }; } if (nNearMoney === 0) { return { ...base, supportsAtm: false, reason: `the nearest liquid strike (${nearestLiquidStrike ?? "—"}) is ${(distanceUsd ?? 0).toFixed(2)} from the money ` + `(spot=${spot}), outside the ±${bandUsd.toFixed(2)} near-the-money band — the surface brackets the money only in name` }; } return { ...base, supportsAtm: true, reason: null }; } function atmTaint(coverage) { return `THE SURFACE DOES NOT KNOW THE MONEY (atm-uncovered): ${coverage.reason ?? "no at-the-money coverage"} — ` + `nLiquid=${coverage.nLiquid} counts liquid strikes, it does not locate them, so this valuation is NOT vouched for (kestrel-7o2.19)`; } // src/adapters/broker/ibkr/feed.ts var DEFAULT_STALE_AFTER_MS = 15000; var DEFAULT_SUBSCRIBE_TIMEOUT_MS = 1e4; var WIDE_SPREAD_RATIO = 0.15; function feedClientOf(transport) { return transport.client(); } class IbkrFeedError extends Error { name = "IbkrFeedError"; reason; constructor(reason, options = {}) { super(`IBKR feed refused: ${reason} (fail-closed; STAND_DOWN)`, options.cause === undefined ? undefined : { cause: options.cause }); this.reason = reason; } } function feedFair(input) { const { underlierPx, strike, right, side, tauYears, legs, asof } = input; const policy = input.atmPolicy ?? "fail-closed"; const target = legs.find((l) => l.strike === strike && l.right === right); const health = quoteHealthOf(target); const px = underlierPx !== null && Number.isFinite(underlierPx) && underlierPx > 0 ? underlierPx : null; const coverage = atmCoverage({ legs, spot: px, tauYears, ...input.bandFraction === undefined ? {} : { bandFraction: input.bandFraction } }); const built = px === null ? null : executionFair({ underlyingSpot: px, strike, right, tauYears, liquidQuotes: legs, asof }); if (built !== null && coverage.supportsAtm) { return { value: built.value, receipt: built.receipt, fallback: null, health, coverage, taint: null }; } if (built !== null && policy === "taint") { return { value: built.value, receipt: built.receipt, fallback: null, health, coverage, taint: atmTaint(coverage) }; } const uncovered = built !== null; const because = uncovered ? ` — ${atmTaint(coverage)}` : ""; const tag = (base) => uncovered ? `fair=fallback(${base};atm-uncovered)${because}` : `fair=fallback(${base})`; const mid = midOf(target); const floor = px === null ? null : intrinsic(px, strike, right); if (side === "buy") { if (mid === null) { return { value: null, receipt: null, fallback: { px: null, annotation: "fair unbuildable: the book is one-sided/dark and a BUY has no two-sided mid to fall back to — price off @bid/@ask/@last (fail-closed, RUNTIME §4)" + (uncovered ? ` [atm-uncovered]${because}` : "") }, health, coverage, taint: null }; } return { value: null, receipt: null, fallback: { px: mid, annotation: tag("mid") }, health, coverage, taint: null }; } if (floor === null) { return { value: null, receipt: null, fallback: { px: null, annotation: "fair unbuildable: no usable underlier, so even the intrinsic floor is unknowable — a SELL is refused rather than sent naked (fail-closed, RUNTIME §4)" }, health, coverage, taint: null }; } if (mid === null) { return { value: null, receipt: null, fallback: { px: floor, annotation: tag("intrinsic;mid-dark") }, health, coverage, taint: null }; } return { value: null, receipt: null, fallback: { px: Math.max(mid, floor), annotation: tag("max(mid,intrinsic)") }, health, coverage, taint: null }; } function quoteHealthOf(q) { const bid = q?.bid ?? null; const ask = q?.ask ?? null; const dark = bid === null && ask === null; const twoSided = bid !== null && ask !== null; if (!twoSided) { return { twoSided: false, dark, spread: null, spreadRegime: dark ? "dark" : "one-sided" }; } const spread = ask - bid; const denom = 0.5 * (bid + ask); const wide = denom <= 0 || spread / denom > WIDE_SPREAD_RATIO; return { twoSided: true, dark: false, spread, spreadRegime: wide ? "wide" : "tight" }; } function midOf(q) { if (q === undefined || q.bid === null || q.ask === null) return null; if (!Number.isFinite(q.bid) || !Number.isFinite(q.ask) || q.ask < q.bid) return null; return 0.5 * (q.bid + q.ask); } class Line { reqId; key; kind; contract; raw = {}; quote = null; asOfSeq = null; asOfTs = null; lastPrintTs = null; reprints = 0; dirty = false; state = "pending"; error = null; deadline; settle; refuse; ready; constructor(reqId, key, kind, contract) { this.reqId = reqId; this.key = key; this.kind = kind; this.contract = contract; this.ready = new Promise((resolve, reject) => { this.settle = resolve; this.refuse = reject; }); this.ready.catch(() => {}); } } function deriveQuote(raw) { const side = (px, sz) => { if (px === undefined || !Number.isFinite(px)) return null; if (px < 0) return null; if (px === 0 && (sz === undefined || sz <= 0)) return null; return px; }; const bid = side(raw.bid, raw.bsz); const ask = side(raw.ask, raw.asz); const sizeOf = (v, present) => present === null || v === undefined || !Number.isFinite(v) || v < 0 ? null : v; const last = raw.last !== undefined && Number.isFinite(raw.last) && raw.last > 0 ? raw.last : undefined; return { bid, ask, bsz: sizeOf(raw.bsz, bid), asz: sizeOf(raw.asz, ask), last }; } function sameQuote(a, b) { return a.bid === b.bid && a.ask === b.ask && a.bsz === b.bsz && a.asz === b.asz && a.last === b.last; } function fieldOf(t) { switch (t) { case import_ib2.IBApiTickType.BID: case import_ib2.IBApiTickType.DELAYED_BID: return "bid"; case import_ib2.IBApiTickType.ASK: case import_ib2.IBApiTickType.DELAYED_ASK: return "ask"; case import_ib2.IBApiTickType.BID_SIZE: case import_ib2.IBApiTickType.DELAYED_BID_SIZE: return "bsz"; case import_ib2.IBApiTickType.ASK_SIZE: case import_ib2.IBApiTickType.DELAYED_ASK_SIZE: return "asz"; case import_ib2.IBApiTickType.LAST: case import_ib2.IBApiTickType.DELAYED_LAST: return "last"; default: return null; } } class IbkrFeedImpl { #cfg; #client; #now; #nextReqId; #scheduler; #log; #router; #staleAfterMs; #timeoutMs; #tauYears; #underlier; #legContracts; #lines = new Map; #spotLine; #emitted = []; #drained = 0; #seq = 0; #opened = false; #closed = false; #degradedReason = null; #registered = []; constructor(cfg, deps) { this.#cfg = cfg; this.#client = deps.client; this.#now = deps.now; this.#nextReqId = deps.nextReqId ?? defaultNextReqId; this.#scheduler = deps.scheduler ?? defaultScheduler; this.#log = deps.log ?? (() => {}); this.#staleAfterMs = cfg.staleAfterMs ?? DEFAULT_STALE_AFTER_MS; this.#timeoutMs = cfg.subscribeTimeoutMs ?? DEFAULT_SUBSCRIBE_TIMEOUT_MS; this.#tauYears = cfg.tauYears ?? 0; this.#underlier = deps.underlier; this.#legContracts = deps.legs ?? []; this.#router = new IbRequestRouter(deps.client, { log: deps.log }); } async open() { if (this.#opened) throw new IbkrFeedError("the feed is already open — a second open on one feed would double-subscribe the shared socket"); this.#opened = true; this.emitMeta(); if (this.#cfg.marketDataType !== undefined) { this.#client.reqMarketDataType?.(this.#cfg.marketDataType); } this.#listen(import_ib2.EventName.tickPrice, (reqId, field, value) => { this.onTick(reqId, field, value); }); this.#listen(import_ib2.EventName.tickSize, (reqId, field, value) => { if (field === undefined || value === undefined) return; this.onTick(reqId, field, value); }); this.#spotLine = this.subscribeOne(this.#underlier, this.#underlier.symbol, "spot"); for (const leg of this.#legContracts) { this.subscribeOne(leg, legKey(this.#underlier.symbol, leg.strike, leg.right), "option"); } const lines = [...this.#lines.values()]; await Promise.all(lines.map((l) => l.ready.then(() => { return; }, () => { return; }))); if (this.#spotLine.state === "failed") { const cause = this.#spotLine.error; this.close(); throw new IbkrFeedError(`the UNDERLIER subscription (${this.#underlier.symbol}) was refused — no underlier means no tape at all${cause === null ? "" : `: ${cause.message}`} [account=${redactAccount(this.#cfg.account)}]`, { cause: cause ?? undefined }); } const failed = lines.filter((l) => l.state === "failed" && l.error !== null).map((l) => l.error); for (const err of failed) { this.#log(`feed: dropping refused leg [reqId=${err.reqId}] ${err.label}${err.message}`); } const live = this.#legContracts.filter((c) => this.lineOf(c)?.state === "live"); return { underlier: this.#underlier, legs: live, failed }; } subscribeOne(contract, key, kind) { const reqId = this.#nextReqId(); const line = new Line(reqId, key, kind, contract); this.#lines.set(reqId, line); const label = `market data for ${key}`; this.#router.open(reqId, label, (err) => { this.failLine(line, err); }); line.deadline = this.#scheduler.setInterval(() => { this.failLine(line, new IbkrRequestError(reqId, label, `the subscription timed out after ${this.#timeoutMs}ms — the gateway sent no tick at all, and a silent subscription is a FAILED one, never an infinite wait`)); }, this.#timeoutMs); try { this.#client.reqMktData(reqId, ibContractOf(contract), "", false, false); } catch (cause) { this.failLine(line, new IbkrRequestError(reqId, label, "the market-data request threw at the socket", { cause })); } return line; } failLine(line, err) { if (line.state === "failed") return; line.state = "failed"; line.error = err; this.clearDeadline(line); this.#router.close(line.reqId); line.refuse(err); } liveLine(line) { if (line.state !== "pending") return; line.state = "live"; this.clearDeadline(line); line.settle(); } clearDeadline(line) { if (line.deadline !== undefined) { this.#scheduler.clearInterval(line.deadline); line.deadline = undefined; } } onTick(reqId, field, value) { const line = this.#lines.get(reqId); if (line === undefined) return; if (line.state === "failed") return; const key = fieldOf(field); if (key === null) return; const now = this.#now(); line.raw[key] = value; line.lastPrintTs = now; this.liveLine(line); const next = deriveQuote(line.raw); if (line.quote !== null && sameQuote(next, line.quote)) { line.reprints++; return; } line.quote = next; line.asOfTs = now; line.reprints = 0; line.dirty = true; } pump() { if (this.#closed) return; const now = this.#now(); let emitted = false; if (this.#spotLine.dirty) { const q = this.#spotLine.quote; const px = q?.last; if (px === undefined) { this.#log(`feed: ${this.#spotLine.key} has a quote but NO observed last trade — no SPOT emitted (a mid is never a price; fail-closed)`); } else { this.emit({ ts: now, stream: "TICK", type: "SPOT", instrument: this.#cfg.instrument, px, bid: q?.bid ?? null, ask: q?.ask ?? null }); this.#spotLine.asOfSeq = this.#seq - 1; this.#spotLine.dirty = false; emitted = true; } } const legLines = this.liveLegs(); const movedLegs = legLines.filter((l) => l.dirty); if (movedLegs.length > 0) { const underlierPx = this.#spotLine.quote?.last; if (underlierPx === undefined) { this.#log(`feed: ${movedLegs.length} leg(s) moved but the underlier has NO observed price — no BOOK emitted (underlier_px is never invented; fail-closed)`); } else { this.emit({ ts: now, stream: "TICK", type: "BOOK", instrument: this.#cfg.instrument, underlier_px: underlierPx, ...this.#cfg.expiry === undefined ? {} : { expiry: this.#cfg.expiry }, legs: legLines.map((l) => optionQuoteOf(l)) }); for (const l of movedLegs) { l.asOfSeq = this.#seq - 1; l.dirty = false; } emitted = true; } } if (!emitted) { this.emit({ ts: now, stream: "TICK", type: "HEARTBEAT", ...this.#cfg.phase === undefined ? {} : { phase: this.#cfg.phase } }); } this.observeHealth(now); } observeHealth(now) { const wm = this.watermarkAt(now); if (wm.health === "DEGRADED" && this.#degradedReason === null) { this.#degradedReason = wm.reason; this.#log(`feed DEGRADED (fail-closed): ${wm.reason ?? "(no reason)"} [account=${redactAccount(this.#cfg.account)}]`); } } events() { return this.#emitted; } drain() { const fresh = this.#emitted.slice(this.#drained); this.#drained = this.#emitted.length; return fresh; } subscriptions() { return [...this.#lines.values()].map((l) => ({ reqId: l.reqId, key: l.key, kind: l.kind, contract: l.contract, state: l.state, error: l.error, ready: l.ready })); } watermark() { return this.watermarkAt(this.#now()); } watermarkAt(now) { const series = [...this.#lines.values()].map((l) => this.seriesWatermark(l, now)); const spot = series.find((s) => s.kind === "spot"); const underlierStale = spot === undefined || spot.stale || spot.failed; const reasons = []; if (spot === undefined || spot.failed) { reasons.push(`the underlier line ${this.#underlier.symbol} is REFUSED/absent — there is no canonical coordinate`); } else if (spot.stale) { reasons.push(`the underlier line ${this.#underlier.symbol} is stale: its VALUE has not changed in ${spot.ageMs ?? "∞"}ms (> ${this.#staleAfterMs}ms), across ${spot.reprints} identical re-print(s) — the line is talking, the market is not`); } const staleLegs = series.filter((s) => s.kind === "option" && s.stale && !s.failed); if (staleLegs.length > 0) { reasons.push(`${staleLegs.length} option leg(s) are stale (${staleLegs.map((s) => s.key).join(", ")})`); } const health = reasons.length > 0 ? "DEGRADED" : "LIVE"; return { now, seq: this.#seq - 1, staleAfterMs: this.#staleAfterMs, health, reason: health === "DEGRADED" ? reasons.join("; ") : null, series, underlierStale }; } seriesWatermark(l, now) { const ageMs = l.asOfTs === null ? null : now - l.asOfTs; const stale = l.state === "failed" || l.asOfTs === null || ageMs !== null && ageMs > this.#staleAfterMs; return { key: l.key, kind: l.kind, asOfSeq: l.asOfSeq, asOfTs: l.asOfTs, lastPrintTs: l.lastPrintTs, reprints: l.reprints, ageMs, stale, dark: l.quote === null || l.quote.bid === null && l.quote.ask === null, failed: l.state === "failed" }; } fair(query) { const legs = this.liveLegs().map((l) => optionQuoteOf(l)); const underlierPx = this.#spotLine.quote?.last ?? null; return feedFair({ underlierPx, strike: query.strike, right: query.right, side: query.side, tauYears: this.#tauYears, legs, ...this.#cfg.atmBandFraction === undefined ? {} : { bandFraction: this.#cfg.atmBandFraction }, ...this.#cfg.atmPolicy === undefined ? {} : { atmPolicy: this.#cfg.atmPolicy }, asof: this.oldestAsOf() }); } oldestAsOf() { const stamps = [this.#spotLine, ...this.liveLegs()].map((l) => l.asOfTs).filter((t) => t !== null); return stamps.length === 0 ? this.#now() : Math.min(...stamps); } liveLegs() { return [...this.#lines.values()].filter((l) => l.kind === "option" && l.state === "live").sort((a, b) => { const ac = a.contract; const bc = b.contract; return ac.strike - bc.strike || ac.right.localeCompare(bc.right); }); } lineOf(c) { return [...this.#lines.values()].find((l) => l.contract.conId === c.conId); } close() { if (this.#closed) return; this.#closed = true; for (const line of this.#lines.values()) { this.clearDeadline(line); if (line.state === "failed") continue; try { this.#client.cancelMktData(line.reqId); } catch {} } this.#router.dispose(); for (const [event, listener] of this.#registered) this.#client.removeListener(event, listener); this.#registered.length = 0; } emitMeta() { const instrument = { symbol: this.#cfg.instrument, assetClass: this.#legContracts.length > 0 ? "option-underlier" : "equity", role: "exec" }; this.emit({ ts: this.#now(), stream: "META", type: "session", session_date: this.#cfg.sessionDate, instruments: [instrument], mode: this.#cfg.mode, bus_schema: BUS_SCHEMA }); } emit(ev) { if (this.#seq === 0 && ev.stream !== "META") { throw new IbkrFeedError(`the first bus event must be META (got ${ev.stream}) — a tape without its header is not a bus`); } this.#emitted.push({ ...ev, seq: this.#seq++ }); } #listen(event, listener) { const wrapped = listener; this.#client.on(event, wrapped); this.#registered.push([event, wrapped]); } } function optionQuoteOf(l) { const c = l.contract; const q = l.quote; const last = q?.last; return { strike: c.strike, right: c.right, bid: q?.bid ?? null, ask: q?.ask ?? null, bsz: q?.bsz ?? null, asz: q?.asz ?? null, ...last === undefined ? {} : { last } }; } function legKey(symbol, strike, right) { return `${symbol}|${strike}${right}`; } function ibContractOf(c) { if (c.kind === "equity") { return { conId: c.conId, symbol: c.symbol, secType: import_ib2.SecType.STK, exchange: c.exchange, currency: c.currency, ...c.primaryExchange === undefined ? {} : { primaryExch: c.primaryExchange } }; } if (c.kind === "index") { return { conId: c.conId, symbol: c.symbol, secType: import_ib2.SecType.IND, exchange: c.exchange, currency: c.currency }; } const opt = c; return { conId: opt.conId, symbol: opt.symbol, secType: import_ib2.SecType.OPT, exchange: opt.exchange, currency: opt.currency, lastTradeDateOrContractMonth: opt.expiry, strike: opt.strike, right: opt.right === "C" ? import_ib2.OptionType.Call : import_ib2.OptionType.Put, multiplier: opt.multiplier, ...opt.tradingClass === undefined ? {} : { tradingClass: opt.tradingClass } }; } function ibkrFeed(cfg, deps) { return new IbkrFeedImpl(cfg, deps); } function stalenessGatedProvider(inner, feed, opts = {}) { const registry = opts.registry ?? defaultSeriesRegistry; const log = opts.log ?? (() => {}); const announced = new Set; const taint = (ref) => { const wm = feed.watermark(); if (wm.health === "LIVE") { announced.clear(); return null; } const rec = registry.classify(ref); if (isUnknown(rec) || rec.kind !== "market") return null; return wm.reason ?? "the feed is DEGRADED"; }; const refuse = (ref, reason) => { const name = ref.segments[0]?.name ?? "(empty path)"; if (!announced.has(name)) { announced.add(name); log(`series "${name}" reads UNKNOWN — the feed is DEGRADED and a market series is only as alive as its line: ${reason} (fail-closed; the dependent trigger cannot fire, RUNTIME §3/§8)`); } return UNKNOWN; }; return { resolve(ref, now) { const reason = taint(ref); return reason === null ? inner.resolve(ref, now) : refuse(ref, reason); }, baseline(ref, stat) { const reason = taint(ref); return reason === null ? inner.baseline(ref, stat) : refuse(ref, reason); }, phase() { return inner.phase(); }, structural(ev, now) { if (feed.watermark().health === "DEGRADED") { log(`structural event reads UNKNOWN — the feed is DEGRADED (a level derived off a dead line is not a level)`); return UNKNOWN; } return inner.structural?.(ev, now) ?? UNKNOWN; }, fillEvent(ev) { return inner.fillEvent?.(ev) ?? UNKNOWN; }, timeOfDayMinutes(now) { return inner.timeOfDayMinutes?.(now) ?? UNKNOWN; }, quantify(ref, now) { return inner.quantify?.(ref, now) ?? UNKNOWN; } }; } async function resolveFeedContracts(query, deps) { const underlier = await resolveUnderlier({ symbol: query.symbol, secType: query.underlierSecType ?? "STK" }, deps); if (query.expiry === undefined) return { underlier, legs: [] }; const spot = query.spot; if (spot === undefined || !Number.isFinite(spot) || spot <= 0) { throw new IbkrFeedError(`a chain slice for ${query.symbol} ${query.expiry} was asked for with NO observed spot (got ${String(spot)}) — the strike window has no money to centre on, and a centre-less pick is the 741–745-at-751.94 defect. Subscribe to the underlier first and centre the window on the price it PRINTS.`); } const legs = []; for (const right of ["C", "P"]) { const listed = await listOptionStrikes({ symbol: query.symbol, expiry: query.expiry, right, ...query.tradingClass === undefined ? {} : { tradingClass: query.tradingClass } }, { ...deps, expiry: query.expiry }); const window = surfaceWindow({ listed, spot, ...query.halfWidth === undefined ? {} : { halfWidth: query.halfWidth } }); deps.log?.(`feed: ${right} surface window @${query.expiry}: ATM=${window.atm} (spot=${spot}), ±${window.halfWidth} LISTED strikes ⇒ [${window.strikes.join(", ")}]`); for (const strike of window.strikes) { const leg = await resolveContract(lookupIdentity(query.symbol, strike, right), { ...deps, expiry: query.expiry, ...query.tradingClass === undefined ? {} : { tradingClass: query.tradingClass } }); if (leg.kind === "option") legs.push(leg); } } legs.sort((a, b) => a.strike - b.strike || a.right.localeCompare(b.right)); return { underlier, legs }; } function lookupIdentity(instrument, strike, right) { return { ref: "ibkr-feed:lookup", plan: "ibkr-feed", plan_instance: "ibkr-feed#0", role: "entry", instrument, side: "buy", qty: 0, px: 0, sourceAnnotation: "ibkr-feed: a CONTRACT-LOOKUP IDENTITY, not an order — market data only (no gate, no venue write path, no order code anywhere in this face)", ...strike === undefined ? {} : { strike }, ...right === undefined ? {} : { right } }; } var reqIdCounter = 20000; var defaultNextReqId = () => ++reqIdCounter; var defaultScheduler = { setInterval: (fn, ms) => setInterval(fn, ms), clearInterval: (handle) => { clearInterval(handle); } }; export { stalenessGatedProvider, resolveFeedContracts, ibkrFeed, feedFair, feedClientOf, WIDE_SPREAD_RATIO, IbkrFeedError, EXEC_FAIR_MODEL, DEFAULT_SUBSCRIBE_TIMEOUT_MS, DEFAULT_STALE_AFTER_MS };