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.

388 lines (385 loc) 15.4 kB
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 };