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.
388 lines (385 loc) • 15.4 kB
JavaScript
import {
require_dist
} from "./bin-ah4h5nq5.js";
import {
IbkrContractError,
redactAccount
} from "./bin-ff04wpv3.js";
import {
__toESM
} from "./bin-wckvcay0.js";
// src/adapters/broker/ibkr/contract.ts
var import_ib = __toESM(require_dist(), 1);
var IB_NO_SECURITY_DEFINITION = 200;
var IBKR_DEFAULT_EXCHANGE = "SMART";
var IBKR_DEFAULT_CURRENCY = "USD";
var DEFAULT_LOOKUP_TIMEOUT_MS = 1e4;
async function resolveContract(intent, deps) {
const hasStrike = intent.strike !== undefined;
const hasRight = intent.right !== undefined;
const exchange = deps.exchange ?? IBKR_DEFAULT_EXCHANGE;
const currency = deps.currency ?? IBKR_DEFAULT_CURRENCY;
if (hasStrike !== hasRight) {
throw refuse(deps, "unsupported", `leg ${describeLeg(intent)} specifies ${hasStrike ? "a strike with NO right" : "a right with NO strike"} — an option identity needs BOTH (ADR-0017), and neither is ever invented`);
}
if (!hasStrike)
return resolveEquity(intent.instrument, describeLeg(intent), deps, exchange, currency);
const expiry = nonEmpty(deps.expiry);
if (expiry === undefined) {
throw refuse(deps, "unsupported", `option leg ${describeLeg(intent)} has NO supplied expiry — the contract layer never guesses an expiry (a "nearest" one is a trade nobody authored)`);
}
const strike = intent.strike;
const right = intent.right;
const query = {
symbol: intent.instrument,
secType: import_ib.SecType.OPT,
exchange,
currency,
lastTradeDateOrContractMonth: expiry,
strike,
right: right === "C" ? import_ib.OptionType.Call : import_ib.OptionType.Put,
...deps.tradingClass === undefined ? {} : { tradingClass: deps.tradingClass }
};
const details = await requestDefinitions(deps, query, describeLeg(intent, expiry));
const matches = details.filter((d) => matchesOption(d.contract, intent.instrument, expiry, strike, right, deps.tradingClass));
const only = exactlyOne(deps, matches, describeLeg(intent, expiry));
return buildOption(deps, only.contract, describeLeg(intent, expiry));
}
async function resolveEquity(symbol, label, deps, exchange, currency) {
const query = {
symbol,
secType: import_ib.SecType.STK,
exchange,
currency
};
const details = await requestDefinitions(deps, query, label);
const matches = details.filter((d) => d.contract.secType === import_ib.SecType.STK && d.contract.symbol === symbol);
const only = exactlyOne(deps, matches, label);
const c = only.contract;
const conId = c.conId;
const localSymbol = nonEmpty(c.localSymbol);
const venueExchange = nonEmpty(c.exchange) ?? exchange;
const venueCurrency = nonEmpty(c.currency);
if (conId === undefined || localSymbol === undefined || venueCurrency === undefined) {
throw refuse(deps, "unresolvable", `the gateway's definition for ${label} is incomplete (conId/localSymbol/currency missing) — a partial definition is never completed by hand`);
}
return {
kind: "equity",
secType: "STK",
conId,
symbol,
exchange: venueExchange,
primaryExchange: nonEmpty(c.primaryExch),
currency: venueCurrency,
localSymbol,
tradingClass: nonEmpty(c.tradingClass),
multiplier: 1
};
}
async function resolveUnderlier(query, deps) {
const currency = deps.currency ?? IBKR_DEFAULT_CURRENCY;
if (query.secType === "STK") {
const exchange = deps.exchange ?? IBKR_DEFAULT_EXCHANGE;
return resolveEquity(query.symbol, `${query.symbol} (spot/equity)`, deps, exchange, currency);
}
return resolveIndex(query.symbol, deps, currency);
}
async function resolveIndex(symbol, deps, currency) {
const pinned = nonEmpty(deps.exchange);
const label = `${symbol} (index/IND)${pinned === undefined ? " [exchange OPEN]" : `@${pinned}`}`;
const query = {
symbol,
secType: import_ib.SecType.IND,
currency,
...pinned === undefined ? {} : { exchange: pinned }
};
const details = await requestDefinitions(deps, query, label);
const matches = details.filter((d) => d.contract.secType === import_ib.SecType.IND && nonEmpty(d.contract.symbol) === symbol && (pinned === undefined || nonEmpty(d.contract.exchange) === pinned));
const only = exactlyOne(deps, matches, label);
const c = only.contract;
const conId = c.conId;
const localSymbol = nonEmpty(c.localSymbol);
const venueExchange = nonEmpty(c.exchange);
const venueCurrency = nonEmpty(c.currency);
if (conId === undefined || localSymbol === undefined || venueExchange === undefined || venueCurrency === undefined) {
throw refuse(deps, "unresolvable", `the gateway's definition for ${label} is incomplete (conId/localSymbol/exchange/currency missing) — a partial definition is never completed by hand`);
}
return {
kind: "index",
secType: "IND",
conId,
symbol,
exchange: venueExchange,
currency: venueCurrency,
localSymbol,
tradingClass: nonEmpty(c.tradingClass)
};
}
function buildOption(deps, c, leg) {
const conId = c.conId;
const localSymbol = nonEmpty(c.localSymbol);
const multiplier = numberish(c.multiplier);
const expiry = expiryOf(c);
const strike = numberish(c.strike);
const right = rightOf(c.right);
const currency = nonEmpty(c.currency);
const exchange = nonEmpty(c.exchange);
if (conId === undefined || localSymbol === undefined || multiplier === undefined || expiry === undefined || strike === undefined || right === undefined || currency === undefined || exchange === undefined) {
throw refuse(deps, "unresolvable", `the gateway's definition for ${leg} is incomplete (missing conId/localSymbol/multiplier/expiry/strike/right/currency/exchange) — a partial definition is never completed by hand`);
}
return {
kind: "option",
secType: "OPT",
conId,
symbol: nonEmpty(c.symbol) ?? "",
exchange,
currency,
localSymbol,
tradingClass: nonEmpty(c.tradingClass),
multiplier,
expiry,
strike,
right
};
}
async function resolveOptionChain(query, deps) {
const exchange = deps.exchange ?? IBKR_DEFAULT_EXCHANGE;
const wanted = query.tradingClass ?? deps.tradingClass;
const label = `option chain for ${query.symbol}@${exchange}${wanted === undefined ? "" : ` class=${wanted}`}`;
const rows = await requestChain(deps, query, label);
const onExchange = rows.filter((r) => r.exchange === exchange && (wanted === undefined || r.tradingClass === wanted));
if (onExchange.length === 0) {
throw refuse(deps, "unresolvable", `the gateway lists NO ${label} — an expiry/strike grid is never invented`, { candidates: 0 });
}
if (onExchange.length > 1) {
throw refuse(deps, "ambiguous", `the gateway lists ${onExchange.length} trading classes for ${label} (${onExchange.map((r) => r.tradingClass).join(", ")}) — ambiguous; pin one with \`tradingClass\` rather than have the layer pick`, { candidates: onExchange.length });
}
const row = onExchange[0];
const multiplier = numberish(row.multiplier);
if (multiplier === undefined) {
throw refuse(deps, "unresolvable", `the ${label} came back with no multiplier — incomplete`);
}
return {
symbol: query.symbol,
exchange: row.exchange,
tradingClass: row.tradingClass,
multiplier,
expirations: [...row.expirations].sort(),
strikes: [...row.strikes].sort((a, b) => a - b)
};
}
async function listOptionStrikes(query, deps) {
const exchange = deps.exchange ?? IBKR_DEFAULT_EXCHANGE;
const currency = deps.currency ?? IBKR_DEFAULT_CURRENCY;
const tradingClass = query.tradingClass ?? deps.tradingClass;
const label = `listed ${query.symbol} ${query.expiry} ${query.right} strikes@${exchange}`;
const open = {
symbol: query.symbol,
secType: import_ib.SecType.OPT,
exchange,
currency,
lastTradeDateOrContractMonth: query.expiry,
right: query.right === "C" ? import_ib.OptionType.Call : import_ib.OptionType.Put,
...tradingClass === undefined ? {} : { tradingClass }
};
const details = await requestDefinitions(deps, open, label);
const strikes = new Set;
for (const d of details) {
const c = d.contract;
if (c.secType !== import_ib.SecType.OPT)
continue;
if (nonEmpty(c.symbol) !== query.symbol)
continue;
if (expiryOf(c) !== query.expiry)
continue;
if (rightOf(c.right) !== query.right)
continue;
if (tradingClass !== undefined && nonEmpty(c.tradingClass) !== tradingClass)
continue;
const s = numberish(c.strike);
if (s !== undefined)
strikes.add(s);
}
if (strikes.size === 0) {
throw refuse(deps, "unresolvable", `the gateway lists NO strikes for ${label} — a strike grid is never invented`, { candidates: 0 });
}
return [...strikes].sort((a, b) => a - b);
}
function contractClientOf(transport) {
return transport.client();
}
function requestDefinitions(deps, query, label) {
return request(deps, label, (reqId, ctx) => {
const collected = [];
ctx.listen(import_ib.EventName.contractDetails, (id, details) => {
if (id === reqId)
collected.push(details);
});
ctx.listen(import_ib.EventName.contractDetailsEnd, (id) => {
if (id === reqId)
ctx.done(collected);
});
deps.client.reqContractDetails(reqId, query);
});
}
function requestChain(deps, query, label) {
return request(deps, label, (reqId, ctx) => {
const rows = [];
ctx.listen(import_ib.EventName.securityDefinitionOptionParameter, (id, exchange, _underlyingConId, tradingClass, multiplier, expirations, strikes) => {
if (id === reqId)
rows.push({ exchange, tradingClass, multiplier, expirations, strikes });
});
ctx.listen(import_ib.EventName.securityDefinitionOptionParameterEnd, (id) => {
if (id === reqId)
ctx.done(rows);
});
const underlyingSecType = query.underlyingSecType === "IND" ? import_ib.SecType.IND : import_ib.SecType.STK;
deps.client.reqSecDefOptParams(reqId, query.symbol, "", underlyingSecType, query.underlyingConId);
});
}
function request(deps, label, body) {
const client = deps.client;
const scheduler = deps.scheduler ?? defaultScheduler;
const timeoutMs = deps.timeoutMs ?? DEFAULT_LOOKUP_TIMEOUT_MS;
const reqId = (deps.nextReqId ?? defaultNextReqId)();
return new Promise((resolve, reject) => {
const registered = [];
let settled = false;
let deadline;
const teardown = () => {
for (const [event, listener] of registered)
client.removeListener(event, listener);
registered.length = 0;
if (deadline !== undefined) {
scheduler.clearInterval(deadline);
deadline = undefined;
}
};
const settle = (fn) => {
if (settled)
return;
settled = true;
teardown();
fn();
};
const ctx = {
listen: (event, listener) => {
const wrapped = listener;
client.on(event, wrapped);
registered.push([event, wrapped]);
},
done: (value) => {
settle(() => {
resolve(value);
});
}
};
ctx.listen(import_ib.EventName.error, (err, code, id) => {
if (id !== reqId)
return;
if (import_ib.isNonFatalError(code, err)) {
deps.log?.(`ib info during ${label} (code=${code})`);
return;
}
settle(() => {
reject(code === IB_NO_SECURITY_DEFINITION ? refuse(deps, "unresolvable", `the gateway has NO security definition for ${label} (IB 200) — an unknown strike/right/expiry/symbol is never rounded toward a nearby one`, { code, cause: err, candidates: 0 }) : refuse(deps, "lookup-failed", `the definition lookup for ${label} failed at the gateway (IB ${code})`, {
code,
cause: err
}));
});
});
deadline = scheduler.setInterval(() => {
settle(() => {
reject(refuse(deps, "lookup-failed", `the definition lookup for ${label} timed out after ${timeoutMs}ms`));
});
}, timeoutMs);
try {
body(reqId, ctx);
} catch (cause) {
settle(() => {
reject(refuse(deps, "lookup-failed", `the definition request for ${label} threw at the socket`, { cause }));
});
}
});
}
function exactlyOne(deps, matches, label) {
if (matches.length === 0) {
throw refuse(deps, "unresolvable", `the gateway matched NO definition for ${label} — an unknown strike/right/expiry/symbol never becomes a nearby guess`, { candidates: 0 });
}
if (matches.length > 1) {
throw refuse(deps, "ambiguous", `the gateway matched ${matches.length} definitions for ${label} (${matches.map((m) => describeCandidate(m.contract)).join(" | ")}) — ambiguous, and picking one of several would route an order to a contract nobody authored; pin \`tradingClass\`/\`exchange\` instead`, { candidates: matches.length });
}
return matches[0];
}
function refuse(deps, failure, reason, options = {}) {
const stamped = `${reason} [account=${redactAccount(deps.account)}]`;
deps.log?.(`contract refused [${failure}]: ${stamped}`);
return new IbkrContractError(failure, stamped, options);
}
function describeLeg(intent, expiry) {
if (intent.strike === undefined || intent.right === undefined) {
return intent.strike === undefined && intent.right === undefined ? `${intent.instrument} (spot/equity)` : `${intent.instrument} strike=${intent.strike ?? "-"} right=${intent.right ?? "-"}`;
}
return `${intent.instrument} ${expiry ?? "(no expiry)"} ${intent.strike}${intent.right}`;
}
function describeCandidate(c) {
return `conId=${c.conId ?? "?"} class=${c.tradingClass ?? "?"} exch=${c.exchange ?? "?"}/${c.primaryExch ?? "-"} local=${c.localSymbol ?? "?"}`;
}
function matchesOption(c, symbol, expiry, strike, right, tradingClass) {
if (c.secType !== import_ib.SecType.OPT)
return false;
if (nonEmpty(c.symbol) !== symbol)
return false;
if (expiryOf(c) !== expiry)
return false;
if (tradingClass !== undefined && nonEmpty(c.tradingClass) !== tradingClass)
return false;
const gotStrike = numberish(c.strike);
if (gotStrike === undefined || Math.abs(gotStrike - strike) > 0.000000001)
return false;
return rightOf(c.right) === right;
}
function expiryOf(c) {
const raw = nonEmpty(c.lastTradeDate) ?? nonEmpty(c.lastTradeDateOrContractMonth);
if (raw === undefined)
return;
const head = raw.split(/\s+/)[0];
return head !== undefined && /^\d{8}$/.test(head) ? head : undefined;
}
function rightOf(right) {
if (typeof right !== "string")
return;
const r = right.trim().toUpperCase();
if (r === "C" || r === "CALL")
return "C";
if (r === "P" || r === "PUT")
return "P";
return;
}
function numberish(v) {
if (typeof v === "number")
return Number.isFinite(v) ? v : undefined;
if (typeof v === "string") {
const t = v.trim();
if (t.length === 0)
return;
const n = Number(t);
return Number.isFinite(n) ? n : undefined;
}
return;
}
function nonEmpty(v) {
if (v === undefined)
return;
const t = v.trim();
return t.length > 0 ? t : undefined;
}
var reqIdCounter = 9000;
var defaultNextReqId = () => ++reqIdCounter;
var defaultScheduler = {
setInterval: (fn, ms) => setInterval(fn, ms),
clearInterval: (handle) => {
clearInterval(handle);
}
};
export { IB_NO_SECURITY_DEFINITION, IBKR_DEFAULT_EXCHANGE, IBKR_DEFAULT_CURRENCY, resolveContract, resolveUnderlier, resolveOptionChain, listOptionStrikes, contractClientOf };