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.
826 lines • 78.1 kB
JavaScript
/**
* # session/sim — the sim Session driver + graded report (RUNTIME §7)
*
* The one object that owns a `sim` run, wired end to end over the shipped substrate. It reads a
* recorded/synthetic bus **as its clock** (`now` = each event's `ts`, RUNTIME §0), drives the
* canonical market state and the {@link PlanEngine} trigger sweep, routes the engine's orders
* through the **Gate** into a {@link SimFillEngine} (the judge, RUNTIME §6), reassesses resting
* orders on every BOOK event (and resting SPOT orders on every SPOT tick carrying an NBBO,
* ADR-0017), feeds the resulting fills **back** into the engine's org facts and onto the emitted
* bus, cash-settles held inventory off the final spot — options at intrinsic, spot inventory
* mark-to-market (ADR-0017) — and returns a graded {@link EpisodeReport}.
*
* One code path, one gate (RUNTIME §7): swapping the gate adapter is what turns this into paper
* or live; nothing else changes. Every mode crosses the same fill model, so grades compare on
* fills, not on fill assumptions (ARCHITECTURE §4).
*
* ## The per-event pass (no look-ahead — state at event N is a function of events ≤ N)
* For each bus event, in order:
* 1. **clock** — the gate's `now` is pinned to the event's `ts` (the Gate seam is `now`-less;
* the driver owns the injected clock, RUNTIME §0).
* 2. **canonical state** — {@link CanonicalState.applyEvent} folds SPOT/HEARTBEAT into the
* signal instrument's one causal coordinate (the market-fact substrate the triggers read).
* 3. **fill reassessment (BOOK only, BEFORE the sweep)** — the market acts first: every leg of
* the book reassesses the matching resting orders through {@link SimFillEngine.onBook}; a
* strict-cross floor fill is emitted **and fed back** into the engine (`engine.onEvent(fill)`)
* so inventory, basis, and TP maintenance advance — all **before** the engine reacts to that
* book. This is the load-bearing ordering (RUNTIME §7): a plan can never cancel/reprice an
* order the same book would have filled.
* 4. **engine sweep** — {@link PlanEngine.onEvent} arms/fires/manages; its PLAN + WAKE land on
* the emitted bus, its order submits/cancels go through the {@link SimGate} into the fill
* engine (each `place`/`cancel` ORDER event drained onto the emitted bus at submission time).
* Orders placed *by this sweep* rest for the **next** book (fills are reassessed on book
* events, one book at a time).
*
* At session end the fill engines **settle** at the final exec spot: filled option inventory
* settles at intrinsic, filled spot inventory marks to the final spot (ADR-0017 — no expiry, no
* intrinsic), still-resting orders expire worthless-unfilled ($0 floor). The report carries the
* floor outcome and the E[$] under `pFill` side by side (RUNTIME §6).
*
* ## Determinism (RUNTIME §0/§7 — a test, not a hope)
* Everything on the path is pure and injected-time; the emitted stream is stamped with a
* monotonic `seq` in emission order and serialized canonically. `determinism_hash` is the
* sha256 of that serialization: two runs on the same inputs produce the identical hash, and a
* zero-documents run produces a stable pass-through hash.
*
* ## Fail-closed (RUNTIME §8, all loud)
* No META header → refuse (a well-formed bus opens with exactly one META). Unknown fill model →
* refuse to grade (never default silently). No instruments → refuse. Bus corruption surfaces
* from {@link readBus} as a loud, line-located error.
*/
import { writeFileSync } from "node:fs";
import { BUS_SCHEMA, canonicalEnvelope, foldBook, groupPlanInstances, readBus, serializeBus } from "../bus/index.js";
import { CanonicalSeriesProvider, CanonicalState, FakeOrgFacts, UNKNOWN, } from "../series/index.js";
import { PlanEngine, armRefusalError } from "../engine/index.js";
import { expiryTauYears, etMinuteOfDay } from "./clock.js";
import { requireMeta, resolveSpecs, specFromMeta } from "./specs.js";
import { MakerFairCalV1, MakerFairV1, SETTLE_MARK_STALE_AFTER_MS, SimFillEngine, SpotFillEngine, StrictCrossV1, } from "../fill/index.js";
import { executionFair } from "../fair/index.js";
import {} from "../lang/index.js";
import { canonicalPlansText } from "../canonical/plans.js";
import { project } from "../blotter/index.js";
import { makeGate } from "../adapters/broker.js";
import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.js";
import { nakedShortTaint, sampledQualified, selectHeadline } from "../grade/headline.js";
import { sha256 } from "../crypto/sha256.js";
import { roundToGrid } from "../protocol/grade.js";
/**
* The machine-readable reason code a session de-arms under when the exec book's time-to-expiry is
* unknowable (kestrel-wcnd) — it prefixes the CONTROL `disarm` note, which the Blotter projects as
* a reason-carrying de-arm record (`disarms[].reason`, kestrel-2pl). Exported so a consumer (and
* the guard fixture) matches the CODE rather than scraping prose.
*/
export const FAIR_TAU_UNKNOWN_CODE = "fair-tau-unknown";
// ─────────────────────────────────────────────────────────────────────────────
// Internals
// ─────────────────────────────────────────────────────────────────────────────
/** The fill-model factory — the ONE place a name maps to an implementation. An unknown name is
* refused loudly: never grade under a fill model you did not name (RUNTIME §6/§8). */
function makeFillModel(name, makerFairParams) {
switch (name) {
case "strict-cross-v1":
if (makerFairParams !== undefined) {
throw new Error("session: --hazard-params applies only to maker-fair-v1 (fail-closed, RUNTIME §8)");
}
return new StrictCrossV1();
case "maker-fair-v1":
// Calibrated curve when params are supplied; the uncalibrated default hazard otherwise.
return makerFairParams !== undefined ? new MakerFairCalV1(makerFairParams) : new MakerFairV1();
default:
throw new Error(`session: unknown fill model ${JSON.stringify(name)} (fail-closed, RUNTIME §8)`);
}
}
/** Normalize a parsed Kestrel node into a module (a bare statement becomes a one-statement
* module) so it can be armed. */
function toModule(node) {
return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] };
}
// ─────────────────────────────────────────────────────────────────────────────
// Graded-bus judge self-description (ADR-0011 part B — the two-bus rule)
//
// A GRADED bus self-describes its judge on its opening META: the fill model that produced its fills,
// the Instance identity, and the Fidelity it may claim. These are session INPUTS (known at open), so
// the driver stamps them onto the META. An INPUT tape stays fill-model-agnostic (no stamp). Pure and
// deterministic — no wall clock, no RNG.
// ─────────────────────────────────────────────────────────────────────────────
// sha256 (the portable, synchronous digest — Bun-native fast path, Worker-portable pure-JS fallback,
// kestrel-alw.17) is imported from ../crypto/sha256.ts so a graded bus hashes byte-identically under
// `bun test`, the CLI, and a Cloudflare Worker isolate. No wall clock, no RNG.
/** The canonical parse-print text of an armed document set — the byte-stable printer projection
* (ADR-0001), documents joined by a blank line (matching the CLI's canonical plans text / the ledger's
* `plans_sha256` input). Its sha256 is {@link InstanceIdentity.version}. A `Module` is a `KestrelNode`,
* so this is the {@link canonicalPlansText} owner (canonical/plans) applied to an armed module set. */
export function canonicalPlansTextOf(modules) {
return canonicalPlansText(modules);
}
/** The fill-model judge stamp for a GRADED bus's META. `name`/`version` are the model's identity;
* `calibration_sha` is a stable digest of the calibration descriptor; `self_limitation` is what the judge
* DECLARES it could not observe (a57.4) — stamped verbatim so the projector never re-guesses it from `name`
* (the mislabel of the calibrated judge, kestrel-o32) ({@link FillModelStamp}). */
export function fillModelStampOf(model) {
const cal = model.calibration;
const calibration_sha = sha256(cal !== undefined ? JSON.stringify({ version: cal.version, calibrated: cal.calibrated }) : "none");
return {
name: model.name,
version: model.version,
calibration_sha,
self_limitation: model.self_limitation.map((l) => ({ code: l.code, note: l.note })),
};
}
/** The Instance identity for a GRADED bus's META (ADR-0011 recommendation 1, ADR-0006 names-are-data):
* `lineage` = the authored NAME (the Pod's when the document set runs a Pod, else the first authored
* plan/book statement's), `version` = `plans_sha256`, `mode` = the session mode, `pod` present ONLY for
* an actual Pod document. A zero-document grade authors nothing ⇒ an empty `lineage`. */
export function instanceIdentityOf(modules, mode) {
const version = sha256(canonicalPlansTextOf(modules));
let pod;
let firstNamed;
for (const m of modules) {
for (const st of m.statements) {
if (st.kind === "pod" && pod === undefined)
pod = st.name;
if ((st.kind === "plan" || st.kind === "book" || st.kind === "pod") && firstNamed === undefined) {
firstNamed = st.name;
}
}
}
const lineage = pod ?? firstNamed ?? "";
return { lineage, version, mode, ...(pod !== undefined ? { pod } : {}) };
}
/** Fidelity level for a mode (CONTEXT: Fidelity): sim/paper are `modeled`, live is `realized`. */
export function fidelityOf(mode) {
return mode === "live" ? "realized" : "modeled";
}
/** Stamp a GRADED bus's opening META: the input session header + the judge self-description
* (`fill_model` + `instance` + `fidelity`) and, when supplied, the author's a57.14 experimental
* `envelope` (the config's captured identity — kestrel-5zl.6) plus the FULL `config_id`
* (`sha256(canonical AgentConfig)` — the sampling-axis-complete identity the grid CellKey keys on,
* m9i.2 owner tweak). Advertises the current {@link BUS_SCHEMA} (v6 — a graded bus is the ONLY stamping
* site that follows the bump; input tapes keep their pinned literal, clock-honest wakes §1) and, for a
* verified clock-honest session, the `clock_honest: true` attestation (ADR-0040 — `true` or ABSENT,
* never `false`). Keeps the input META's `seq`/`ts`. Pure. Only a GRADED bus carries these; an INPUT
* tape's META stays judge-less (the two-bus rule, ADR-0011). The `envelope`/`config_id` keys are OMITTED
* entirely when absent — a config-less graded bus is byte-identical to before (backward-compatible;
* a57.14 leaves it `certified`). */
export function stampGradedMeta(inputMeta, judge) {
return {
seq: inputMeta.seq,
ts: inputMeta.ts,
stream: "META",
type: "session",
session_date: inputMeta.session_date,
instruments: inputMeta.instruments,
mode: inputMeta.mode,
bus_schema: BUS_SCHEMA,
fill_model: judge.fillModel,
instance: judge.instance,
fidelity: judge.fidelity,
...(judge.envelope !== undefined ? { envelope: judge.envelope } : {}),
...(judge.configId !== undefined ? { config_id: judge.configId } : {}),
...(judge.cellConfigId !== undefined ? { cell_config_id: judge.cellConfigId } : {}),
// The sampled-channel qualification claim (kestrel-9gu.12) — the headline gate's bus-derivable
// input. Omitted entirely when absent, so every unqualified graded bus is byte-identical to before.
...(judge.sampledQualification !== undefined ? { sampled_qualification: judge.sampledQualification } : {}),
// The clock-honest attestation (ADR-0040): `true` or ABSENT, never `false` (clock-honest wakes §5).
...(judge.clockHonest === true ? { clock_honest: true } : {}),
};
}
/** Assemble the canonical GRADED bus for a session: the judge-stamped opening META (seq 0) followed by
* the engine's emitted reactions (PLAN/WAKE/ORDER/TELEMETRY, incl. the settle-outcome records),
* re-stamped `seq` 1..N so the stream is gap-free from 0 and opens with exactly one META — a well-formed
* bus {@link readBus} accepts. This is the self-describing artifact the Blotter projector (slice 2) will
* consume; slice 1 uses it to PROVE totals{floor,expected} + orders[].support re-derive from bus bytes
* with no engine-state scrape. Pure. */
export function assembleGradedBus(gradedMeta, emitted) {
let seq = 0;
const out = [{ ...gradedMeta, seq: seq++ }];
// Seq-space REFERENCES re-stamp with the seqs they name (clock-honest wakes §1): a v6 deliberation
// record's `wake_seq` points at its WAKE checkpoint's seq in the EMITTED stream, so assembling the
// graded bus (which re-stamps every seq to make room for the META at 0) must remap it through the
// same old→new map — otherwise the reader's record-vs-checkpoint identity would verify against the
// wrong event. The map is explicit (never an assumed "+1") so the invariant survives any future
// re-stamping shape.
const newSeqOf = new Map();
for (const e of emitted) {
const stamped = seq++;
newSeqOf.set(e.seq, stamped);
if (e.stream === "WAKE" && e.type === "deliberation") {
const remapped = newSeqOf.get(e.wake_seq);
if (remapped === undefined) {
throw new Error(`session: deliberation record (emitted seq ${e.seq}) names wake_seq ${e.wake_seq}, which is not an earlier emitted event (fail-closed, clock-honest wakes §1)`);
}
out.push({ ...e, seq: stamped, wake_seq: remapped });
}
else {
out.push({ ...e, seq: stamped });
}
}
return out;
}
/**
* Session-scoped own-fill telemetry — the `fillEvent` provider hook (RUNTIME §3). It observes the
* ORDER events the fill engine drains onto the emitted bus and answers `filled`/`rejected`/
* `cancelled` fill-lifecycle triggers as a **definite `true` once seen** (else definite `false`,
* since it IS wired). Deliberately session-scoped and all-or-nothing: per-plan / per-leg /
* `partial-fill` / `unfilled` semantics are ABSENT-with-reason — single-leg fills are never
* partial (documented in SURFACES.md).
*
* A LEG QUALIFIER never reaches here: `ComposedProvider.fillEvent` (src/engine/plans.ts) refuses it
* upstream with a logged de-arm reason (kestrel-s3h8), precisely BECAUSE this latch is session-wide —
* answering `filled leg 1` from it would fire a plan on a stranger's fill. Do not "support" the
* qualifier here by ignoring it; scope it for real (kestrel-qbwo.2) or leave the refusal standing.
*/
class FillTelemetry {
#filled = false;
#rejected = false;
#cancelled = false;
/** Fold one drained bus event; only ORDER events matter. */
observe(ev) {
if (ev.stream !== "ORDER")
return;
if (ev.type === "fill")
this.#filled = true;
else if (ev.type === "reject")
this.#rejected = true;
else if (ev.type === "cancel" && ev.reason === "cancelled")
this.#cancelled = true;
}
/** Answer a fill-lifecycle trigger. */
assess(ev) {
switch (ev.event) {
case "filled":
return this.#filled;
case "rejected":
return this.#rejected;
case "cancelled":
return this.#cancelled;
default:
return UNKNOWN; // partial-fill / unfilled: not modeled for single-leg fills
}
}
}
/** The append-only emitted stream: stamps a monotonic `seq` in emission order and serializes
* canonically (the determinism substrate, RUNTIME §0/§1). */
class EmittedStream {
events = [];
#seq = 0;
/** Stamp the next `seq` and land the event in CANONICAL field order. The canonicalization is
* {@link canonicalEnvelope} — the SAME owner {@link import("../bus/write.ts").BusWriter} stamps
* through, never a second copy of the order — because a bare `{ seq: this.#seq++, ...ev }` would
* preserve the CALLER's key order verbatim: an emitter spreading a payload that carries
* `ts`/`stream`/`type` mid-object then serializes a non-canonical record onto the graded bus
* (`serializeEvent` is `JSON.stringify`, insertion-order-dependent). That was kestrel-pta5 —
* `theta_bleed` put `ts` after `type` — patched at the one call site with the class left open;
* kestrel-byfl closes the class here, so no present or future emitter can reintroduce it. */
emit(ev) {
const stamped = canonicalEnvelope(this.#seq++, ev);
this.events.push(stamped);
return stamped;
}
hash() {
return sha256(serializeBus(this.events));
}
}
/** The sim Gate: rests engine order intents in the {@link SimFillEngine} — or, for a SPOT/equity
* intent (no strike/right, ADR-0017 / kestrel-20f.14), in the instrument-keyed
* {@link SpotFillEngine} — and cancels them, pinning the injected clock (`now`) the driver sets
* each event. Every `place`/`cancel` drains the fill engines' fresh ORDER events onto the
* emitted bus at submission time. */
export class SimGate {
fill;
spotFill;
spotAllowed;
drain;
now = 0;
intents = [];
constructor(fill, spotFill,
/** Whether spot orders may grade in this session — only the strict-cross family has a spot
* judge today; a spot order under `maker-fair-v1` is REFUSED loudly (no spot hazard model
* exists — never grade under a judge that cannot assess the instrument, RUNTIME §8). */
spotAllowed, drain) {
this.fill = fill;
this.spotFill = spotFill;
this.spotAllowed = spotAllowed;
this.drain = drain;
}
submit(intent) {
this.intents.push(intent);
// Route on the instrument kind (ADR-0017): an option intent carries strike+right; a spot
// intent carries neither — it rests in the SpotFillEngine keyed on the instrument alone.
if (intent.strike === undefined || intent.right === undefined) {
if (!this.spotAllowed) {
throw new Error("session: a spot/equity order grades only under strict-cross-v1 — no spot hazard model exists for maker-fair-v1 (fail-closed, RUNTIME §8; ADR-0017)");
}
const ref = this.spotFill.place({
ref: intent.ref,
plan: intent.plan,
plan_instance: intent.plan_instance,
instrument: intent.instrument,
side: intent.side,
qty: intent.qty,
px: intent.px,
}, this.now);
this.drain();
return ref;
}
const ref = this.fill.place({
ref: intent.ref,
plan: intent.plan,
plan_instance: intent.plan_instance,
instrument: intent.instrument,
side: intent.side,
qty: intent.qty,
px: intent.px,
strike: intent.strike,
right: intent.right,
// Thread the directional-guard evidence (kestrel-9gu.6): the engine derived the leg's
// moneyness + covered-state onto the intent, and the FillOrder must carry them so the
// maker-fair directional guard runs end to end (the far-OTM SELL cap / covered-wing exemption
// / BUY unlock). Omitting them is exactly the drop that let a Session's far-OTM standalone
// SELL bypass the cap.
...(intent.moneyness !== undefined ? { moneyness: intent.moneyness } : {}),
...(intent.covered !== undefined ? { covered: intent.covered } : {}),
}, this.now);
this.drain();
return ref;
}
cancel(ref) {
// Returns whether a resting order was actually removed. The engine's own cancels ignore the return
// (a double-cancel / already-filled ref is a harmless no-op); an AGENT `cancelOrder` reads it to
// fail-closed refuse a ref that matches no resting order (kestrel-5zl.5). Widening void→boolean still
// satisfies the `Gate.cancel(): void` interface (a boolean return is assignable to a void one).
// Try the options book first, then the spot book (a ref lives in exactly one — the engine's
// monotonic ids never collide across the two).
const removed = this.fill.cancel(ref, this.now) || this.spotFill.cancel(ref, this.now);
this.drain();
return removed;
}
}
/** Deterministic report/grader $-grid rounding so dollars/prices are byte-stable in the report (RUNTIME
* §0). Re-homed onto the ONE grid the grader owns ({@link ../protocol/grade.ts REPORT_GRID} / `roundToGrid`,
* kestrel-z473.6) — never a local `1e8` — so an outsider's recomputation is byte-identical. */
const round = roundToGrid;
// ─────────────────────────────────────────────────────────────────────────────
// Closing put-call-parity recovery candidate (kestrel-xwf annotated recovery)
// ─────────────────────────────────────────────────────────────────────────────
/**
* Derive a closing settle spot from **put-call parity** on a two-sided book: at (near-)expiry the
* forward ≈ spot, so `C − P = S − K ⇒ S = K + (C_mid − P_mid)` for the strike nearest `nearSpot` that
* quotes BOTH a two-sided call and a two-sided put. Returns the parity spot + the strike used, or
* `null` when no strike has both sides two-sided (parity not derivable → fail-closed to a taint).
*
* Mids are used ONLY as a settle-mark of last resort when the primary feed died — never as an
* execution anchor (the never-a-mid-anchor rule governs FILL prices, RUNTIME §4) — and the result is
* always flagged as recovered-from-stale, never silently banked as a live mark.
*
* The caller supplies the CLOSING legs: {@link SessionCore} feeds this only quotes whose values are
* FRESH at the settle instant (carried on a BOOK event within the declared settle-mark staleness
* threshold, {@link SETTLE_MARK_STALE_AFTER_MS}) — a two-sided pair surviving in the FOLDED book
* from hours before the option feed died is NOT a closing strike, and "recovering" from its stale
* mids would launder one dead feed through another (the DEGRADED-DATA cell's dark-to-close shape).
*/
export function closingParitySpot(book, nearSpot) {
if (book === undefined)
return null;
const mid = (q) => q.bid !== null && q.ask !== null && q.ask >= q.bid ? (q.bid + q.ask) / 2 : null;
const calls = new Map();
const puts = new Map();
for (const leg of book.legs) {
const m = mid(leg);
if (m === null)
continue;
(leg.right === "C" ? calls : puts).set(leg.strike, m);
}
let best = null;
let bestDist = Infinity;
for (const [k, c] of calls) {
const p = puts.get(k);
if (p === undefined)
continue;
const dist = Math.abs(k - nearSpot);
if (dist < bestDist) {
bestDist = dist;
best = { spot: k + c - p, strike: k };
}
}
return best;
}
/**
* The sim runtime wired end to end — the ONE place the load-bearing per-event pass lives
* (RUNTIME §7). Owns the fill engine (the judge), the emitted stream (determinism substrate),
* the Gate, canonical market state, and the {@link PlanEngine}. Documents are armed
* incrementally ({@link SessionCore.arm}); {@link SessionCore.step} advances one bus event with
* **fills-before-sweep** ordering; {@link SessionCore.settle} cash-settles and drains. Both the
* one-shot driver and the stepped day runner drive the identical core, so their emitted streams —
* and therefore their determinism hashes — are produced by exactly the same code (no duplicated
* event loop to drift).
*/
export class SessionCore {
meta;
emitted = new EmittedStream();
fill;
/** The instrument-keyed resting-order engine for SPOT/equity orders (ADR-0017,
* kestrel-20f.14) — the spot sibling of {@link fill}; the Gate routes an intent with no
* strike/right here. Idle (zero events, zero settle outcomes) on an options-only session. */
spotFill;
gate;
engine;
state;
execSpec;
signalSpec;
/** The GATE the engine routes orders through (RUNTIME §7) — the reference {@link gate} SimGate for a
* sim session, or the injected {@link LiveGateBinding.gate} for a paper/live one (OSS-ADR-0053). The
* ONE thing that differs between the modes; every event the driver pins its `now`. */
#activeGate;
/** The injected live-progression hooks (OSS-ADR-0053), or `undefined` for a sim session — the
* discriminator the per-event pass reads to switch on the live one-bus + venue-fill feedback. */
#liveBinding;
/** Venue fills queued by {@link LiveGateWiring.appendVenueEvent} during a sweep, fed back into the
* engine AFTER it (the broker fills-after-sweep ordering). Always empty on a sim session. */
#pendingVenueFills = [];
#fairTauYears;
/** kestrel-wcnd: latched once the τ-fail-closed de-arm has fired, so the reason is logged ONCE
* per session (it cannot change — the exec book's expiry is a property of the tape). */
#fairTauDisarmed = false;
/** Whether `@fair` is the anchor this session GRADES against — i.e. `maker-fair-v1`, whose fill
* hazard is a function of `|px − fair|` (RUNTIME §6). `strict-cross-v1` ignores `@fair` entirely,
* so an unknown τ moves no decision it makes (kestrel-wcnd, see {@link #failClosedOnUnknownTau}). */
#fairIsGraded;
#fillTelemetry = new FillTelemetry();
#books = new Map();
/** The fill model object (the judge), kept for the GRADED bus's META self-description (ADR-0011 B). */
#fillModelObj;
/** The author's captured experimental envelope (a57.14 / 5zl.6), or `undefined` for a config-less run. */
#envelope;
/** The run's FULL ConfigId (m9i.2 owner tweak), or `undefined` for a config-less run. */
#configId;
/** The run's cell config axis (fillSeed stripped — ADR-0016 §3), or `undefined` when it equals `#configId`
* (a seedless run) or there is no config. Stamped as `meta.cell_config_id`; the axis `cellOf` keys on. */
#cellConfigId;
/** The sampled-channel qualification claim (kestrel-9gu.12), or `undefined` — the fail-closed default
* under which the headline gate resolves to the strict-cross floor (9gu.8 AC#7). */
#sampledQualification;
/** The clock-honest attestation (ADR-0040): `true` for a verified clocked session, else `undefined`. */
#clockHonest;
/** The armed document set, in arm order — the source of the Instance identity (lineage/version). */
#modules = [];
/** The last AUTHORED standing module (the View/Wake/Plan book) — the multi-session carry document
* (the multi-session horizon design). Excludes synthesized one-shot `placeOrder` arms (they are transient order actions, not
* the authored book), so a carried document never re-fires a stale order next session. */
#standingModuleAuthored;
#drainedCount = 0;
#spotDrainedCount = 0;
#settleSpot;
#settleTs;
#settle;
// Settle-mark provenance (kestrel-xwf): WHEN the current #settleSpot VALUE was established
// (`asOf` — a frozen feed re-printing the same px does not refresh it; the 2025-04-09 failure
// mode), and the last spot-bearing event of any kind (`lastObserved` — feed-death detection).
// Tracked as injected-clock `ts` only, never input `seq` (seq shifts under JOURNAL interleaving
// and emitted bytes must not depend on it — the a57.11 invariant). The SPOT settle reads the
// same `asOf` as its mark watermark (ADR-0017 staleMark provenance).
#settleSpotAsOfTs = null;
#settleSpotLastObservedTs = null;
/** The resolved settle mark (kestrel-xwf) — provenance + verdict + annotated recovery, after {@link settle}. */
#settleMark;
#spotSettle;
/** kestrel-xwf parity-candidate freshness: per exec-symbol leg (`strike|right`), the last two-sided
* quote CARRIED on a BOOK event and the `ts` it was carried. A leg carried with a dark side is
* DELETED (the market said the quote is gone). At settle, only legs fresh within the declared
* settle-mark staleness threshold qualify as a CLOSING strike for the parity recovery — a
* two-sided pair surviving only in the fold from before a feed death is not a closing quote. */
#parityQuotes = new Map();
constructor(opts) {
this.meta = opts.meta;
this.execSpec = opts.specs.find((s) => s.role === "exec") ?? opts.specs[0];
this.signalSpec = opts.specs.find((s) => s.role === "signal") ?? opts.specs[0];
this.#fairTauYears = opts.fairTauYears;
this.#fairIsGraded = opts.fillModel === "maker-fair-v1";
this.#settleTs = opts.firstTs;
const model = makeFillModel(opts.fillModel, opts.makerFairParams);
this.#fillModelObj = model;
this.#envelope = opts.envelope;
this.#configId = opts.configId;
this.#cellConfigId = opts.cellConfigId;
this.#clockHonest = opts.clockHonest;
// The sampled-channel qualification claim (kestrel-9gu.12) is REFUSED LOUDLY when structurally
// contradictory (RUNTIME §8): qualification without a seeded run has nothing it could qualify, and
// qualification under an uncalibrated judge would license an uncalibrated headline — the exact thing
// the gate exists to forbid (9gu.8 AC#7; ADR-0016 §5). Absent (every run today) ⇒ fail-closed floor.
if (opts.sampledQualification !== undefined) {
if (opts.fillSeed === undefined) {
throw new Error("session: sampledQualification claimed with the sampled channel OFF (no fillSeed) — nothing to qualify (fail-closed, kestrel-9gu.12)");
}
if (model.calibration?.calibrated !== true) {
throw new Error("session: sampledQualification claimed under an uncalibrated judge — never an uncalibrated headline (fail-closed, kestrel-9gu.12 / 9gu.8 AC#7)");
}
}
this.#sampledQualification = opts.sampledQualification;
// Arm the seeded sampled fill channel when the config declared a fillSeed (ADR-0016 / kestrel-9gu.8.3);
// absent ⇒ the sampler is off (fail-closed — the deterministic floor + expected rails only).
this.fill = new SimFillEngine({
model,
multiplier: this.execSpec.multiplier,
...(opts.fillSeed !== undefined ? { sampler: { runSeed: opts.fillSeed } } : {}),
});
// The spot sibling (ADR-0017): equity multiplier is the exec spec's (1 for an equity — the
// asset-class default of specFromMeta). Only the strict-cross family has a spot judge today.
this.spotFill = new SpotFillEngine({ multiplier: this.execSpec.multiplier });
// One Session path, only the gate differs (kestrel-7o2.4): the SIM driver selects its gate through
// the mode-keyed factory. `sim` returns the identical SimGate over the identical ingredients — a pure
// refactor, byte-identical to the hardwired `new SimGate(...)` this replaced.
this.gate = makeGate("sim", {
sim: {
fill: this.fill,
spotFill: this.spotFill,
spotAllowed: opts.fillModel === "strict-cross-v1",
drain: () => void this.#drain(),
},
});
this.state = new CanonicalState({ instrument: this.signalSpec.symbol });
// The LIVE-gate seam (OSS-ADR-0053): if a paper/live driver injected a `liveGate` factory, build its
// gate now (canonical state + the folded books + the emitted stream all exist) and route the engine
// through it instead of the reference SimGate. The SimGate above is still built — it stays INERT (no
// order ever rests in its fill engine) so `settle`/`buildReport` keep working untouched, and so the
// sim path is byte-identical (this whole branch is skipped when `liveGate` is absent). The venue's
// own ORDER events land on the session's single live bus through `appendVenueEvent`; a fill is queued
// for the post-sweep feedback the per-event pass drains.
this.#liveBinding = opts.liveGate?.({
state: this.state,
execSpec: this.execSpec,
signalSpec: this.signalSpec,
books: this.#books,
appendVenueEvent: (ev) => {
const { seq: _venueSeq, ...rest } = ev;
const stamped = this.emitted.emit(rest);
if (stamped.stream === "ORDER" && stamped.type === "fill")
this.#pendingVenueFills.push(stamped);
return stamped;
},
});
this.#activeGate = this.#liveBinding?.gate ?? this.gate;
const baseProvider = new CanonicalSeriesProvider(this.state, new FakeOrgFacts(), {
timeOfDayMinutes: (now) => etMinuteOfDay(now),
fillEvent: (ev) => this.#fillTelemetry.assess(ev),
});
// The paper path wraps the provider in the feed-death staleness gate (RUNTIME §3/§8) so a plan
// cannot fire off a dead line; sim leaves it bare (byte-identical).
const provider = this.#liveBinding?.wrapSeriesProvider?.(baseProvider) ?? baseProvider;
this.engine = new PlanEngine({
instruments: [...opts.specs],
seriesProvider: provider,
gate: this.#activeGate,
busWriter: { emit: (ev) => void this.emitted.emit(ev) },
rUsd: opts.rUsd,
now: opts.firstTs,
fairTauYears: opts.fairTauYears,
...(opts.tapeRegimeScopes !== undefined ? { tapeRegimeScopes: opts.tapeRegimeScopes } : {}),
});
// Multi-session open (the multi-session horizon design, kestrel-5zl.16.2): seed the carried inventory as this session's
// opening book. Each carried lot re-opens at its carry-mark basis, emitting an opening `place`+`fill`
// (drained onto the graded stream) so the Blotter projects the position and `settle` marks it. Refs are
// ordinal-namespaced so a carried lot can never collide with a fresh `agent-order-N` this session. An
// absent / empty carry seeds nothing — the existing flat-open path, byte-identical.
const carry = opts.openingCarry;
if (carry !== undefined && carry.positions.length > 0) {
const sessionOrdinal = carry.priorOrdinal + 1;
this.gate.now = opts.firstTs;
carry.positions.forEach((p, i) => {
this.fill.seedFilled({
ref: `carry:${sessionOrdinal}:${i}`,
...(p.plan !== undefined ? { plan: p.plan } : {}),
...(p.plan_instance !== undefined ? { plan_instance: p.plan_instance } : {}),
instrument: p.instrument,
side: p.side,
qty: p.qty,
px: p.basis,
strike: p.strike,
right: p.right,
}, p.basis, opts.firstTs);
});
this.#drain(); // carry the seeded place/fill ORDER events onto the emitted stream (+ own-fill telemetry)
}
}
/** Carry both fill engines' fresh ORDER events onto the emitted stream (stamping `seq`) and fold
* each into own-fill telemetry. Returns the stamped events (the caller feeds fills back). A gate
* operation touches exactly one engine, so the options-then-spot drain order never interleaves
* one call's events (deterministic; an options-only session drains zero spot events). */
#drain() {
const fresh = [
...this.fill.events.slice(this.#drainedCount),
...this.spotFill.events.slice(this.#spotDrainedCount),
];
this.#drainedCount = this.fill.events.length;
this.#spotDrainedCount = this.spotFill.events.length;
return fresh.map((e) => {
const stamped = this.emitted.emit(e);
this.#fillTelemetry.observe(stamped);
return stamped;
});
}
/** Arm one already-normalized document module (RUNTIME §5). Retained (in arm order) as the source
* of the GRADED bus's Instance identity (lineage = the authored name; version = plans_sha256). */
arm(mod, opts) {
// Arm FIRST, record the lineage only on success: a document the engine refuses to arm must NOT enter
// the Instance identity, so a fail-closed driver (which catches the throw) never stamps a phantom,
// never-armed module onto the graded META. Two refusal shapes (OSS-ADR-0045): a `plan-name-in-use`
// (F4) THROWS from armDocument directly; a per-statement `unknown-series` refusal comes back as DATA
// on the report — this session driver refuses the WHOLE document (aggregating every coded refusal, so
// nothing is silently dropped), matching the prior whole-document fail-loud boundary.
const report = this.engine.armDocument(mod, opts);
if (report.refusals.length > 0)
throw armRefusalError(report.refusals);
this.#modules.push(mod);
// The AUTHORED book is the standing document that carries across sessions (the multi-session horizon
// design, kestrel-5zl.16.2). A synthesized one-shot `placeOrder` arm is a transient order action, NOT
// the authored book — it must never become the carried document (or it would re-fire a stale order next
// session). Recorded only on a SUCCESSFUL arm (after armDocument), so a refused document never carries.
if (opts?.synthesizedOrder !== true)
this.#standingModuleAuthored = mod;
}
/** Cancel a resting order by its real Gate ref on behalf of an agent `cancelOrder` (kestrel-5zl.5 / #3c).
* Pins the injected clock and routes through the engine so the engine dump (the wake snapshot's
* `resting[]` source) and the emitted bus (the ORDER `cancel`) stay consistent in the SAME snapshot —
* never one wake late. Returns whether a resting order was actually removed (fail-closed refuse on a
* ref that matches nothing currently resting). */
cancelOrder(ref, now) {
this.gate.now = now;
return this.engine.cancelOrderByRef(ref, now);
}
/** FLATTEN an instrument on behalf of an agent `flatten` action (bd if0 / 75n). Pins the injected clock
* and routes through the engine, which — for every managing plan holding the instrument — cancels the
* plan's resting orders (neutralizing the rolling TP re-post) AND crosses a COVERED close through the
* SAME Gate a fired Plan uses (never-naked holds; SELL floored at intrinsic). The gate drains its ORDER
* cancel/place onto the emitted stream in the same step, so the wake snapshot's `resting[]` (engine dump)
* and `engineLog` (bus projection) agree within one snapshot. Returns the closed contracts + plans
* flattened (the driver's audit; a flat book returns `{0,0}` — a clean no-op). */
flatten(instrument, now) {
this.gate.now = now;
return this.engine.flatten(instrument, now);
}
/** The judge-stamped opening META of this session's GRADED bus (ADR-0011 part B): the input header
* plus `fill_model` + `instance` + `fidelity`, self-describing the judge that produced these fills.
* A pure derivation of the session inputs (fill model, armed documents, mode) — known at open. */
get gradedMeta() {
return stampGradedMeta(this.meta, {
fillModel: fillModelStampOf(this.#fillModelObj),
instance: instanceIdentityOf(this.#modules, this.meta.mode),
fidelity: fidelityOf(this.meta.mode),
...(this.#envelope !== undefined ? { envelope: this.#envelope } : {}),
...(this.#configId !== undefined ? { configId: this.#configId } : {}),
...(this.#cellConfigId !== undefined ? { cellConfigId: this.#cellConfigId } : {}),
...(this.#sampledQualification !== undefined ? { sampledQualification: this.#sampledQualification } : {}),
...(this.#clockHonest === true ? { clockHonest: true } : {}),
});
}
/** The canonical GRADED bus: the judge-stamped META (seq 0) + every emitted engine reaction (incl.
* the settle-outcome telemetry), re-stamped gap-free from 0 — the self-describing artifact a Blotter
* projector consumes, and the bus slice 1's enabling-property proof re-derives totals + support from. */
gradedBus() {
return assembleGradedBus(this.gradedMeta, this.emitted.events);
}
/**
* Project this session's canonical GRADED bus into its {@link Blotter} (kestrel-a57.1 slice 2,
* ADR-0011). A pure projection of {@link gradedBus} — the Bus is the truth, the Blotter its
* regenerable projection (ADR-0010); no engine-state scrape. The existing {@link EpisodeReport}
* (see {@link buildReport}) and the CLI/Ledger stand UNCHANGED beside it — the report→Blotter
* unification is a later cleanup. Call at finalize (after {@link settle}), as the driver does.
*/
blotter() {
return project(this.gradedBus());
}
/** Supersede the standing document set at `now` (RUNTIME §5, "document supersession"): de-arm
* un-fired plans, halt acquisition + cancel unfilled children, inventory + TP/EXIT ride. The
* caller {@link arm}s the replacement immediately after. */
supersede(now) {
this.gate.now = now;
this.engine.supersede(now);
this.#drain(); // any cancels the supersession pulled land on the emitted stream now
}
/**
* FAIL CLOSED on an unknowable τ (kestrel-wcnd). Reached when the exec instrument's BOOK names no
* resolvable `expiry` (absent / unparseable / an unresolvable tag like `1dte` or `weekly`), so
* {@link expiryTauYears} returns `null` and `@fair` is UNBUILDABLE for this book.
*
* The predecessor could not reach this state at all: it read τ off the SESSION DATE, so a book
* with no expiry — or any expiry — silently got a same-day τ. That silent assumption is the bug;
* an unknown tenor now de-arms with a machine-readable reason ({@link FAIR_TAU_UNKNOWN_CODE} on a
* CONTROL `disarm`, which the Blotter projects as a reason-carrying de-arm record, kestrel-2pl).
*
* Scope is deliberately exact, not maximal:
* - **Only under `maker-fair-v1`**, the model whose graded fill hazard IS a function of `@fair`
* (RUNTIME §6). With no `@fair` it degrades to `pFill: 0` / `kind: "no-fair"` for the whole
* session — a *silent* zero-fill floor that reads like a legitimate "nothing filled" grade.
* `strict-cross-v1` never reads `@fair`, so an unknown τ moves nothing it decides and de-arming
* a session it can still grade honestly would be refusal, not fail-closure. (The AUTHOR-side
* `@fair` anchor keeps its own ladder either way: `resolveFair` falls back to an ANNOTATED book
* value that always names itself — never a silent mid, RUNTIME §4.)
* - **Only the exec instrument's book** — the one `@fair` is priced off. A signal-side chain
* prices nothing.
* - **Once per session** — the tape's expiry cannot change, so a second record would be noise.
*
* The reason is CLASSIFIED, never echoed: it names `expiry-absent` / `expiry-unresolvable`, never
* the raw token. An expiry DATE is calendar identity, and CONTROL notes are projected onto author
* surfaces — echoing `2024-06-21` would re-leak the date the frame seam strips (`assertDateBlind`).
*/
#failClosedOnUnknownTau(ev) {
if (this.#fairTauDisarmed)
return;
if (!this.#fairIsGraded)
return;
if (ev.instrument !== this.execSpec.symbol || ev.legs.length === 0)
return;
this.#fairTauDisarmed = true;
const why = ev.expiry === undefined ? "expiry-absent" : "expiry-unresolvable";
this.emit({
ts: ev.ts,
stream: "CONTROL",
type: "disarm",
note: `${FAIR_TAU_UNKNOWN_CODE}: ${why} on the exec book — time-to-expiry is unknowable, so @fair is UNKNOWN under maker-fair-v1; de-armed (never a same-day-expiry assumption)`,
});
this.supersede(ev.ts);
}
/** Emit a raw (seq-less) bus event onto the emitted stream — the day driver's CONTROL/WAKE
* handshake records (never ORDER; those come from the gate/fill engine). */
emit(ev) {
return this.emitted.emit(ev);
}
/**
* Advance one bus event with the load-bearing ordering (RUNTIME §7): pin the clock, fold
* canonical state, then — on a BOOK event — reassess resting orders for fills (the market acts
* FIRST, fills fed back into the engine) BEFORE the engine sweep reacts to that same book.
*/
step(ev) {
// JOURNAL is author metadata, NOT an engine input (a57.11); TELEMETRY is engine OUTPUT and
// observational only (a57.9) — neither is ever an engine input. The per-event pass never reacts
// to them: returning before touching the clock, canonical state, or the engine is what makes
// the determinism invariant hold by construction — "same input events in ⇒ same engine events
// out", regardless of any JOURNAL/TELEMETRY records interleaved on the bus (e.g. when a prior
// emitted bus is replayed as input). It also means a record appended after the last market
// event cannot move the settle clock.
if (ev.stream === "JOURNAL" || ev.stream === "TELEMETRY")
return;
const live = this.#liveBinding;
// LIVE ONE-BUS (OSS-ADR-0053): a paper/live session has no separately-recorded input tape — the
// venue's own tape events and the engine's emissions share ONE Bus in arrival order. So the input
// event is appended to the emitted stream FIRST, before the sweep interleaves the engine's own
// reactions. A sim session keeps the two-bus separation (input replayed, output graded apart), so
// this is skipped and the emitted stream stays engine-output-only — byte-identical.
if (live !== undefined)
this.#appendInput(ev);
this.#activeGate.now = ev.ts;
this.#settleTs = ev.ts;
this.state.applyEvent(ev);
if (ev.stream === "TICK" && ev.type === "BOOK") {
this.#observeSpot(ev.underlier_px, ev.ts);
this.#books.set(ev.instrument, foldBook(ev, this.#books.get(ev.instrument)));
// Track parity-candidate freshness (kestrel-xwf): a two-sided leg CARRIED on this event is a
// live closing quote as of `ev.ts`; a carried dark side retires it. Un-carried legs keep their
// last freshness — and age out of the closing window naturally at settle.
if (ev.instrument === this.execSpec.symbol) {
for (const leg of ev.legs) {
const key = `${leg.strike}|${leg.right}`;
if (leg.bid !== null && leg.ask !== null)
this.#parityQuotes.set(key, { leg, ts: ev.ts });
else
this.#parityQuotes.delete(key);
}
}
// kestrel-wcnd: τ comes from the CONTRACT's own expiry, carried on THIS book event — never
// from the session date. `null` ⇒ the expiry names no resolvable date ⇒ `@fair` is UNKNOWN
// for this book, and the session fails closed (below) instead of assuming same-day.
const tau = this.#fairTauYears(ev.ts, ev.expiry);
if (tau === null)
this.#failClosedOnUnknownTau(ev);
reassessBook(ev, this.fill, () => this.#drain(), this.engine, tau);
}
// SPOT-tick reassessment (ADR-0017 / kestrel-20f.14): the spot instrument's own NBBO rides the
// SPOT tick's additive bid/ask, so resting SPOT orders reassess HERE — the market acts first,
// fills fed back into the engine BEFORE its sweep reacts to this same tick (the identical
// fills-before-sweep ordering the BOOK path keeps). An options tape carries no bid/ask, so the
// quote is dark, nothing crosses, and the emitted stream is byte-identical to before. The
// observation also folds the settle mark + its provenance (kestrel-xwf).
if (ev.stream === "TICK" && ev.type === "SPOT") {
this.#observeSpot(ev.px, ev.ts);
this.spotFill.onQuote(ev.instrument, { bid: ev.bid ?? null, ask: ev.ask ?? null, last: ev.px }, ev.ts);
for (const s of this.#drain()) {
if (s.stream === "ORDER" && s.type === "fill")
this.engine.onEvent(s);
}
}
// THE ENGINE SWEEP (RUNTIME §7). On a sim session this is a bare `engine.onEvent(ev)`, byte-identical
// to before. On a live session it also drains the venue fills queued DURING the sweep back into the
// engine (the broker fills-AFTER-sweep ordering — the venue acted, the engine reacts next), and the
// whole thing runs inside the driver's error boundary so a wall's refusal (ClampRefused /
// IbkrOrderRefused) becomes a typed session STAND_DOWN instead of an unmapped throw.
const runSweep = () => {
this.engine.onEvent(ev);
if (live !== undefined) {
for (let f = this.#pendingVenueFills.shift(); f !== undefined; f = this.#pendingVenueFills.shift()) {
live.onVenueFill?.(f);
this.engine.onEvent(f);
}
}
};
if (live?.guardSweep !== undefined)
live.guardSweep(r