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.

221 lines (204 loc) 13 kB
/** * # grade/headline — the three-channel P&L contract + the sampled qualification gate (kestrel-9gu.12) * * One session's P&L is reported as **three channels side by side** (RUNTIME §6; fill-realism forensics * 2026-07-13), never one number pretending to be all of them: * * • **floor** — realized $ under the deterministic strict-cross floor. The honest LOWER bound: only * definite, price-priority fills count. Calibration-free, bankable, byte-stable. * • **expected** — E[$] under the survival product `1 − Π(1 − pFill)` (ADR-0016). The analytic * expectation, banked per-cell by support (`bankableEv`, 9gu.3/ADR-0014) — an honest BOUND beside * the realization, never itself the headline (an expectation is not an outcome). * • **sampled** — the seeded causal realization (one Bernoulli per resting episode, ADR-0016 §3), * present ONLY on a `fillSeed` run. One replicate of the ensemble. * * **The headline is SELECTED, not assumed** (owner rule, kestrel-9gu.12 / 9gu.8.6 / 9gu.8 AC#7): * strict-cross floor stays the headline UNTIL the sampled channel passes its qualification gate — * calibration + causality + fidelity, recorded as a {@link SampledQualificationClaim} — and even a * qualified run's sampled channel is REFUSED the headline when any of its realizations rests on * extrapolated support (the `bankableEv` doctrine: never an uncalibrated headline). The gate FAILS * CLOSED: no claim on record ⇒ not qualified ⇒ floor headline, with the reasons on the record. * * The EXPECTED channel is never the headline: it is an expectation (a bound), and headline candidates * are realizations only — `floor` (deterministic) or `sampled` (seeded), per the owner-approved * fill-stack contract. * * Both graded surfaces — the {@link ../session/sim.ts EpisodeReport} and the {@link ../blotter Blotter} * — select through this ONE module (one policy, never forked), each assembling its * {@link SampledGateEvidence} from its own substrate (the settle report in-process; bus bytes in the * projector). Pure and deterministic throughout: fixed reason order, no wall clock, no RNG (RUNTIME §0). */ import type { SampledQualificationClaim } from "../bus/index.ts"; export type { SampledQualificationClaim } from "../bus/index.ts"; // ───────────────────────────────────────────────────────────────────────────── // The three channels // ───────────────────────────────────────────────────────────────────────────── /** One of the three P&L channels (kestrel-9gu.12). */ export type PnlChannel = "floor" | "expected" | "sampled"; /** The three channels, side by side (rounded, report-grid $). `sampled` is ABSENT — not `0` — when the * sampled channel is off (no `fillSeed`): an un-run channel is never conflated with a zero outcome. */ export interface PnlChannels { /** Realized $ under the strict-cross floor — the honest lower bound. */ readonly floor: number; /** E[$] under the survival product — the analytic bound beside the realization. */ readonly expected: number; /** The seeded causal realization (one ensemble replicate), when the channel ran. */ readonly sampled?: number; } // ───────────────────────────────────────────────────────────────────────────── // The qualification gate (kestrel-9gu.8.6; 9gu.8 AC#7) // ───────────────────────────────────────────────────────────────────────────── /** The evidence the gate judges — assembled by each surface from its own substrate, never guessed. */ export interface SampledGateEvidence { /** Did the sampled channel RUN (a seeded `fillSeed` run that produced sampled outcomes)? */ readonly sampledArmed: boolean; /** Did ANY sampled realization rest on extrapolated support (an uncalibrated hazard cell / dark * internalization draw)? Such a run's sampled channel is never the headline (`bankableEv`, ADR-0016 §5). */ readonly sampledExtrapolated: boolean; /** The owner-recorded qualification claim (9gu.8.6), when one is on the record. Absent ⇒ the study has * not passed ⇒ fail-closed not-qualified. */ readonly qualification?: SampledQualificationClaim; } /** The gate's verdict: `qualified` iff EVERY condition held; otherwise every failing condition is a * recorded reason, in a fixed order (byte-stable across re-projection). */ export interface SampledQualification { readonly qualified: boolean; readonly reasons: readonly string[]; } /** * The qualification-gate predicate (kestrel-9gu.12 seam for kestrel-9gu.8.6). The sampled channel * qualifies as headline iff ALL of: * * 1. the channel actually ran (`sampledArmed` — a seeded run); * 2. a {@link SampledQualificationClaim} is on the record with ALL THREE legs passed — calibration, * causality, fidelity — and a non-empty `reference` naming where the approval is recorded; * 3. no sampled realization rests on extrapolated support (`bankableEv` — never an uncalibrated * headline, ADR-0016 §5). * * FAIL-CLOSED DEFAULT: as of kestrel-9gu.12 no qualification study has passed (the study is * kestrel-9gu.8.6), so no run carries a claim and every headline resolves to the strict-cross floor — * exactly the owner-recorded sequencing (9gu.8 AC#7). Pure; reasons are emitted in the fixed order * above so the verdict is byte-stable. */ export function sampledQualified(e: SampledGateEvidence): SampledQualification { const reasons: string[] = []; if (!e.sampledArmed) { reasons.push("sampled channel off (no fillSeed) — nothing to qualify (fail-closed, ADR-0016 §5)"); } const q = e.qualification; if (q === undefined) { reasons.push( "no sampled-channel qualification on record — calibration + causality + fidelity study not passed (kestrel-9gu.8.6; 9gu.8 AC#7, fail-closed)", ); } else { // Fixed leg order: calibration, causality, fidelity — byte-stable reasons. if (!q.calibration) reasons.push("qualification leg 'calibration' not passed (kestrel-9gu.8.6)"); if (!q.causality) reasons.push("qualification leg 'causality' not passed (kestrel-9gu.8.6)"); if (!q.fidelity) reasons.push("qualification leg 'fidelity' not passed (kestrel-9gu.8.6)"); if (q.reference.length === 0) { reasons.push("qualification claim carries no reference — approval provenance unstated (fail-closed)"); } } if (e.sampledExtrapolated) { reasons.push( "a sampled realization rests on extrapolated support — never an uncalibrated headline (bankableEv, ADR-0016 §5)", ); } return { qualified: reasons.length === 0, reasons }; } // ───────────────────────────────────────────────────────────────────────────── // Headline selection // ───────────────────────────────────────────────────────────────────────────── /** * The selected headline: WHICH channel leads, its value, the gate verdict that selected it, and the * taint that rides it. `channel` is only ever a REALIZATION — `"floor"` (today's default) or * `"sampled"` (once qualified) — never `"expected"`. */ export interface HeadlinePnl { /** The selected headline channel: `floor` until the sampled channel qualifies (9gu.8 AC#7). */ readonly channel: PnlChannel; /** The selected channel's $ value (the other channels remain beside it as honest bounds). */ readonly usd: number; /** The qualification-gate verdict that made the selection — reasons on the record when floor holds. */ readonly gate: SampledQualification; /** `true` when the session's accounting itself is untrustworthy — e.g. a mark-uncertain settle * (kestrel-xwf) or expected-$ banked on extrapolated support. A taint rides EVERY channel (all three * settle against the same mark), so it taints whichever channel is the headline — it never silently * switches channels. */ readonly tainted: boolean; /** The reasons behind `tainted`, verbatim (empty ⇔ untainted). */ readonly reasons: readonly string[]; } /** * Select the headline channel (kestrel-9gu.12): `sampled` iff the gate passed AND the channel is * present; otherwise `floor`. `taintReasons` are the session-level accounting taints (a mark-uncertain * settle taints all three channels' headline — the xwf composition) — they NEVER change the selection, * only stamp it, so a tainted number always renders with its taint attached rather than being silently * replaced by a different number. Pure and byte-stable. */ export function selectHeadline( channels: PnlChannels, gate: SampledQualification, taintReasons: readonly string[], ): HeadlinePnl { const useSampled = gate.qualified && channels.sampled !== undefined; return { channel: useSampled ? "sampled" : "floor", usd: useSampled ? channels.sampled! : channels.floor, gate, tainted: taintReasons.length > 0, reasons: [...taintReasons], }; } // ───────────────────────────────────────────────────────────────────────────── // The never-naked taint (kestrel-22j.4) // ───────────────────────────────────────────────────────────────────────────── /** One FILLED leg in causal (bus) order — the minimal projection {@link nakedShortTaint} reads. A * `buy` adds to the running position at `key`, a `sell` subtracts; the taint watches that net cross * below zero. `key` is the position identity a fill nets against (instrument, plus strike+right for an * option leg). */ export interface FilledLeg { readonly key: string; readonly side: "buy" | "sell"; readonly qty: number; } /** * The never-naked taint (kestrel-22j.4). Scan the filled legs in CAUSAL order and watch the running * net position per key: if it ever crosses below zero the run legged into a **net-naked short** — an * uncovered short (unbounded on a call / short equity), the exact state the bounded-risk / never-naked * non-negotiable forbids. Such a run can never headline as a clean win, however positive its P&L, so * the crossing is returned as a taint reason (one per key, in first-cross order — byte-stable) for * {@link selectHeadline} to stamp on the headline. An empty result ⇔ the book stayed long-or-flat * throughout. Pure; no wall clock, no RNG (RUNTIME §0). */ export function nakedShortTaint(legs: readonly FilledLeg[]): string[] { const net = new Map<string, number>(); const flagged = new Set<string>(); const reasons: string[] = []; for (const leg of legs) { const after = (net.get(leg.key) ?? 0) + (leg.side === "buy" ? leg.qty : -leg.qty); net.set(leg.key, after); if (after < 0 && !flagged.has(leg.key)) { flagged.add(leg.key); reasons.push( `net-naked short at ${leg.key}: filled position went to ${after} (an uncovered short) — never-naked forbids this from headlining as a clean win (kestrel-22j.4)`, ); } } return reasons; } /** * Parse an UNTRUSTED value (a config field / META bytes) into a {@link SampledQualificationClaim}, * failing closed to `undefined` on ANY malformation — a missing leg, a non-boolean, an empty or * non-string reference. A malformed claim is treated exactly like no claim (the gate then refuses), * never coerced onto a passing shape (RUNTIME §8). */ export function readSampledQualificationClaim(raw: unknown): SampledQualificationClaim | undefined { if (typeof raw !== "object" || raw === null) return undefined; const r = raw as Record<string, unknown>; if (typeof r.calibration !== "boolean") return undefined; if (typeof r.causality !== "boolean") return undefined; if (typeof r.fidelity !== "boolean") return undefined; if (typeof r.reference !== "string" || r.reference.length === 0) return undefined; return { calibration: r.calibration, causality: r.causality, fidelity: r.fidelity, reference: r.reference }; }