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
JavaScript
/**
* # 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