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.

249 lines (216 loc) 15.8 kB
/** * # grade/measurement — alpha measurements carry a KIND, and kinds never blend (kestrel-a57.6) * * A measurement of alpha is not one number; it is a number **plus the evidence semantics that produced it**. * The three kinds carry DIFFERENT evidence, so silently blending or ranking one against another is a category * error — the same class of error ADR-0006 forbids across measurement *versions* ("EVs across measurement * versions refuse naive comparison"). This module is the KIND-axis sibling of that rule: **alphas across KINDS * refuse naive comparison**, and the enforcement is the TypeScript compiler. * * 1. RULE ALPHA — a MECHANICAL TEMPLATE run over EVERY eligible opportunity on a tape (systematic, no * discretion). Its evidence is the template's realized measure over the whole universe. * 2. SELECTION ALPHA — POINT-IN-TIME CURATION (the author PICKED which opportunities to act on), measured * VS A FROZEN COUNTERFACTUAL on the SAME FUTURE TAPE (what those same picks, frozen, * would have realized). Its evidence is SELECTION skill: realized minus the frozen leg. * 3. DIAGNOSTIC — exploratory / inspection only. Carries an explicit `non_promotable: true` marker and * has NO ordering surface at all (no `compare`, no `rank`, no `promote`). * * ## The barrier (compile-time) * Each kind is a distinctly **branded** record (a private `unique symbol` phantom brand ⇒ nominal, mutually * non-assignable, even if fields ever coincided). `compare` / `rank` / `promote` are pinned with `NoInfer<M>`: * the measurement type `M` is inferred ONLY from the first parameter, so a second argument of a different kind * has nothing to unify with and fails to type-check. `PromotableAlpha` excludes diagnostics, so the diagnostic * kind has no accepting overload anywhere — its only surface is construction and inspection. * * ## Diagnostics never promote (dual gate — fail-closed) * (compile) `promote<M extends PromotableAlpha>` will not bind a `DiagnosticMeasurement`; (runtime) if a caller * bypasses the types (an `any` / erased boundary), {@link promote} returns a typed refusal — `{ ok: false, * reason, kind }` — never throws, never silently succeeds. This mirrors the module's fail-closed convention * ({@link bankableEv} floors by RETURN; the receipt projector degrades with a logged reason) rather than * inventing a new error style. * * Ranking is on ONE named scalar per kind — never a blended omnibus (honors the m9i no-omnibus rule). Pure and * deterministic: no wall clock, no RNG; off the runtime/record path (no bus/Blotter/grammar/golden bytes move). */ import type { MeasurementVersions } from "./receipt.ts"; // ───────────────────────────────────────────────────────────────────────────── // The nominal brand — a private phantom symbol, one string tag per kind // ───────────────────────────────────────────────────────────────────────────── declare const BRAND: unique symbol; /** A phantom, compile-time-only tag that makes two otherwise-structural records mutually non-assignable. */ type Brand<B extends string> = { readonly [BRAND]: B }; // ───────────────────────────────────────────────────────────────────────────── // Shared value shapes // ───────────────────────────────────────────────────────────────────────────── /** The identity of a mechanical template (rule alpha) — the id plus a content hash of its definition. */ export interface TemplateRef { readonly template_id: string; readonly template_hash: string; } /** The future tape a measurement was realized on — the id plus a content hash of the tape's bytes. */ export interface TapeRef { readonly tape_id: string; readonly tape_hash: string; } /** * A realized alpha measure: ONE declared scalar (`realized_r`) plus the banked expected-$ and the sample size * behind it. Ranking within a kind is on the ONE named scalar, never a blended composite of these fields. */ export interface AlphaScore { readonly realized_r: number; readonly expected_dollars: number; readonly n: number; } /** The frozen leg of a selection measurement — what the SAME picks, frozen, realized on the SAME future tape. */ export interface FrozenCounterfactual { readonly realized: AlphaScore; readonly frozen_hash: string; } // ───────────────────────────────────────────────────────────────────────────── // (1) RULE ALPHA — a mechanical template over the whole eligible universe // ───────────────────────────────────────────────────────────────────────────── /** The honest fields of a rule-alpha measurement (the {@link ruleAlpha} builder stamps the kind + brand). */ export interface RuleAlphaInput { readonly template: TemplateRef; readonly tape: TapeRef; /** Every eligible opportunity the mechanical rule ran over — the systematic universe, no selection. */ readonly universe: number; readonly realized: AlphaScore; readonly measurement_versions: MeasurementVersions; } /** A rule-alpha measurement: mechanically applied over EVERY opportunity. Nominal (branded). */ export type RuleAlphaMeasurement = RuleAlphaInput & { readonly kind: "rule_alpha" } & Brand<"RuleAlpha">; /** Build a {@link RuleAlphaMeasurement}. Pure: stamps the discriminant kind; no clock, no RNG. */ export function ruleAlpha(input: RuleAlphaInput): RuleAlphaMeasurement { return { ...input, kind: "rule_alpha" } as RuleAlphaMeasurement; } // ───────────────────────────────────────────────────────────────────────────── // (2) SELECTION ALPHA — discretionary picks vs a frozen counterfactual (same tape) // ───────────────────────────────────────────────────────────────────────────── /** The honest fields of a selection-alpha measurement (the {@link selectionAlpha} builder stamps kind + brand). */ export interface SelectionAlphaInput { /** The curation: how many opportunities existed vs how many the author PICKED to act on. */ readonly picks: { readonly universe: number; readonly picked: number }; readonly tape: TapeRef; readonly realized: AlphaScore; /** Stored INLINE (not referenced): the same picks, frozen, realized on the SAME future tape. */ readonly frozen_counterfactual: FrozenCounterfactual; readonly measurement_versions: MeasurementVersions; } /** A selection-alpha measurement: discretionary curation vs its frozen leg. Nominal (branded). */ export type SelectionAlphaMeasurement = SelectionAlphaInput & { readonly kind: "selection_alpha" } & Brand<"SelectionAlpha">; /** Build a {@link SelectionAlphaMeasurement}. Pure: stamps the discriminant kind; no clock, no RNG. */ export function selectionAlpha(input: SelectionAlphaInput): SelectionAlphaMeasurement { return { ...input, kind: "selection_alpha" } as SelectionAlphaMeasurement; } // ───────────────────────────────────────────────────────────────────────────── // (3) DIAGNOSTIC — inspection only; never promotes, no ordering surface // ───────────────────────────────────────────────────────────────────────────── /** The honest fields of a diagnostic measurement (the {@link diagnostic} builder stamps the non-promotable marker). */ export interface DiagnosticInput { readonly probe: string; readonly observed: AlphaScore; readonly note: string; } /** * A diagnostic measurement: exploratory / inspection only. Carries an explicit literal-`true` `non_promotable` * marker and is NOT part of {@link PromotableAlpha}, so it has no `compare` / `rank` / `promote` surface. Nominal. */ export type DiagnosticMeasurement = DiagnosticInput & { readonly kind: "diagnostic"; readonly non_promotable: true; } & Brand<"Diagnostic">; /** Build a {@link DiagnosticMeasurement}. Pure: stamps the kind + the explicit non-promotable marker. */ export function diagnostic(input: DiagnosticInput): DiagnosticMeasurement { return { ...input, kind: "diagnostic", non_promotable: true } as DiagnosticMeasurement; } // ───────────────────────────────────────────────────────────────────────────── // The promotable universe — rule + selection ONLY (diagnostics excluded by design) // ───────────────────────────────────────────────────────────────────────────── /** * The kinds that can be compared / ranked / promoted. Diagnostics are DELIBERATELY absent: they are the * category that must never be laundered into a promotable claim, so they have no ordering surface at all. */ export type PromotableAlpha = RuleAlphaMeasurement | SelectionAlphaMeasurement; // ───────────────────────────────────────────────────────────────────────────── // The single named scalar per kind (never a blended omnibus) // ───────────────────────────────────────────────────────────────────────────── interface Scalar { /** The NAME of the scalar being ordered on — so a comparison always declares what it ranked by. */ readonly name: string; readonly value: number; } /** * The ONE named scalar a kind is ordered by: * • rule alpha → `realized_r` (the mechanical rule's realized measure over its whole universe); * • selection alpha → `selection_edge` = realized − frozen counterfactual (the marginal value of the curation). * Never a composite — the honesty rule is that ranking is on a single, named measure. */ function scalarOf(m: PromotableAlpha): Scalar { if (m.kind === "rule_alpha") { return { name: "realized_r", value: m.realized.realized_r }; } return { name: "selection_edge", value: m.realized.realized_r - m.frozen_counterfactual.realized.realized_r, }; } // ───────────────────────────────────────────────────────────────────────────── // compare / rank — same-kind only (NoInfer<M> is the cross-kind barrier) // ───────────────────────────────────────────────────────────────────────────── /** The result of a same-kind comparison: the sign of the ordering PLUS the name of the scalar it ranked by. */ export interface Comparison { readonly order: -1 | 0 | 1; readonly measure: string; } /** * Compare two measurements OF THE SAME KIND on that kind's one named scalar. `M` is inferred solely from `a`; * `NoInfer<M>` pins `b` to the same kind, so a cross-kind second argument fails to compile. Diagnostics are not * in {@link PromotableAlpha}, so they have no `compare` overload at all. Pure, total, deterministic. */ export function compare<M extends PromotableAlpha>(a: M, b: NoInfer<M>): Comparison { const sa = scalarOf(a); const sb = scalarOf(b); const order: -1 | 0 | 1 = sa.value > sb.value ? 1 : sa.value < sb.value ? -1 : 0; return { order, measure: sa.name }; } /** * Rank measurements OF THE SAME KIND, descending by that kind's one named scalar. `M` is inferred solely from * the first element; every remaining element is pinned to the same kind via `NoInfer<M>`, so a mixed-kind list * (or a list containing a diagnostic) fails to compile. Pure: returns a sorted COPY; the input is never mutated. */ export function rank<M extends PromotableAlpha>(xs: readonly [M, ...NoInfer<M>[]]): M[] { return [...xs].sort((a, b) => scalarOf(b).value - scalarOf(a).value); } // ───────────────────────────────────────────────────────────────────────────── // promote — fail-closed; diagnostics can NEVER promote // ───────────────────────────────────────────────────────────────────────────── /** * The typed outcome of a promotion attempt — a discriminated union, fail-closed by RETURN (never a thrown * error, never a silent default). `ok:false` carries the machine-readable `reason` and the offending `kind`. */ export type PromotionOutcome = | { readonly ok: true; readonly candidate: PromotableAlpha } | { readonly ok: false; readonly reason: string; readonly kind: string }; /** * Promote a promotable alpha to a candidate. At compile time only {@link PromotableAlpha} binds `M`, so a * diagnostic has no accepting path. At RUNTIME this is defense-in-depth for a caller that bypassed the types * (an `any` / erased boundary): it inspects the actual object and FAILS CLOSED — a diagnostic, a non-promotable * marker, or any unrecognized kind returns a typed refusal with a logged reason. It never throws, never silently * succeeds. Pure and deterministic. */ export function promote<M extends PromotableAlpha>(candidate: M): PromotionOutcome { // Read the runtime shape through a widened view so the guard is NOT narrowed away by the compile-time type — // a types-bypassed caller may hand us an object whose real kind is "diagnostic". const probe = candidate as { readonly kind?: unknown; readonly non_promotable?: unknown }; const kind = typeof probe.kind === "string" ? probe.kind : "unknown"; const isPromotableKind = kind === "rule_alpha" || kind === "selection_alpha"; if (probe.non_promotable === true || !isPromotableKind) { return { ok: false, reason: `diagnostic / non-promotable measurement can never promote (kind=${kind})`, kind, }; } return { ok: true, candidate }; }