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
JavaScript
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
};