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.

1,082 lines (1,070 loc) 172 kB
import { canonicalize, canonicalizeJson, jsonHash, nakedShortTaint, project, readSampledQualificationClaim, sampledQualified, selectHeadline } from "./bin-t58k60v9.js"; import { PANE_CATALOG, atmStraddleExtrinsic, buildOptionsAnalytics, paneById, reduceOptionSurface, renderBriefing, renderWakeDelta, requireMeta, resolveSpecs, resolveView, windowServableBy } from "./bin-p5jfg5b0.js"; import { canonicalPlansText } from "./bin-29be75ss.js"; import { ARM_BOUND_NOTHING_MARKER, MakerFairCalV1, MakerFairV1, PlanEngine, SETTLE_MARK_STALE_AFTER_MS, SimFillEngine, SpotFillEngine, StrictCrossV1, armRefusalError, heldPositionAsOfGradedBus } from "./bin-n77kea5n.js"; import { standDownTurn } from "./bin-mn7wcxhv.js"; import { sha256 } from "./bin-8pchjxn2.js"; import { CanonicalSeriesProvider, CanonicalState, FakeOrgFacts, UNKNOWN, executionFair, isUnknown } from "./bin-am3351y2.js"; import { canonicalEnvelope, groupPlanInstances, serializeBus } from "./bin-73jr945c.js"; import { intrinsic } from "./bin-df1tn094.js"; import { BUS_SCHEMA, foldBook, readBus, roundToGrid } from "./bin-3ft0k1jq.js"; import { OPEN_ORDINAL, assertNever, budgetStr, parse, planClause, print, printTrigger, reconcileTurn } from "./bin-2ywrx58g.js"; import { __require } from "./bin-wckvcay0.js"; // src/session/day.ts import { existsSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs"; import { join } from "node:path"; // src/session/agent.ts var RESERVED_TAPE_TIME_FIELDS = [ "measuredMs", "measured_ms", "bufferMs", "buffer_ms", "costMs", "cost_ms", "returnTs", "return_ts", "wakeSeq", "wake_seq", "deliberation" ]; function assertHandlerResponseTimeless(turn) { for (const field of RESERVED_TAPE_TIME_FIELDS) { if (Object.prototype.hasOwnProperty.call(turn, field)) { throw new Error(`WakeHandler response carries driver-owned tape-time field ${JSON.stringify(field)} — the adapter is the only place wall time may EXIST, but tape-time accounting (the deliberation record, the latency-fold, ADR-0040) is the DRIVER's, minted once above the seam; a response owning it would drift a second per-adapter clock (refused fail-closed, CONTEXT.md "WakeHandler")`); } } return turn; } var OPEN_WAKE_KEY = "open"; var OPEN_ORDINAL2 = -1; function wakeKeyOf(frame) { return `${frame.wakeSource}#${frame.wakeSourceOrdinal}#${frame.wakeReason ?? ""}`; } var BACKTEST_CONFIG = { model: "fixed-plan", tokenizer: "none", format: "json", temperature: 0, thinkingLevel: "none", label: "fixed-plan" }; var INSERTED_VANTAGE_SOURCES = new Set(["own-fill"]); // src/session/armed-plan-terms.ts var ENFORCED_PLAN_STATES = new Set(["armed", "fired", "managing"]); function dteFromSessionDate(sessionDate, expiryDate) { const utcMidnight = (d) => { const [y, m, day] = d.split("-").map((s) => Number.parseInt(s, 10)); return Date.UTC(y ?? 0, (m ?? 1) - 1, day ?? 1); }; return Math.round((utcMidnight(expiryDate) - utcMidnight(sessionDate)) / 86400000); } function dateBlindExpiry(e, sessionDate) { return e.kind === "expiry-date" ? { kind: "expiry-dte", dte: dteFromSessionDate(sessionDate, e.date) } : e; } function dateBlindLeg(l, sessionDate) { return l.kind === "leg" && l.expiry !== undefined ? { ...l, expiry: dateBlindExpiry(l.expiry, sessionDate) } : l; } function dateBlindClause(c, sessionDate) { if (c.kind === "do" || c.kind === "also" || c.kind === "reload") { return { ...c, legs: c.legs.map((l) => dateBlindLeg(l, sessionDate)) }; } return c; } function isEntry(c) { return c.kind === "do" || c.kind === "also" || c.kind === "reload"; } function isExit(c) { return c.kind === "tp" || c.kind === "exit"; } function isInvalidation(c) { return c.kind === "invalidate" || c.kind === "cancel-if"; } function armedPlanEntryOf(p, sessionDate) { const when = p.when !== undefined ? printTrigger(p.when) : undefined; const entries = p.clauses.filter(isEntry).map((c) => planClause(dateBlindClause(c, sessionDate))); const exits = p.clauses.filter(isExit).map((c) => planClause(dateBlindClause(c, sessionDate))); const invalidations = p.clauses.filter(isInvalidation).map((c) => planClause(dateBlindClause(c, sessionDate))); const sizeEnvelope = p.budget !== undefined ? budgetStr(p.budget) : undefined; return { name: p.name, ...when !== undefined ? { when } : {}, ...entries.length > 0 ? { entries } : {}, ...exits.length > 0 ? { exits } : {}, ...invalidations.length > 0 ? { invalidations } : {}, ...sizeEnvelope !== undefined ? { sizeEnvelope } : {} }; } function armedPlanTermsOf(asts, enforcedNames, sessionDate) { const latestByName = new Map; for (const p of asts) latestByName.set(p.name, p); const plans = []; const seen = new Set; for (const p of asts) { if (seen.has(p.name)) continue; if (!enforcedNames.has(p.name)) continue; seen.add(p.name); plans.push(armedPlanEntryOf(latestByName.get(p.name), sessionDate)); } if (plans.length === 0) return; return { plans }; } // src/grade/receipt.ts var DECISION_KINDS = new Set([ "accept", "reparameterize", "novel_plan", "decline", "stand_down" ]); var ARMING_DECISIONS = new Set([ "accept", "reparameterize", "novel_plan" ]); var COUNTERFACTUAL_KINDS = new Set([ "ungated", "null", "bracket" ]); // src/grade/receipts.ts var DECISION_CONTROL_TYPES = new Set(["propose", "decline", "stand_down"]); function encodeDecision(d) { return JSON.stringify(d); } var UNSTATED_THESIS = { when: { session_phase: null, regime: null, event: null, trend_state: null, volatility_state: null }, where: { product: null, direction: null, strike: null, moneyness: null, book_state: null }, how: { entry_method: null, size_contracts: null, reload: null, invalidation: null, exit: null }, conviction: 0, day_type_probabilities: { unstated: 1 } }; var DRIVER_COMMISSION_MODEL = "none-v0"; var DRIVER_RISK_ENVELOPE = "budget-r-v1"; var DRIVER_GRADER = "grade-v1"; function driverMeasurementVersions(judge) { return { fill_model: judge.fillModel, commission_model: DRIVER_COMMISSION_MODEL, bus_fidelity: judge.fidelity, risk_envelope: DRIVER_RISK_ENVELOPE, grader: DRIVER_GRADER }; } function decisionControlFields(d) { const descriptor = { decision_kind: d.decision_kind, plan_text: d.plan_text, counterfactual: d.counterfactual ?? "null", measurement_versions: d.measurement_versions, thesis: d.thesis ?? UNSTATED_THESIS, ...d.rationale !== undefined ? { rationale: d.rationale } : {} }; return { type: d.control, ...d.target !== undefined ? { target: d.target } : {}, note: encodeDecision(descriptor) }; } // src/session/clock.ts var MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000; var ET_ZONE = "America/New_York"; var ET_FORMAT = new Intl.DateTimeFormat("en-US", { timeZone: ET_ZONE, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); function etParts(nowMs) { const parts = ET_FORMAT.formatToParts(new Date(nowMs)); const get = (t) => { const v = parts.find((p) => p.type === t)?.value; return v === undefined ? 0 : Number(v); }; return { y: get("year"), mo: get("month"), d: get("day"), h: get("hour"), mi: get("minute"), s: get("second") }; } function etMinuteOfDay(nowMs) { const p = etParts(nowMs); return p.h * 60 + p.mi; } function etOffsetMs(utcMs) { const p = etParts(utcMs); const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.h, p.mi, p.s); return asUtc - utcMs; } function etWallClockMs(sessionDate, hour, minute) { const [y, mo, d] = sessionDate.split("-").map(Number); const guess = Date.UTC(y ?? 1970, (mo ?? 1) - 1, d ?? 1, hour, minute); return guess - etOffsetMs(guess); } var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/; function expiryCloseMs(sessionDate, expiry) { if (expiry === undefined) return null; const token = expiry.trim(); if (token.toLowerCase() === "0dte") return etWallClockMs(sessionDate, 16, 0); const iso = ISO_DATE.exec(token); if (iso === null) return null; const [y, mo, d] = [Number(iso[1]), Number(iso[2]), Number(iso[3])]; const probe = new Date(Date.UTC(y, mo - 1, d)); if (probe.getUTCFullYear() !== y || probe.getUTCMonth() !== mo - 1 || probe.getUTCDate() !== d) return null; return etWallClockMs(token, 16, 0); } function expiryTauYears(sessionDate) { return (now, expiry) => { const settleMs = expiryCloseMs(sessionDate, expiry); if (settleMs === null) return null; return Math.max(0, (settleMs - now) / MS_PER_YEAR); }; } // src/session/sim.ts import { writeFileSync } from "node:fs"; // src/session/harness/prompt.ts var BASELINE_SYSTEM_PROMPT = [ "You are Kestrel, a disciplined options-trading agent acting inside a deterministic simulator.", "At each vantage you are handed a FROZEN, date-blind cockpit frame (relative time only — never a calendar date).", "You decide what to DO and reply with EXACTLY ONE JSON object and nothing else (no prose, no markdown fences).", "", "The JSON object is a turn:", ' { "journal": "<optional one-line pre-hoc thesis + invalidation>", "actions": [ <Action>, ... ] }', "", "Each Action is one of these tagged shapes:", ' { "kind": "supersede", "document": "<a Kestrel View/Wake/Plan document>", "note": "<optional>" }', " - Author or revise your standing book. The FIRST turn (OPEN) supersede is your initial arm.", " - `document` MUST be valid Kestrel source, e.g. PLAN rider budget 0.5R ttl +60m", " WHEN phase open DO buy 1 atm C @ lean(bid, fair, 0.5)", ' { "kind": "scheduleWake", "at": { "kind": "inMinutes", "minutes": <n> }, "reason": "<why>" }', ' - or "at": { "kind": "atClockET", "clockET": "HH:MM" }. Sets your next monitoring wake.', ' { "kind": "placeOrder", "order": { "side": "buy"|"sell", "instrument": "<sym>", "strike": <n>,', ' "right": "C"|"P", "qty": <n>, "price": "<Kestrel price expr, e.g. @fair or cap fair,0.80>" } }', " - A single-leg OPTION order. price is a Kestrel expression, never a bare mid. SELL is floored at intrinsic.", ' - To cross EQUITY/SPOT immediately, DROP strike+right (ADR-0017): { "kind": "placeOrder",', ' "order": { "side": "buy"|"sell", "instrument": "<sym>", "qty": <shares>, "price": "@mid" } }.', ' { "kind": "cancelOrder", "ref": "<resting order ref>" }', ' { "kind": "standDown", "reason": "<why>" } - de-arm clean; inventory rides its TP/EXIT. The safe default.', "", 'To PASS (do nothing this wake — standing plans keep managing), reply with an empty actions list: { "actions": [] }.', "Bounded risk always: never place a naked sell, never anchor a price on the mid, keep within your R budget.", "If you are unsure, PASS or standDown rather than guess. Output ONLY the JSON object." ].join(` `); var BASELINE_PROMPT_PROFILE = "baseline-v0"; var AUTHORING_EXAMPLE_DOCUMENTS = { optionsRider: [ "PLAN atm-rider budget 0.5R ttl +60m", " WHEN spot > vwap", " DO buy 1 atm C @ lean(bid, fair, 0.5)", " TP +80% frac 0.5 @ fair", " EXIT spot < vwap held 60s @ fair" ].join(` `), equityReversion: [ "PLAN eq-revert budget 1R ttl 16:00", " USING exec SPY", " WHEN spot crosses below vwap", " DO buy 100 shares @ mid", " TP +2%", " EXIT spot crosses above vwap @ bid" ].join(` `), equityBreakout: [ "PLAN eq-breakout budget 1R ttl 16:00", " USING exec QQQ", " WHEN spot crosses above hod", " DO buy 100 shares @ ask+2c", " TP +2%", " EXIT spot crosses below vwap @ bid" ].join(` `), levelBreakState: [ "PLAN brk-entry budget 1R", " USING exec SPY", " WHEN spot < 499.29", " DO buy 100 shares @ ask+2c", " TP +2%", " EXIT spot > 500 @ bid" ].join(` `), view: ["VIEW cockpit", " tape 5m", " chain", " levels"].join(` `) }; var AUTHORING_EXAMPLE_DOCUMENT_LIST = [ AUTHORING_EXAMPLE_DOCUMENTS.optionsRider, AUTHORING_EXAMPLE_DOCUMENTS.equityReversion, AUTHORING_EXAMPLE_DOCUMENTS.equityBreakout, AUTHORING_EXAMPLE_DOCUMENTS.levelBreakState, AUTHORING_EXAMPLE_DOCUMENTS.view ]; function supersedeTurnJson(document) { return JSON.stringify({ actions: [{ kind: "supersede", document }] }); } var AUTHORING_SYSTEM_PROMPT = [ "You are Kestrel, a disciplined trading agent acting inside a deterministic simulator.", "At each vantage you are handed a FROZEN, date-blind cockpit frame (relative time + an HH:MM ET clock only — never a calendar date).", "You decide what to DO and reply with EXACTLY ONE JSON object and nothing else (no prose, no markdown fences).", "", "═══ THE TURN ENVELOPE ═══", "Your whole reply is ONE turn object:", ' { "journal": "<optional one-line pre-hoc thesis + invalidation>", "actions": [ <Action>, ... ] }', 'To PASS (do nothing this wake — your standing plans keep managing themselves), reply with an empty list: { "actions": [] }', "", "Each Action is exactly one of these tagged shapes:", ' { "kind": "supersede", "document": "<a Kestrel PLAN/VIEW/WAKE document>", "note": "<optional>" }', " Author or revise your standing book. Your FIRST turn (OPEN) supersede is your initial arm; a later", " supersede REPLACES the whole document (use it to tighten a stop, add a leg, or re-arm).", ' { "kind": "scheduleWake", "at": { "kind": "inMinutes", "minutes": <n> }, "reason": "<why>" }', ' or "at": { "kind": "atClockET", "clockET": "HH:MM" }. Sets your next monitoring wake.', ' { "kind": "placeOrder", "order": { "side": "buy"|"sell", "instrument": "<sym>", "strike": <n>,', ' "right": "C"|"P", "qty": <n>, "price": "<Kestrel price expr, e.g. @fair or cap fair,0.80>" } }', " A single-leg OPTION order. price is a Kestrel expression, never a bare mid. SELL is floored at intrinsic.", ' To cross EQUITY/SPOT NOW, DROP strike+right (ADR-0017 — a spot leg has neither): { "kind": "placeOrder",', ' "order": { "side": "buy"|"sell", "instrument": "<sym>", "qty": <shares>, "price": "@ask+2c" } }. Same @-anchors as a plan leg.', " PRICE TO YOUR INTENT: a resting BUY fills only when the ask crosses STRICTLY BELOW it. To cross NOW —", " a breakout/continuation you must CHASE — LIFT THE OFFER: price STRICTLY THROUGH the ask (a marketable", " number ABOVE it, e.g. `@ask+2c`; `@ask` alone does NOT cross under the strict `<` floor). `@mid` /", " `@lean(bid, ask, …)` are RESTING / FADE prices BELOW the ask — they fill only when price PULLS BACK to", " you, so a `@mid` buy on a rising tape NEVER fills (the ask climbs away). Mirror for a sell (lift = through the bid).", ' { "kind": "cancelOrder", "ref": "<a resting order ref from the frame kernel>" }', ' { "kind": "flatten", "instrument": "<sym>", "note": "<optional>" }', " CLOSE your whole position in <sym> NOW: cancels the managing plan's resting orders for it AND crosses a", " COVERED sell to settle — one step, always covered (never naked). Unlike standDown (de-arm, inventory RIDES),", " flatten LIQUIDATES. A flat book is a clean no-op. After a flatten the position is flat and its 1R is freed.", ' { "kind": "standDown", "reason": "<why>" } — de-arm clean; inventory rides its own TP/EXIT. The safe default.', "", "═══ HOW TO WRITE A KESTREL `document` (this is where most mistakes happen) ═══", "A Kestrel document is MULTI-LINE and INDENTATION-STRUCTURED:", " • line 1 is the PLAN header: PLAN <name> [budget <n>R] [ttl <+45m|16:00>] [regime {intraday: trend}]", " Do NOT add a `regime {…}` gate unless the frame shows a regime/predictor feed: with no feed it", " fail-closes and the plan NEVER arms — it sits `authored (blocked: regime … UNKNOWN)` in the KERNEL", " (acting) plan states, holding no position. Omit the gate on a plain SPOT/equity tape.", " • EVERY clause after it is on its OWN line, indented by two spaces: USING, WHEN, DO, TP, EXIT, RELOAD, ALSO.", " • `WHEN` and `DO` are ALWAYS separate lines. NEVER put two clauses on one line — `WHEN … DO …` is", ' the single most common parse error ("unexpected `DO` after the WHEN clause"). One clause per line.', " • WHEN/DO/TP/EXIT are clauses INSIDE the PLAN block — never top-level statements on their own.", " • TP on a percentage/multiple KEEPS its sigil: `TP +80%` or `TP 2x` (a bare `TP +2` is rejected).", "", "Because the document lives INSIDE a JSON string, each line break is a literal \\n and the two-space", "indent is two spaces after it. So this plan you want to author:", "", AUTHORING_EXAMPLE_DOCUMENTS.optionsRider, "", "is written in your JSON turn EXACTLY like this (copy this shape — note the \\n between every clause):", " " + supersedeTurnJson(AUTHORING_EXAMPLE_DOCUMENTS.optionsRider), "", "═══ WORKED, COPYABLE EXAMPLES (generic shapes — teach the grammar, decide the trade yourself) ═══", "", "① Arm an OPTIONS rider (a call with a take-profit + a time-stop bracket). The document + its JSON turn:", AUTHORING_EXAMPLE_DOCUMENTS.optionsRider, " " + supersedeTurnJson(AUTHORING_EXAMPLE_DOCUMENTS.optionsRider), "", "② Arm an EQUITY plan — when the frame is a SPOT/equity tape with NO option chain, TRADE THE SHARES.", " An equity leg is `buy N shares` (no strike, no right) under `USING exec <SYM>`. Do NOT stand down", " just because there are no option legs — reach for the equity grammar. (a) FADE a cross — price COMES", " to you, so REST the buy at `@ mid` below the level; it fills on the pullback:", AUTHORING_EXAMPLE_DOCUMENTS.equityReversion, " " + supersedeTurnJson(AUTHORING_EXAMPLE_DOCUMENTS.equityReversion), " (b) CHASE a breakout — a continuation UP that you must LIFT THE OFFER to catch. A `@ mid` buy would", " NEVER fill (the ask climbs away), so price `@ ask+2c` — STRICTLY THROUGH the ask — to cross now:", AUTHORING_EXAMPLE_DOCUMENTS.equityBreakout, " " + supersedeTurnJson(AUTHORING_EXAMPLE_DOCUMENTS.equityBreakout), " (c) ARM ON THE RIGHT SIDE OF THE LEVEL. `spot crosses below|above X` is an EDGE — it fires ONLY on a", " FRESH crossing, so spot must still be on the ENTRY side when you arm: to catch a BREAK, arm the edge at", " OPEN, AHEAD of it. If spot has ALREADY crossed X, `crosses` NEVER fires (no transition to observe) — the", " plan sits armed forever and never enters (the #1 missed-entry bug). For an already-through level use the", " STATE trigger `WHEN spot < X` (or `> X`): it is true whenever the condition HOLDS, INCLUDING already-true", " at arm, so it fires the instant you arm below the line:", AUTHORING_EXAMPLE_DOCUMENTS.levelBreakState, " " + supersedeTurnJson(AUTHORING_EXAMPLE_DOCUMENTS.levelBreakState), " (d) DON'T GATE A MID-SESSION ENTRY ON `phase`. `phase` is a LEVEL state — pre|open|regular|close|post —", " and `open` is the MOMENTARY opening print (09:30), already elapsed before your first wake: every", " mid-session wake reads `phase regular`. So `WHEN phase open` armed mid-session is DEAD — it is false", " at every tick, NEVER fires, and fails SILENT (no reject, no fill — the plan just sits `armed`). To gate", " a mid-session entry, gate on PRICE: `WHEN spot crosses above <level>` (edge — arm ahead of the break) or", " `WHEN spot > <level>` (state — fires when already through), as in (a)–(c). Never `phase open`.", "", "③ Request a VIEW (name, then one pane per indented line; a pane is a name + plain args, no `:`/`{}`):", AUTHORING_EXAMPLE_DOCUMENTS.view, " " + supersedeTurnJson(AUTHORING_EXAMPLE_DOCUMENTS.view), "", "④ Stand down (de-arm cleanly — the correct move on an edgeless/untradeable tape):", " " + JSON.stringify({ actions: [{ kind: "standDown", reason: "edgeless chop — no defined-risk setup; standing down" }] }), "", "⑤ Pass (do nothing this wake; standing plans keep managing):", " " + JSON.stringify({ actions: [] }), "", "═══ PRICE EXPRESSIONS (the `@ …` and order `price`) ═══", "Never anchor on a bare mid. Legal anchors: fair, intrinsic, basis, bid, ask, mid, last, join, improve, stub.", "Combine them: @fair @lean(bid, fair, 0.5) @min(fair, mid) cap fair peg +2c / -1c offsets.", "FADE vs CHASE decides your price (a resting BUY fills only when the ask crosses STRICTLY BELOW it):", " • FADE — let price come to you: REST a buy at/below the mid (`@mid`, `@lean(bid, ask, …)`); it fills on the pullback.", " • CHASE a breakout — LIFT THE OFFER: price a buy STRICTLY THROUGH the ask (`@ask+2c`); it crosses now.", " A resting `@mid` buy on a rising tape NEVER fills. Mirror for a sell: fade rests above the mid, chase lifts through the bid.", "", "═══ DISCIPLINE ═══", "Bounded risk always: never place a naked sell, never anchor a price on the mid, keep within your R budget.", "SIZE BY COST BASIS, NOT STOP DISTANCE (ADR-0017): an equity/spot leg's bounded risk is its FULL cost basis", "`qty × entry_px` (an option's is `qty × premium × mult`), so `qty ≤ 1R_budget / entry_px` — size to the kernel's", "`sizing: max ~N shares` cue. Sizing by stop-loss distance oversizes and the entry is SILENTLY clamped (never fires).", "On an edgeless or untradeable tape the correct move is to PASS or standDown for $0 — manufacturing edge is punished.", "If you are unsure, PASS or standDown rather than guess. Output ONLY the JSON turn object." ].join(` `); var AUTHORING_PROMPT_PROFILE = "authoring-v1"; function paneMenuLines() { return PANE_CATALOG.map((e) => ` • ${e.id} — ${e.title}: ${e.description} [${e.attribution}, ~${e.tokenCostEstimate} tok]`); } function requestViewJson(view, reason) { return JSON.stringify({ requestView: { view, reason } }); } var VIEWSHOP_SYSTEM_PROMPT = [ AUTHORING_SYSTEM_PROMPT, "", "═══ YOU MAY ASK FOR A DIFFERENT VIEW (the authoring loop) ═══", "Right now you are at a FROZEN vantage — one moment, which you may look at again. If the current screen", "does not show what you need to test the thesis your BRIEF points at, do NOT force a Plan you do not", "believe and do NOT stand down for a look you could have had. Ask for a DIFFERENT lens on the SAME frozen", "market. Reply with a view request INSTEAD of a turn (never both actions AND requestView):", ' { "requestView": { "view": "<a Kestrel VIEW document>", "reason": "<why you need this lens now>" } }', "The `view` is a VIEW document exactly like example ③ above (a name + optional budget, then one pane per", "indented line). For instance:", " " + requestViewJson(AUTHORING_EXAMPLE_DOCUMENTS.view, "need the near-money chain + levels before I commit"), "", "The panes you may select (this menu IS the single source the renderer materializes from — a pane not", "listed here cannot be shown):", ...paneMenuLines(), "", "A view costs ATTENTION, not TIME (the T-5m invariant): it NEVER advances the clock, NEVER consumes a", "wake, NEVER reveals future data — you are looking again at the SAME moment. View requests are BOUNDED:", "you get a small number of lenses and a shared attention budget, and when it is spent the session stands", "down flat. Shop only when a lens would genuinely change your decision; the default View is meant to be", "enough that shopping is rarely worth its cost.", "", "═══ AUTHORING ERRORS ARE RECOVERABLE ═══", "If your author (a Plan document OR a view request) is malformed, you will be handed a GUIDING ERROR that", "names the defect. That is NOT a rejection — read it, fix ONLY what it names, and resubmit. A repair", "counts against the SAME bounded budget as a view request, so fix it in as few attempts as you can.", "", "═══ YOUR CONTEXT HAS TWO CHANNELS — MANDATE vs BRIEF (keep them apart) ═══", " • the MANDATE is HARD and machine-checked (envelope, R budget, never-naked). It is a FENCE and the", " ONLY input to admission — you may never do what it forbids, and it never tells you what to look for.", " • the BRIEF is SOFT, directional English (your goal, approach, persona). It directs what you go LOOK", " for and how you author. It NEVER authorizes anything and NEVER enters an admission check.", "Decide which View to request FROM YOUR BRIEF: if the current lens will not let you test the thesis your", "Brief points at, request the lens that would — within budget. Then commit a Plan, stand down, or pass." ].join(` `); var VIEWSHOP_PROMPT_PROFILE = "viewshop-v1"; var MANAGE_SYSTEM_PROMPT = [ AUTHORING_SYSTEM_PROMPT, "", "═══ YOU ARE A MANAGE-ONLY WATCHER (this OVERRIDES the authoring mandate above) ═══", "A STRATEGIST has ALREADY authored and ARMED the standing Plan+Brief+Mandate you are handed. You are", "the small, fast WATCHER that MANAGES that armed book at every wake — you are NOT its author. Your job", "is to keep the armed book healthy WITHIN its budget and Mandate: press a winner toward its target, and", "CUT a loser whose premise has broken. You manage the POSITION, not just the plan object.", "", "═══ READ YOUR BOOK FIRST — P&L, then PREMISE (do this EVERY wake, before you decide) ═══", "Mark your position to the tape and check the thesis is still alive BEFORE you choose a move:", " • UNREALIZED P&L: take each open position's `basis` (its entry, on the POSITIONS line) and compare", " it to the CURRENT price in the market pane. For a long, `(spot − basis) × qty` — a spot BELOW your", " basis is an OPEN LOSS you are carrying RIGHT NOW, not a paper abstraction.", " • PREMISE — JUDGE IT FROM THE FRAME, NOT FROM PLAN TEXT YOU CANNOT SEE. You are handed the POSITION and", " the tape, never the strategist's authored thesis prose — so RECONSTRUCT the premise from what the frame", " SHOWS you: your direction (the kernel POSITIONS line) plus the LEVEL your entry leans on, read off the", " `levels` and `tape` panes on your screen. A long implies a bullish premise — a break/level HOLDING —", " so ask whether that is STILL TRUE against those panes. If price has fallen back THROUGH the level your", " long leans on — a poke that failed, a breakout that reversed — the setup is INVALIDATED. That is a", " FAKEOUT: the reason you are long is GONE, and the position is now an un-thesised loser bleeding toward", " the close.", "", "═══ WHEN THE PREMISE IS BROKEN AND YOU ARE LOSING, CUT IT — THIS IS THE EXPECTED MOVE ═══", "A losing position on a broken premise is the ONE situation a watcher exists to handle. Do NOT ride it,", 'and do NOT merely NARRATE it — a JOURNAL that says "this is a fakeout, I should exit" paired with an', "empty actions list is a NO-OP: the book keeps bleeding to the close. To exit you must CHANGE THE BOOK.", "The CLEAN way to get flat is ONE action — `flatten`:", ' { "actions": [ { "kind": "flatten", "instrument": "<SYM>", "note": "premise broke — cutting" } ] }', "It cancels the managing plan's resting protective/TP sell that RESERVES your shares AND crosses a COVERED", 'closing sell to settle, in ONE step — so you never fight the "never naked" refusal, and it can never leave', "you short. Use `flatten` to close your WHOLE position in <SYM> now; after it, you are flat and your 1R is freed.", "", "To close only PART of the position (a TRIM, not a full flatten), do the two-step manage turn by hand:", " 1. CANCEL the resting protective / take-profit SELL that RESERVES your shares (its `ref` is on the", " RESTING line of the frame kernel) — an armed plan usually rests a TP over the whole lot, and while it", ' reserves the inventory a fresh closing sell alongside it is refused "never naked" (already spoken for).', " 2. PLACE the closing SELL of the size you want gone, priced to CROSS NOW:", ' { "kind": "placeOrder", "order": { "side": "sell", "instrument": "<SYM>", "qty": <part of your qty>, "price": "@bid-2c" } }', " — an equity leg (drop strike+right), lifted THROUGH the bid (`@bid-2c`, strictly below it) so it fills", " at once. A resting `@bid`/`@mid` sell can sit unfilled as the tape drops away — to GET OUT, cross.", 'So the trim turn is: { "actions": [ { "kind": "cancelOrder", "ref": "<resting sell ref>" },', ' { "kind": "placeOrder", "order": { "side": "sell", "instrument": "<SYM>", "qty": <part of your qty>, "price": "@bid-2c" } } ] }', "Both flatten and the two-step route straight through the admission Gate (closing a long you hold is never a", "naked sell, floored at intrinsic) — this is what RECLAIMS the loss. Prefer `flatten` for a full exit.", "", "TWO MOVES THAT LOOK LIKE AN EXIT BUT ARE NOT — do NOT reach for these to cut a loser:", " • `standDown` DE-ARMS THE PLAN BUT DOES NOT LIQUIDATE — your inventory keeps riding to settle. On a", " plan with no protective stop, standing down leaves the losing shares FULLY exposed to the close.", " standDown is a clean de-arm, NOT a way out of a position — to get flat, `flatten` (or SELL, step 2 above).", " • `supersede` (arming a new plan, or bolting a new EXIT onto the book) is BEYOND your authority — it is", " REFUSED at the tier boundary and does NOTHING (your frozen book is HELD, the loss runs on). Reaching", ' for it to "add a stop" just wastes the turn. Manage the book you hold with ORDERS, not authorship.', "", "YOU MAY (manage WITHIN the armed Plan + budget):", " • CUT a loser — `flatten` for a full exit, or cancel-then-SELL to trim (as above) — take profit, trim", " a runner, reload, add, scale or hedge; place / cancel / adjust orders that MANAGE the armed book,", " priced with the SAME `@…` grammar as above and SIZED within the standing R budget;", " • `standDown` to de-arm cleanly when the plan is spent AND you are flat (or content to let residual", " inventory ride its own TP/EXIT) — never as a substitute for SELLING a loser;", ' • `scheduleWake` your next monitoring wake, or PASS ({ "actions": [] }). But PASS is correct ONLY when', " the armed plan is genuinely managing itself — winning toward its target, or its premise still intact", " with nothing to adjust. PASS is NOT the default, and it is the WRONG move over a losing, premise-broken", " position: an empty reply there is a no-op that books the full loss.", "The admission Gate still bounds every order you place (never naked, floored at intrinsic, within the", "envelope): judgment does not buy authority — an order OUTSIDE the Mandate is refused fail-closed.", "", "YOU MAY NOT (this is the strategist's job, not yours):", " • arm NEW standing authority. DO NOT emit a `supersede` — that AUTHORS/RE-ARMS a whole Plan (a new", " thesis, a new mandate). It is BEYOND your manage-only authority: a `supersede` from you is REFUSED", " at the tier boundary and escalated, never admitted. Manage the plan you were handed with ORDERS; do", " not re-author it — and do not use it as a roundabout way to add the stop you should just SELL into.", "", "═══ MINIMAL ESCALATION — when you need a NEW plan, ASK; do not guess ═══", "Cutting a broken-premise loser is YOUR job — do it with a cancel-then-SELL, do NOT escalate to dodge it.", "Escalate ONLY when the situation genuinely needs a NEW thesis/mandate beyond managing the armed book (a", "regime change that calls for a fresh plan, not merely an exit). To escalate, `standDown` with a `reason`", "— or journal — that OPENS with the token", " RE-BRIEF: <one line on what broke and why a re-brief is needed>", "The driver routes a RE-BRIEF: escalation to the strategist for a re-author; your armed book is not torn", "down by you on a whim. Escalate SPARINGLY (minimal escalation): most wakes need only a manage action or", "a PASS. Reserve escalation for a real regime break the armed Plan cannot absorb — never as a way to", "avoid the closing SELL that cutting a loser requires." ].join(` `); var MANAGE_PROMPT_PROFILE = "manage-v1"; var PROMPT_PROFILE_SYSTEM = new Map([ [BASELINE_PROMPT_PROFILE, BASELINE_SYSTEM_PROMPT], [AUTHORING_PROMPT_PROFILE, AUTHORING_SYSTEM_PROMPT], [VIEWSHOP_PROMPT_PROFILE, VIEWSHOP_SYSTEM_PROMPT], [MANAGE_PROMPT_PROFILE, MANAGE_SYSTEM_PROMPT] ]); var REPROMPT_WITHHELD = [ "Your last turn was REJECTED and NOTHING from it was applied (a turn is ALL-OR-NOTHING).", "The specific reason was withheld here to keep this frame date-blind. Your standing book is UNCHANGED", 'and no order was placed. Resubmit a corrected turn, or pass with { "actions": [] }. Emit ONLY the JSON turn.' ].join(` `); // src/adapters/broker.ts function orderEvent(action, ts, intent, extra) { return { ts, 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: intent.qty, ...intent.strike !== undefined ? { strike: intent.strike } : {}, ...intent.right !== undefined ? { right: intent.right } : {}, px: extra.px, ...extra.reason !== undefined ? { reason: extra.reason } : {} }; } class PaperBroker { now = 0; #events = []; #drain; constructor(deps) { deps.multiplier; this.#drain = deps.drain; } get events() { return this.#events; } submit(intent) { this.#events.push(orderEvent("place", this.now, intent, { px: intent.px })); this.#events.push(orderEvent("fill", this.now, intent, { px: intent.px, reason: "paper" })); this.#drain(); return intent.ref; } cancel(_ref) { this.#drain(); } positions() { const snap = {}; for (const ev of this.#events) { if (ev.type !== "fill") continue; const key = positionKeyOf(ev); snap[key] = (snap[key] ?? 0) + (ev.side === "buy" ? ev.qty : -ev.qty); } return snap; } } class ReplayFeed { #events; constructor(events) { this.#events = events; } events() { return this.#events; } } var UNBOUNDED_PAPER_LIMITS = { maxOrderQty: Number.POSITIVE_INFINITY, maxPositionQty: Number.POSITIVE_INFINITY, maxNotionalUsd: Number.POSITIVE_INFINITY }; class LiveArmToken { limits; grant; #brand = true; constructor(limits, grant) { this.limits = limits; this.grant = grant; this.#brand; } static has(x) { return typeof x === "object" && x !== null && #brand in x; } } var PAPER_VENUES = new Map; function registerPaperVenue(venue, make) { PAPER_VENUES.set(venue, make); } function registeredPaperVenues() { return [...PAPER_VENUES.keys()].sort(); } function resolvePaperVenue(venue) { if (venue === undefined) return; const make = PAPER_VENUES.get(venue); if (make === undefined) { const known = registeredPaperVenues(); throw new Error(`makeGate: mode 'paper' names the UNREGISTERED venue ${JSON.stringify(venue)} (registered: ${known.length === 0 ? "none" : known.join(", ")}) — fail-closed, never a silent fallthrough`); } return make(); } function isKillSwitch(x) { return typeof x === "object" && x !== null && typeof x.tripped === "boolean" && typeof x.trip === "function"; } function isRiskLimits(x) { return typeof x === "object" && x !== null && typeof x.maxOrderQty === "number" && typeof x.maxPositionQty === "number" && typeof x.maxNotionalUsd === "number"; } function venueKillSwitchOf(broker) { const k = broker.killSwitch; return isKillSwitch(k) ? k : undefined; } function venueLimitsOf(broker) { const l = broker.limits; return isRiskLimits(l) ? l : undefined; } function venueMultiplierOf(broker) { const m = broker.multiplierOf; if (typeof m !== "function") return; return (intent) => broker.multiplierOf(intent); } class LiveGateRefused extends Error { mode = "live"; constructor(reason) { super(reason); this.name = "LiveGateRefused"; } } class NotImplemented extends Error { constructor(what) { super(`kestrel-7o2.4: ${what} is not implemented in this increment (SEAM + RED phase)`); this.name = "NotImplemented"; } } function makeGate(mode, deps) { switch (mode) { case "sim": { const sim = deps.sim; if (sim === undefined) { throw new Error("makeGate: mode 'sim' requires deps.sim (the SimFillEngine ingredients) — none present (fail-closed)"); } return new SimGate(sim.fill, sim.spotFill, sim.spotAllowed, sim.drain); } case "paper": { const broker = deps.broker ?? resolvePaperVenue(deps.venue); if (broker === undefined) { throw new Error(`makeGate: mode 'paper' requires deps.broker (a BrokerAdapter) or a registered deps.venue (registered: ${registeredPaperVenues().join(", ") || "none"}) — none present (fail-closed)`); } const cfg = deps.paper; const killSwitch = cfg?.killSwitch ?? venueKillSwitchOf(broker); const limits = cfg?.limits ?? venueLimitsOf(broker) ?? UNBOUNDED_PAPER_LIMITS; const arm = cfg?.arm ?? makePaperArm(limits); const brokerPull = () => broker.positions?.() ?? {}; return makeLiveClamp({ underlying: broker, arm, multiplier: cfg?.multiplier ?? venueMultiplierOf(broker) ?? 1, positions: cfg?.positions ?? brokerPull, brokerPositions: cfg?.brokerPositions ?? brokerPull, ...killSwitch !== undefined ? { killSwitch } : {}, ...cfg?.tolerance !== undefined ? { tolerance: cfg.tolerance } : {} }); } case "live": { const arm = deps.liveArm; if (arm === undefined) { throw new LiveGateRefused("live authority requires an explicit, human-signed Kestrel arm — none present (fail-closed; live is not wallet-signable, protocol/index.ts, RUNTIME §8)"); } if (!isLiveArm(arm)) { throw new LiveGateRefused("live authority requires a human-signed LiveArm minted by armLive — the supplied arm was not (a PaperArm or forgery can never authorize live, fail-closed)"); } throw new NotImplemented("live gate routing"); } default: { const never = mode; throw new Error(`makeGate: unknown mode ${JSON.stringify(never)} (fail-closed)`); } } } function positionKeyOf(o) { return o.strike !== undefined && o.right !== undefined ? `${o.instrument}|${o.strike}|${o.right}` : o.instrument; } function isLiveArm(arm) { return LiveArmToken.has(arm); } class PaperArmToken { limits; #paperBrand = true; constructor(limits) { this.limits = limits; this.#paperBrand; } static has(x) { return typeof x === "object" && x !== null && #paperBrand in x; } } function makePaperArm(limits) { return new PaperArmToken(limits); } function isPaperArm(arm) { return PaperArmToken.has(arm); } class ClampRefused extends Error { limit; ref; constructor(limit, ref, reason) { super(reason); this.name = "ClampRefused"; this.limit = limit; this.ref = ref; } } function makeKillSwitch() { let tripped = false; let reason = null; return { get tripped() { return tripped; }, get reason() { return reason; }, trip(r) { if (!tripped) { tripped = true; reason = r; } } }; } function makeLiveClamp(deps) { return new LiveClampImpl(deps); } class LiveClampImpl { #underlying; #limits; provenance; #positions; #brokerPositions; #multiplier; #tolerance; killSwitch; constructor(deps) { if (isLiveArm(deps.arm)) { this.provenance = "live"; } else if (isPaperArm(deps.arm)) { this.provenance = "paper"; } else { throw new LiveGateRefused("makeLiveClamp: the supplied arm was minted by neither armLive (a verified human signature ⇒ LiveArm) nor makePaperArm (config ⇒ PaperArm) — the L0 clamp can never be built on fabricated authority (fail-closed)"); } this.#underlying = deps.underlying; this.#limits = deps.arm.limits; this.#positions = deps.positions; this.#brokerPositions = deps.brokerPositions; this.#multiplier = deps.multiplier; this.#tolerance = deps.tolerance ?? 0; this.killSwitch = deps.killSwitch ?? makeKillSwitch(); } get now() { return this.#underlying.now; } set now(v) { this.#underlying.now = v; } get events() { return this.#underlying.events; } submit(intent) { if (this.killSwitch.tripped) { throw new ClampRefused("killed", intent.ref, `live transmission halted — kill-switch tripped: ${this.killSwitch.reason ?? "(no reason)"} (fail-closed)`); } const limits = this.#limits; if (!Number.isFinite(intent.qty)) { throw new ClampRefused("not-a-number", intent.ref, `order qty ${intent.qty} is not a finite number — every ceiling comparison against it is false, so it would satisfy maxOrderQty/maxPositionQty/maxNotionalUsd and transmit (fail-closed, bounded-risk)`); } if (!Number.isFinite(intent.px)) { throw new ClampRefused("not-a-number", intent.ref, `order px ${intent.px} is not a finite number — notional = px·qty·multiplier would be NaN and clear maxNotionalUsd silently (fail-closed, bounded-risk)`); } if (!Number.isInteger(intent.qty) || intent.qty <= 0) { throw new ClampRefused("malformed-intent", intent.ref, `order qty ${intent.qty} is not a positive integer — a negative/zero/fractional qty satisfies maxOrderQty and maxNotionalUsd by comparison (both are \`>\` tests a non-positive value passes) and sign-inverts the projected position, so it would transmit unbounded (fail-closed, bounded-risk)`); } if (intent.px <= 0) { throw new ClampRefused("malformed-intent", intent.ref, `order px ${intent.px} is not a positive number — notional = px·qty·multiplier would be zero or negative and the maxNotionalUsd ceiling ${limits.maxNotionalUsd} would enforce nothing (fail-closed, bounded-risk)`); } if (intent.qty > limits.maxOrderQty) { throw new ClampRefused("order-size", intent.ref, `order qty ${intent.qty} exceeds maxOrderQty ${limits.maxOrderQty} (fail-closed, bounded-risk)`); } const key = positionKeyOf(intent); const current = this.#positions()[key] ?? 0; const projected = current + (intent.side === "buy" ? intent.qty : -intent.qty); if (Math.abs(projected) > limits.maxPositionQty) { throw new ClampRefused("position", intent.ref, `projected net position ${projected} at ${key} exceeds maxPositionQty ${limits.maxPositionQty} (fail-closed)`); } const multiplier = typeof this.#multiplier === "function" ? this.#multiplier(intent) : this.#multiplier; if (!Number.isFinite(multiplier) || multiplier <= 0) { throw new ClampRefused("multiplier", intent.ref, `contract multiplier ${multiplier} is not a finite positive number — notional = px·qty·multiplier would be meaningless and the maxNotionalUsd ceiling ${limits.maxNotionalUsd} would enforce nothing (fail-closed, bounded-risk)`); } const notional = intent.px * intent.qty * multiplier; if (notional > limits.maxNotionalUsd) { throw new ClampRefused("notional", intent.ref, `order notional ${notional} exceeds maxNotionalUsd ${limits.maxNotionalUsd} (fail-closed)`); } return this.#underlying.submit(intent); } cancel(ref) { this.#underlying.cancel(ref); } positions() { return this.#brokerPositions(); } reconcile() { const expected = this.#positions(); const actual = this.#brokerPositions(); 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) { this.killSwitch.trip(`reconciliation break at ${key}: engine expected ${e}, broker PULL reported ${a} (Δ ${a - e}, tolerance ${this.#tolerance}) — halting live transmission (ADR-0034 §4, fail-closed)`); return; } } } } // src/session/paper.ts class PaperSessionRefused extends Error { name = "PaperSessionRefused"; failure; reason; constructor(failure, reason, options = {}) { super(`paper session refused [${failure}]: ${reason} (fail-closed; STAND_DOWN)`, options.cause === undefined ? undefined : { cause: options.cause }); this.failure = failure; this.reason = reason; } } class PaperBus { events = []; #seq = 0; append(ev) { const { seq: _venueSeq, ...rest } = ev; const stamped = { seq: this.#seq++, ...rest }; this.events.push(stamped); return stamped; } } var DEFAULT_HOOKS = { sleep: (ms) => new Promise((r) => setTimeout(r, ms)), now: () => Date.now(), notify: (line) => void process.stderr.write(line + ` `) }; var DEFAULT_PUMP_MS = 1000; function assertBoundedLimits(limits) { if (limits === undefined) { throw new PaperSessionRefused("limits-absent", "a paper session requires explicit RiskLimits — an absent ceiling falls through to UNBOUNDED_PAPER_LIMITS, which makes the seam's L0 clamp a benign no-op"); } for (const [name, v] of [ ["maxOrderQty", limits.maxOrderQty], ["maxPositionQty", limits.maxPositionQty], ["maxNotionalUsd", limits.maxNotionalUsd] ]) { if (!Number.isFinite(v) || v <= 0) { throw new PaperSessionRefused("limits-absent", `RiskLimits.${name} must be a FINITE positive ceiling (got ${String(v)}) — an unbounded ceiling is not a ceiling`); } } return limits; } function ibkrPaperVenue(transportDeps) { return { async open(cfg, req) { const [{ IbkrTransport }, { contractClientOf }, feedMod, brokerMod, dryrun] = await Promise.all([ import("./transport-ym0rs5kx.js"), import("./contract-zvzr2y3k.js"), import("./feed-5k6ma6n3.js"), import("./broker-62kwzf5m.js"), import("./order-dryrun-8jcbxyea.js") ]); const transport = new IbkrTransport(cfg, { log: req.log, ...transportDeps }); await transport.connect(); const contractDeps = { client: contractClientOf(transport), ...cfg.account === undefined ? {} : { account: cfg.account }, log: req.log }; const { underlier, legs } = await feedMod.resolveFeedContracts({ symbol: req.instrument, ...req.expiry === undefined ? {} : { expiry: req.expiry }, ...req.spot === undefined ? {} : { spot: req.spot }, ...req.halfWidth === undefined ? {} : { halfWidth: req.halfWidth } }, contractDeps); const feed = feedMod.ibkrFeed({ instrument: req.instrument, sessionDate: req.sessionDate, mode: cfg.mode, ...req.expiry === undefined ? {} : { expiry: req.expiry }, ...req.tauYears === undefined ? {} : { tauYears: req.tauYears }, ...req.marketDataType === undefined ? {} : { marketDataType: req.marketDataType }, ...cfg.account === undefined ? {} : { account: cfg.account } }, { client: feedMod.feedClientOf(transport), underlier, legs, now: req.now, log: req.log }); const tradableUnderlier = underlier.kind === "equity" ? [underlier] : []; const contracts = brokerMod.contractBook([...tradableUnderlier, ...legs]); const orderClient = brokerMod.orderClientOf(transport); const nextOrderId = dryrun.mintOrderId(orderClient); return { feed, broker: (r) => brokerMod.ibkrBroker(cfg, { ...r, client: orderClient, contracts, nextOrderId }), status: () => transport.status(), pulse: () => transport.pulse(), close: () => { feed.close(); transport.disconnect(); } }; } }; } function toModule(node) { return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] }; } async function openPaperSession(opts) { const hooks = { ...DEFAULT_HOOKS, ...opts.hooks }; const log = (line) => hooks.notify(`paper: ${line}`); const limits = assertBoundedLimits(opts.limits); if (opts.documents.length === 0) { throw new PaperSessionRefused("documents-absent", "a paper session must arm at least one standing document — a session with nothing armed has no judgment in it"); } const { resolveIbkrConfig, describeIbkrConfig, IBKR_LIVE_PORTS } = await import("./config-n9zp4g89.js"); const cfg = resolveIbkrConfig(opts.gateway ?? {}); if (cfg.mode !== "paper") { throw new PaperSessionRefused("mode-gate", `the paper session driver is PAPER-ONLY by construction (resolved mode=${cfg.mode}) — it can only ever build a PAPER gate. Live real-money routing is owner-gated (kestrel-7o2.10). Four things enforce that, none of which a flag/env/config value can lower: this mode gate; the driver's single gate construction, whose mode argument is the literal "paper" (no variable to poison, no branch to flip); the HANDSHAKE ACCOUNT ASSERTION (a session is refused and its socket closed unless EVERY account the gateway reports is a PAPER account — any non-paper or unclassifiable one, including in a mixed report, refuses, ADR-0034 §3); and the live-port refusal`); } const liveListener = IBKR_LIVE_PORTS.get(cfg.port); if (liveListener !== undefined) { throw new PaperSessionRefused("live-port", `port ${cfg.port} is IB's own REAL-MONEY API port (${liveListener}) — a paper session never aims at it, and there is no flag to proceed anyway. Use the PAPER endpoint (IB Gateway paper 4002, TWS paper 7497). Nothing was opened; no socket was ever attempted`); } log(`opening ${describeIbkrConfig(cfg)} — limits: qty≤${limits.maxOrderQty} pos≤${limits.maxPositionQty} notional≤$${limits.maxNotionalUsd}`); const modules = opts.documents.map(toModule); const venue = opts.venue ?? ibkrPaperVenue(opts.transport); let session; try { session = await venue.open(cfg, { instrument: opts.instrument,