UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

361 lines (360 loc) 19.8 kB
/** * # session/grid — the Simulate config-comparison harness: buildGrid + CellAggregate + leaderboard (kestrel-5zl.8) * * THE POINT of Simulate: the SAME recorded tape graded under N configs → a GRID of graded * {@link import("../blotter").Blotter}s → a pure comparison LEADERBOARD. This module is the two PURE * derivations that turn a set of FINALIZED Blotters into that grid + table — no wall clock, no RNG * (RUNTIME §0) — and it is a MEASUREMENT / reporting layer: it only READS the finalized record, and never * writes a byte back onto the graded bus or the Blotter (zero golden / e2e churn), and stamps NO new schema * field. It builds strictly on the run-identity layer (5zl.7 {@link cellOf}) + the config layer (5zl.6): * * 1. {@link buildGrid} — `(runs: readonly Blotter[]) → Grid`. Group the finalized Blotters by their 5zl.7 * {@link cellOf} CellKey (tape × fill_model × config-ConfigId). Runs sharing a CellKey are REPLICATES in * one comparison cell; each replicate keeps a DISTINCT SimRunId, read straight off the finalized record * as `blotter.session.bus.sha256` (which equals `SimRunId(gradedBus)` — the 5zl.7 pure-read * reconciliation, so no raw bus is needed). Each cell is a {@link CellAggregate} over its replicate runs. * * 2. {@link CellAggregate} — the honest measurements across a cell's replicate runs, using the Session/day * (one finalized Blotter = one run) as the INDEPENDENT EVIDENCE UNIT, and REPORTING UNCERTAINTY: every * numeric axis is a {@link Dist} — `{ n, mean, min, max, stdev }` — a distribution/spread over the * replicates, NEVER a single cherry-picked point (a single-run cell reports `n = 1`, spread `0` — still a * distribution, never a bare scalar). The axes come straight off the Blotter: financial (EV / P&L), risk, * process, fidelity, provider, cost — plus a {@link CertificationBreakdown}. * * 3. {@link leaderboard} — `(grid: Grid) → readonly LeaderboardRow[]`. A pure comparison table: ONE ROW PER * CELL, the honest multi-axis measurements reported SIDE BY SIDE with per-axis uncertainty, and NO opaque * omnibus score (the m9i principle: "report the financial, risk, process, attention, provider, and * interface measurements WITHOUT an opaque omnibus score"). The axes are laid out; they are never blended * into one number, and never sorted into a rank — the caller reads the axes it cares about. * * ## Determinism (RUNTIME §0 — a CI invariant) * Both derivations are PURE and INPUT-ORDER-INSENSITIVE: the same runs in ANY order ⇒ a byte-identical grid * and table. buildGrid sorts the cells by CellKey and each cell's replicates by SimRunId BEFORE any * aggregation, so every reduction (mean, stdev, …) folds over a FIXED order — no order-dependent float drift. * * ## Honesty (the 5zl.8 mandate) * The config axis of a cell is the FULL ConfigId (`session.config_id`, m9i.2 owner tweak) — a PROVISIONAL * run keys by its own stamped ConfigId (or, absent any config identity, the NO_ENVELOPE sentinel), so it * lands in its OWN cell, distinct from every differently-configured cell, and can NEVER be silently absorbed * into a certified cell's numbers (a provisional and a certified run can only share a cell by sharing the * ENTIRE config — and an identical config yields an identical completeness verdict). The cell's * {@link CertificationBreakdown} carries the certified/provisional COUNTS and a WORST-CASE verdict * (provisional if ANY replicate is provisional) — the projector's grade is surfaced, never laundered. * Provider / cost read the run's `session.envelope`, absent (`null`) for a no-envelope / provisional cell — * an honest gap, never a fabricated identity. */ import { InstanceRunId, SequenceCellKey, cellOf } from "./run-identity.js"; /** Summarize a fixed-order list of per-run measurements as a {@link Dist}. PURE — folds in the caller's * (deterministic) order, so the same values always yield byte-identical floats. `values` is never empty (a * cell always holds ≥ 1 run). Exported so other measurement layers (e.g. the percept-lab score log, * kestrel-m9i.12) fold their replicate probes with the SAME semantics and their rows port into this grid. */ export function distOf(values) { const n = values.length; let sum = 0; let min = values[0]; let max = values[0]; for (const v of values) { sum += v; if (v < min) min = v; if (v > max) max = v; } const mean = sum / n; let sq = 0; for (const v of values) sq += (v - mean) * (v - mean); const stdev = Math.sqrt(sq / n); return { n, mean, min, max, stdev }; } /** * The buy-and-hold P&L of an underlier over the horizon (kestrel-m9i.19): `qty × (close − open)`, where * `open = prices[0]` and `close = prices[last]`. PURE + byte-stable (no wall clock, no RNG — RUNTIME §0): the * interior prints don't matter, the hold is open → close and `qty` scales it linearly. FAIL-CLOSED — an * underlier price the baseline needs but cannot resolve (an empty series, or a non-finite open / close / qty) * is a typed refusal, NEVER a silent 0 (a silent 0-alpha would launder market direction as judgment). */ export function buyAndHoldBaseline(hold) { const { symbol, prices, qty } = hold; if (prices.length === 0) { throw new Error(`grid: buyAndHoldBaseline cannot resolve an empty underlier price series for ${symbol} (fail-closed — never a silent 0-alpha)`); } const open = prices[0]; const close = prices[prices.length - 1]; if (!Number.isFinite(open) || !Number.isFinite(close)) { throw new Error(`grid: buyAndHoldBaseline cannot resolve a non-finite underlier price for ${symbol} (open=${open}, close=${close}) (fail-closed — never a silent 0-alpha)`); } if (!Number.isFinite(qty)) { throw new Error(`grid: buyAndHoldBaseline cannot resolve a non-finite qty for the ${symbol} hold (qty=${qty}) (fail-closed — never a silent 0-alpha)`); } return qty * (close - open); } // ───────────────────────────────────────────────────────────────────────────── // buildGrid — group finalized Blotters by CellKey, aggregate each cell's replicates // ───────────────────────────────────────────────────────────────────────────── /** The SimRunId of a finalized run, read straight off the record (`session.bus.sha256`, which the projector * already computed and equals `SimRunId(gradedBus)` — the 5zl.7 pure-read reconciliation). */ function runIdOf(blotter) { return blotter.session.bus.sha256; } /** Aggregate one cell's already-sorted replicate runs into a {@link CellAggregate}. PURE. `runs` is * non-empty and in a FIXED (SimRunId-sorted) order, so every reduction is deterministic. When a per-cell * `hold` is supplied (kestrel-m9i.19), the financial axis additionally carries the buy-and-hold BASELINE + * benchmark-relative ALPHA; without one the axis is byte-identical to today's (the field is omitted). */ function aggregateCell(cell, runs, hold) { // Financial — the floor / expected totals and the support-partitioned expected, per run. const claimExpected = (b, support) => { const c = b.fill_claim.find((f) => f.support === support); return c === undefined ? 0 : c.expected; }; const financialBase = { floor: distOf(runs.map((b) => b.totals.floor)), expected: distOf(runs.map((b) => b.totals.expected)), calibrated: distOf(runs.map((b) => claimExpected(b, "calibrated"))), extrapolated: distOf(runs.map((b) => claimExpected(b, "extrapolated"))), }; // The benchmark rides ALONGSIDE the absolute EV, computed ONLY when a per-cell hold is supplied. baseline = // the buy-and-hold P&L of the cell underlier; alpha_i = expected_i − baseline (a Dist WITH uncertainty, so // alpha.mean = cell EV − baseline, the 5zl.16.8 identity, over the SAME fixed replicate order as `expected`). // FAIL-CLOSED: an unresolvable underlier price propagates buyAndHoldBaseline's typed refusal — never a // silent 0-alpha. When absent, the `benchmark` field is OMITTED (not `undefined`), so an un-baselined cell // is byte-identical to today's grid (no churn — existing goldens/e2e intact). const financial = hold === undefined ? financialBase : (() => { const baseline = buyAndHoldBaseline(hold); return { ...financialBase, benchmark: { symbol: hold.symbol, baseline, alpha: distOf(runs.map((b) => b.totals.expected - baseline)), }, }; })(); // Risk — headline taint + the directional-cap disposition tallied over every order in every run. let taintedRuns = 0; const reasons = new Set(); const cap = { applied: 0, exempt: 0, na: 0 }; for (const b of runs) { if (b.totals.headline.tainted) taintedRuns += 1; for (const r of b.totals.headline.reasons) reasons.add(r); for (const o of b.orders) { if (o.directional_cap === "applied") cap.applied += 1; else if (o.directional_cap === "exempt") cap.exempt += 1; else if (o.directional_cap === "na") cap.na += 1; } } const risk = { tainted_runs: taintedRuns, headline_reasons: [...reasons].sort(), directional_cap: cap, }; // Process — orders / fills / reprices / plans, per run. const process = { orders: distOf(runs.map((b) => b.orders.length)), fills: distOf(runs.map((b) => b.orders.filter((o) => o.filled).length)), reprices: distOf(runs.map((b) => b.orders.reduce((a, o) => a + o.reprice_count, 0))), plans: distOf(runs.map((b) => b.plans.length)), }; // Fidelity — the level breakdown + the (cell-invariant) judge self-limitation. const levels = {}; for (const b of runs) { const lvl = b.session.fidelity.level; levels[lvl] = (levels[lvl] ?? 0) + 1; } const fidelity = { levels, self_limitation: runs[0].session.fidelity.self_limitation, }; // Provider / cost — the a57.14 envelope, invariant across the cell (the config axis of the CellKey). A // no-envelope cell (a provisional / NO-LLM run) has neither — surfaced honestly as `null`, never fabricated. const envelope = runs[0].session.envelope ?? null; const provider = envelope; const cost = envelope === null ? null : { input: distOf(runs.map((b) => b.session.envelope.token_accounting.input)), output: distOf(runs.map((b) => b.session.envelope.token_accounting.output)), thinking: distOf(runs.map((b) => b.session.envelope.token_accounting.thinking)), }; // Certification — the projector's grade, surfaced with counts + a worst-case verdict. let certified = 0; let provisional = 0; for (const b of runs) { if (b.certification.verdict === "certified") certified += 1; else provisional += 1; } const certification = { certified, provisional, verdict: provisional > 0 ? "provisional" : "certified", }; // Rankability — the DERIVED leaderboard-eligibility axis (kestrel-m9i.25), surfaced DISTINCTLY from // certification: the rankable/not-rankable replicate counts + a WORST-CASE verdict (the cell is rankable // ONLY if every replicate is rankable — one not-rankable replicate makes the cell not rankable). let rankableRuns = 0; let notRankableRuns = 0; for (const b of runs) { if (b.rankability.rankable) rankableRuns += 1; else notRankableRuns += 1; } const rankable = { rankable: rankableRuns, not_rankable: notRankableRuns, verdict: notRankableRuns === 0, }; return { cell, runs: runs.length, runIds: runs.map(runIdOf), financial, risk, process, fidelity, provider, cost, certification, rankable, }; } /** * Group a set of FINALIZED Blotters into the config-comparison {@link Grid} (kestrel-5zl.8). Each run is keyed * by its 5zl.7 {@link cellOf} CellKey (tape × fill_model × config-ConfigId); runs sharing a key are REPLICATES * aggregated into one {@link CellAggregate} (each keeping a distinct SimRunId). PURE and * INPUT-ORDER-INSENSITIVE: cells are sorted by CellKey and each cell's replicates by SimRunId before any * aggregation, so the same runs in ANY order yield a byte-identical grid (RUNTIME §0). Reads finalized * Blotters only — writes no bus/Blotter byte, stamps no schema field. * * ADDITIVE (kestrel-m9i.19): the optional 2nd arg supplies a per-cell buy-and-hold BASELINE. A cell whose * CellKey appears in `opts.baselines` gets a `financial.benchmark` (the baseline + benchmark-relative alpha, * ALONGSIDE absolute EV); every other cell — and the whole grid when no options are supplied — is BYTE-IDENTICAL * to today's. The baseline touches neither CellKey nor SimRunId nor the leaderboard row shape. FAIL-CLOSED: an * unresolvable supplied underlier price propagates {@link buyAndHoldBaseline}'s typed refusal (never a silent 0). */ export function buildGrid(runs, opts) { const baselines = opts?.baselines; const byCell = new Map(); for (const b of runs) { const key = cellOf(b); const bucket = byCell.get(key); if (bucket === undefined) byCell.set(key, [b]); else bucket.push(b); } const cells = []; for (const key of [...byCell.keys()].sort()) { const bucket = byCell.get(key); // Sort replicates by SimRunId so every downstream reduction folds over a FIXED order (determinism). const sorted = [...bucket].sort((a, b) => (runIdOf(a) < runIdOf(b) ? -1 : runIdOf(a) > runIdOf(b) ? 1 : 0)); cells.push(aggregateCell(key, sorted, baselines?.get(key))); } return { cells }; } /** * Project a {@link Grid} into the comparison LEADERBOARD (kestrel-5zl.8): ONE ROW PER CELL, the honest * multi-axis measurements reported SIDE BY SIDE with per-axis uncertainty, and NO opaque omnibus score — the * axes are laid out, never blended into one number and never sorted into a rank (the m9i principle). PURE and * deterministic: the row order mirrors the grid's (CellKey-sorted), so the same grid always yields a * byte-identical table. */ export function leaderboard(grid) { return grid.cells.map((c) => ({ cell: c.cell, runs: c.runs, runIds: c.runIds, financial: c.financial, risk: c.risk, process: c.process, fidelity: c.fidelity, provider: c.provider, cost: c.cost, certification: c.certification, rankable: c.rankable, })); } /** * Fold the ORDERED per-session Blotters of a multi-session Instance into ONE {@link InstanceBlotter} * (kestrel-5zl.16.8) — a PURE read of the finalized sequence (no wall clock, no RNG — RUNTIME §0; no bus / * Blotter byte written, no schema field stamped). Where {@link buildGrid} folds order-INSENSITIVE REPLICATES * into a distribution, this folds an order-SENSITIVE SEQUENCE into one CHAINED record — the ordering IS the * semantics. Three things: * * 1. CHAINS per-session cash across the sequence: session N+1 OPENS from session N's carried cash * (`openingCash[k] = closingCash[k-1] = Σ_{j<k} realized_j`, `realized_k = totals.floor`), exactly * re-deriving the chain `carry.ts` threads (`carry.cash = prior.cash + settle.floorTotal`). Deterministic * + byte-stable: the ledger folds in SEQUENCE order (no clock, no RNG). `grade.financial.cash` is the * final running cash. * 2. a sequence-level GRADE — the financial axis chained; the non-financial axes reuse the EXISTING grid * axes ({@link aggregateCell}) over the sessions-as-replicate-runs, so a LENGTH-1 Instance's non-financial * axes are BYTE-IDENTICAL to `buildGrid([bl]).cells[0]` (NO existing-grid churn). NO opaque omnibus score. * 3. a benchmark-relative ALPHA hook — `alpha = instanceEV − baseline`, `instanceEV = Σ totals.expected`. * alpha lives INSIDE the financial axis, one honest measure — never a collapsed single score. * * FAIL-CLOSED: an empty (unstitchable) sequence, or a missing / non-finite baseline, is a typed refusal — * never a silent default (no silent 0-alpha). */ export function buildInstanceBlotter(orderedBlotters, opts) { // Fail-closed — an empty sequence has no chain to open. if (orderedBlotters.length === 0) { throw new Error("grid: buildInstanceBlotter needs a non-empty session sequence (fail-closed)"); } // Fail-closed — a missing / non-finite baseline is a typed refusal, never a silent 0-alpha default. const baseline = opts?.baseline; if (typeof baseline !== "number" || !Number.isFinite(baseline)) { throw new Error("grid: buildInstanceBlotter needs a finite `baseline` benchmark (fail-closed — never a silent 0-alpha default)"); } // (1) The chained-cash ledger — order-SENSITIVE, folded in SEQUENCE order (each session opens from the // prior's carried cash). This re-derives, purely, the exact chain carry.ts threads (cash = prior.cash + // settle.floorTotal, and settle.floorTotal === totals.floor). const sessions = []; let running = 0; for (let k = 0; k < orderedBlotters.length; k++) { const b = orderedBlotters[k]; const openingCash = running; const realized = b.totals.floor; const closingCash = openingCash + realized; sessions.push({ ordinal: k, runId: runIdOf(b), openingCash, realized, closingCash }); running = closingCash; } // (3) The financial axis — chained cash + the benchmark-relative alpha hook. const instanceEV = orderedBlotters.reduce((acc, b) => acc + b.totals.expected, 0); const financial = { cash: running, // the final running cash of the chain (Σ per-session realized floor) instanceEV, alpha: instanceEV - baseline, baseline, }; // (2) The non-financial axes — reuse the EXISTING grid aggregation over the sessions-as-replicate-runs, in // the SAME SimRunId-sorted FIXED order buildGrid folds a cell in (so a length-1 Instance's non-financial // axes are byte-identical to buildGrid([bl]).cells[0], and every reduction is deterministic — RUNTIME §0). const sorted = [...orderedBlotters].sort((a, b) => (runIdOf(a) < runIdOf(b) ? -1 : runIdOf(a) > runIdOf(b) ? 1 : 0)); const sequenceCellKey = SequenceCellKey(orderedBlotters.map((b) => cellOf(b))); const agg = aggregateCell(sequenceCellKey, sorted); const grade = { financial, risk: agg.risk, process: agg.process, fidelity: agg.fidelity, provider: agg.provider, cost: agg.cost, certification: agg.certification, }; return { // The 5zl.16.5 identity chain, read straight off the ordered finalized sequence. instanceRunId: InstanceRunId(orderedBlotters.map((b) => runIdOf(b))), sequenceCellKey, sessions, grade, }; }