/** * # 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 type { BookEvent, BookState, BusEvent, ExperimentalEnvelope, Fidelity, FillModelStamp, InstanceIdentity, MetaEvent, Mode, NewBusEvent, OptionQuote, OrderEvent, PlanOutcome, PlanState, Right, SampledQualificationClaim, } from "../bus/index.ts"; import { BUS_SCHEMA, canonicalEnvelope, foldBook, groupPlanInstances, readBus, serializeBus } from "../bus/index.ts"; import { CanonicalSeriesProvider, CanonicalState, FakeOrgFacts, UNKNOWN, type SeriesProvider, type TriState, } from "../series/index.ts"; import { PlanEngine, armRefusalError, type Gate, type InstrumentSpec, type OrderIntent, type OrderRole } from "../engine/index.ts"; import type { FillEvent } from "../lang/index.ts"; import { expiryTauYears, etMinuteOfDay, type FairTauProvider } from "./clock.ts"; import { requireMeta, resolveSpecs, specFromMeta } from "./specs.ts"; import { MakerFairCalV1, MakerFairV1, SETTLE_MARK_STALE_AFTER_MS, SimFillEngine, SpotFillEngine, StrictCrossV1, type EpisodeSigmoidParams, type FillModel, type OrderOutcome, type SettleMark, type SpotSettleReport, } from "../fill/index.ts"; import { executionFair } from "../fair/index.ts"; import { type KestrelNode, type Module, type PlanStatement } from "../lang/index.ts"; import { canonicalPlansText } from "../canonical/plans.ts"; import { project, type Blotter } from "../blotter/index.ts"; import { makeGate } from "../adapters/broker.ts"; import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.ts"; import { nakedShortTaint, sampledQualified, selectHeadline, type FilledLeg, type HeadlinePnl } from "../grade/headline.ts"; import type { AgentConfig } from "./agent.ts"; import type { CarriedLot } from "../fill/index.ts"; import type { SessionCarry } from "./carry.ts"; import { sha256 } from "../crypto/sha256.ts"; import { roundToGrid } from "../protocol/grade.ts"; // ───────────────────────────────────────────────────────────────────────────── // Public surface // ───────────────────────────────────────────────────────────────────────────── /** The two shipped, named fill models (RUNTIME §6). Any other name is refused (fail-closed). */ export type FillModelName = "strict-cross-v1" | "maker-fair-v1"; /** * 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" as const; /** Options for {@link runSimSession}. Exactly one of `busPath`/`events` supplies the bus. * `documents` are already-parsed Kestrel nodes (a bare statement or a full module — both are * normalized to a module and armed). Instrument contract facts are not on the v1 bus META, so * they are derived from the asset class (tick `0.01`, strike step `1`, multiplier `100` for an * option-underlier else `1`, role carried from META) unless `instruments` overrides them. * `fairTauYears` injects time-to-expiry so `@fair` (and the maker-fair hazard) can be built; * **absent, it defaults to the CONTRACT-expiry tau** ({@link expiryTauYears}: `max(0, 16:00 ET on * the book's OWN expiry − now)`, RUNTIME §4) — so `@fair` is buildable by default, decays toward * the contract's expiry (not this session's close, kestrel-wcnd), and goes UNKNOWN (fail-closed, * see {@link SessionCore.step}) when a book names no resolvable expiry. */ export interface RunSimOptions { readonly busPath?: string; readonly events?: readonly BusEvent[]; readonly documents: readonly KestrelNode[]; readonly fillModel: FillModelName; readonly rUsd: number; readonly out?: string; readonly instruments?: readonly InstrumentSpec[]; readonly fairTauYears?: FairTauProvider; /** Calibrated maker-fair fill params (the `episode-sigmoid-u-v1` form). When present with * `fillModel: "maker-fair-v1"`, the session grades under the calibrated curve instead of the * uncalibrated default hazard (RUNTIME §6). Ignored by `strict-cross-v1`. */ readonly makerFairParams?: EpisodeSigmoidParams; /** An optional agent config (kestrel-5zl.6). When supplied AND it declares an authoring identity, its * a57.14 experimental envelope is captured (`envelopeForConfig`) and stamped on the graded META. A * config-less run (the default) stamps no envelope — byte-identical to before (backward-compatible). */ readonly config?: AgentConfig; } /** One transition in a plan's lifecycle trace (RUNTIME §5), reconstructed from the emitted PLAN * stream. */ export interface PlanLifecycleStep { readonly ts: number; readonly state: PlanState; readonly outcome?: PlanOutcome; readonly reason?: string; } /** A plan's lifecycle trace + its terminal state (the outcome when `done`, else the last * lifecycle state). ONE trace is one exact Plan **instance** (kestrel-22j.15): a legal same-name replacement * is its OWN trace beside its predecessor, never collapsed into it. */ export interface PlanTrace { /** The authored plan NAME — the Lineage aggregation key (a name may legally recur; NOT the instance id). */ readonly plan: string; /** The exact Plan-instance identity this trace belongs to (kestrel-22j.15) — the id the engine minted, * carried on the emitted PLAN stream. Distinct per instance, so two same-name instances are two traces. */ readonly plan_instance: string; readonly lifecycle: readonly PlanLifecycleStep[]; readonly final_state: PlanState | PlanOutcome; } /** One gate order, joined across the intent (resolution annotation, role), the engine child * (esc stage), and the fill engine outcome (`pFill`, floor fill, settle P&L). */ export interface OrderReport { readonly ref: string; readonly plan: string; /** The authoring plan's exact INSTANCE identity (kestrel-22j.15) — carried from the order intent so an * order joins its exact instance, not merely its recurring name. */ readonly plan_instance: string; readonly role: OrderRole; readonly instrument: string; readonly side: "buy" | "sell"; readonly qty: number; /** The option strike; ABSENT for a spot/equity order (ADR-0017 — no fictional strike). */ readonly strike?: number; /** The option right; ABSENT for a spot/equity order (ADR-0017). */ readonly right?: Right; readonly px: number; /** The price-resolution annotation (`fair=fallback(mid)`, …) — a silent mid is forbidden, * RUNTIME §4. */ readonly annotation: string; /** The esc stage this resting order occupied (RUNTIME §4/§5) — the ladder rung, NOT a count of * reprices (the honest session-wide reprice tally is `session.reprice_count`, F6). */ readonly esc_stages: number; /** Expected fill probability `1 − survival` (`1` on a floor fill). */ readonly pFill: number; /** Did the strict-cross floor fire? */ readonly filled: boolean; /** The floor fill price, else `null`. */ readonly fillPx: number | null; /** Realized $ under the floor (0 unless floor-filled). */ readonly floorPnl: number; /** E[$] under `pFill`. */ readonly expectedPnl: number; /** Present + `true` when the order was cancelled mid-session (esc-replaced, TTL/INVALIDATE * pulled, or CANCEL-IF) rather than filled or settled. */ readonly cancelled?: boolean; } /** The graded sim report (RUNTIME §7). Stamps its judge (`fill_model`) and carries the * determinism hash of the emitted event stream. */ export interface EpisodeReport { readonly session: { readonly date: string; readonly instruments: readonly string[]; readonly mode: "sim"; readonly fill_model: string; readonly bus_events: number; /** sha256 of the emitted PLAN/WAKE/ORDER stream — pins the engine's OUTPUT (RUNTIME §0/§7). */ readonly determinism_hash: string; /** sha256 of the raw INPUT bus (serialized events) — pins what the run was graded against, so * determinism_hash (output) and bus_sha256 (input) together bind the whole computation (F6). */ readonly bus_sha256: string; /** Count of actual cancel/replace reprice submissions across the session (peg + esc), from the * engine — the honest reprice tally (F6; `orders[].esc_stages` is the ladder rung, not a count). */ readonly reprice_count: number; /** The **last bus event `ts`** — the session clock's final value (settle time). Injected, never * wall-clock (RUNTIME §0); the natural deterministic `now` for recording this run into the * ledger (ADR-0006). Equals `firstTs` for an empty bus. */ readonly settle_ts: number; }; readonly plans: readonly PlanTrace[]; readonly orders: readonly OrderReport[]; /** The three-channel P&L contract (kestrel-9gu.12): floor + expected always, sampled on a seeded run, * and the HEADLINE selected by the fail-closed qualification gate (floor until 9gu.8.6 passes). */ readonly totals: { /** Realized $ under the deterministic strict-cross floor — the honest lower bound. */ readonly realized_floor_usd: number; /** E[$] under the survival product — the analytic bound beside the realization. */ readonly expected_usd: number; /** The seeded causal realization (one ensemble replicate, ADR-0016). Present ONLY on a `fillSeed` * run — an un-run channel is never conflated with a zero outcome (kestrel-9gu.12). */ readonly sampled_usd?: number; readonly premium_spent: number; /** The settle mark's provenance + fail-closed staleness verdict + annotated recovery * (kestrel-xwf): the spot the book cash-settled against (`px` — the parity-recovered value when * `source: "parity"`), WHEN the primary mark's value was last established on the input tape * (`as_of_ts`; a frozen feed re-printing the same px does not refresh it), its age vs the settle * instant, and the `stale` verdict under the declared threshold. `stale: true` means the totals * above are graded against a mark the tape stopped supporting — NON-BANKABLE (a parity recovery * annotates, it does not pardon), never a silently fresh-looking number (the taint also rides the * Blotter's `totals.headline`). `source`/`mark_uncertain`/`note` are the recovery annotation: * `"parity"` = recovered honestly from a two-sided closing book; `"stale"` = refused — * `mark_uncertain: true`, the last mark retained. Explicit `null`s (never omitted keys) so the * report's shape is fixed. */ readonly settle_mark: { readonly px: number; readonly as_of_ts: number | null; readonly last_observed_ts: number | null; readonly age_ms: number | null; readonly stale_after_ms: number; readonly stale: boolean; readonly source: "market" | "parity" | "stale"; readonly mark_uncertain: boolean; readonly note: string | null; }; /** WHICH channel is the headline + the gate verdict that selected it + the xwf settle taint * ({@link HeadlinePnl}, kestrel-9gu.12). `floor` today; `sampled` once the 9gu.8.6 qualification is * on the record (never with extrapolated-support sampled realizations — bankableEv stays). The * settle-mark staleness verdict above feeds this headline's taint (a stale mark — including a * parity-recovered one — taints whichever channel leads). */ readonly headline: HeadlinePnl; }; readonly emitted: Readonly>; } /** * @deprecated Renamed to {@link EpisodeReport} (kestrel-markets-jcn0): the * deterministic execution unit is an **Episode**, not a "Kestrel Session" * ("session" is now reserved for the stateful TerminalSession seat, * PLAT-ADR-0052). This alias keeps `engine.session.SessionReport` type-importers * compiling across the rename; it will be removed after the 0.8.0 public cutover. * The on-the-wire report shape (its `session` payload field, `sessionId` keys) is * unchanged — this is a type-name-only rename, wire contract untouched. */ export type SessionReport = EpisodeReport; // ───────────────────────────────────────────────────────────────────────────── // 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: FillModelName, makerFairParams?: EpisodeSigmoidParams): FillModel { 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: KestrelNode): Module { 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: readonly Module[]): string { 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: FillModel): FillModelStamp { 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: readonly Module[], mode: Mode): InstanceIdentity { const version = sha256(canonicalPlansTextOf(modules)); let pod: string | undefined; let firstNamed: string | undefined; 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: Mode): Fidelity { 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: MetaEvent, judge: { fillModel: FillModelStamp; instance: InstanceIdentity; fidelity: Fidelity; envelope?: ExperimentalEnvelope; configId?: string; cellConfigId?: string; sampledQualification?: SampledQualificationClaim; /** The clock-honest attestation (ADR-0040 / kestrel-w7la.1): pass `true` ONLY when the session ran * clocked semantics AND the data-floor verification held. The stamp is `true` or ABSENT — never * `false` (`false` is reserved platform-side as a revocation value; clock-honest wakes §5). */ clockHonest?: true; }, ): MetaEvent { 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: MetaEvent, emitted: readonly BusEvent[]): BusEvent[] { let seq = 0; const out: BusEvent[] = [{ ...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 } as BusEvent); } else { out.push({ ...e, seq: stamped } as BusEvent); } } 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: BusEvent): void { 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: FillEvent): TriState { 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 { readonly events: BusEvent[] = []; #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: NewBusEvent): BusEvent { const stamped = canonicalEnvelope(this.#seq++, ev); this.events.push(stamped); return stamped; } hash(): string { 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 implements Gate { now = 0; readonly intents: OrderIntent[] = []; constructor( private readonly fill: SimFillEngine, private readonly spotFill: SpotFillEngine, /** 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). */ private readonly spotAllowed: boolean, private readonly drain: () => void, ) {} submit(intent: OrderIntent): string { 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: string): boolean { // 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: Pick | undefined, nearSpot: number, ): { spot: number; strike: number } | null { if (book === undefined) return null; const mid = (q: OptionQuote): number | null => 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: { spot: number; strike: number } | null = 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; } // ───────────────────────────────────────────────────────────────────────────── // SessionCore — the reusable sim substrate (the per-event pass, single-sourced) // ───────────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────────── // The LIVE-gate seam (OSS-ADR-0053) — how a non-sim progression (paper/live) plugs its own Gate // and venue-fill feedback into the ONE per-event pass, so sim and paper differ ONLY by the adapters // at these seams. Absent (every sim/day/simulate caller) ⇒ the core builds its reference SimGate over // its own fill engine and its per-event pass is byte-identical to before. // ───────────────────────────────────────────────────────────────────────────── /** The wired core internals a {@link SessionCoreOptions.liveGate} factory is handed to build its Gate. * It reads canonical state / the folded books the core already owns (never a second copy) and appends * the venue's own ORDER events onto the session's single live bus. */ export interface LiveGateWiring { readonly state: CanonicalState; readonly execSpec: InstrumentSpec; readonly signalSpec: InstrumentSpec; /** The core's folded option books (per instrument) — the same map a Frame renders, read by a paper * gate's price-anchor wall so the driver keeps no second book of its own. */ readonly books: ReadonlyMap; /** Append ONE venue-emitted ORDER event onto the session's single live bus (stamps `seq`, stripping * any venue-local one), returning the stamped event. A `fill` is additionally queued to be fed back * into the engine AFTER the sweep (the broker fills-after-sweep ordering — the venue acts, the engine * reacts next). */ appendVenueEvent(ev: BusEvent | NewBusEvent): BusEvent; } /** What a {@link SessionCoreOptions.liveGate} factory returns — the injected Gate plus the two thin * hooks the live progression needs the core's per-event pass to call. Every field but `gate` is * optional; the sim path uses none of this. */ export interface LiveGateBinding { /** The gate orders route through (the L0-clamped paper/live gate) — REPLACES the reference SimGate. * Its `now` is pinned by the core each event exactly as `SimGate.now` is (RUNTIME §0). */ readonly gate: Gate & { now: number }; /** Wrap the core's canonical series provider before the engine reads it — the paper path wraps it in * the feed-death staleness gate so a plan cannot fire off a dead line. Absent ⇒ the bare provider. */ wrapSeriesProvider?(base: SeriesProvider): SeriesProvider; /** Fold a venue fill into the driver's expected-position ledger (the reconciliation input), called * per fill BEFORE it is fed back to the engine. */ onVenueFill?(fill: OrderEvent): void; /** Run the engine sweep (+ the post-sweep venue-fill feedback) inside the driver's own error boundary * — the paper path maps a wall's `ClampRefused`/`IbkrOrderRefused` throw into a typed session * STAND_DOWN here. Absent ⇒ the sweep runs bare and any throw propagates. */ guardSweep?(apply: () => void): void; } /** What a {@link SessionCore} is constructed from — the pieces both the one-shot * {@link runSimSession} and the stepped {@link import("./day.ts").runDaySession} agree on. */ export interface SessionCoreOptions { readonly meta: MetaEvent; readonly specs: readonly InstrumentSpec[]; readonly fillModel: FillModelName; readonly rUsd: number; readonly fairTauYears: FairTauProvider; readonly makerFairParams?: EpisodeSigmoidParams; /** The initial injected clock (the first bus event's `ts`). */ readonly firstTs: number; /** The author's a57.14 experimental envelope, captured from the run's config (kestrel-5zl.6), stamped on * the graded META at open. Absent for a config-less run (a NO-LLM Backtest / day session) ⇒ no envelope. */ readonly envelope?: ExperimentalEnvelope; /** The FULL `ConfigId` of the run's config (`deriveConfigId(canonicalizeConfig(config))`, kestrel-5zl.6), * stamped on the graded META alongside `envelope` (m9i.2 owner tweak — the seeded RUN identity feeding * `SimRunId`). Supplied under the SAME condition as `envelope`; absent ⇒ no stamp. */ readonly configId?: string; /** The **cell config axis** (`cellConfigId(config)` — the full ConfigId with the `fillSeed` REPLICATE axis * stripped, kestrel-9gu.8.1 / ADR-0016 §3), stamped as `meta.cell_config_id`. Supplied ONLY when it DIVERGES * from {@link configId} (the config carried a `fillSeed`); absent for a seedless run, whose cell axis equals * `config_id` (so the graded bus stays byte-identical). The config axis the grid `cellOf` keys the cell on: * seed-only replicates share it. */ readonly cellConfigId?: string; /** The prior session's {@link SessionCarry} (the multi-session horizon design, kestrel-5zl.16). When present with non-empty * `positions`, this session OPENS with that carried inventory seeded into the fill engine at its * carry-mark basis (emitting an opening `place`+`fill` per lot). **Absent — or an `emptyCarry()` — * seeds nothing, so every existing single-session caller (and a length-1 sequence) is byte-identical * to today's flat open.** The standing-document re-arm is the driver's concern, not the core's. */ readonly openingCarry?: SessionCarry; /** The SamplingConfig fill-realization seed (kestrel-9gu.8.1/.8.3, ADR-0016). Present ⇒ the fill engine * draws CAUSAL sampled fills off a `fillSeed`-keyed per-episode substream; absent ⇒ the sampled channel is * OFF (fail-closed — floor + expected only). Derived from the run's config via `fillSeedOf`. */ readonly fillSeed?: number; /** The owner-recorded sampled-channel qualification claim (kestrel-9gu.8.6 → 9gu.12), derived from the * run's config via `sampledQualificationOf`. Present ⇒ stamped on the graded META and judged by the * headline gate. REFUSED LOUDLY (RUNTIME §8) when structurally contradictory: a claim on an unseeded run * (nothing to qualify) or under an uncalibrated judge (never an uncalibrated headline). Absent — every * run today — ⇒ the gate fails closed and strict-cross floor stays the headline (9gu.8 AC#7). */ readonly sampledQualification?: SampledQualificationClaim; /** The complete regime-scope vocabulary of the tape (kestrel-ocyf), pre-scanned from the full bus by * callers that hold it up front (`runSimSession` / `runSimulateSession`). Threaded to the engine so * `armDocument` can issue the DEFINITIVE dead-on-arrival notice for a regime gate whose scope is * never written on this tape (vs the conditional not-yet-written notice when the future is unknown). */ readonly tapeRegimeScopes?: readonly string[]; /** The clock-honest attestation input (ADR-0040 / kestrel-w7la.1): the driver passes `true` ONLY when * the session ran clocked semantics AND the §4 data-floor verification held (it refuses at open * otherwise, so a constructed core with this flag is an attested one). Stamped as the graded META's * `clock_honest: true`; absent ⇒ latency-blind ⇒ no stamp, byte-identical META. */ readonly clockHonest?: true; /** The LIVE-gate seam (OSS-ADR-0053): a factory that, given the wired core internals, returns the * gate a NON-sim progression (paper/live) routes orders through, plus its venue-fill hooks. Present * ONLY for the paper/live drivers; ABSENT for every sim/day/simulate caller — and when absent the * core builds its reference SimGate over its own fill engine and its per-event pass is byte-identical * to before (the sim corpus never sees this field). The factory is called ONCE at construction, after * canonical state + the emitted stream exist. */ readonly liveGate?: (wiring: LiveGateWiring) => LiveGateBinding; } /** * 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 { readonly meta: MetaEvent; readonly emitted = new EmittedStream(); readonly fill: SimFillEngine; /** 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. */ readonly spotFill: SpotFillEngine; readonly gate: SimGate; readonly engine: PlanEngine; readonly state: CanonicalState; readonly execSpec: InstrumentSpec; readonly signalSpec: InstrumentSpec; /** 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`. */ readonly #activeGate: Gate & { now: number }; /** 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. */ readonly #liveBinding: LiveGateBinding | undefined; /** 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. */ readonly #pendingVenueFills: OrderEvent[] = []; readonly #fairTauYears: FairTauProvider; /** 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}). */ readonly #fairIsGraded: boolean; readonly #fillTelemetry = new FillTelemetry(); readonly #books = new Map(); /** The fill model object (the judge), kept for the GRADED bus's META self-description (ADR-0011 B). */ readonly #fillModelObj: FillModel; /** The author's captured experimental envelope (a57.14 / 5zl.6), or `undefined` for a config-less run. */ readonly #envelope: ExperimentalEnvelope | undefined; /** The run's FULL ConfigId (m9i.2 owner tweak), or `undefined` for a config-less run. */ readonly #configId: string | undefined; /** 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. */ readonly #cellConfigId: string | undefined; /** 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). */ readonly #sampledQualification: SampledQualificationClaim | undefined; /** The clock-honest attestation (ADR-0040): `true` for a verified clocked session, else `undefined`. */ readonly #clockHonest: true | undefined; /** The armed document set, in arm order — the source of the Instance identity (lineage/version). */ readonly #modules: Module[] = []; /** 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: Module | undefined; #drainedCount = 0; #spotDrainedCount = 0; #settleSpot: number | undefined; #settleTs: number; #settle: ReturnType | undefined; // 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: number | null = null; #settleSpotLastObservedTs: number | null = null; /** The resolved settle mark (kestrel-xwf) — provenance + verdict + annotated recovery, after {@link settle}. */ #settleMark: SettleMark | undefined; #spotSettle: SpotSettleReport | undefined; /** 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. */ readonly #parityQuotes = new Map(); constructor(opts: SessionCoreOptions) { 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 as (BusEvent | NewBusEvent) & { seq?: number }; const stamped = this.emitted.emit(rest as NewBusEvent); if (stamped.stream === "ORDER" && stamped.type === "fill") this.#pendingVenueFills.push(stamped as OrderEvent); 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(): BusEvent[] { 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: Module, opts?: { synthesizedOrder?: boolean }): void { // 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: string, now: number): boolean { 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: string, now: number): { closedQty: number; plansFlattened: number } { 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(): MetaEvent { 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 as const } : {}), }); } /** 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(): BusEvent[] { 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(): 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: number): void { 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: BookEvent): void { 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: NewBusEvent): BusEvent { 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: BusEvent): void { // 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 = (): void => { 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(runSweep); else runSweep(); } /** Append one INPUT tape event onto the session's single live bus (OSS-ADR-0053), stripping the * venue's own `seq` so the session stamps its own monotonic one — two `seq` spaces on one bus is not * a bus. Live sessions only; a sim session never calls this. */ #appendInput(ev: BusEvent): BusEvent { const { seq: _venueSeq, ...rest } = ev as BusEvent & { seq?: number }; return this.emitted.emit(rest as NewBusEvent); } /** Fold one spot observation into the settle mark + its provenance (kestrel-xwf). The as-of * refreshes ONLY when the observed VALUE changes: a frozen feed that keeps re-printing the same px * (2025-04-09: 174.13 from 13:01 through the close) must not launder an hours-old mark into a * fresh-looking one. Every observation refreshes `lastObserved` (feed-death disambiguation). A * genuinely motionless px past the threshold therefore also grades stale — fail-closed: taint for * a human, never a silent fresh number (RUNTIME §8). */ #observeSpot(px: number, ts: number): void { this.#settleSpotLastObservedTs = ts; if (this.#settleSpot === px && this.#settleSpotAsOfTs !== null) return; // frozen re-print: as-of stays this.#settleSpot = px; this.#settleSpotAsOfTs = ts; } /** Cash-settle at the final exec spot (RUNTIME §6): held OPTION inventory → intrinsic; held * SPOT inventory → mark-to-final-spot (ADR-0017 — no expiry, no intrinsic; the mark carries the * kestrel-xwf as-of watermark, so a stale final print is flagged, never silently marked to); * resting → $0. Threads the settle mark's provenance so the settle spot never grades without its * as-of/staleness verdict (kestrel-xwf). Drains the expired-unfilled cancels + settle telemetry * onto the emitted stream. Call once, * after the last step. * * `opts.carry` (the multi-session horizon design, kestrel-5zl.16.3) — a NON-final session of a multi-session Instance — * marks held OPTION inventory to close and lets it SURVIVE into {@link carriedInventory} (the emitted bus is * byte-identical to an intrinsic settle; only the bus-silent carried hand-off differs). Absent / `false` * — the final or a standalone session — is today's terminal intrinsic settle exactly (the position dies), * so a length-1 sequence is byte-for-byte unchanged. SPOT inventory carry (the overnight equity swing) is * NOT built yet (kestrel-20f.8): a carry settle that would strand a net spot position REFUSES loudly * rather than silently terminal-marking it (fail-closed, RUNTIME §8); a flat/closed spot book carries fine. * * kestrel-xwf: the mark's provenance is threaded to the engine, which grades it fail-closed (stale = * unstated as-of OR older than the declared threshold) and — when STALE — applies the closing * put-call-parity candidate derived here from the last folded two-sided book of the exec symbol * (the ANNOTATED recovery: `source: "parity"`, the book settles against the recovered spot) or * honestly refuses (`source: "stale"`, mark-uncertain, the last mark retained). The unified * `settle_mark` TELEMETRY record rides the graded bus whenever at least one order settled. */ settle(opts?: { readonly carry?: boolean }): ReturnType { // Fail-closed-LOUD close (kestrel-j43g): a still-armed, never-fired plan whose entry WHEN spent the // whole session UNRESOLVABLE — it names a series this tape never wrote, or it never once reached a // definite verdict — de-arms HERE with the classified reason, instead of ending the session `armed` // with no receipt on any surface. A plan with no `ttl` never reaches the y6vo ttl drop site, so this // is the only close-out it can get. Deterministic: the injected settle instant, never a clock // (RUNTIME §0). A definite-but-never-crossed plan is legitimately waiting and is left untouched, so // an ordinary idle session stays byte-identical. Emitted BEFORE the fill settle so the plan's // terminal step precedes the settle telemetry, and drained by the `#drain()` below. this.engine.deArmUnresolvedAtClose(this.#settleTs); // The parity CANDIDATE (kestrel-xwf annotated recovery) — derived from the CLOSING legs only: // quotes carried two-sided on a BOOK event within the declared staleness threshold of the settle // instant (a pair surviving only in the fold from before a feed death is not a closing strike). // The engine applies the candidate only on a STALE verdict. const closingLegs = [...this.#parityQuotes.values()] .filter((q) => this.#settleTs - q.ts <= SETTLE_MARK_STALE_AFTER_MS) .map((q) => q.leg); const parity = closingParitySpot(closingLegs.length > 0 ? { legs: closingLegs } : undefined, this.#settleSpot ?? 0); this.#settle = this.fill.settle(this.#settleSpot ?? 0, this.#settleTs, { ...(opts?.carry !== undefined ? { carry: opts.carry } : {}), mark: { asOfTs: this.#settleSpotAsOfTs, lastObservedTs: this.#settleSpotLastObservedTs, ...(parity !== null ? { parity: { spot: round(parity.spot), strike: parity.strike } } : {}), }, }); this.#settleMark = this.#settle.settleMark; // Spot inventory marks to the same RESOLVED settle spot, carrying the SAME kestrel-xwf // value-established as-of as its watermark (a frozen re-print never refreshes it) — `staleMark` // flags a mark whose source predates the settle instant (ADR-0017 provenance). this.#spotSettle = this.spotFill.settle( this.#settleMark.px, this.#settleTs, this.#settleSpotAsOfTs ?? this.#settleTs, ); if (opts?.carry === true) { const net = new Map(); for (const o of this.#spotSettle.outcomes) { if (!o.floorFilled) continue; net.set(o.instrument, (net.get(o.instrument) ?? 0) + (o.side === "buy" ? o.qty : -o.qty)); } for (const [instrument, qty] of net) { if (qty !== 0) { throw new Error( `session: carry settle with a net spot position (${qty} ${instrument}) — spot overnight carry is not built (kestrel-20f.8); fail-closed, RUNTIME §8`, ); } } } this.#drain(); return this.#settle; } /** The carry-mark lots a `settle({ carry: true })` marked-and-kept — this session's contribution to the * next session's opening book (the multi-session horizon design). Empty after a final / intrinsic settle. */ carriedInventory(): readonly CarriedLot[] { return this.fill.carriedInventory(); } /** The spot settle report (mark-to-final-spot, ADR-0017), after {@link settle}. Empty outcomes * on an options-only session. */ get spotSettle(): SpotSettleReport | undefined { return this.#spotSettle; } /** The standing AUTHORED module at session end — the last authored View/Wake/Plan book (the carried * standing document, re-armed fresh next session), or `undefined` if this session authored no book * (synthesized one-shot orders do not count). */ get standingModule(): Module | undefined { return this.#standingModuleAuthored; } /** Every armed plan STATEMENT the core holds, in arm order (kestrel-wa0j.29) — the typed ASTs the sim * driver captures the ENFORCED terms from for the `armed-plan` pane (the watcher's role-keyed percept: * it manages plans whose document text it otherwise never sees). A read-only projection of the armed * module set — it holds the SAME ASTs the engine armed, never a re-parse. */ get armedPlanStatements(): readonly PlanStatement[] { const out: PlanStatement[] = []; for (const mod of this.#modules) for (const s of mod.statements) if (s.kind === "plan") out.push(s); return out; } /** The resolved settle-mark provenance + verdict + annotated recovery (kestrel-xwf), available * after {@link settle} (the engine's {@link SettleMark}, unchanged). */ get settleMark(): SettleMark | undefined { return this.#settleMark; } /** Build the graded report (after {@link settle}). `busEventCount` and `busSha256` pin the * input bus alongside the emitted-stream determinism hash (F6). */ buildReport(busEventCount: number, busSha256: string): EpisodeReport { if (this.#settle === undefined) throw new Error("session: buildReport before settle"); return buildReport( this.meta, busEventCount, this.emitted, this.gate, this.#settle, busSha256, this.#settleTs, this.#spotSettle, this.#sampledQualification, ); } /** The latest folded option book per instrument — the chain a Frame renders. */ get books(): ReadonlyMap { return this.#books; } /** The current injected clock (the last stepped event's `ts`, or `firstTs` before any step). */ get settleTs(): number { return this.#settleTs; } } // `requireMeta`/`resolveSpecs`/`specFromMeta` — the driver's PURE header rules (META → instruments) — // live in `./specs.ts` (factored light for the CLI's `validate --arm` context derivation, kestrel-elu9) // and are re-exported here unchanged so every existing consumer (simulate/paper/session barrel) keeps // importing them from the sim driver. NOTE: one `import` + an export LIST, deliberately — pairing an // `import` with a separate `export … from` of the same names makes `bun build --splitting` emit a // chunk with duplicate export statements, which node rejects at load ("Duplicate export of // 'requireMeta'"), killing every bundled verb. export { requireMeta, resolveSpecs, specFromMeta }; /** The sha256 of the serialized input bus — pins WHAT a run was graded against (F6). */ export function busSha256Of(events: readonly BusEvent[]): string { return sha256(serializeBus(events)); } /** The tape's complete regime-scope vocabulary (kestrel-ocyf): every scope any REGIME event on the * bus writes, de-duplicated. Callers holding the full bus pass this to the engine so `armDocument` * can prove a regime gate dead on arrival (scope outside this set ⇒ it can never arm on this tape). */ export function tapeRegimeScopesOf(events: readonly BusEvent[]): string[] { const scopes = new Set(); for (const e of events) if (e.stream === "REGIME") scopes.add(e.scope); return [...scopes]; } // ───────────────────────────────────────────────────────────────────────────── // runSimSession // ───────────────────────────────────────────────────────────────────────────── /** * Drive one `sim` session end to end and return its graded {@link EpisodeReport} (RUNTIME §7). * Pure with respect to its inputs (given `events`, no I/O at all); with `busPath` it reads the * bus once. If `out` is given, the report is also written there as pretty JSON. Fails closed on * a missing META, an unknown fill model, or no instruments (RUNTIME §8). A thin driver over * {@link SessionCore}: arm all documents up front, step every event, settle, build. */ export function runSimSession(opts: RunSimOptions): EpisodeReport { if (opts.busPath === undefined && opts.events === undefined) { throw new Error("session: runSimSession needs either `busPath` or `events`"); } const events: BusEvent[] = opts.events !== undefined ? [...opts.events] : [...readBus(opts.busPath!)]; const meta = requireMeta(events); const specs = resolveSpecs(meta, opts.instruments); // Dynamic tau by default (RUNTIME §4): `@fair` (and the maker-fair hazard) decay toward // intrinsic across the session off the session clock — `max(0, 16:00 ET on session_date − now)`. // An explicit `fairTauYears` (e.g. the CLI's `--tau-hours` constant) still overrides it. const fairTauYears = opts.fairTauYears ?? expiryTauYears(meta.session_date); // Capture the a57.14 envelope only when a config is supplied AND declares an authoring identity // (kestrel-5zl.6). A config-less run — every existing runSimSession caller — stamps nothing, so its // graded bus stays byte-identical (backward-compatible; the projector leaves it `certified`). // The FULL ConfigId rides alongside under the SAME condition (m9i.2 owner tweak): the grid CellKey // keys on it, so temperature-only config variants land in DISTINCT cells. const envelope = opts.config !== undefined ? envelopeForConfig(opts.config) : undefined; const configId = envelope !== undefined ? deriveConfigId(canonicalizeConfig(opts.config!)) : undefined; // The cell config axis (fillSeed stripped — ADR-0016 §3): stamped ONLY when it DIVERGES from configId // (the config carried a fillSeed) — a seedless run's cell axis equals configId, so no stamp and the graded // bus stays byte-identical. This keeps seed-only variants pooled as replicates in ONE cell while configId // keeps the seeded RUN identity (SimRunId). const cellConfigId = envelope !== undefined ? cellConfigIdOf(opts.config!) : undefined; const cellConfigStamp = cellConfigId !== undefined && cellConfigId !== configId ? cellConfigId : undefined; // Arm the seeded sampled fill channel iff the config declares a fillSeed (kestrel-9gu.8.3); a config-less // run — and any config that omits the seed — leaves the sampler OFF (fail-closed, byte-identical). const fillSeed = opts.config !== undefined ? fillSeedOf(opts.config) : undefined; // The sampled-channel qualification claim (kestrel-9gu.12): read fail-closed off the config; absent — // every config today, until the 9gu.8.6 study passes — the headline gate resolves to the floor. const sampledQualification = opts.config !== undefined ? sampledQualificationOf(opts.config) : undefined; const core = new SessionCore({ meta, specs, fillModel: opts.fillModel, rUsd: opts.rUsd, fairTauYears, firstTs: events[0]?.ts ?? 0, // The full bus is in hand — pre-scan its regime vocabulary so a regime gate on a scope this tape // never writes gets the DEFINITIVE dead-on-arrival notice at arm (kestrel-ocyf), not a hedge. tapeRegimeScopes: tapeRegimeScopesOf(events), ...(opts.makerFairParams !== undefined ? { makerFairParams: opts.makerFairParams } : {}), ...(envelope !== undefined ? { envelope } : {}), ...(configId !== undefined ? { configId } : {}), ...(cellConfigStamp !== undefined ? { cellConfigId: cellConfigStamp } : {}), ...(fillSeed !== undefined ? { fillSeed } : {}), ...(sampledQualification !== undefined ? { sampledQualification } : {}), }); for (const doc of opts.documents) core.arm(toModule(doc)); for (const ev of events) core.step(ev); core.settle(); const report = core.buildReport(events.length, busSha256Of(events)); if (opts.out !== undefined) writeReport(opts.out, report); return report; } /** Reassess every leg of a book event against the resting orders, emit floor fills, and feed * each fill back into the engine so inventory/basis/TP advance (RUNTIME §5–§7). */ function reassessBook( ev: BookEvent, fill: SimFillEngine, drain: () => BusEvent[], engine: PlanEngine, // kestrel-wcnd: τ is RESOLVED BY THE CALLER against this book's own `expiry` (the caller holds the // provider + the fail-closed ladder), so this function can never re-derive it from the session date. // `null` ⇒ `@fair` is UNKNOWN for every leg here (fail-closed) — never a same-day default. tau: number | null, ): void { for (const leg of ev.legs) { const fairVal = fairFor(ev, leg, tau); fill.onBook(ev.instrument, leg, ev.ts, fairVal); for (const s of drain()) { if (s.stream === "ORDER" && s.type === "fill") engine.onEvent(s); } } } /** ExecutionFair for one leg at a book event, or `undefined` when unbuildable (no injected tau, * or the fair model returns null). Only `maker-fair-v1` consumes it; `strict-cross-v1` ignores * it (RUNTIME §6). */ function fairFor(ev: BookEvent, leg: OptionQuote, tau: number | null): number | undefined { if (tau === null) return undefined; const r = executionFair({ underlyingSpot: ev.underlier_px, strike: leg.strike, right: leg.right, tauYears: tau, liquidQuotes: ev.legs, asof: ev.ts, }); return r === null ? undefined : r.value; } // ───────────────────────────────────────────────────────────────────────────── // Report assembly // ───────────────────────────────────────────────────────────────────────────── function buildReport( meta: MetaEvent, busEvents: number, emitted: EmittedStream, gate: SimGate, settle: ReturnType, busSha256: string, settleTs: number, spotSettle?: SpotSettleReport, sampledQualification?: SampledQualificationClaim, ): EpisodeReport { // Per-order telemetry FROM THE BUS (a57.9): esc stage per order ref + the session reprice tally // are read off the engine's emitted TELEMETRY stream — the report consumes the bus, never // scrapes engine internals. Fold `order` records: the last `esc_stage` per ref is the final // ladder rung; each `reprice` marks one cancel/replace, so their count is the honest session // tally (F6). Fail-closed on a bus that carries no telemetry (e.g. an older emitted stream): the // map stays empty and `reprice_count` stays 0 — a documented default (esc stage 0 below), never // a crash and never an invented value (RUNTIME §8). const escByRef = new Map(); let repriceCount = 0; for (const e of emitted.events) { if (e.stream !== "TELEMETRY") continue; if (e.type === "order") { escByRef.set(e.order_id, e.esc_stage); // last-wins ⇒ final esc rung for this ref if (e.reprice) repriceCount += 1; } } // Join settle outcomes across BOTH judges (ADR-0017): the options engine's intrinsic settle and // the spot engine's mark-to-final-spot settle share the accounting subset the report reads; a // ref lives in exactly one engine, so the union is collision-free. interface OutcomeLite { readonly floorFilled: boolean; readonly floorFillPx: number | null; readonly expectedFillProb: number; readonly floorPnl: number; readonly expectedPnl: number; } const outByRef = new Map(settle.outcomes.map((o) => [o.ref, o])); for (const o of spotSettle?.outcomes ?? []) outByRef.set(o.ref, o); const cancelledRefs = new Set(); for (const e of emitted.events) { if (e.stream === "ORDER" && e.type === "cancel" && e.reason === "cancelled") cancelledRefs.add(e.order_id); } // Orders — one entry per submitted intent, joined across intent / engine child / settle. const orders: OrderReport[] = gate.intents.map((intent) => { const oc = outByRef.get(intent.ref); const esc = escByRef.get(intent.ref) ?? 0; const filled = oc?.floorFilled ?? false; const pFill = oc?.expectedFillProb ?? (filled ? 1 : 0); const fillPx = filled ? (oc?.floorFillPx ?? null) : null; return { ref: intent.ref, plan: intent.plan, plan_instance: intent.plan_instance, role: intent.role, instrument: intent.instrument, side: intent.side, qty: intent.qty, ...(intent.strike !== undefined ? { strike: intent.strike } : {}), ...(intent.right !== undefined ? { right: intent.right } : {}), px: round(intent.px), annotation: intent.sourceAnnotation, esc_stages: esc, pFill: round(pFill), filled, fillPx: fillPx === null ? null : round(fillPx), floorPnl: round(oc?.floorPnl ?? 0), expectedPnl: round(oc?.expectedPnl ?? 0), ...(cancelledRefs.has(intent.ref) && !filled ? { cancelled: true as const } : {}), }; }); // Plan lifecycle traces from the emitted PLAN stream, ONE trace per exact INSTANCE (kestrel-22j.15). // Group BY INSTANCE first — not by name — so a legal same-name replacement keeps its own lifecycle + // terminal outcome instead of collapsing onto its predecessor. The emitted stream always carries the // engine-minted `plan_instance`, so this keys on the exact id; the fold is shared with the Blotter // projector (groupPlanInstances) so the two projections can never drift. Lineage aggregation stays a // SEPARATE roll-up over `plan` (the name). const plans: PlanTrace[] = groupPlanInstances(emitted.events).map((g) => { const lifecycle: PlanLifecycleStep[] = g.events.map((pe) => ({ ts: pe.ts, state: pe.state, ...(pe.outcome !== undefined ? { outcome: pe.outcome } : {}), ...(pe.reason !== undefined ? { reason: pe.reason } : {}), })); const last = lifecycle[lifecycle.length - 1]!; return { plan: g.plan, plan_instance: g.instance, lifecycle, final_state: last.outcome ?? last.state }; }); // Totals — floor / expected across BOTH settle reports; premium = filled BUY debits (for a spot // buy that IS the cost basis — the bounded risk of a long equity, ADR-0017). let premium = 0; for (const o of settle.outcomes) { if (o.side === "buy" && o.floorFilled) premium += o.qty * o.px * settle.multiplier; } for (const o of spotSettle?.outcomes ?? []) { if (o.side === "buy" && o.floorFilled) premium += o.qty * o.px * spotSettle!.multiplier; } const floorTotal = settle.floorTotal + (spotSettle?.floorTotal ?? 0); const expectedTotal = settle.expectedTotal + (spotSettle?.expectedTotal ?? 0); // Emitted counts by stream. const emittedCounts: Record = {}; for (const e of emitted.events) emittedCounts[e.stream] = (emittedCounts[e.stream] ?? 0) + 1; // ── the three-channel headline (kestrel-9gu.12) ──────────────────────────── // floor + expected always (summed across BOTH judges); sampled ONLY on a seeded run (`sampledTotal` // present ⇔ the sampler was armed). The headline is SELECTED through the ONE gate policy // (src/grade/headline.ts): floor until the 9gu.8.6 qualification claim is on the record, and never // when any sampled realization rests on extrapolated support (bankableEv, ADR-0016 §5). A stale // settle mark (kestrel-xwf) taints the headline — every channel settled against the same untrustworthy // spot — WITHOUT switching it (loud, never silent). A parity recovery ANNOTATES but never PARDONS: a // stale parity mark still taints (kestrel-pez unification). The taint reasons mirror the Blotter // projector's `settle_mark` phrasing so the two surfaces agree. const settleMarkRec = settleMarkRecordOf(settle.settleMark); const markTaintReasons: string[] = []; if (settleMarkRec.stale) { const staleness = settleMarkRec.age_ms === null ? `settle mark ${settleMarkRec.px} has unstated provenance (no spot observation on the tape)` : `settle mark ${settleMarkRec.px} is stale: value last established ${settleMarkRec.age_ms}ms before settle (> ${settleMarkRec.stale_after_ms}ms threshold)`; markTaintReasons.push( settleMarkRec.source === "parity" ? `${staleness}; recovered via closing put-call parity (annotated recovery) — P&L settled against it is non-bankable (kestrel-xwf)` : settleMarkRec.mark_uncertain ? `${staleness}; settle mark uncertain — no closing put-call parity derivable, last-known mark retained — P&L settled against it is non-bankable (kestrel-xwf, fail-closed)` : `${staleness} — P&L settled against it is non-bankable (kestrel-xwf)`, ); } // The never-naked taint (kestrel-22j.4): a run whose FILLED book ever nets to a naked short (an // uncovered short — unbounded on a call / short equity) can never headline as a clean win, however // positive its P&L. Read the filled legs in CAUSAL order off the emitted ORDER stream (the same // bytes the Blotter sees) and watch the running net per position key — a taint that RIDES the // headline exactly like the xwf settle-mark taint, never switching the selected channel. const filledLegs: FilledLeg[] = []; for (const e of emitted.events) { if (e.stream === "ORDER" && e.type === "fill") { const key = e.strike !== undefined && e.right !== undefined ? `${e.instrument} ${e.strike}${e.right}` : e.instrument; filledLegs.push({ key, side: e.side, qty: e.qty }); } } const taintReasons = [...markTaintReasons, ...nakedShortTaint(filledLegs)]; const sampledUsd = settle.sampledTotal !== undefined ? round(settle.sampledTotal) : undefined; const headlineGate = sampledQualified({ sampledArmed: sampledUsd !== undefined, sampledExtrapolated: settle.outcomes.some((o) => o.sampled?.filled === true && o.support === "extrapolated"), ...(sampledQualification !== undefined ? { qualification: sampledQualification } : {}), }); const floorUsd = round(floorTotal); const expectedUsd = round(expectedTotal); const headline = selectHeadline( { floor: floorUsd, expected: expectedUsd, ...(sampledUsd !== undefined ? { sampled: sampledUsd } : {}) }, headlineGate, taintReasons, ); return { session: { date: meta.session_date, instruments: meta.instruments.map((i) => i.symbol), mode: "sim", fill_model: settle.fillModel, bus_events: busEvents, determinism_hash: emitted.hash(), bus_sha256: busSha256, reprice_count: repriceCount, settle_ts: settleTs, }, plans, orders, totals: { realized_floor_usd: floorUsd, expected_usd: expectedUsd, ...(sampledUsd !== undefined ? { sampled_usd: sampledUsd } : {}), premium_spent: round(premium), settle_mark: settleMarkRec, headline, }, emitted: emittedCounts, }; } /** The report's fixed-shape settle-mark record (kestrel-xwf) — the engine's {@link SettleMark} with * explicit `null`s (never omitted keys), so a report consumer always sees the provenance + verdict + * annotated recovery. */ function settleMarkRecordOf(m: SettleMark): EpisodeReport["totals"]["settle_mark"] { return { px: m.px, as_of_ts: m.asOfTs, last_observed_ts: m.lastObservedTs, age_ms: m.ageMs, stale_after_ms: m.staleAfterMs, stale: m.stale, source: m.source, mark_uncertain: m.markUncertain, note: m.note ?? null, }; } /** Serialize + write a report to a path as pretty JSON (used by {@link runSimSession}'s `out` * and the CLI). */ export function writeReport(path: string, report: EpisodeReport): void { writeFileSync(path, JSON.stringify(report, null, 2) + "\n"); }