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
JavaScript
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,