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.

832 lines 149 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.js"; import { founderViewFor } from "./founder-registry.js"; import { paneRefusal } from "./refusals.js"; import { SPOT_STALE_AFTER_MS } from "./options-analytics.js"; import { DASH, isSpotLeg, money, num, pad, px, signedInt, usd } from "./format.js"; import { candleLine, tapeBounds } from "./tape-geometry.js"; import { resolveScheme, rollingAnchorOf, schemeHasBoundary } from "./session-scheme.js"; import { PANE_BOUNDARY_REQUIREMENT, schemeConditionedDefect } from "./paradigm-ledger.js"; // ───────────────────────────────────────────────────────────────────────────── // The pane builders — the EXISTING v1 panes, moved here verbatim (byte-identical output) // ───────────────────────────────────────────────────────────────────────────── // ── instruments ───────────────────────────────────────────────────────────── function renderInstruments(instruments) { 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) { 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, lv) { 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"]; /** Render ONE named level cell, byte-identical to that cell's rendering inside {@link renderLevels}. */ function levelCell(name, lv) { 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, args) { const lv = ctx.market.levels; const cells = []; 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.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, 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, bucketMin, sessionTag) { // 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, factor) { const out = []; 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, window) { 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, args) { const windowArg = args.find((a) => a.kind === "arg-window"); const sessionArg = args.find((a) => 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, ordinal, windowArg) { 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, session, ordinal, windowArg) { 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) { 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, chain, dte) { // 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, n) { const seen = new Set(); const ordered = []; 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, args) { const countArg = args.find((a) => a.kind === "arg-count"); const expiryArg = args.find((a) => 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, expiry, countArg) { 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) { return plan === undefined ? "" : ` (${plan})`; } function renderPositions(positions) { 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) => 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) { return p.unrealUsd === undefined ? "" : ` unreal ${usd(p.unrealUsd)}`; } function renderResting(resting) { if (resting.length === 0) return "resting:\n (none)"; const lines = resting.map((o) => { 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) { if (fills.length === 0) return "fills since last:\n (none)"; const lines = fills.map((f) => { 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) { // 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) { 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) { return [ "KERNEL (acting)", renderPositions(kernel.positions), renderResting(kernel.resting), renderFills(kernel.fillsSinceLast), renderBudget(kernel.budget), renderPlans(kernel), ].join("\n"); } /** Every poke past `level` in `dir`, in tape order. Pure: a total function of the OHLC rows. */ function pokesForLevel(rows, level, dir) { const pierces = (r) => (dir === "up" ? r.high > level : r.low < level); const pastBy = (p) => (dir === "up" ? p - level : level - p); const pokes = []; 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, lv) { 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) { const lv = market.levels; const levels = [ { 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, level, dir) { 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, lv) { 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, vwap) { 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) { const lv = market.levels; if (market.tape.length === 0) { return `level-interaction · ${market.instrument} · (no prints this window)`; } const breakLevels = [ { 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) { 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) { 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 = []; 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; 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 (neither clean impulse nor clean grind)`; } return [head, l2, l3, read].join("\n"); } // ── series-summary (kestrel-wa0j.63) — the tape's trend as NUMBERS, embedder-legible ───────── // // A CALC block of numeric TREND statistics over the served tape — the v5 VELOCITY [CALC] stat block // PORTED (docs/paper/examples/percept-v5/open.txt: `VELOCITY [CALC] … 1-min |move| p50=… p90=… max=… n=…`) // and EXTENDED with the geometry-named stats the render only ever carried as ASCII bar-art: drift over // the served window, close-vs-VWAP signed distance, and the least-squares slope of the closes. PURE over // `market.tape` (OHLC closes) + `market.levels.vwap` (a level the frame already carries) — invents no value, // sources no new data, no wall clock, no RNG. // // WHY IT EXISTS (embed-geom-1, the EMBEDDING channel — a first): the trend facts the render carries live // ONLY as glyph tape (rotated candles) — embedder-illegible (trend probes read ~0.45 everywhere across // embedders because a bar-art trend is not a number an embedder can read). A numeric summary line closes // that gap. This is the first render change motivated by the embedding channel rather than the LLM channel; // its ledger cell evidence must therefore carry BOTH instruments (the quiz delta AND the embedder-probe // delta), since embedder-legibility for the trigger tier (the cascade) is half its value claim (kestrel-wa0j.60). // // TIER-MACHINERY (s6ng): this pane LITERALIZES DERIVE-tier constructs — %-above-VWAP (close-vs-vwap here) // and trend direction (drift/slope sign here). A quiz item over those constructs RECLASSIFIES DERIVE→RECALL // on any render carrying this pane (the answer is now read off the line, not derived), and the // literalization-value measurement (pane-off vs pane-on) applies — the line's reading value is measurable // on day one. SALIENCE-class affordance (kestrel-bwmz): it carries a difficulty-impact stamp obligation // BEFORE any bench-item use. // // Definitions (precise + deterministic, documented like `failed-breaks` so the read is replayable): // • WINDOW — the served tape rows (`market.tape`), oldest first. n_buckets = rows.length. // • DRIFT — the window's close-to-close move: `close_last − close_first`, in signed points, and as a // signed percent of `close_first` (`drift / close_first · 100`). A zero first close carries no percent // (division by zero) — the points still render, the percent is `—`. Needs ≥2 rows (a close-to-close // move is undefined on one row) — a 1-row/empty window renders absent-with-reason. // • CLOSE-vs-VWAP — the last close's signed distance from VWAP: `close_last − vwap`, in points, and as a // signed percent of vwap (`dist / vwap · 100`). Reads ONLY the last close + the carried `vwap`, so it // renders whenever both are present (independent of window length). Absent VWAP ⇒ absent-with-reason // (a distance from an absent mean is not a value we invent); a zero vwap carries no percent (`—`). // • SLOPE — the ordinary-least-squares slope of the closes against the bucket index x = 0,1,…,n−1 // (one unit per bucket): fit `close_i ≈ a + b·x_i`, and b is the reported slope in points/bucket: // b = Σ_i (x_i − x̄)(y_i − ȳ) / Σ_i (x_i − x̄)² (x̄, ȳ the means; y_i = close_i) // A positive b is an up-trend over the window, negative a down-trend — the CONTINUOUS companion to // drift's endpoint read. Needs ≥2 rows (a line needs two points); denominator 0 (all x equal, only at // n=1) never occurs past the ≥2 guard. Undefined on a 1-row/empty window ⇒ absent-with-reason. // • VELOCITY (the v5 port) — the 1-bucket |move| distribution: per-bucket `|Δ_i| = |close_i − close_{i-1}|` // for i = 1…n−1 (n−1 deltas). Reports p50, p90, max, and n (the delta count). Percentiles are // NEAREST-RANK on the ascending sample (rank = ⌈p·n⌉, clamped ≥1) — the repo's Window-doctrine // convention (matching `p99NearestRank` / range-velocity), so for small n p90 collapses to max exactly // as the v5 block shows (`p90=3.00 max=3.00 n=5`). Undefined on a 1-row/empty window (no deltas). // • FAIL-CLOSED — empty tape ⇒ `(no prints this window)`; a 1-row tape ⇒ drift/slope/velocity each render // absent-with-reason (need ≥2 buckets), while close-vs-vwap still renders IF vwap is present (it is a // point stat, honestly computable from one close — never suppressed, never invented). // • ONE CANONICAL FORM — every number renders through the shared {@link ./format.ts} primitives // (`num`/`money`/`px`), 2dp, one form; no epoch-shaped token appears (date-blind grep stays clean). /** The nearest-rank percentile of a non-empty sample at fraction `p` (ascending sort; rank = ⌈p·n⌉, * clamped ≥1). The repo's Window-doctrine convention — the generalization of {@link p99NearestRank} * to an arbitrary p (p50/p90 here). Pure: a total function of the sample. */ function percentileNearestRank(sample, p) { const sorted = [...sample].sort((a, b) => a - b); const rank = Math.max(1, Math.ceil(p * sorted.length)); return sorted[rank - 1]; } /** The least-squares slope b of the closes against the bucket index x = 0…n−1 (points/bucket). Pure. * Caller guarantees n ≥ 2, so Σ(x−x̄)² > 0 (never a divide-by-zero). See the block comment for the formula. */ function leastSquaresSlope(closes) { const n = closes.length; const xBar = (n - 1) / 2; // mean of 0…n−1 let yBar = 0; for (const y of closes) yBar += y; yBar /= n; let num2 = 0; let den = 0; for (let i = 0; i < n; i += 1) { const dx = i - xBar; num2 += dx * (closes[i] - yBar); den += dx * dx; } return num2 / den; } /** The close-vs-VWAP line: the last close's signed distance fro