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.
661 lines (657 loc) • 28.7 kB
JavaScript
import {
ClampRefused,
makeKillSwitch,
positionKeyOf,
registerPaperVenue
} from "./bin-xx1qrxe3.js";
import {
require_dist
} from "./bin-ah4h5nq5.js";
import {
describeIbkrConfig
} from "./bin-ff04wpv3.js";
import {
__toESM
} from "./bin-wckvcay0.js";
// src/adapters/broker/ibkr/broker.ts
var import_ib = __toESM(require_dist(), 1);
class IbkrOrderRefused extends Error {
name = "IbkrOrderRefused";
failure;
ref;
constructor(failure, reason, options = {}) {
super(`IBKR paper order REFUSED [${failure}]: ${reason} (fail-closed; NOTHING transmitted; STAND_DOWN)`, options.cause === undefined ? undefined : { cause: options.cause });
this.failure = failure;
this.ref = options.ref;
}
}
function orderClientOf(transport) {
return transport.client();
}
function contractBook(contracts) {
const byKey = new Map;
for (const c of contracts) {
byKey.set(c.kind === "option" ? positionKeyOf({ instrument: c.symbol, strike: c.strike, right: c.right }) : positionKeyOf({ instrument: c.symbol }), c);
}
return { get: (leg) => byKey.get(positionKeyOf(leg)) };
}
function expiryInstantUtc(expiry) {
if (!/^\d{8}$/.test(expiry))
return;
const y = Number(expiry.slice(0, 4));
const m = Number(expiry.slice(4, 6));
const d = Number(expiry.slice(6, 8));
const ms = Date.UTC(y, m - 1, d);
const back = new Date(ms);
if (back.getUTCFullYear() !== y || back.getUTCMonth() !== m - 1 || back.getUTCDate() !== d)
return;
return ms;
}
var PX_EPSILON = 0.000000001;
var CONSERVATIVE_MULTIPLIER = 100;
var IB_ORDER_CANCELLED = 202;
var IB_CANCEL_REJECTION_CODES = new Set([10147, 10148]);
function isStillLiveOrderError(code, err) {
if (IB_CANCEL_REJECTION_CODES.has(code))
return true;
return /cannot be cancel|still live|, ?state:/i.test(err.message);
}
class IbkrPaperBroker {
now = 0;
#cfg;
#client;
#contracts;
#drain;
#limits;
#budgetUsd;
#noUncoveredShort;
#intrinsicOf;
#priceAnchorOf;
#expectedPositions;
#settlementOf;
#nextOrderId;
#tolerance;
#tif;
#log;
killSwitch;
#events = [];
#byRef = new Map;
#byIbId = new Map;
#seenExecs = new Set;
#seenCommissions = new Set;
#execPositions = new Map;
#venueBaseline = new Map;
#workingSellQty = new Map;
#seed;
#settlements = new Map;
#registered = [];
constructor(cfg, deps) {
if (cfg.mode === "live") {
throw new IbkrOrderRefused("mode-gate", `the IBKR order face is PAPER-ONLY (${describeIbkrConfig(cfg)}) — live routing is kestrel-7o2.10 and requires the human-signed LiveArm (protocol excludes \`live\` from WALLET_SIGNABLE_SCOPES); a config flag can never grant it`);
}
this.#cfg = cfg;
this.#client = deps.client;
this.#contracts = deps.contracts;
this.#drain = deps.drain;
this.#limits = deps.limits;
this.#budgetUsd = deps.budgetUsd ?? deps.limits.maxNotionalUsd;
this.#noUncoveredShort = deps.noUncoveredShort ?? true;
this.#intrinsicOf = deps.intrinsicOf;
this.#priceAnchorOf = deps.priceAnchorOf;
this.#expectedPositions = deps.expectedPositions;
this.#settlementOf = deps.settlementOf;
this.#nextOrderId = deps.nextOrderId;
this.#tolerance = deps.tolerance ?? 0;
this.#tif = deps.tif ?? import_ib.TimeInForce.DAY;
this.#log = deps.log ?? (() => {});
this.killSwitch = deps.killSwitch ?? makeKillSwitch();
this.#seed = deps.seedPositions ?? {};
this.#attach();
try {
this.#client.reqPositions();
} catch (cause) {
this.killSwitch.trip(`could not subscribe to the venue's POSITION push (reqPositions threw: ${String(cause)}) — the broker's own statement of record is unavailable, so every risk wall would be reading a guess. Halting paper transmission (fail-closed)`);
}
}
get events() {
return this.#events;
}
get limits() {
return this.#limits;
}
multiplierOf(intent) {
return this.#contracts.get(intent)?.multiplier ?? CONSERVATIVE_MULTIPLIER;
}
preflight(intent) {
if (this.killSwitch.tripped) {
throw new ClampRefused("killed", intent.ref, `paper transmission halted — kill-switch tripped: ${this.killSwitch.reason ?? "(no reason)"} (fail-closed)`);
}
if (!Number.isFinite(intent.qty) || !Number.isInteger(intent.qty) || intent.qty <= 0) {
throw new IbkrOrderRefused("invalid-order", `qty ${intent.qty} is not a positive whole number of contracts/shares — refusing at the boundary rather than letting IB be this system's validator (a negative qty would sign-invert straight through the never-naked wall)`, { ref: intent.ref });
}
if (!Number.isFinite(intent.px) || intent.px <= 0) {
throw new IbkrOrderRefused("invalid-order", `px ${intent.px} is not a positive finite limit price — the adapter transmits the engine's number or it REFUSES; it never repairs one`, { ref: intent.ref });
}
if (this.#byRef.has(intent.ref)) {
throw new IbkrOrderRefused("invalid-order", `ref ${intent.ref} is ALREADY on the ledger — a second order under a live ref would clobber its line (and orphan any reservation it holds). Refs are the engine's correlation key and are never reused`, { ref: intent.ref });
}
const contract = this.#contracts.get(intent);
if (contract === undefined) {
throw new IbkrOrderRefused("unresolved-contract", `leg ${positionKeyOf(intent)} has no pre-resolved gateway contract — an identity is NEVER guessed at the tick (resolve it ahead of the hot path via resolveContract, kestrel-7o2.6)`, { ref: intent.ref });
}
const key = positionKeyOf(intent);
const heldBefore = this.positions()[key] ?? 0;
const workingSellQty = this.#workingSellQty.get(key) ?? 0;
const availableBefore = heldBefore - workingSellQty;
const signed = intent.side === "buy" ? intent.qty : -intent.qty;
const heldAfter = availableBefore + signed;
const multiplier = contract.multiplier;
const notionalUsd = intent.px * intent.qty * multiplier;
const isBuy = intent.side === "buy";
const riskReducing = isBuy && availableBefore < 0 || !isBuy && heldAfter >= 0;
let maxLossUsd = 0;
if (!riskReducing) {
if (isBuy) {
maxLossUsd = notionalUsd;
} else {
const shortQty = -heldAfter;
if (contract.kind === "equity") {
throw new IbkrOrderRefused("unbounded-risk", `a SHORT EQUITY at ${key} (net ${heldAfter}) has an UNBOUNDED max_loss (the stock can rise without limit) — bounded risk cannot be satisfied at ANY budget, so it is refused REGARDLESS of the no-uncovered-short policy (kestrel-buos: bounded risk is the invariant)`, { ref: intent.ref });
}
if (contract.right === "C") {
throw new IbkrOrderRefused("unbounded-risk", `a naked short CALL at ${key} (net ${heldAfter}) has an UNBOUNDED max_loss — refused by the BOUNDED-RISK invariant itself, regardless of policy (kestrel-buos)`, { ref: intent.ref });
}
const perContract = Math.max(contract.strike - intent.px, 0) * multiplier;
maxLossUsd = perContract * shortQty;
if (!Number.isFinite(maxLossUsd)) {
throw new IbkrOrderRefused("unbounded-risk", `max_loss for the short PUT at ${key} is not computable (non-finite) — fail-closed (kestrel-buos)`, { ref: intent.ref });
}
if (maxLossUsd > this.#budgetUsd + PX_EPSILON) {
throw new IbkrOrderRefused("over-budget", `the short PUT at ${key} (net ${heldAfter}) carries max_loss $${maxLossUsd} which exceeds the risk budget $${this.#budgetUsd} — bounded risk requires size × max_loss ≤ budget (kestrel-buos, fail-closed)`, { ref: intent.ref });
}
if (this.#noUncoveredShort) {
throw new IbkrOrderRefused("naked-short", `${intent.side} ${intent.qty} of ${key} would leave a NET SHORT of ${shortQty} (uncovered). Its max_loss $${maxLossUsd} is FINITE and within budget, but the no-uncovered-short POLICY is ON (the default — the 0DTE book keeps it hard). Refusing (kestrel-buos: turn the policy off to sell puts; bounded risk still binds)`, { ref: intent.ref });
}
}
}
let intrinsicFloor;
if (intent.side === "sell" && contract.kind === "option") {
const intr = this.#intrinsicOf(intent);
if (intr === undefined || !Number.isFinite(intr)) {
throw new IbkrOrderRefused("intrinsic-unknown", `the intrinsic of ${key} is UNKNOWN, so the SELL floor cannot be proven — refusing rather than ASSUMING the floor is satisfied (a silent default is never a default here)`, { ref: intent.ref });
}
intrinsicFloor = intr;
if (intent.px + PX_EPSILON < intr) {
throw new IbkrOrderRefused("below-intrinsic", `SELL ${key} at ${intent.px} is BELOW its intrinsic ${intr} — a sell is floored at intrinsic, always. The adapter is a transmitter and will not re-price it up`, { ref: intent.ref });
}
}
if (intent.qty > this.#limits.maxOrderQty) {
throw new ClampRefused("order-size", intent.ref, `order qty ${intent.qty} exceeds maxOrderQty ${this.#limits.maxOrderQty} (fail-closed, bounded-risk)`);
}
if (Math.abs(heldAfter) > this.#limits.maxPositionQty) {
throw new ClampRefused("position", intent.ref, `projected net position ${heldAfter} at ${key} exceeds maxPositionQty ${this.#limits.maxPositionQty} (fail-closed)`);
}
if (notionalUsd > this.#limits.maxNotionalUsd) {
throw new ClampRefused("notional", intent.ref, `order notional ${notionalUsd} (px ${intent.px} x qty ${intent.qty} x multiplier ${multiplier}) exceeds maxNotionalUsd ${this.#limits.maxNotionalUsd} (fail-closed)`);
}
const anchor = this.#priceAnchorOf(intent);
if (anchor === undefined) {
throw new IbkrOrderRefused("anchor-unresolvable", `no OBSERVED two-sided book for ${key} — the PRICE ANCHOR is UNRESOLVABLE, and an unaudited price is never authorizable (fail-closed; the book is never assumed)`, { ref: intent.ref });
}
if (anchor.freshness !== "live") {
throw new IbkrOrderRefused("anchor-stale", `the book for ${key} is ${anchor.freshness.toUpperCase()} (bid=${anchor.bid} ask=${anchor.ask}) — delayed/frozen data is a HEALTH SIGNAL, never a PRICE ANCHOR (RUNTIME §4). Anything that PRICES an order requires LIVE data; without it the anchor is UNRESOLVABLE`, { ref: intent.ref });
}
if (!Number.isFinite(anchor.bid) || !Number.isFinite(anchor.ask) || anchor.bid <= 0 || anchor.ask <= 0 || anchor.ask + PX_EPSILON < anchor.bid) {
throw new IbkrOrderRefused("anchor-unresolvable", `the observed book for ${key} is DARK / ONE-SIDED / CROSSED (bid=${anchor.bid} ask=${anchor.ask}) — that is not a book, and a fabricated level is exactly what fail-closed forbids`, { ref: intent.ref });
}
if (!Number.isFinite(anchor.fair)) {
throw new IbkrOrderRefused("anchor-unresolvable", `@fair for ${key} is UNKNOWN — the price cannot be corroborated, and it is never ASSUMED sound (a silent default is never a default here)`, { ref: intent.ref });
}
if (!anchor.fairTrusted) {
throw new IbkrOrderRefused("fair-untrusted", `the @fair ${anchor.fair} for ${key} carries an UNTRUSTED receipt (a stale index, a frozen input, or no ATM coverage) — refusing to transmit on a fair the receipt cannot vouch for (kestrel-ltrf). @fair is UNDERLYING-anchored, so it may legitimately sit away from the posted book ${anchor.bid} / ${anchor.ask}; the wall gates on the receipt's TRUST, never on a naive book clamp`, { ref: intent.ref });
}
const ibContract = ibContractOf(contract);
const ibOrder = {
action: intent.side === "buy" ? import_ib.OrderAction.BUY : import_ib.OrderAction.SELL,
orderType: import_ib.OrderType.LMT,
totalQuantity: intent.qty,
lmtPrice: intent.px,
tif: this.#tif,
transmit: true,
orderRef: intent.ref,
...this.#cfg.account === undefined ? {} : { account: this.#cfg.account }
};
return {
ref: intent.ref,
contract,
ibContract,
ibOrder,
side: intent.side,
qty: intent.qty,
limitPx: intent.px,
multiplier,
notionalUsd,
maxLossUsd,
heldBefore,
workingSellQty,
heldAfter,
intrinsicFloor,
anchor,
clamp: "cleared",
sourceAnnotation: intent.sourceAnnotation
};
}
submit(intent) {
const ticket = this.preflight(intent);
const ibOrderId = this.#nextOrderId();
if (this.#byIbId.has(ibOrderId)) {
throw new IbkrOrderRefused("invalid-order", `nextOrderId re-issued ibOrderId ${ibOrderId}, already correlated to ref ${this.#byIbId.get(ibOrderId)} — a duplicated order id would re-point correlation and fold one order's executions under another (inflating the risk wall's read). Refusing (kestrel-7o2.8)`, { ref: intent.ref });
}
const order = { ...ticket.ibOrder, orderId: ibOrderId };
const rec = {
ref: intent.ref,
ibOrderId,
intent,
contract: ticket.contract,
submittedQty: intent.qty,
filledQty: 0,
fillNotional: 0,
commissionUsd: 0,
remainingQty: undefined,
avgFillPx: undefined,
phase: "pending",
placed: false,
terminal: false,
reservedQty: 0,
execIds: new Set
};
this.#byRef.set(intent.ref, rec);
this.#byIbId.set(ibOrderId, intent.ref);
if (intent.side === "sell") {
const key = positionKeyOf(intent);
rec.reservedQty = intent.qty;
this.#workingSellQty.set(key, (this.#workingSellQty.get(key) ?? 0) + intent.qty);
}
try {
this.#client.placeOrder(ibOrderId, ticket.ibContract, order);
} catch (cause) {
rec.unknownFate = true;
rec.phase = "pending";
this.#log(`placeOrder THREW for ${intent.ref} [ibOrderId=${ibOrderId}] — UNKNOWN FATE (may be LIVE at the venue). Keeping its correlation + reservation; it will fold if it fills (Route C, fail-closed): ${String(cause)}`);
throw new IbkrOrderRefused("transmit-failed", `placeOrder threw on the IB Gateway socket for ${intent.ref} (${describeIbkrConfig(this.#cfg)}) — the order's fate is UNKNOWN and its correlation + reservation are KEPT (never assume not-filled)`, { ref: intent.ref, cause });
}
this.#log(`transmitted ${intent.side} ${intent.qty} ${positionKeyOf(intent)} @ ${intent.px} [ibOrderId=${ibOrderId}]`);
this.#drain();
return intent.ref;
}
cancel(ref) {
const rec = this.#byRef.get(ref);
if (rec === undefined || rec.terminal) {
this.#drain();
return;
}
try {
this.#client.cancelOrder(rec.ibOrderId);
} catch (cause) {
this.#log(`cancelOrder threw for ${ref} [ibOrderId=${rec.ibOrderId}]: ${String(cause)}`);
}
this.#drain();
}
positions() {
const snap = {};
for (const [key, qty] of Object.entries(this.#seed))
snap[key] = qty;
for (const [key, qty] of this.#venueBaseline)
snap[key] = qty;
for (const [key, qty] of this.#execPositions)
snap[key] = (snap[key] ?? 0) + qty;
return snap;
}
ledger() {
return [...this.#byRef.values()].map((r) => ({
ref: r.ref,
ibOrderId: r.ibOrderId,
intent: r.intent,
contract: r.contract,
submittedQty: r.submittedQty,
filledQty: r.filledQty,
avgFillPx: r.avgFillPx,
commissionUsd: r.commissionUsd,
remainingQty: r.remainingQty,
phase: r.phase
}));
}
reconcile() {
for (const r of this.#byRef.values()) {
if (r.filledQty > r.submittedQty + this.#tolerance) {
this.killSwitch.trip(`reconciliation break (OVER-FILL) on ${r.ref} [ibOrderId=${r.ibOrderId}]: engine submitted ${r.submittedQty}, broker reported executing ${r.filledQty} — the BROKER's report is authoritative; halting paper transmission (ADR-0034 §4, fail-closed)`);
return;
}
}
const expected = this.#expectedPositions();
const actual = this.positions();
for (const key of new Set([...Object.keys(expected), ...Object.keys(actual)])) {
const e = expected[key] ?? 0;
const a = actual[key] ?? 0;
if (Math.abs(a - e) > this.#tolerance) {
const settled = this.#settlementAbsolving(key, e, a);
if (settled !== undefined) {
this.#recordSettlement(settled);
continue;
}
this.killSwitch.trip(`reconciliation break at ${key}: engine expected ${e}, broker PULL reported ${a} (delta ${a - e}, tolerance ${this.#tolerance}) — halting paper transmission (ADR-0034 §4, fail-closed)`);
return;
}
}
}
settlements() {
return [...this.#settlements.values()];
}
#settlementAbsolving(key, expected, actual) {
if (this.#settlementOf === undefined)
return;
if (Math.abs(actual) > this.#tolerance)
return;
if (!(expected > this.#tolerance))
return;
const contract = this.#contractForKey(key);
if (contract === undefined || contract.kind !== "option")
return;
const expiryAt = expiryInstantUtc(contract.expiry);
if (expiryAt === undefined)
return;
if (!Number.isFinite(this.now) || this.now < expiryAt)
return;
const receipt = this.#settlementOf(key);
if (receipt === undefined)
return;
if (!Number.isFinite(receipt.qty) || Math.abs(receipt.qty - expected) > this.#tolerance)
return;
if (!Number.isFinite(receipt.cashUsd) || receipt.cashUsd < 0)
return;
if (!Number.isFinite(receipt.settledAt) || receipt.settledAt < expiryAt)
return;
return {
key,
qty: expected,
cashUsd: receipt.cashUsd,
expiry: contract.expiry,
settledAt: receipt.settledAt,
observedAt: this.now
};
}
#contractForKey(key) {
for (const r of this.#byRef.values()) {
if (positionKeyOf(r.intent) === key)
return r.contract;
}
return;
}
#recordSettlement(rec) {
if (this.#settlements.has(rec.key))
return;
this.#settlements.set(rec.key, rec);
this.#log(`SETTLEMENT at ${rec.key}: the engine expected ${rec.qty}, the venue reports 0 — the leg reached its own expiry (${rec.expiry}) and the venue posted $${rec.cashUsd} of settlement cash at ${rec.settledAt}. Recorded as a settlement, NOT a reconciliation break (kestrel-7o2.24, ADR-0034 q3)`);
}
#attach() {
this.#on(import_ib.EventName.openOrder, (orderId, _c, _o, _s) => {
const rec = this.#byIbId.get(orderId) === undefined ? undefined : this.#byRef.get(this.#byIbId.get(orderId));
if (rec === undefined || rec.terminal)
return;
this.#ensurePlaced(rec);
this.#drain();
});
this.#on(import_ib.EventName.orderStatus, (orderId, status, filled, remaining, _avg) => {
const ref = this.#byIbId.get(orderId);
const rec = ref === undefined ? undefined : this.#byRef.get(ref);
if (rec === undefined || rec.terminal)
return;
rec.remainingQty = remaining;
switch (status) {
case import_ib.OrderStatus.PendingSubmit:
case import_ib.OrderStatus.PreSubmitted:
case import_ib.OrderStatus.Submitted:
case import_ib.OrderStatus.ApiPending:
case import_ib.OrderStatus.PendingCancel:
this.#ensurePlaced(rec);
break;
case import_ib.OrderStatus.Filled:
this.#ensurePlaced(rec);
if (rec.filledQty >= rec.submittedQty)
this.#terminate(rec, "filled");
break;
case import_ib.OrderStatus.Cancelled:
case import_ib.OrderStatus.ApiCancelled:
this.#ensurePlaced(rec);
this.#emit("cancel", rec, rec.intent.px, `venue status ${status}`);
this.#terminate(rec, "cancelled");
break;
case import_ib.OrderStatus.Inactive:
this.#ensurePlaced(rec);
this.#emit("reject", rec, rec.intent.px, `venue status ${status}`);
this.#terminate(rec, "rejected");
break;
case import_ib.OrderStatus.Unknown:
this.#log(`ib orderStatus Unknown for ${rec.ref} [ibOrderId=${orderId}] — no state inferred`);
break;
default:
break;
}
this.#drain();
});
this.#on(import_ib.EventName.execDetails, (_reqId, _c, execution) => {
const orderId = execution.orderId;
const execId = execution.execId;
if (orderId === undefined || execId === undefined)
return;
if (this.#seenExecs.has(execId))
return;
const ref = this.#byIbId.get(orderId);
const rec = ref === undefined ? undefined : this.#byRef.get(ref);
if (rec === undefined)
return;
const shares = execution.shares;
const price = execution.price;
if (shares === undefined || price === undefined || shares <= 0)
return;
this.#seenExecs.add(execId);
rec.execIds.add(execId);
const postTerminal = rec.terminal;
rec.filledQty += shares;
rec.fillNotional += shares * price;
rec.avgFillPx = rec.fillNotional / rec.filledQty;
const key = positionKeyOf(rec.intent);
const signed = rec.intent.side === "buy" ? shares : -shares;
this.#execPositions.set(key, (this.#execPositions.get(key) ?? 0) + signed);
this.#release(rec, shares);
if (!postTerminal) {
this.#ensurePlaced(rec);
this.#emit("fill", rec, price, undefined, shares);
if (rec.filledQty >= rec.submittedQty)
this.#terminate(rec, "filled");
} else {
this.#log(`POST-TERMINAL execution ${execId} on ${rec.ref} (phase=${rec.phase}) — folded into broker truth, NOT emitted`);
}
this.#tripOnObservation(rec, execId, postTerminal);
this.#drain();
});
this.#on(import_ib.EventName.commissionReport, (report) => {
const execId = report.execId;
const commission = report.commission;
if (execId === undefined || commission === undefined)
return;
if (this.#seenCommissions.has(execId))
return;
const rec = [...this.#byRef.values()].find((r) => this.#execBelongsTo(r, execId));
if (rec === undefined)
return;
this.#seenCommissions.add(execId);
rec.commissionUsd += commission;
});
this.#on(import_ib.EventName.error, (err, code, reqId) => {
if (reqId === undefined || import_ib.isNonFatalError(code, err))
return;
const ref = this.#byIbId.get(reqId);
const rec = ref === undefined ? undefined : this.#byRef.get(ref);
if (rec === undefined || rec.terminal)
return;
if (isStillLiveOrderError(code, err)) {
this.#log(`ib error ${code} on ${rec.ref} [ibOrderId=${reqId}] is a CANCEL-REJECTION — the order is STILL LIVE, not terminal. Keeping its reservation and awaiting the venue's terminal status (Route A, fail-closed): ${err.message}`);
return;
}
if (code === IB_ORDER_CANCELLED) {
this.#emit("cancel", rec, rec.intent.px, `ib error ${code}: ${err.message}`);
this.#terminate(rec, "cancelled");
this.#drain();
return;
}
this.#emit("reject", rec, rec.intent.px, `ib error ${code}: ${err.message}`);
this.#terminate(rec, "rejected");
this.#drain();
});
this.#on(import_ib.EventName.position, (_account, contract, pos, _avgCost) => {
const key = positionKeyFromContract(contract);
if (key === undefined)
return;
const foldSince = this.#execPositions.get(key) ?? 0;
const priorBaseline = this.#venueBaseline.has(key) ? this.#venueBaseline.get(key) : this.#seed[key] ?? 0;
if (foldSince !== 0 && pos === priorBaseline) {
this.#log(`STALE position push for ${key}: pos ${pos} equals the prior baseline while ${foldSince} has already been folded on top — IGNORING the rebase so a sold-down position is not re-inflated (Route B, fail-closed)`);
} else {
this.#venueBaseline.set(key, pos);
this.#execPositions.set(key, 0);
}
this.#scanBrokerTruthForBreaks();
this.#drain();
});
}
detach() {
for (const [event, listener] of this.#registered) {
this.#client.removeListener(event, listener);
}
this.#registered = [];
}
#execBelongsTo(rec, execId) {
return rec.execIds.has(execId);
}
#ensurePlaced(rec) {
if (rec.placed)
return;
rec.placed = true;
rec.phase = "working";
this.#emit("place", rec, rec.intent.px);
}
#terminate(rec, phase) {
rec.phase = phase;
rec.terminal = true;
this.#release(rec, rec.reservedQty);
}
#release(rec, qty) {
if (rec.reservedQty <= 0 || qty <= 0)
return;
const released = Math.min(qty, rec.reservedQty);
rec.reservedQty -= released;
const key = positionKeyOf(rec.intent);
const next = (this.#workingSellQty.get(key) ?? 0) - released;
if (next > 0)
this.#workingSellQty.set(key, next);
else
this.#workingSellQty.delete(key);
}
#tripOnObservation(rec, execId, postTerminal) {
if (this.killSwitch.tripped)
return;
if (postTerminal) {
this.killSwitch.trip(`reconciliation break (POST-TERMINAL FILL) on ${rec.ref} [ibOrderId=${rec.ibOrderId}, execId=${execId}]: the venue EXECUTED an order this face had already latched ${rec.phase} — the BROKER's report is authoritative, so this is a break, not an event. Halting paper transmission (ADR-0034 §4, fail-closed)`);
return;
}
if (rec.filledQty > rec.submittedQty + this.#tolerance) {
this.killSwitch.trip(`reconciliation break (OVER-FILL) on ${rec.ref} [ibOrderId=${rec.ibOrderId}]: engine submitted ${rec.submittedQty}, broker reported executing ${rec.filledQty} — the BROKER's report is authoritative; halting paper transmission (ADR-0034 §4, fail-closed)`);
return;
}
this.#scanBrokerTruthForBreaks();
}
#scanBrokerTruthForBreaks() {
if (this.killSwitch.tripped)
return;
if (!this.#noUncoveredShort)
return;
for (const [key, qty] of Object.entries(this.positions())) {
if (qty < -this.#tolerance) {
this.killSwitch.trip(`reconciliation break (NEGATIVE POSITION) at ${key}: the broker's own report now leaves ${qty} — an UNCOVERED SHORT, which the no-uncovered-short policy (ON) forbids. Halting paper transmission (fail-closed)`);
return;
}
}
}
#emit(action, rec, px, reason, qty) {
const intent = rec.intent;
this.#events.push({
ts: this.now,
stream: "ORDER",
type: action,
order_id: intent.ref,
...intent.plan !== undefined ? { plan: intent.plan } : {},
...intent.plan_instance !== undefined ? { plan_instance: intent.plan_instance } : {},
instrument: intent.instrument,
side: intent.side,
qty: qty ?? intent.qty,
...intent.strike !== undefined ? { strike: intent.strike } : {},
...intent.right !== undefined ? { right: intent.right } : {},
px,
...reason !== undefined ? { reason } : {}
});
}
#on(event, listener) {
const wrapped = listener;
this.#client.on(event, wrapped);
this.#registered.push([event, wrapped]);
}
}
function ibContractOf(c) {
if (c.kind === "option") {
return {
conId: c.conId,
symbol: c.symbol,
secType: import_ib.SecType.OPT,
exchange: c.exchange,
currency: c.currency,
localSymbol: c.localSymbol,
lastTradeDateOrContractMonth: c.expiry,
strike: c.strike,
right: c.right === "C" ? import_ib.OptionType.Call : import_ib.OptionType.Put,
multiplier: c.multiplier,
...c.tradingClass === undefined ? {} : { tradingClass: c.tradingClass }
};
}
return {
conId: c.conId,
symbol: c.symbol,
secType: import_ib.SecType.STK,
exchange: c.exchange,
currency: c.currency,
localSymbol: c.localSymbol,
...c.primaryExchange === undefined ? {} : { primaryExch: c.primaryExchange }
};
}
function positionKeyFromContract(c) {
const symbol = c.symbol;
if (symbol === undefined || symbol.length === 0)
return;
if (c.secType === import_ib.SecType.OPT) {
const strike = c.strike;
const right = c.right === import_ib.OptionType.Call ? "C" : c.right === import_ib.OptionType.Put ? "P" : undefined;
if (strike === undefined || right === undefined)
return;
return positionKeyOf({ instrument: symbol, strike, right });
}
return positionKeyOf({ instrument: symbol });
}
function ibkrBroker(cfg, deps) {
return new IbkrPaperBroker(cfg, deps);
}
var IBKR_PAPER_VENUE = "ibkr";
function installIbkrPaperVenue(broker) {
registerPaperVenue(IBKR_PAPER_VENUE, () => broker);
return broker;
}
export { IbkrOrderRefused, orderClientOf, contractBook, ibContractOf, ibkrBroker, IBKR_PAPER_VENUE, installIbkrPaperVenue };