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.

119 lines (118 loc) 7.96 kB
/** * # session/instance — the Instance sequence runner (the multi-session horizon design, kestrel-5zl.16.1) * * A multi-session **Instance** is an ordered sequence of single-session runs (CONTEXT: *Instance / * Session*): a fresh {@link SessionCore} per session over its own Bus, finalised to its own Blotter, the * whole sequence chained by a {@link SessionCarry} (the one typed hand-off, `./carry.ts`). This runner is * the **one orchestration layer ABOVE** the per-session drivers — it does not touch the per-event pass. * Each session is driven by the existing {@link runSimulateSession}; the runner only threads the carry: * * ``` * runInstance(sequence, cfg): * carry = emptyCarry() // flat open, empty book * for (k, session) of sequence: * final = (k === last) * res = runSimulateSession({ …session, openingCarry: carry, carryClose: !final, sessionOrdinal: k }) * blotters.push(res.blotter); carry = res.carry * return { blotters, carries, instanceRunId, … } * ``` * * **The single-session driver is the degenerate length-1 call.** `runInstance` over a one-element sequence * opens with `emptyCarry()` (no seeding, no re-arm) and settles the final session at intrinsic * (`carryClose:false`) — so its per-session graded bus, `SimRunId`, and Blotter are **byte-identical** to a * standalone `runSimulateSession` of that session. That length-1 invariance is the regression guard that * this layer is purely additive (proven in `tests/session.instance.test.ts`). * * **Determinism / fail-closed.** The carry into session `k+1` is a pure function of session `k`'s graded * bus (no wall clock, no RNG crosses a boundary — RUNTIME §0), so the whole sequence replays * byte-identically. A structurally-incomplete carry is refused loudly by {@link assertCarry} at the seam. */ import { emptyCarry, assertCarry } from "./carry.js"; import { runSimulateSession } from "./simulate.js"; import { InstanceRunId, SequenceCellKey, cellOf } from "./run-identity.js"; import { reanchorFrontier } from "./day.js"; /** * Drive an ordered sequence of Sessions as one Instance, threading the {@link SessionCarry} (the multi-session horizon design, * kestrel-5zl.16.1). Each session is a fresh {@link runSimulateSession}; the runner opens session 0 flat * ({@link emptyCarry}), carry-marks every non-final session's settle (`carryClose`), and settles the final * session at intrinsic — so a length-1 sequence is byte-identical to a standalone run. * * `async` because the per-session driver is (the live loop is deliberately non-deterministic; recorded / * fixed adapters resolve synchronously). Refuses an empty sequence loudly (fail-closed). * * `seedCarry` (OPTIONAL, kestrel-5zl.16.9) seeds the OPENING carry of session 0 — the minimal seam that lets * a NON-EMPTY opening frontier be driven end-to-end BEFORE the b3l producer (kestrel-b3l.1) can populate one * (until it lands, `carryFrom` only ever propagates an empty frontier, so the re-anchoring path would be * untestable through the real runner without this). It is `assertCarry`-checked (fail-closed by omission). * ABSENT ⇒ the sequence opens flat ({@link emptyCarry} with the cfg band) — DEFAULT BEHAVIOUR UNCHANGED, so * every existing caller and the length-1 byte-invariance are untouched. */ export async function runInstance(sequence, cfg, seedCarry) { if (sequence.length === 0) { throw new Error("instance: runInstance needs a non-empty session sequence (fail-closed)"); } const blotters = []; const carries = []; const captured = []; const simRunIds = []; const gradedBuses = []; const droppedWakes = []; // The LOUD drop channel (mustFix B/D): announce every non-re-anchorable carried wake, never silently. A // caller may capture it; the default writes the reasoned refusal to the real stderr (off the graded path). const onDrop = cfg.onDrop ?? ((d) => { process.stderr.write(`instance: dropped a carried wake into session ${d.sessionOrdinal}${d.reason}: ${d.why} (fail-closed, kestrel-5zl.16.9)\n`); }); // The opening carry: a caller-supplied seed (fail-closed-checked), else the flat empty open. DEFAULT // BEHAVIOUR (no seed) is byte-identical to before — `{ ...emptyCarry(), band }`. let carry = seedCarry !== undefined ? assertCarry(seedCarry) : { ...emptyCarry(), band: cfg.band ?? "day" }; for (let k = 0; k < sequence.length; k++) { const session = sequence[k]; const isFinal = k === sequence.length - 1; // Re-inject the OPENING carry's frontier as authored vantages for THIS session (kestrel-5zl.16.9): an // `atClockET` pending wake re-anchors to THIS session's `session_date` (its bus supplies the date to // `computeWakes` inside the driver — never the origin's). An unresolvable clock, and an `inMinutes` // offset (not re-anchorable at this seam, mustFix D), are DROPPED fail-closed and surfaced below — never // silently swallowed. An empty frontier (every carry today, until the b3l producer lands) re-injects // nothing and drops nothing, so a standalone / length-1 run is byte-identical. const { wakes: reanchored, dropped } = reanchorFrontier(carry.frontier); // Surface EVERY dropped wake with its reason (mustFix B/D): a structurally-unresolvable `atClockET` or a // non-re-anchorable `inMinutes` refuses LOUDLY here — recorded on the result AND streamed through // `onDrop`. The pre-fix seam discarded `.dropped` on the floor (the review's finding 3), so an // unresolvable clock was swallowed with no reason; it now fails closed observably. for (const d of dropped) { const rec = { sessionOrdinal: k, reason: d.reason, why: d.why }; droppedWakes.push(rec); onDrop(rec); } const seededWakes = [...(session.wakes ?? []), ...reanchored]; const res = await runSimulateSession({ ...(session.busPath !== undefined ? { busPath: session.busPath } : {}), ...(session.events !== undefined ? { events: session.events } : {}), agent: cfg.agentFor(k), fillModel: cfg.fillModel, rUsd: cfg.rUsd, ...(seededWakes.length > 0 ? { wakes: seededWakes } : {}), ...(session.authorCutoff !== undefined ? { authorCutoff: session.authorCutoff } : {}), ...(session.structural !== undefined ? { structural: session.structural } : {}), ...(session.instruments !== undefined ? { instruments: session.instruments } : {}), ...(session.fairTauYears !== undefined ? { fairTauYears: session.fairTauYears } : {}), ...(session.makerFairParams !== undefined ? { makerFairParams: session.makerFairParams } : {}), openingCarry: { ...carry, band: session.band ?? carry.band }, // The FINAL session settles at intrinsic (today's terminal behaviour — the position dies); every // earlier session carry-marks its held inventory to close so it survives to the next open. carryClose: !isFinal, sessionOrdinal: k, }); blotters.push(res.blotter); carries.push(res.carry); captured.push(res.captured); gradedBuses.push(res.gradedBus); // the judge-stamped record (ADR-0010) — a pure read, surfaced for assertion simRunIds.push(res.blotter.session.bus.sha256); // = SimRunId(gradedBus), a pure read (kestrel-5zl.7) carry = res.carry; } // The Instance identity chain (kestrel-5zl.16.5) — pure reads of the finalised sequence. const instanceRunId = InstanceRunId(simRunIds); const sequenceCellKey = SequenceCellKey(blotters.map((bl) => cellOf(bl))); return { blotters, carries, captured, simRunIds, instanceRunId, sequenceCellKey, gradedBuses, droppedWakes }; }