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.
340 lines (339 loc) • 15.6 kB
JavaScript
/**
* # frame/render-json — the `json` Rendering: a THIRD format adapter at the one-walk seam (ADR-0052 §2)
*
* The `json` Rendering materializes a {@link ./types.ts Kernel}/Frame into **typed, structural JSON**
* carrying every Field WHOLE — the machine-legible twin of the canonical text screen
* ({@link ./render.ts}) and the token-optimal ARM ({@link ./render-arm.ts}). Like both text adapters,
* the kernel goes through the ONE cockpit-kernel walk ({@link ./kernel-walk.ts walkKernel}): the
* {@link JsonKernelSink} is the third {@link ./kernel-walk.ts KernelSink}, visiting the SAME 8
* fixed-order sections in the SAME order, so a new/reordered kernel section lands in json from the
* single edit in `kernel-walk.ts`. This module NEVER touches canonical/ARM bytes — the renderer epoch
* corpus (canonical text + both ARMs) is unchanged; the json Rendering's byte contract is its own
* golden fixtures, not the epoch.
*
* ## The invariants this adapter lives inside (ADR-0052 §2, ADR-0009 — not up for grading)
* - **Fields cross WHOLE.** A {@link ./types.ts Field} (a predictor/regime {@link ./types.ts Claim})
* is emitted as `{ value, attribution, source?, modelVer?, confidence?, asOfSeq? }` — a `MODEL`
* Field keeps its receipt (`source` + `modelVer`) and `confidence`; an `OBS`/`CALC` value crosses as
* its raw typed value (it carries no receipt). Provenance is never invented: a raw kernel number
* (a health rate, a strike, an envelope) is NOT dressed up with a fabricated attribution.
* - **null Fields cross as explicit UNKNOWN.** An absent/`null`/non-finite value is carried as the
* explicit {@link UNKNOWN} marker — NEVER omitted, NEVER defaulted to `0`/`false`. This mirrors the
* canonical codebook (ADR-0041 §1: the `UNKNOWN` marker, never a bare `?`/omission); the whole
* point is that a reader can tell "unknown" from "absent" from "zero".
* - **Invents no value.** Every number/string is a field on the input; there is no layout to compute
* (json is structural), so a Rendering here never derives a value canonical draws (an axis bound, a
* column) — it carries the raw Fields, from which a consumer (the honest chart, ADR-0052 §3)
* computes its own view.
* - **Fail-closed honesty.** A dishonest `MODEL` claim (non-MODEL, or missing its receipt/confidence)
* is REFUSED at the seam ({@link ./types.ts assertClaimHonest}) exactly as the canonical adapter
* refuses it — a degraded claim is never serialized as if honest.
* - **Deterministic + byte-stable.** Serialization runs through the ONE canonical JSON module
* ({@link ../canonical/json.ts canonicalizeJson}: recursively ordered keys, kept array order, no
* trailing newline) — the SAME bytes the Bus/Blotter content-address, so the same Frame always
* yields byte-identical json. No wall clock, no RNG (RUNTIME §0).
*/
import { canonicalizeJson } from "../canonical/json.js";
import { assertBriefHashHonest } from "./brief.js";
import { assertClaimHonest } from "./types.js";
import { bucketEngineLog, walkKernel, } from "./kernel-walk.js";
// ─────────────────────────────────────────────────────────────────────────────
// Explicit UNKNOWN — a null Field crosses the wire as an explicit marker, never omitted/defaulted
// ─────────────────────────────────────────────────────────────────────────────
/** The explicit unknown marker (ADR-0052 §2 / the ADR-0041 §1 codebook). An absent/`null`/non-finite
* value crosses the json wire as this literal — NEVER an omitted key (which reads as "absent") and
* NEVER a defaulted `0`/`false` (which reads as a real value). The one honest reading of a null Field. */
export const UNKNOWN = "UNKNOWN";
/** A number Field, or explicit UNKNOWN when absent/`null`/non-finite (never `0`, never omitted). */
function maybeNum(x) {
return x === null || x === undefined || !Number.isFinite(x) ? UNKNOWN : x;
}
/** A string Field, or explicit UNKNOWN when absent/empty (never omitted). */
function maybeStr(s) {
return s === null || s === undefined || s === "" ? UNKNOWN : s;
}
// ─────────────────────────────────────────────────────────────────────────────
// The json KernelSink — the THIRD adapter over the ONE walk (ADR-0052 §1/§2)
// ─────────────────────────────────────────────────────────────────────────────
const emptyEngineLog = { buckets: [], other: [] };
function fieldJson(f) {
const out = { value: f.value, attribution: f.attribution };
if (f.source !== undefined)
out.source = f.source;
if (f.modelVer !== undefined)
out.modelVer = f.modelVer;
if (f.confidence !== undefined)
out.confidence = f.confidence;
if (f.asOfSeq !== undefined)
out.asOfSeq = f.asOfSeq;
return out;
}
function engineActionsJson(items) {
return items.map((e) => ({ id: e.id, kind: e.kind, asOfSeq: e.asofSeq, reason: maybeStr(e.reason) }));
}
/**
* The json {@link KernelSink} — {@link walkKernel}'s third adapter. It accumulates a typed
* {@link KernelJson}: the SAME 8 fixed-order sections, every section ALWAYS assigned (absent-not-hidden),
* Fields carried WHOLE, nulls as explicit {@link UNKNOWN}. Refuses a dishonest claim (fail-closed),
* exactly as the canonical sink does. PURE — no wall clock, no RNG.
*/
class JsonKernelSink {
frame = "OPEN";
mandateJson = null;
briefJson = null;
wakeJson = UNKNOWN;
dataHealthJson = [];
unavailableJson = [];
positionsJson = [];
restingJson = [];
budgetJson = UNKNOWN;
sizingJson = UNKNOWN;
ownerEnvelopeJson = UNKNOWN;
ownerActsJson = [];
engineLogJson = emptyEngineLog;
claimsJson = [];
sentinel(frameKind) {
this.frame = frameKind;
}
frameLine() {
// The frame kind rides {@link sentinel}; json folds it into the `frame` key (no separate line).
}
mandate(m) {
this.mandateJson = {
objective: m.objective,
rUsd: maybeNum(m.rUsd),
successCriterion: m.successCriterion,
riskRule: m.riskRule,
};
}
brief(b) {
// Fail-closed (kestrel-voy9), exactly as the canonical sink does before emitting the hash: a
// supplied `{ text, hash }` whose hash disagrees with its text would attribute the WRONG thesis.
assertBriefHashHonest(b);
const out = {
text: b.text,
hash: b.hash,
channel: "directional; NOT a constraint, NOT an admission input",
...(b.version !== undefined ? { version: b.version } : {}),
};
this.briefJson = out;
}
// §1 WAKE — reason / severity / RELATIVE deadline (never a wall-clock/date).
wake(w) {
this.wakeJson =
w === undefined || w === null
? UNKNOWN
: { reason: w.reason, severity: w.severity, deadlineMinToClose: maybeNum(w.deadline) };
}
// §2 DATA-HEALTH per vehicle + the NAMED unavailable capabilities (empty ⇒ [], never omitted).
dataHealth(rows, unavailable) {
this.dataHealthJson = rows.map((h) => ({
instrument: h.instrument,
bidPresentRate: maybeNum(h.bidPresentRate),
twoSided: h.twoSided === true,
staleS: maybeNum(h.staleS),
dark: h.dark === true,
}));
this.unavailableJson = [...unavailable];
}
// §3 POSITIONS / INVENTORY-CLAIMS — an equity/spot leg carries neither strike nor right (ADR-0017).
positions(positions) {
this.positionsJson = positions.map((p) => ({
instrument: p.instrument,
strike: maybeNum(p.strike),
right: p.right === undefined || p.right === null ? UNKNOWN : p.right,
qty: p.qty,
basis: maybeNum(p.basis),
unrealUsd: maybeNum(p.unrealUsd),
structure: maybeStr(p.structure),
claimOwner: maybeStr(p.claimOwner),
plan: maybeStr(p.plan),
}));
}
// §4 RESTING ORDERS — live AND clamped as independent flags (a live flag never masks a clamp).
resting(resting) {
this.restingJson = resting.map((o) => ({
ref: o.ref,
side: o.side,
instrument: o.instrument,
strike: maybeNum(o.strike),
right: o.right === undefined || o.right === null ? UNKNOWN : o.right,
qty: o.qty,
px: maybeNum(o.px),
live: o.live === true,
clamped: o.clamped === true,
note: maybeStr(o.note),
}));
}
// §5 BUDGET / REMAINING-R + the sizing headroom.
budget(b) {
if (b === undefined || b === null) {
this.budgetJson = UNKNOWN;
this.sizingJson = UNKNOWN;
return;
}
this.budgetJson = {
remainingR: maybeNum(b.remainingR),
planEnvelope: maybeNum(b.planEnvelope),
bookEnvelope: maybeNum(b.bookEnvelope),
ownerEnvelope: maybeNum(b.ownerEnvelope),
};
this.sizingJson = sizingJson(b.sizing);
}
// §6 OWNER ENVELOPE + ACTS — the envelope echo reads the same §5 budget.
ownerActs(b, acts) {
this.ownerEnvelopeJson = b !== undefined && b !== null ? maybeNum(b.ownerEnvelope) : UNKNOWN;
this.ownerActsJson = acts.map((a) => ({ id: a.id, kind: a.kind, asOfSeq: a.asofSeq }));
}
// §7 L0/L1 ENGINE LOG — bucketed fired | cancelled | rejected | clamped (+ catch-all `other`).
engineLog(elog) {
const { buckets, other } = bucketEngineLog(elog);
this.engineLogJson = {
buckets: buckets.map((b) => ({ kind: b.kind, count: b.items.length, actions: engineActionsJson(b.items) })),
other: engineActionsJson(other),
};
}
// §8 PREDICTOR / REGIME CLAIMS — each an HONEST MODEL Field carried WHOLE (fail-closed on a dishonest one).
claims(claims) {
this.claimsJson = claims.map((c) => {
assertClaimHonest(c); // fail-closed: a dishonest claim is REFUSED, never serialized as if honest.
return { claimType: c.claimType, field: fieldJson(c.field) };
});
}
result() {
return {
frame: this.frame,
mandate: this.mandateJson,
brief: this.briefJson,
wake: this.wakeJson,
dataHealth: this.dataHealthJson,
unavailable: this.unavailableJson,
positions: this.positionsJson,
resting: this.restingJson,
budget: this.budgetJson,
sizing: this.sizingJson,
ownerEnvelope: this.ownerEnvelopeJson,
ownerActs: this.ownerActsJson,
engineLog: this.engineLogJson,
claims: this.claimsJson,
};
}
}
function sizingJson(s) {
if (s === undefined || s === null)
return UNKNOWN;
const out = {
instrument: s.instrument,
unit: s.unit,
basisPerUnit: maybeNum(s.basisPerUnit),
maxUnits: maybeNum(s.maxUnits),
remainingUsd: maybeNum(s.remainingUsd),
...(s.note !== undefined ? { note: s.note } : {}),
};
return out;
}
/**
* Materialize the cockpit kernel as typed {@link KernelJson} — the ONE walk ({@link walkKernel})
* driving the {@link JsonKernelSink}. This is the json Rendering at the walkKernel seam. PURE +
* deterministic; refuses a dishonest claim (fail-closed, {@link assertClaimHonest}).
*/
export function renderKernelJson(kernel, frameKind) {
const sink = new JsonKernelSink();
walkKernel(kernel, frameKind, sink);
return sink.result();
}
function instrumentsJson(instruments) {
return instruments.map((i) => ({
symbol: i.symbol,
assetClass: i.assetClass,
role: maybeStr(i.role),
multiplier: maybeNum(i.multiplier),
tick: maybeNum(i.tick),
}));
}
function levelsJson(lv) {
return {
spot: maybeNum(lv.spot),
priorClose: maybeNum(lv.priorClose),
hod: maybeNum(lv.hod),
lod: maybeNum(lv.lod),
vwap: maybeNum(lv.vwap),
orHigh: maybeNum(lv.orHigh),
orLow: maybeNum(lv.orLow),
};
}
function tapeJson(rows) {
return rows.map((r) => ({
clock: r.clock,
open: maybeNum(r.open),
high: maybeNum(r.high),
low: maybeNum(r.low),
close: maybeNum(r.close),
volume: maybeNum(r.volume),
}));
}
function chainJson(rows) {
return rows.map((r) => ({
strike: maybeNum(r.strike),
right: r.right,
bid: maybeNum(r.bid),
ask: maybeNum(r.ask),
fair: maybeNum(r.fair),
fairNote: maybeStr(r.fairNote),
}));
}
function marketJson(market) {
return {
instrument: market.instrument,
levels: levelsJson(market.levels),
tapeBucketMin: maybeNum(market.tapeBucketMin),
tape: tapeJson(market.tape),
chainDte: maybeNum(market.chainDte),
chain: chainJson(market.chain),
};
}
/**
* Render the OPEN keyframe briefing as the typed json Rendering (ADR-0052 §2) — the kernel through
* the seam ({@link renderKernelJson}) plus the raw market/instrument Fields (the honest-chart
* substrate). Fields WHOLE, nulls explicit UNKNOWN, no value invented. Refuses a dishonest claim
* (fail-closed). PURE + deterministic.
*/
export function renderBriefingJson(input) {
return {
kind: "OPEN",
timeToCloseMin: maybeNum(input.timeToCloseMin),
phase: maybeStr(input.phase),
clockET: maybeStr(input.clockET),
kernel: renderKernelJson(input.kernel, "OPEN"),
instruments: instrumentsJson(input.instruments),
market: marketJson(input.market),
};
}
/**
* Render a WAKE delta frame as the typed json Rendering (ADR-0052 §2). Structural, so it carries the
* full since-last kernel + market (json is NOT the KV-cache-optimized text delta path — a consumer
* diffs structurally). Fields WHOLE, nulls explicit UNKNOWN. Refuses a dishonest claim (fail-closed).
* PURE + deterministic.
*/
export function renderWakeDeltaJson(input) {
return {
kind: "WAKE",
wakeIndex: input.wakeIndex,
minutesSinceLast: maybeNum(input.minutesSinceLast),
timeToCloseMin: maybeNum(input.timeToCloseMin),
phase: maybeStr(input.phase),
clockET: maybeStr(input.clockET),
wakeReason: maybeStr(input.wakeReason),
kernel: renderKernelJson(input.kernel, "WAKE"),
market: marketJson(input.market),
};
}
/**
* Serialize a json Rendering to its byte-stable canonical bytes — through the ONE canonical JSON
* module ({@link ../canonical/json.ts canonicalizeJson}: recursively ordered keys, kept array order,
* NO trailing newline), the SAME bytes the Bus/Blotter content-address. The same Frame always yields
* byte-identical text (determinism, RUNTIME §0). Deterministic + pure.
*/
export function serializeFrameJson(frame) {
return canonicalizeJson(frame);
}