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.
111 lines (110 loc) • 6.57 kB
JavaScript
/**
* # session/carry — SessionCarry, the one typed cross-boundary hand-off (the multi-session horizon design, kestrel-5zl.16.2)
*
* A multi-session Instance is an **ordered sequence of single-session runs**, each a fresh
* {@link SessionCore} over its own Bus, finalised to its own Blotter. The ONE channel between session
* *k*'s finalize and session *k+1*'s open is a {@link SessionCarry} — a **pure `carryFrom(...)`
* derivation** of the finalised session (a READ of the graded record, never a second writer, mirroring
* the {@link import("./run-identity.ts").SimRunId} pure-read discipline). What rides it:
*
* - **`positions`** — held inventory the carry-mark settle marked to close and let survive (the lots
* that did NOT expiry-settle), carried at their **carry-mark** basis. Session *k+1* re-opens them as
* its starting book (seeded into the fill engine at that basis), so the overnight gap P&L lands in
* *k+1*, never in *k*.
* - **`cash`** — the running Instance realised P&L (`prior.cash + this session's realised`). The `R`
* budget re-anchors per session (the session is the independent risk unit) — that is a property of the
* next session's construction, not of this value.
* - **`document`** — the standing authored book (View/Wake/Plan), re-armed fresh at each session open,
* minting **new plan-instance ids per session** (the engine mints per-arm, so a fresh arm = fresh ids).
* - **`frontier`** — pending unfired wakes (re-anchored to the next session). `inMinutes` offsets span
* any horizon natively; `atClockET` re-anchoring is the b3l router concern (kestrel-5zl.16.9, P1).
* - **`journal`** — author memory (JOURNAL), Instance-level context so the agent reasons across days. It
* is author metadata, **never an engine input** (a57.11), so it cannot perturb determinism.
*
* **Fail-closed by omission.** Anything NOT on this struct does not survive a boundary. A Plan's `ttl` /
* `within` window is a property of a per-session Plan instance, so it CANNOT ride the carry — it
* re-anchors when the document re-arms. {@link assertCarry} refuses a structurally-incomplete carry
* loudly (never a silent default). {@link emptyCarry} is the flat-open, empty-book origin of a sequence
* (and the degenerate opening of a length-1 Instance — byte-identical to today's flat open).
*/
/** The flat-open, empty-book origin of a sequence (the multi-session horizon design) — no inventory, zero running cash, no
* standing document, an empty frontier/journal, the default `day` band, and `priorOrdinal = -1`. The
* degenerate opening of a length-1 Instance: constructing a {@link import("./sim.ts").SessionCore} with
* this carry is byte-identical to today's flat open (no seeding, no re-arm). */
export function emptyCarry() {
return { positions: [], cash: 0, document: null, frontier: [], journal: [], band: "day", priorOrdinal: -1 };
}
/** Fail-closed structural check (the multi-session horizon design fail-closed-by-omission): every required field must be
* present and well-typed, or the carry is refused loudly — never a silent default at the boundary. */
export function assertCarry(c) {
const bad = (why) => {
throw new Error(`carry: refused — ${why} (fail-closed by omission, the multi-session horizon design)`);
};
if (!Array.isArray(c.positions))
bad("positions missing");
if (typeof c.cash !== "number" || !Number.isFinite(c.cash))
bad("cash missing or non-finite");
if (c.document !== null && typeof c.document !== "object")
bad("document must be a Module or null");
if (!Array.isArray(c.frontier))
bad("frontier missing");
if (!Array.isArray(c.journal))
bad("journal missing");
if (c.band !== "scalp" && c.band !== "day" && c.band !== "swing" && c.band !== "position")
bad("band invalid");
if (typeof c.priorOrdinal !== "number")
bad("priorOrdinal missing");
for (const p of c.positions) {
if (typeof p.ref !== "string" || p.ref.length === 0)
bad("position ref missing");
if (typeof p.qty !== "number" || !Number.isFinite(p.qty))
bad(`position ${p.ref} qty invalid`);
if (typeof p.basis !== "number" || !Number.isFinite(p.basis))
bad(`position ${p.ref} basis invalid`);
if (typeof p.multiplier !== "number" || !Number.isFinite(p.multiplier))
bad(`position ${p.ref} multiplier invalid`);
}
return c;
}
/**
* Derive the {@link SessionCarry} handed to session `ordinal+1` — a PURE read of the finalised session
* (the multi-session horizon design, kestrel-5zl.16.2). Never writes a byte back onto the graded bus or Blotter.
*
* - `positions` = the carry-mark lots the settle marked-and-kept ({@link SessionCore.carriedInventory}).
* - `cash` = `prior.cash + settle.floorTotal` (the session's realised, chained into the Instance total).
* - `document` = the session's standing authored module (the last armed), else the prior document — so a
* session that authors nothing still re-arms the standing book next session.
* - `journal` = the prior journal plus this session's JOURNAL bodies (accumulated author memory).
* - `frontier` = the prior frontier (the runner re-injects it into the next session's seed wakes via
* {@link import("./day.ts").reanchorFrontier}, 5zl.16.9; POPULATING it from beyond-window scheduled wakes
* is the b3l router's producer half).
*/
export function carryFrom(args) {
const { core, settle, prior, ordinal } = args;
const mult = core.execSpec.multiplier;
const positions = core.carriedInventory().map((lot) => ({
ref: lot.order.ref,
...(lot.order.plan !== undefined ? { plan: lot.order.plan } : {}),
...(lot.order.plan_instance !== undefined ? { plan_instance: lot.order.plan_instance } : {}),
instrument: lot.order.instrument,
side: lot.order.side,
qty: lot.order.qty,
strike: lot.order.strike,
right: lot.order.right,
basis: lot.basis,
multiplier: mult,
}));
const journalNotes = core.emitted.events
.filter((e) => e.stream === "JOURNAL")
.map((e) => ({ body: e.body }));
const standing = core.standingModule;
return assertCarry({
positions,
cash: prior.cash + settle.floorTotal,
document: standing ?? prior.document,
frontier: prior.frontier,
journal: [...prior.journal, ...journalNotes],
band: prior.band,
priorOrdinal: ordinal,
});
}