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.

885 lines (830 loc) 165 kB
/** * # frame/pane-catalog — the ONE pane registry: single source for render / View / prompt (kestrel-4gl.13) * * Every available (non-kernel) pane is catalogued HERE, exactly once, with its metadata + its pure * builder. This registry is what three consumers all read, so they can never drift: * 1. the **renderer** ({@link ./render.ts}) MATERIALIZES panes from it; * 2. the **View language** ({@link ../lang/ast.ts ViewStatement}) SELECTS panes from it (by id); * 3. a **prompt** will later ADVERTISE the menu from it (`title` + one-line `description`). * * The **cockpit kernel is NOT in this catalog** — it is built in code and LEADS every frame, * non-configurably, OUTSIDE any View selection (its reserved ids `kernel`/`cockpit` are refused, * {@link ../frame/types.ts assertPaneIdAllowed}). This catalog is the *configurable body* only. * * ## Single-source guarantee (the whole point) * A pane that is NOT in {@link PANE_CATALOG} CANNOT render (there is no other builder path), and a * View naming an id absent from the catalog FAILS CLOSED ({@link resolveView}) — never a silent * drop. Adding a new pane is one entry here; it is then selectable, renderable, and advertisable * with no change anywhere else. * * ## Attribution class (charter) + token cost * Each entry declares an {@link PaneAttribution} (`OBS` observed | `CALC` derived | `MODEL` a model * output — a MODEL pane carries a receipt in its rendered text, e.g. the chain's `fair` fairNote) * and a documented `tokenCostEstimate` (a rough chars/4 figure for the prompt menu + the View * budget planner; the REAL cost is measured at materialization under the active tokenizer). * * ## Purity (ADR-0009) * Every builder is a PURE function of its {@link PaneBuildContext} — no wall clock, no RNG; the same * context renders a byte-identical block. Invents no value (`—` for the unknowns, {@link ./format.ts}). */ import { KernelHonestyError, assertPaneIdAllowed, makeField } from "./types.ts"; import type { SeatId } from "./seat.ts"; import { founderViewFor } from "./founder-registry.ts"; // The canonical render tokenizer contract (kestrel-2fab): the budget guard measures a materialized // pane's real cost under it. Homed in the render barrel — importing the pure type here does NOT pull // in the renderer (`./render.ts` imports THIS module), so no cycle is created. import type { Tokenizer } from "../render/index.ts"; import { paneRefusal } from "./refusals.ts"; import type { ArmedPlanTerms, ArmedPlanTermsEntry, ChainRow, InstrumentSpec, Kernel, LevelSet, MarketPane, Position, PriorSessionTape, PriorVantage, RestingOrder, FillRecord, Budget, TapeRow, VehicleHealth, } from "./types.ts"; import { SPOT_STALE_AFTER_MS } from "./options-analytics.ts"; import type { OptionsAnalytics, OptionsExpiry } from "./options-analytics.ts"; import { DASH, isSpotLeg, money, num, pad, px, signedInt, usd } from "./format.ts"; import { candleLine, tapeBounds } from "./tape-geometry.ts"; import { resolveScheme, rollingAnchorOf, schemeHasBoundary, type SessionScheme } from "./session-scheme.ts"; import { PANE_BOUNDARY_REQUIREMENT, schemeConditionedDefect } from "./paradigm-ledger.ts"; // ───────────────────────────────────────────────────────────────────────────── // The build context a pane builder reads (a pure projection of the frame input) // ───────────────────────────────────────────────────────────────────────────── /** Everything a body-pane builder may read — a pure projection of the OPEN/WAKE frame input. A * builder is a total function of THIS; it never reaches outside it (no wall clock, no RNG). */ export interface PaneBuildContext { readonly instruments: readonly InstrumentSpec[]; readonly market: MarketPane; readonly kernel: Kernel; readonly frameKind: "OPEN" | "WAKE"; /** The prior author vantage's SERVED values (kestrel-wa0j.48) — a pure projection of the WAKE * frame input's {@link ../frame/types.ts PriorVantage}. Present only on a WAKE frame the driver * threaded a prior vantage into; the `delta` pane fails closed to absent-with-reason without it. * Absent on OPEN and on any frame with no prior vantage (additive: those frames are byte-identical). */ readonly priorVantage?: PriorVantage; /** Minutes since the author's last vantage (the WAKE frame's `minutesSinceLast`) — the `(<N>m ago)` * cue on the `delta` header. Absent on OPEN / when unknown ⇒ the header omits the age (never a * fabricated gap). PURE: a value carried on the frame input, not a wall-clock read. */ readonly minutesSinceLast?: number | null; /** The prior sessions' tapes (kestrel-wa0j.20) — a pure projection of the frame input's * {@link ../frame/types.ts PriorSessionTape}[]. Present only on a frame the driver threaded a multiday * tape into; `tape d-N` serves ordinal N's rows as its OWN single-session block, else it renders the * Train 1B absence line (byte-identical). Absent on a single-day frame (additive, byte-identical). */ readonly priorSessions?: readonly PriorSessionTape[]; /** The ARMED plans' enforced terms (kestrel-wa0j.29) — a pure projection of the frame input's * {@link ../frame/types.ts ArmedPlanTerms}. Present only on a frame the driver threaded armed-plan * terms into (a wake with a plan in an enforced state); the `armed-plan` pane fails closed to exactly * one absent-with-reason line without it. Absent ⇒ byte-identical (the pane is catalog-only). */ readonly armedPlan?: ArmedPlanTerms; } // ───────────────────────────────────────────────────────────────────────────── // The pane builders — the EXISTING v1 panes, moved here verbatim (byte-identical output) // ───────────────────────────────────────────────────────────────────────────── // ── instruments ───────────────────────────────────────────────────────────── function renderInstruments(instruments: readonly InstrumentSpec[]): string { if (instruments.length === 0) return "instruments:\n (none)"; const lines = instruments.map((i) => { const role = i.role !== undefined ? ` ${i.role}` : ""; const mult = `mult ${px(i.multiplier)}`; const tick = `tick ${px(i.tick)}`; return ` ${i.symbol} ${i.assetClass}${role} ${mult} ${tick}`; }); return ["instruments:", ...lines].join("\n"); } // ── levels ─────────────────────────────────────────────────────────────────── /** * The `spot` cell: the price, plus a `[STALE <age>]` tag once the feed that printed it has gone * quiet longer than the backstop (kestrel-rs4). A dead feed and a genuinely frozen market render * the SAME price — an untagged one is read as a clean current value, so the author acts on a level * the tape stopped supporting. The tag is the only thing that tells the two apart, and it is worth * its tokens ONLY when it says something: a fresh spot renders bare (byte-identical to a pre-rs4 * frame), as does a spot the caller supplied no age for (an absent age is not a freshness claim). * `spot` itself is never suppressed or invented — the value prints, the tag keeps it honest (the * chain pane's stale-spot flag idiom). */ function spotCell(lv: LevelSet): string { const age = lv.spotStaleSeconds; if (age === undefined || age === null || age * 1000 <= SPOT_STALE_AFTER_MS) return `spot ${px(lv.spot)}`; return `spot ${px(lv.spot)} [STALE ${Math.round(age / 60)}m]`; } function renderLevels(instrument: string, lv: LevelSet): string { const parts = [ spotCell(lv), `prior_close ${px(lv.priorClose)}`, `hod ${px(lv.hod)}`, `lod ${px(lv.lod)}`, `vwap ${px(lv.vwap)}`, `or ${px(lv.orLow)}–${px(lv.orHigh)}`, ]; return `levels · ${instrument}\n ${parts.join(" · ")}`; } // ── levels level-set inflection (kestrel-wa0j.24 / ADR-0041 §1) ───────────────── // // The `levels` pane gains a LEVEL-SET slot: a variadic run of LevelName idents naming WHICH level // cells render (`levels vwap hod` renders only vwap + hod). No-arg `levels` is unchanged (all six // cells, byte-identical). Each ident is a ground ADDRESS over the closed LEVEL_ROSTER — never an // expression (ADR-0041 §1). DENOTATION: an ident names one cell of the frozen {@link LevelSet}; the // cell renders its value (or `—` when the LevelSet carries none — absent-not-hidden, unchanged). The // selected cells render in AUTHORED order (the arg selects WHICH cells; it never invents a value), // so `levels vwap hod` and `levels hod vwap` are DISTINCT byte-stable forms (biunique spelling→bytes, // ADR-0041 §1 — no collision). An ident OUTSIDE the roster fails closed NAMING the roster (a named // level-SET like `registry` is a LATENT future inflection, not an individual level — ADR-0041 §3). /** The closed roster of individual-level idents the `levels` level-set slot addresses — exactly the * cells `renderLevels` prints, in its canonical order. (A named level-SET, e.g. `registry`, is a * distinct LATENT inflection — the band-keyed level-set train — not a member of THIS roster.) */ const LEVEL_ROSTER = ["spot", "prior_close", "hod", "lod", "vwap", "or"] as const; type LevelRosterName = (typeof LEVEL_ROSTER)[number]; /** Render ONE named level cell, byte-identical to that cell's rendering inside {@link renderLevels}. */ function levelCell(name: LevelRosterName, lv: LevelSet): string { switch (name) { case "spot": return spotCell(lv); case "prior_close": return `prior_close ${px(lv.priorClose)}`; case "hod": return `hod ${px(lv.hod)}`; case "lod": return `lod ${px(lv.lod)}`; case "vwap": return `vwap ${px(lv.vwap)}`; case "or": return `or ${px(lv.orLow)}–${px(lv.orHigh)}`; } } /** * The `levels` pane's ARGED builder (kestrel-wa0j.24 / ADR-0041 §1): a variadic LevelName level-set. * Reached ONLY with args already refined to sort LevelName (bare idents) by {@link bindArgs}; this * builder applies the `renderable`-rung VALUE check — each ident must NAME a cell in the closed * {@link LEVEL_ROSTER}. An unknown ident fails closed NAMING the roster + citing the LATENT * named-level-set cell (ADR-0041 §3 four-state paradigm). Selected cells render in authored order. */ function renderLevelsWithArgs(ctx: PaneBuildContext, args: readonly PaneSelectionArg[]): string { const lv = ctx.market.levels; const cells: string[] = []; for (const a of args) { if (a.kind !== "arg-ident") { // Unreachable after bindArgs (sort LevelName ⇐ ident) — fail closed rather than trust it. throw paneRefusal({ kind: "unreachable-bound-arg", pane: "levels", expected: "bound LevelName idents", args: [a] }); } if (!(LEVEL_ROSTER as readonly string[]).includes(a.name)) { throw paneRefusal({ kind: "unknown-value-naming-roster", pane: "levels", slot: "level-set", value: a.name, roster: LEVEL_ROSTER }); } cells.push(levelCell(a.name as LevelRosterName, lv)); } return `levels · ${ctx.market.instrument}\n ${cells.join(" · ")}`; } // ── tape (the incumbent rotated candlestick, rendering-variants.md #2) ───────── /** * Render the rotated-candlestick tape. Price → column position; time flows down the page * (append-only). The axis bounds are the data's own extremes (real input numbers — the renderer * derives layout, never a value). Each row: leading indent to the low column, `─` wick to the * high column, `█` body over open→close. A flat window collapses to a single block column. The * candle geometry is the shared {@link ../frame/tape-geometry.ts candleLine} (one geometry, drawn * identically by the ARM candlestick adapter); this function owns only the header + row prefix. */ function renderTape(rows: readonly TapeRow[], bucketMin: number, sessionTag?: string): string { // kestrel-wa0j.20: an OPTIONAL header session tag names a PRIOR session (`d-N` [+ label]) when this // block is a `tape d-N` render. Absent (the d-0 / no-arg path) ⇒ the header is BYTE-IDENTICAL to // before — a prior-session block differs from a d-0 block of the same rows ONLY by this ` · <tag>`. const tag = sessionTag !== undefined ? ` · ${sessionTag}` : ""; if (rows.length === 0) { return [`tape ${bucketMin}m${tag} · (no prints this window)`].join("\n"); } const { lo, hi } = tapeBounds(rows); const anchorClock = rows[0]!.clock; const header = `tape ${bucketMin}m${tag} · axis ${px(lo)}→${px(hi)} · anchor @ ${anchorClock} ET`; const body = rows.map((r) => `${r.clock} ${candleLine(r, lo, hi)}`); return [header, ...body].join("\n"); } /** * CALC re-bucketing (kestrel-wa0j.19 §1): aggregate consecutive runs of `factor` served rows into * ONE wider bucket. Deterministic AGGREGATION of real inputs — every number on an output row is a * function of the input rows' own numbers (open = the run's FIRST open, high = max high, low = min * low, close = the run's LAST close, clock = the CLOSING row's clock — the bucket closed there, * volume = the sum WHEN every grouped row carries one; a partially-volumed group omits it rather * than present a partial sum as the bucket's volume). A TRAILING PARTIAL run aggregates the rows * that exist — it is real data, and nothing on it is invented. Pure; input order preserved. * Exported so the aggregation semantics are directly testable (and reusable by the session lane). */ export function rebucketTape(rows: readonly TapeRow[], factor: number): TapeRow[] { const out: TapeRow[] = []; for (let i = 0; i < rows.length; i += factor) { const run = rows.slice(i, i + factor); const first = run[0]!; const last = run[run.length - 1]!; let high = -Infinity; let low = Infinity; let volume = 0; let allVolumed = true; for (const r of run) { if (r.high > high) high = r.high; if (r.low < low) low = r.low; if (r.volume === null || r.volume === undefined || !Number.isFinite(r.volume)) allVolumed = false; else volume += r.volume; } out.push({ clock: last.clock, open: first.open, high, low, close: last.close, ...(allVolumed ? { volume } : {}), }); } return out; } /** * TRUE iff a `tape <window>` arg is SERVABLE by a context whose rows arrive bucketed at `bucketMin` * minutes: the window has a fixed minute width AND is a positive INTEGER MULTIPLE of the served * bucket (equal ⇒ factor 1). The ONE shared validity predicate (kestrel-wa0j.19 §1): the session * lane checks a scheduled View's window arg against it at SCHEDULE time (so a bad window is refused * before it ever reaches an unguarded delivery render), and the arged tape builder enforces the * SAME predicate at materialization — the two can never drift. Pure unit arithmetic, no context. */ export function windowServableBy( bucketMin: number, window: { readonly value: number; readonly unit: string }, ): boolean { const w = windowMinutes(window); if (w === null || !Number.isFinite(bucketMin) || bucketMin <= 0) return false; const factor = w / bucketMin; return Number.isInteger(factor) && factor >= 1; } /** * The tape pane's ARGED builder (kestrel-wa0j.3 / T3a + kestrel-wa0j.19 §1): `tape 5m` — a single * WINDOW arg names the bucket width the author wants. The context serves rows PRE-BUCKETED at * `ctx.market.tapeBucketMin`; a requested window that is an INTEGER MULTIPLE of that width is * served by CALC re-bucketing ({@link rebucketTape} — deterministic aggregation of the real rows, * so `tape 5m` materializes over the product driver's 1m buckets), an EQUAL window renders the * rows as-is (byte-identical to the no-arg tape), and a SUB-RESOLUTION or NON-MULTIPLE window * FAILS CLOSED naming pane + arg + the served width (the served rows cannot honestly express it — * never mismatched data under a requested label). * * MIGRATED onto the ParamSlot substrate (kestrel-wa0j.24 / ADR-0041 §1): the ARITY + SORT refusals * (an ident arg, extra args) are the uniform {@link bindArgs} `mat`-rung surface — this builder is * reached with the args already refined (an optional Window + an optional SessionOrdinal), so it keeps * only the `renderable`-rung reads (which need the Frame). Accepted forms render BYTE-IDENTICALLY to * before the migration; the servability message is unchanged. * * SESSION addressing (SessionOrdinal, Train 1B / ADR-0041 §1): `tape d-0` names the CURRENT session * (the frozen frame's own tape) and collapses to the window path — byte-identical to the un-sessioned * render. `tape d-N` (N ≥ 1) names a PRIOR session, a TOTAL `renderable` judgment (ADR-0041 §3: absence * is a value): scheme-conditioned absence when the instrument's {@link ../frame/session-scheme.ts * SessionScheme} declares no session-delimiting `close` boundary (a perp's rolling UTC day — reuse the * wa0j.44 conditioning path, never an invented session), else a frame-conditioned absence naming the * multiday DATA train (Train 1B lands the ADDRESS; kestrel-wa0j.20 lands the DATA). d-0 is date-blind * (relative), exactly like the servability label. */ function renderTapeWithArgs(ctx: PaneBuildContext, args: readonly PaneSelectionArg[]): string { const windowArg = args.find((a): a is Extract<PaneSelectionArg, { kind: "arg-window" }> => a.kind === "arg-window"); const sessionArg = args.find((a): a is Extract<PaneSelectionArg, { kind: "arg-ordinal" }> => a.kind === "arg-ordinal"); // A PRIOR session (d-N, N ≥ 1): a total renderable absence (never an invented session). d-0 falls // through to the current-session window path below. if (sessionArg !== undefined && sessionArg.ordinal >= 1) { return renderTapePriorSession(ctx, sessionArg.ordinal, windowArg); } const available = ctx.market.tapeBucketMin; // Session-only `tape d-0`: the current session at the served bucket — byte-identical to no-arg tape. if (windowArg === undefined) return renderTape(ctx.market.tape, available); const requested = windowMinutes(windowArg.window); if (requested === null || !windowServableBy(available, windowArg.window)) { throw paneRefusal({ kind: "window-not-servable", pane: "tape", arg: windowArg, servedBucketMin: available }); } const factor = requested / available; // factor 1 (requested === served): the rows as-is — byte-identical to the no-arg tape. if (factor === 1) return renderTape(ctx.market.tape, requested); // factor > 1: CALC re-bucketing — aggregate runs of `factor` served rows into the requested width. return renderTape(rebucketTape(ctx.market.tape, factor), requested); } /** The SessionOrdinal `d-N` (N ≥ 1) prior-session render (Train 1B ADDRESS + kestrel-wa0j.20 DATA, * ADR-0041 §1/§3) — a TOTAL `renderable` judgment. Three outcomes, single-sourced (the census + this * render read the same tables, honoring the authority arrow — materialization consults the * scheme/boundary DATA, never the ledger STATE): * • SCHEME-conditioned ABSENCE (reuse the wa0j.44 path): the instrument's SessionScheme declares no * session-delimiting `close` boundary (a perp's rolling UTC day) ⇒ there is no prior-session address * under this scheme — render the sanctioned periphrasis (anchor to the declared rolling extreme). * • SERVED (kestrel-wa0j.20): the scheme HAS the boundary AND the frame CARRIES ordinal N's tape ⇒ * render that session's OWN complete single-session block (the EXISTING renderer over its rows; its * own axis from its own extremes; header naming `d-N` + the label) — sessions are NEVER folded. * • FRAME-conditioned ABSENCE: the scheme has the boundary but the frame carries no session at `d-N` * (the v1 single-day tape) — the Train 1B absence line, deliberately NOT ratchet-pinned so this * absence→data flip needs no re-pin. */ function renderTapePriorSession( ctx: PaneBuildContext, ordinal: number, windowArg: Extract<PaneSelectionArg, { kind: "arg-window" }> | undefined, ): string { const scheme = schemeOfMarket(ctx); const label = windowArg !== undefined ? ` ${windowArg.window.value}${windowArg.window.unit}` : ""; const addr = `d-${ordinal}`; const head = `tape${label} · ${ctx.market.instrument} · ${addr}`; const defect = schemeConditionedDefect("tape", scheme); if (defect !== undefined) { const anchor = rollingAnchorOf(scheme) ?? "rolling extreme"; return `${head} · (${defect.reason} (${scheme}) — no prior-session address under this SessionScheme; ${defect.periphrasis}: ${anchor})`; } // kestrel-wa0j.20 — the frame may CARRY ordinal N's tape (the DATA that fills the Train 1B address). // Serve it as its OWN block; NEVER fold with the current session (that is wa0j.20b, behind PR #194). const carried = ctx.priorSessions?.find((s) => s.ordinal === ordinal); if (carried !== undefined && carried.rows.length > 0) { return renderPriorSessionBlock(ctx, carried, ordinal, windowArg); } return `${head} · (frozen frame carries no session at ${addr} — the current session only; multiday tape data threads with kestrel-wa0j.20)`; } /** Render one carried prior session as its OWN complete single-session tape block (kestrel-wa0j.20). * Reuses the EXISTING single-session {@link renderTape} + the SAME window servability/re-bucketing path * the d-0 render walks — so a `tape d-N` block is byte-format IDENTICAL to a `tape d-0` render of the * same rows except the header names the ordinal (+ the session label when present). Its axis is derived * from ITS OWN extremes (renderTape reads the rows it is handed); no cross-session anchor, no fold. */ function renderPriorSessionBlock( ctx: PaneBuildContext, session: PriorSessionTape, ordinal: number, windowArg: Extract<PaneSelectionArg, { kind: "arg-window" }> | undefined, ): string { const available = ctx.market.tapeBucketMin; const tag = session.sessionLabel !== undefined ? `d-${ordinal} (${session.sessionLabel})` : `d-${ordinal}`; // No window arg: the prior session at the served bucket (mirrors the `tape d-0` no-window path). if (windowArg === undefined) return renderTape(session.rows, available, tag); const requested = windowMinutes(windowArg.window); if (requested === null || !windowServableBy(available, windowArg.window)) { throw paneRefusal({ kind: "window-not-servable", pane: "tape", arg: windowArg, servedBucketMin: available }); } const factor = requested / available; // factor 1 (requested === served): the rows as-is; factor > 1: CALC re-bucketing (same as d-0). if (factor === 1) return renderTape(session.rows, requested, tag); return renderTape(rebucketTape(session.rows, factor), requested, tag); } // ── chain (near-money) — bid/ask/fair + receipt + dark flags ─────────────────── function darkFlag(row: ChainRow): string { const bidDark = row.bid === null; const askDark = row.ask === null; if (bidDark && askDark) return "dark"; if (bidDark) return "bid dark"; if (askDark) return "ask dark"; return DASH; } function renderChain(underlier: string, chain: readonly ChainRow[], dte?: number | null): string { // The RELATIVE days-to-expiry tag (kestrel-wa0j.19 §4) — restores the `0dte` the header lost in // the one-renderer swap (the pre-swap day renderer stamped `(<dte>dte)`; the machine channel kept // it as `AuthorFrame.chain.dte`). Relative, never a date (date-blind). `null`/absent ⇒ omitted — // the header is byte-identical to today (the TapeRow.volume idiom). const dteTag = dte !== undefined && dte !== null && Number.isFinite(dte) ? ` · ${dte}dte` : ""; if (chain.length === 0) { return `chain (near-money) · ${underlier}${dteTag}\n (no legs)`; } const head = ` ${pad("strike", 8)}${pad("R", 3)}${pad("bid", 8)}${pad("ask", 8)}${pad("fair", 10)}flags`; const lines = chain.map((r) => { const fair = r.fairNote !== undefined && r.fair !== null && r.fair !== undefined ? `${px(r.fair)} ${r.fairNote}` : px(r.fair); // The fair cell carries a variable-length receipt annotation; pad to at least its own // length + a 2-space gutter so the flags column never jams against it. const fairCell = pad(fair, Math.max(10, fair.length + 2)); return ` ${pad(px(r.strike), 8)}${pad(r.right, 3)}${pad(px(r.bid), 8)}${pad(px(r.ask), 8)}${fairCell}${darkFlag(r)}`; }); return [`chain (near-money) · ${underlier}${dteTag}`, head, ...lines].join("\n"); } // ── chain Count inflection (kestrel-wa0j.24 / ADR-0041 §1) ────────────────────── // // The `chain` pane gains a COUNT slot: `chain 12` addresses the 12 NEAREST-THE-MONEY strikes of the // frozen near-money chain. DENOTATION: a Count is a ground cardinal naming N nearest strikes — an // ADDRESS over the frozen chain, never a computation (ADR-0041 §1). The frame already emits legs // nearest-money FIRST (see `renderChain` in src/session/day.ts — "nearest-the-money first is the // tape's own order"), so selection preserves that order (no re-sort, no invented ordering; robust // even when spot is unknown). No-arg `chain` is unchanged (renders every carried leg). // // SHORTFALL is FAIL-CLOSED per absent-not-hidden (ADR-0041 §3), NOT a hard refusal and NEVER silently // fewer: a Count the frozen chain cannot fully serve renders EVERY available strike (real data — // invent nothing) PLUS an explicit absent-with-reason line for the missing strikes. `chain N` where // N ≥ the available strike count therefore renders the whole chain + the shortfall line; `chain N` // where N equals the available count is byte-identical to the no-arg chain (the selection anchor). /** Select the rows of the N nearest-money DISTINCT strikes, preserving the frame's nearest-first * order (no re-sort), and report how many distinct strikes the chain carries. Pure. */ function selectNearMoneyStrikes(chain: readonly ChainRow[], n: number): { readonly rows: ChainRow[]; readonly availableStrikes: number } { const seen = new Set<number>(); const ordered: number[] = []; for (const r of chain) { if (!seen.has(r.strike)) { seen.add(r.strike); ordered.push(r.strike); } } const keep = new Set(ordered.slice(0, n)); return { rows: chain.filter((r) => keep.has(r.strike)), availableStrikes: ordered.length }; } /** * The `chain` pane's ARGED builder (kestrel-wa0j.24 + Train 1B / ADR-0041 §1): an optional Count * (the N nearest-money strikes) + an optional ExpiryOrdinal (which expiry). Reached with the args * already refined by {@link bindArgs}; this builder applies the `renderable`-rung reads. * * EXPIRY addressing (ExpiryOrdinal, Train 1B): `chain e-0` names the NEAREST expiry — the frozen * near-money chain — and collapses to the Count path (byte-identical to the un-expiry render). * `chain e-N` (N ≥ 1) names a FURTHER-OUT expiry: the frozen frame carries ONE expiry of chain, so * this is a TOTAL renderable absence (ADR-0041 §3: absence is a value) naming the multi-expiry DATA * train (kestrel-wa0j.20) — never a fabricated expiry. On the nearest expiry a Count SHORTFALL stays * absent-not-hidden (available strikes + a named shortfall line, never silently fewer). */ function renderChainWithArgs(ctx: PaneBuildContext, args: readonly PaneSelectionArg[]): string { const countArg = args.find((a): a is Extract<PaneSelectionArg, { kind: "arg-count" }> => a.kind === "arg-count"); const expiryArg = args.find((a): a is Extract<PaneSelectionArg, { kind: "arg-expiry" }> => a.kind === "arg-expiry"); // A FURTHER-OUT expiry (e-N, N ≥ 1): a total renderable absence (never a fabricated expiry). e-0 // falls through to the nearest-expiry Count path below. if (expiryArg !== undefined && expiryArg.expiry >= 1) { return renderChainFurtherExpiry(ctx, expiryArg.expiry, countArg); } // e-0 (or no expiry arg) + no Count: the nearest expiry's whole chain — byte-identical to no-arg chain. if (countArg === undefined) { return renderChain(ctx.market.instrument, ctx.market.chain, ctx.market.chainDte); } const n = countArg.count; const { rows, availableStrikes } = selectNearMoneyStrikes(ctx.market.chain, n); const base = renderChain(ctx.market.instrument, rows, ctx.market.chainDte); if (availableStrikes >= n) return base; // fully served — the N nearest strikes (byte-identical to no-arg when N = available) const missing = n - availableStrikes; // Absent-not-hidden (ADR-0041 §3): render the available strikes, then NAME the shortfall — never a // silent fewer-than-requested, never a fabricated leg to pad the count. const shortfall = ` (requested ${n} near-money strikes; ${availableStrikes} available — ${missing} absent with reason: ` + `the frozen near-money chain carries only ${availableStrikes} strike${availableStrikes === 1 ? "" : "s"})`; return `${base}\n${shortfall}`; } /** The ExpiryOrdinal `e-N` (N ≥ 1) further-out-expiry absence render (Train 1B / ADR-0041 §1/§3) — a * TOTAL `renderable` judgment: the address is valid (it bound at `mat`) but the frozen frame carries * only the nearest expiry, so the pane renders the absence AS A VALUE (naming the multi-expiry data * train, kestrel-wa0j.20), never a fabricated expiry. A carried Count is echoed (the requested count is * moot when the expiry is absent — say so, never silently drop it). */ function renderChainFurtherExpiry( ctx: PaneBuildContext, expiry: number, countArg: Extract<PaneSelectionArg, { kind: "arg-count" }> | undefined, ): string { const countLabel = countArg !== undefined ? ` ${countArg.count}` : ""; const addr = `e-${expiry}`; return ( `chain (near-money)${countLabel} · ${ctx.market.instrument} · ${addr} · ` + `(frozen chain carries no expiry at ${addr} — the nearest expiry only; multi-expiry chain data threads with kestrel-wa0j.20)` ); } // ── macro — absent-with-reason in v1 (EVALUATION.md briefing spec) ───────────── const MACRO_PANE = "macro: unavailable (v1 harness)"; // ── acting-detail (RUNTIME §5–6) — the plan-lifecycle view ───────────────────── function planTag(plan: string | undefined): string { return plan === undefined ? "" : ` (${plan})`; } function renderPositions(positions: readonly Position[]): string { if (positions.length === 0) return "positions:\n (none)"; // ADR-0017 (kestrel-orx): an equity/spot leg has NO strike/right — render `<instrument> shares` and // DROP the option-only `fair` column (a spot leg's fair is not a chain leg); an option leg keeps // today's `<strike><right> … fair …` form BYTE-IDENTICAL. const lines = positions.map((p: Position) => isSpotLeg(p) ? ` ${signedInt(p.qty)} ${p.instrument} shares basis ${px(p.basis)}${unrealTag(p)}${planTag(p.plan)}` : ` ${signedInt(p.qty)} ${p.instrument} ${px(p.strike)}${p.right} basis ${px(p.basis)} fair ${px(p.fair)}${unrealTag(p)}${planTag(p.plan)}`, ); return ["positions:", ...lines].join("\n"); } /** The running unrealized-P&L tag (kestrel-c11) — the position's `unrealUsd` in DOLLARS, so the agent * READS its P&L instead of computing (and mis-scaling) it. Absent ⇒ no tag (purely additive); `null` * ⇒ `unreal —` (mark UNKNOWN, fail-closed — never a fabricated 0). */ function unrealTag(p: Position): string { return p.unrealUsd === undefined ? "" : ` unreal ${usd(p.unrealUsd)}`; } function renderResting(resting: readonly RestingOrder[]): string { if (resting.length === 0) return "resting:\n (none)"; const lines = resting.map((o: RestingOrder) => { const note = o.note !== undefined ? ` [${o.note}]` : ""; // ADR-0017 (kestrel-orx): equity/spot ⇒ `<instrument> shares`; option ⇒ `<strike><right>` (unchanged). const leg = isSpotLeg(o) ? "shares" : `${px(o.strike)}${o.right}`; return ` ${o.side.toUpperCase()} ${o.qty} ${o.instrument} ${leg} @ ${px(o.px)}${note}${planTag(o.plan)}`; }); return ["resting:", ...lines].join("\n"); } function renderFills(fills: readonly FillRecord[]): string { if (fills.length === 0) return "fills since last:\n (none)"; const lines = fills.map((f: FillRecord) => { const at = f.clock !== undefined ? ` @${f.clock}` : ""; // ADR-0017 (kestrel-orx): equity/spot ⇒ `<instrument> shares`; option ⇒ `<strike><right>` (unchanged). const leg = isSpotLeg(f) ? "shares" : `${px(f.strike)}${f.right}`; return ` ${f.side.toUpperCase()} ${f.qty} ${f.instrument} ${leg} @ ${px(f.px)}${at}${planTag(f.plan)}`; }); return ["fills since last:", ...lines].join("\n"); } function renderBudget(budget: Budget | null | undefined): string { // kestrel-wa0j.47: labelled `premium budget` (dollars of premium spend), never bare `budget` — // the kernel's `-- BUDGET / REMAINING-R --` section quotes the RISK envelope in R units, and one // word carrying two units across two blocks is exactly the confusable-surface ADR-0041 §1 forbids. if (budget === null || budget === undefined) return `premium budget: ${DASH}`; const total = budget.total !== undefined ? ` (total ${px(budget.total)}` : ""; const maxR = budget.maxConcurrentR !== undefined ? `, maxR ${px(budget.maxConcurrentR)})` : total !== "" ? ")" : ""; return `premium budget: used ${money(budget.used)} / remaining ${money(budget.remaining)}${total}${maxR}`; } function renderPlans(kernel: Kernel): string { if (kernel.plans.length === 0) return "plans:\n (none)"; const lines = kernel.plans.map((p) => { const outcome = p.outcome !== undefined ? `(${p.outcome})` : ""; // kestrel-50w: an authored plan stuck on an unsatisfiable regime gate carries the engine's arm-block // reason — render it as `(blocked: <reason>)` so a bare `authored` (a live plan awaiting its WHEN) is // DISTINGUISHABLE from one that can never arm (the phantom-position trap). const blocked = p.blockedReason !== undefined ? ` (blocked: ${p.blockedReason})` : ""; const note = p.note !== undefined ? ` — ${p.note}` : ""; return ` ${p.name}: ${p.state}${outcome === "" ? "" : " " + outcome}${blocked}${note}`; }); return ["plans:", ...lines].join("\n"); } /** The trailing plan-lifecycle detail block ("KERNEL (acting)"): fills-since-last + plan states + * the detailed inventory view. The safety/control half of the same Kernel LEADS every frame as the * cockpit block ({@link ./render.ts renderKernel}); this is its plan-lifecycle complement. */ function renderActingDetail(kernel: Kernel): string { return [ "KERNEL (acting)", renderPositions(kernel.positions), renderResting(kernel.resting), renderFills(kernel.fillsSinceLast), renderBudget(kernel.budget), renderPlans(kernel), ].join("\n"); } // ── failed-breaks (kestrel-4gl.13.1) — the fake-out trap made legible ────────── // // A CALC tally of pokes past HOD/LOD/OR-high/OR-low that did NOT HOLD this session — the ORB // fake-out poke IS a failed break, so this pane literally shows the trap before the agent chases it. // PURE over `market.tape` (OHLC bars) + `market.levels` (the break levels). No receipt, no new data. // // Definitions (precise, so the count is deterministic): // • LEVEL — a break level with a direction: UP levels (hod, orHigh) are broken by trading ABOVE; // DOWN levels (lod, orLow) by trading BELOW. VWAP is a MEAN, not a break level → not counted here. // • POKE — a maximal run of ≥1 CONSECUTIVE bars that PIERCE the level (up: `bar.high > L`; // down: `bar.low < L`). A bar that only touches (`high === L`) does not pierce (strict). // • HELD — the poke's run reaches the LAST bar of the window AND that last bar CLOSES past the // level (up: `close > L`; down: `close < L`) — i.e. price is still through the level at the last // observation (the break sustained). // • FAILED — a poke that did not hold: either a later bar brought price back inside (the run ends // before the window does), or the run reached the end but the last bar CLOSED back inside (an // intrabar poke rejected at the close — the textbook fake-out). // • EXCURSION — the max distance a poke reached strictly past the level (its trap depth). // • RUN — the poke's length in bars (the "time-above/below", in whole tape buckets — the frozen // projection carries no sub-bar time, so we report bars, never invented seconds). /** A break level and the side a break of it points. UP = broken above (hod/orHigh); DOWN = below. */ interface BreakLevel { readonly label: string; readonly value: number | null | undefined; readonly dir: "up" | "down"; } /** One poke's outcome over the window: how far past it reached, how many bars it lasted, held?. */ interface Poke { readonly excursion: number; readonly bars: number; readonly held: boolean; } /** Every poke past `level` in `dir`, in tape order. Pure: a total function of the OHLC rows. */ function pokesForLevel(rows: readonly TapeRow[], level: number, dir: "up" | "down"): readonly Poke[] { const pierces = (r: TapeRow): boolean => (dir === "up" ? r.high > level : r.low < level); const pastBy = (p: number): number => (dir === "up" ? p - level : level - p); const pokes: Poke[] = []; const n = rows.length; let i = 0; while (i < n) { if (!pierces(rows[i]!)) { i += 1; continue; } let j = i; let excursion = 0; while (j < n && pierces(rows[j]!)) { const r = rows[j]!; const ex = pastBy(dir === "up" ? r.high : r.low); if (ex > excursion) excursion = ex; j += 1; } const reachesEnd = j === n; const lastClose = rows[j - 1]!.close; const held = reachesEnd && (dir === "up" ? lastClose > level : lastClose < level); pokes.push({ excursion, bars: j - i, held }); i = j; } return pokes; } /** One `failed-breaks` line for a level. Absent level ⇒ explicit unknown (invents no value). */ function failedBreakLine(rows: readonly TapeRow[], lv: BreakLevel): { readonly line: string; readonly failed: number } { if (lv.value === null || lv.value === undefined || !Number.isFinite(lv.value)) { return { line: ` ${lv.label} ${DASH}: level unknown`, failed: 0 }; } const pokes = pokesForLevel(rows, lv.value, lv.dir); const probed = pokes.length; const held = pokes.filter((p) => p.held).length; const failedPokes = pokes.filter((p) => !p.held); const failed = failedPokes.length; const head = ` ${lv.label} ${px(lv.value)}: probed ${probed}×`; if (probed === 0) return { line: head, failed: 0 }; const detail = `${head} · held ${held}× · failed ${failed}×`; if (failed === 0) return { line: `${detail} · clean break (held)`, failed: 0 }; const maxExc = failedPokes.reduce((m, p) => (p.excursion > m ? p.excursion : m), 0); const longest = failedPokes.reduce((m, p) => (p.bars > m ? p.bars : m), 0); return { line: `${detail} · max excursion ${num(maxExc, 2)} · longest ${longest} bar${longest === 1 ? "" : "s"}`, failed, }; } /** The failed-break tally: per-level poke/held/failed counts + trap depth, and a session total. */ function renderFailedBreaks(market: MarketPane): string { const lv = market.levels; const levels: readonly BreakLevel[] = [ { label: "hod", value: lv.hod, dir: "up" }, { label: "lod", value: lv.lod, dir: "down" }, { label: "orHigh", value: lv.orHigh, dir: "up" }, { label: "orLow", value: lv.orLow, dir: "down" }, ]; if (market.tape.length === 0) { return `failed-breaks · ${market.instrument} · (no prints this window)`; } const built = levels.map((l) => failedBreakLine(market.tape, l)); const total = built.reduce((s, b) => s + b.failed, 0); const header = `failed-breaks · ${market.instrument} · ${total} failed this session`; return [header, ...built.map((b) => b.line)].join("\n"); } // ── level-interaction (kestrel-4gl.13.2) — the poke as exhaustion, not breakout ─ // // A CALC read of HOW price behaved at each key level (HOD/LOD/VWAP/OR edges): touches, and the // REJECTION CHARACTER of the most-recent interaction — wick (exhaustion, closed back inside) vs body // (continuation, closed through). Complements `failed-breaks` (counts) with SHAPE. PURE over the same // OHLC tape + levels; no receipt, no new data. // // Definitions: // • TOUCH — a bar whose range straddles the level (`low ≤ L ≤ high`): price traded AT the level. // • CHARACTER (break levels) — from the LAST touching bar (the current texture): "broke" if it // CLOSED through the level, "rejected" if it closed back inside. The reach past the level splits // into BODY-past (`|body edge − L|`, the part the candle body held) and WICK-past (the rest, the // part it gave back). A big wick + tiny body that closed inside is a textbook exhaustion print; // a body that closed through is a real break. // • VWAP (a MEAN, two-sided) — reports touches + which side the last touch CLOSED (reclaim/loss) // and the reach on each side of the mean; the reject/break verb does not apply to a mean. /** The wick-vs-body split of the last touching bar past a break level. Pure geometry. */ function pastSplit(r: TapeRow, level: number, dir: "up" | "down"): { readonly wick: number; readonly body: number } { const bodyHi = Math.max(r.open, r.close); const bodyLo = Math.min(r.open, r.close); if (dir === "up") { const body = Math.max(0, bodyHi - level); const wick = r.high - Math.max(bodyHi, level); return { wick, body }; } const body = Math.max(0, level - bodyLo); const wick = Math.min(bodyLo, level) - r.low; return { wick, body }; } /** One `level-interaction` line for a break level (hod/lod/orHigh/orLow). */ function breakInteractionLine(rows: readonly TapeRow[], lv: BreakLevel): string { if (lv.value === null || lv.value === undefined || !Number.isFinite(lv.value)) { return ` ${lv.label} ${DASH}: level unknown`; } const level = lv.value; const touches = rows.filter((r) => r.low <= level && level <= r.high); if (touches.length === 0) return ` ${lv.label} ${px(level)}: no touch`; const last = touches[touches.length - 1]!; const broke = lv.dir === "up" ? last.close > level : last.close < level; const verb = broke ? "broke (body through)" : "rejected (wick, closed inside)"; const { wick, body } = pastSplit(last, level, lv.dir); const wickLabel = lv.dir === "up" ? "upper-wick" : "lower-wick"; return ` ${lv.label} ${px(level)}: ${touches.length} touch${touches.length === 1 ? "" : "es"} · ${verb} · ${wickLabel} ${num(wick, 2)} / body ${num(body, 2)}`; } /** One `level-interaction` line for VWAP (a mean — reclaim/loss + reach each side, no break verb). */ function vwapInteractionLine(rows: readonly TapeRow[], vwap: number | null | undefined): string { if (vwap === null || vwap === undefined || !Number.isFinite(vwap)) { return ` vwap ${DASH}: level unknown`; } const touches = rows.filter((r) => r.low <= vwap && vwap <= r.high); if (touches.length === 0) return ` vwap ${px(vwap)}: no touch`; const last = touches[touches.length - 1]!; const side = last.close > vwap ? "closed above (reclaim)" : last.close < vwap ? "closed below (loss)" : "closed at vwap"; const up = last.high - vwap; const down = vwap - last.low; return ` vwap ${px(vwap)}: ${touches.length} touch${touches.length === 1 ? "" : "es"} · ${side} · reach up ${num(up, 2)} / down ${num(down, 2)}`; } /** The level-interaction detail: rejection character (wick vs body) at each key level. */ function renderLevelInteraction(market: MarketPane): string { const lv = market.levels; if (market.tape.length === 0) { return `level-interaction · ${market.instrument} · (no prints this window)`; } const breakLevels: readonly BreakLevel[] = [ { label: "hod", value: lv.hod, dir: "up" }, { label: "lod", value: lv.lod, dir: "down" }, { label: "orHigh", value: lv.orHigh, dir: "up" }, { label: "orLow", value: lv.orLow, dir: "down" }, ]; const lines = [ breakInteractionLine(market.tape, breakLevels[0]!), // hod breakInteractionLine(market.tape, breakLevels[1]!), // lod vwapInteractionLine(market.tape, lv.vwap), breakInteractionLine(market.tape, breakLevels[2]!), // orHigh breakInteractionLine(market.tape, breakLevels[3]!), // orLow ]; return [`level-interaction · ${market.instrument}`, ...lines].join("\n"); } // ── range-velocity (kestrel-4gl.13.3) — the current move's RANGE + VELOCITY, the quality of momentum ─ // // A CALC read of the realized RANGE (the tape's own excursion) and the VELOCITY of the current move // (signed close-to-close), and whether that velocity is IMPULSIVE (expanding into the last bar, at/near // its own p99 — a fundable break) or GRINDING (collapsing, far below p99 — a poke on decelerating // velocity is unfunded, the textbook EQ fakeout). PURE over `market.tape` ALONE (+ the instrument for // the header): reads NO level / chain / kernel / model input — CALC, no new data sourced. Aligned with // the runtime Window doctrine (CONTEXT.md): range = excursion, velocity = signed, move = magnitude, // p99 = the window's own trailing baseline (the per-bar |Δclose| distribution's nearest-rank max). // // Definitions (precise, so the read is deterministic): // • RANGE — the tape's realized excursion `max(high) − min(low)` over the window (its OWN extremes, // NOT `levels.hod/lod` — "Data: the tape. Nothing new." per spec §3, so the "no new data" proof // is maximal — range never reaches for a level Field). // • VELOCITY — signed close-to-close: per-bar `Δ_i = close_i − close_{i-1}`; the window's NET velocity // is `close_last − close_first`, and MOVE is its magnitude `|net|`. // • p99 — the nearest-rank 99th percentile of the per-bar `|Δ|` distribution (for small n this is the // max — matching the runtime's `p99 === max{1..10}`). The current bar's velocity is judged vs it. // • IMPULSIVE — the current bar `|Δ_last|` is at/near p99 (≥ half) AND accelerating // (`|Δ_last| > |Δ_prev|`): the break is being driven — fundable. // • GRINDING — the current bar is far below p99 (< half) AND decelerating (`|Δ_last| < |Δ_prev|`): // velocity is collapsing — a poke here is unfunded (the EQ fakeout — a poke on decelerating velocity). // • FAIL-CLOSED — no prints ⇒ `(no prints this window)`; a single bar (no close-to-close delta) ⇒ // velocity UNKNOWN (never a fabricated impulsive/grinding read); < 2 deltas ⇒ the accel/decel read // is UNKNOWN/insufficient, never invented. /** The nearest-rank 99th percentile of a non-empty sample (ascending sort; rank = ceil(0.99·n), clamped * to ≥1). For small n this is the max — matching the runtime Window doctrine's `p99 === max{1..10}` * (CONTEXT.md; tests/series.test.ts). Pure — a total function of the sample. */ function p99NearestRank(sample: readonly number[]): number { const sorted = [...sample].sort((a, b) => a - b); const rank = Math.max(1, Math.ceil(0.99 * sorted.length)); return sorted[rank - 1]!; } /** * `range-velocity` — realized RANGE + VELOCITY of the current move (CALC, no receipt, no new data). * Is the break IMPULSIVE (accelerating at/near its p99 — fundable) or GRINDING (decelerating far below * p99 — a poke here is unfunded, the EQ fakeout)? PURE over `market.tape` (+ the instrument header); * reads no level/chain/kernel/model Field. FAIL-CLOSED: no prints ⇒ absent-not-hidden; a single bar ⇒ * velocity UNKNOWN; < 3 bars ⇒ the accel/decel verdict is UNKNOWN — never a fabricated velocity. * DATE-BLIND: renders bar counts + prices + trend words only (no clock/date token). */ function renderRangeVelocity(market: MarketPane): string { const rows = market.tape; const head = `range-velocity · ${market.instrument}`; if (rows.length === 0) { return `${head} · (no prints this window)`; } // RANGE — the tape's OWN realized excursion (max high − min low), never levels.hod/lod (no new data). let hi = -Infinity; let lo = Infinity; for (const r of rows) { if (r.high > hi) hi = r.high; if (r.low < lo) lo = r.low; } const range = hi - lo; // A single bar carries no close-to-close velocity — fail closed to UNKNOWN (invents no velocity). if (rows.length < 2) { return [ head, ` range ${num(range, 2)} · velocity ${DASH} · 1 bar (insufficient for a close-to-close velocity)`, " read: UNKNOWN — need ≥2 bars for a velocity read (insufficient)", ].join("\n"); } // VELOCITY — signed close-to-close deltas; the window's net + its magnitude (move); the p99 baseline. const deltas: number[] = []; for (let i = 1; i < rows.length; i += 1) deltas.push(rows[i]!.close - rows[i - 1]!.close); const absDeltas = deltas.map((d) => Math.abs(d)); const net = rows[rows.length - 1]!.close - rows[0]!.close; const move = Math.abs(net); const p99 = p99NearestRank(absDeltas); const lastAbs = absDeltas[absDeltas.length - 1]!; const barVel = deltas[deltas.length - 1]!; // the signed current-bar velocity const bars = rows.length; const ratioPct = p99 > 0 ? Math.round((lastAbs / p99) * 100) : null; const ratioStr = ratioPct === null ? DASH : `${ratioPct}%`; const l2 = ` range ${num(range, 2)} · velocity ${money(net)} · move ${num(move, 2)} over ${bars} bars`; const l3base = ` bar velocity now ${money(barVel)} · p99 ${num(p99, 2)} (${ratioStr} of p99)`; // The accel/decel read needs ≥2 deltas (≥3 bars); fewer ⇒ UNKNOWN, never a fabricated verdict. const prevAbs = absDeltas.length >= 2 ? absDeltas[absDeltas.length - 2]! : null; if (prevAbs === null || ratioPct === null) { return [ head, l2, `${l3base} · trend UNKNOWN`, " read: UNKNOWN — need ≥3 bars to judge accelerating vs decelerating (insufficient)", ].join("\n"); } const trend = lastAbs > prevAbs ? "ACCELERATING" : lastAbs < prevAbs ? "DECELERATING" : "STEADY"; const l3 = `${l3base} · ${trend}`; let read: string; if (trend === "ACCELERATING" && ratioPct >= 50) { read = " read: IMPULSIVE — break on accelerating velocity at/near its p99, fundable"; } else if (trend === "DECELERATING" && ratioPct < 50) { read = " read: GRINDING — poke on decelerating velocity far below p99, unfunded (EQ fakeout)"; } else { read = ` read: MIXED — velocity ${ratioStr} of p99, ${trend.toLowerCase()}; inconclusive (neit