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.

133 lines 9.71 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 type { BusEvent } from "../bus/index.ts"; import type { InstrumentSpec } from "../engine/index.ts"; import type { EpisodeSigmoidParams } from "../fill/index.ts"; import type { Blotter } from "../blotter/index.ts"; import type { Agent, CapturedTurns } from "./agent.ts"; import { type SessionCarry, type TimescaleBand } from "./carry.ts"; import type { FillModelName } from "./sim.ts"; import { type TimeOfDay } from "./day.ts"; /** One session in an Instance's ordered tape (the multi-session horizon design). Exactly one of `busPath`/`events` supplies * the session's Bus; the per-session knobs mirror {@link import("./simulate.ts").RunSimulateOptions}. The * Instance-level pieces (the agent, fill model, R) live on {@link InstanceConfig}, not here. */ export interface SessionInput { readonly busPath?: string; readonly events?: readonly BusEvent[]; /** Authored wake vantages (ET times-of-day), strictly inside this session's window. */ readonly wakes?: readonly TimeOfDay[]; /** The author acting cutoff (an ET time-of-day); defaults to the canonical T-5 (09:25 ET). */ readonly authorCutoff?: TimeOfDay; /** Add the structural close cadence (T-2h…T-5m) for this session. */ readonly structural?: boolean; readonly instruments?: readonly InstrumentSpec[]; readonly fairTauYears?: (now: number) => number | null; readonly makerFairParams?: EpisodeSigmoidParams; /** This session's wake-cadence band (the multi-session horizon design) — overrides the Instance default. The wake-floor * wiring is kestrel-5zl.16.7 (P1); carried on the input now so it is a fail-closed-present field. */ readonly band?: TimescaleBand; } /** The Instance-level configuration shared across every session (the multi-session horizon design). `agentFor` is a factory * invoked once per session, in order, with the session ordinal — so each session gets its own agent * (a fresh {@link import("./agent.ts").recordedAgent} for replay, or a stateful live agent that reasons * across days). A stateless agent is `() => sharedAgent`. */ export interface InstanceConfig { readonly agentFor: (ordinal: number) => Agent; readonly fillModel: FillModelName; readonly rUsd: number; /** The Instance's default wake-cadence band (the multi-session horizon design), seeded onto the opening carry. Default `day`. */ readonly band?: TimescaleBand; /** Called ONCE per carried wake the runner cannot re-anchor onto a session (kestrel-5zl.16.9 mustFix * B/D) — an unresolvable `atClockET` clock, or an `inMinutes` offset that has no target vantage at this * seam. The runner ALSO records every drop on {@link InstanceResult.droppedWakes} (the structured report); * this hook is the LOUD side-channel so a drop is never merely a return value nothing reads. Omitted ⇒ the * real `process.stderr` (a drop is announced, never silent — AGENTS fail-closed). Off the graded path (no * clock/RNG read), so determinism is unaffected; a test pins it to capture the drops quietly. */ readonly onDrop?: (drop: DroppedWake) => void; } /** One carried wake the runner refused to re-anchor onto a session (kestrel-5zl.16.9), tagged with the * ordinal of the session it was refused AT. Surfaced on {@link InstanceResult.droppedWakes} and through * {@link InstanceConfig.onDrop} — the observable, reasoned refusal (never a silent swallow). */ export interface DroppedWake { /** The ordinal `k` of the session this carried wake was refused entry to (its opening carry held it). */ readonly sessionOrdinal: number; /** The pending wake's original `reason` (author/platform provenance — carried verbatim). */ readonly reason: string; /** WHY it could not be re-anchored (an unresolvable clock, or a non-re-anchorable `inMinutes` offset). */ readonly why: string; } /** The result of running a whole Instance (the multi-session horizon design). Per-session Blotters (each a pure projection of * its own graded bus, ADR-0011) and the ordered {@link SessionCarry}s, plus the per-session `SimRunId`s * (the graded-bus content hash the sequence identity — {@link import("./run-identity.ts").InstanceRunId}, * kestrel-5zl.16.5 — chains). The sequence-level {@link InstanceBlotter} aggregation is 5zl.16.8 (P1). */ export interface InstanceResult { readonly blotters: readonly Blotter[]; /** Each session's OUT-carry, in order — the last is the Instance's final state. */ readonly carries: readonly SessionCarry[]; /** Each session's captured agent turns, in order — the deterministic replay input, per session. */ readonly captured: readonly CapturedTurns[]; /** Each session's `SimRunId` (`= blotter.session.bus.sha256`, a pure read) — the ordered per-session * identities the {@link InstanceRunId} hashes into one Instance identity. */ readonly simRunIds: readonly string[]; /** The whole Instance's identity — `sha256` of the ORDERED per-session SimRunIds (kestrel-5zl.16.5). * Order-sensitive and deterministic: same tapes + same agent turns ⇒ identical id. */ readonly instanceRunId: string; /** The grid CELL key for this Instance as a sequence cell (kestrel-5zl.16.5) — the ordered per-session * {@link cellOf} keys. A length-1 Instance's key is the single session's cell key UNCHANGED. */ readonly sequenceCellKey: string; /** Each session's canonical GRADED bus, in order (a pure read of `res.gradedBus` — the judge-stamped * record, ADR-0010). Surfaced so a caller can assert bus-level facts a Blotter hash hides — most of all * that a re-anchored `atClockET` wake landed a `WAKE` checkpoint at the TARGET session's own 16:00 epoch * (kestrel-5zl.16.9). Byte-for-byte the same records `project(gradedBus)` folds into {@link blotters}. */ readonly gradedBuses: readonly (readonly BusEvent[])[]; /** Every carried wake the runner REFUSED to re-anchor, in session order (kestrel-5zl.16.9 mustFix B/D) — * the observable, reasoned record that an unresolvable `atClockET` / non-re-anchorable `inMinutes` failed * closed LOUDLY at the carry seam rather than vanishing. Empty on a clean run (every carry today, until * the b3l producer populates a frontier). Also streamed live through {@link InstanceConfig.onDrop}. */ readonly droppedWakes: readonly DroppedWake[]; } /** * 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 declare function runInstance(sequence: readonly SessionInput[], cfg: InstanceConfig, seedCarry?: SessionCarry): Promise<InstanceResult>; //# sourceMappingURL=instance.d.ts.map