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.

107 lines (106 loc) 6.28 kB
/** * # protocol/support — the four-cell SIGNED partition + named READ VIEWS (kestrel-dpy, ADR-0014) * * PROMOTED ONTO THE PROTOCOL SURFACE (kestrel-h314, D5). This module used to live at `src/support/index.ts` * (a runtime leaf); it is PURE (imports nothing) and it owns the fill-support vocabulary the certified * `fill_claim` evidence on the protocol {@link ./index.ts Blotter} is partitioned by, so it now sits on the * zero-dependency protocol surface and is re-exported from `kestrel.markets/protocol`. `src/support/index.ts` * remains as a thin re-export of THIS module (one source, ADR-0014 — the fill seam, the Blotter projector, * the OSS grader, and any self-hosted outsider all read the SAME `bankableEv` / `bankableEvOf` policy). The * platform then deletes its `apps/api/src/grade/support.ts` redeclaration and imports these from the package * (closing "seam candidate 6" duplication). * * The single owner of the run's expected-$ **honesty question**. ADR-0014: *sign policy is a property * of the QUESTION asked of a run, never of the run itself.* So the support partition is **written once** * — as four SIGNED cells, `{calibrated, extrapolated} × {gain, loss}` — and every policy (`raw`, * `bankable`, and any future one) is a pure READ VIEW over it, never a write-time collapse. One module * owns the vocabulary ({@link FillSupport}), the partition ({@link partition}), and the views; the fill * engine, the Blotter projector, and the grader all consume this ONE module so a second, forkable policy * can never drift out of sync. * * The forcing case (ADR-0014): grading one tape under a floor model (`strict-cross`) and an offline * ceiling (`maker-fair`, fills `@fair`) is a deliberate bracket, and the ceiling's cells are * `extrapolated` by construction. Two legitimate consumers ask different questions: * * - **raw** — *"what does this run claim under its own fill model?"* The experiment / comparison view * (fill-model brackets + config grids): the full, support-LABELED total — extrapolated gains KEPT, * because censoring the ceiling's upside makes the floor↔ceiling bracket unreadable. Label, never * censor. * - **bankable** — *"what may be banked, ranked, sold?"* Calibrated cells in FULL (both signs), plus * extrapolated **losses**; extrapolated **gains are REFUSED** (contribute 0). The asymmetry is * deliberate and conservative: a strategy can neither hill-climb into an uncalibrated corner nor * park its losses there. Leaderboards, seasons, and any attested receipt read this view only. * * The bug this closes: the Blotter's `fill_claim` used to partition expected-$ **unsigned**, so a * consumer that "subtracts the extrapolated slice" would erase extrapolated LOSSES together with the * gains — flattering exactly the loss-parking strategy the doctrine exists to punish, and disagreeing * with `bankableEv`. A SIGNED partition on the record lets a subtracting consumer remove only the * refused `gain`, so extrapolated losses can never be zeroed. Pure and deterministic (RUNTIME §0). */ /** The grader/RunRecord's banking predicate: a cell's expected-$ **gain** is bankable **only** when its * support is `"calibrated"`. An `"extrapolated"` cell — or an unstated one — is refused (conservative, * fail-closed default), so a strategy cannot hill-climb into an uncalibrated corner of the fill model * (e.g. the far-OTM-offer fantasy or a dark internalization lift). */ export function isCalibratedSupport(support) { return support === "calibrated"; } /** * Fold per-cell expected-$ into the four SIGNED cells (PURE). Fail-closed: an unstated support flag is * bucketed as `extrapolated` (never silently banked); each cell's value is split by sign so a gain and a * loss in the same support class land in different cells and can never cancel at write time. The empty * fold is the all-zero partition. */ export function partition(cells) { let calGain = 0; let calLoss = 0; let extGain = 0; let extLoss = 0; for (const cell of cells) { if (isCalibratedSupport(cell.support)) { if (cell.ev >= 0) calGain += cell.ev; else calLoss += cell.ev; } else { // Unstated support fails closed to extrapolated (ADR-0014) — never banked as calibrated. if (cell.ev >= 0) extGain += cell.ev; else extLoss += cell.ev; } } return { calibrated: { gain: calGain, loss: calLoss }, extrapolated: { gain: extGain, loss: extLoss }, }; } /** * The **raw** view (ADR-0014): the full, support-LABELED total — all four cells summed, so extrapolated * gains are KEPT. This is the experiment / comparison number the fill-model floor↔ceiling brackets and * the config grids read; the Grade's obligation here is to label the support, never to censor it. */ export function rawEv(p) { return p.calibrated.gain + p.calibrated.loss + p.extrapolated.gain + p.extrapolated.loss; } /** * The **bankable** view (ADR-0014): calibrated cells in FULL (both signs) + extrapolated LOSSES, and it * REFUSES extrapolated GAINS (they contribute 0). The one number leaderboards, seasons, and attested * receipts read. Exactly the landed per-cell {@link bankableEv} policy, aggregated: * `bankableEvOf(partition(cells)) === Σ bankableEv(cell.ev, cell.support)` (one policy, never forked). */ export function bankableEvOf(p) { return p.calibrated.gain + p.calibrated.loss + p.extrapolated.loss; } /** * The per-cell banking policy, RE-EXPRESSED here as the single source (grade re-exports this; it does not * fork a second copy). A `"calibrated"` cell banks its expected-$ as-is (both signs). An `"extrapolated"` * / unstated cell REFUSES a paper GAIN (→ 0) but does NOT forgive a paper LOSS — zeroing a negative EV * would flatter a strategy that routes losing fills into uncalibrated corners (the fail-OPEN inverse of * the hill-climb this guard prevents). So an extrapolated cell banks only its non-positive part. */ export function bankableEv(ev, support) { if (isCalibratedSupport(support)) return ev; return Math.min(ev, 0); }