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.

140 lines (139 loc) 9.34 kB
/** * # session/market-pane — the acting frame's market pane + its `@fair` chain column * * The date-blind MARKET half of every frame the Simulate driver ({@link import("./simulate.ts")}) serves: * spot/levels/tape + the option chain, with the chain's `fair` column priced through the SAME pure * {@link import("../fair/index.ts").executionFair} the fill engine prices `@fair` with. Extracted from the * driver so the pane projection lives beside its own small interface. * * - {@link marketPaneOf} — `(AuthorFrame, instrument, options?, fairCtx?) → MarketPane`; * - {@link chainLegFair} / {@link ChainFairContext} — ExecutionFair for ONE near-money chain leg; * - {@link buildFrameOptions} / {@link OptionsOverlayConfig} — the perception arm's live `market.options`, * folded causally to a frame cutoff and relabeled date-blind ({@link dateBlindOptions}). * * PURE + injected-time: no wall clock, no RNG. Every projection is a byte-identical function of its * inputs; the `fair` column is intrinsic-floored with a receipt or `—` (fail-closed — NEVER a fabricated * fair, a silent mid is forbidden, RUNTIME §4), and the options fold reads only the causal ≤-cutoff * prefix (no look-ahead) and relabels expiry DATE labels to relative-day tags before any token reaches * the date-blind agent frame. */ import { executionFair } from "../fair/index.js"; import { buildOptionsAnalytics, reduceOptionSurface, } from "../frame/options-analytics.js"; /** ExecutionFair for ONE near-money chain leg, reusing the SAME pure {@link executionFair} the fill * engine prices `@fair` through (`sim.ts` `fairFor`): the chain's own `underlierPx` as the underlier, the * WHOLE chain as the liquid-quote surface the vol is backed out of, and tau from the injected * `fairTauYears` at the frame cutoff. Returns the intrinsic-floored value + a `b76 nLiq=<n>` receipt * note, or **`null`** when unbuildable (no injected tau, no usable underlier, or zero liquid strikes) — * the caller then leaves `fair` unset so the chain renders `—` (fail-closed, honest — NEVER a fabricated * fair; a silent mid is forbidden, RUNTIME §4). Pure + injected-time: no wall clock, no RNG. */ function chainLegFair(chain, leg, fairCtx) { // kestrel-wcnd: τ off the CHAIN's own expiry — the SAME (provider, expiry) pair the fill engine // prices `@fair` through, so the served `fair` column can never diverge from the graded anchor. const tau = fairCtx.tauYears(fairCtx.nowTs, fairCtx.expiry); if (tau === null) return null; const r = executionFair({ underlyingSpot: chain.underlierPx, strike: leg.strike, right: leg.right, tauYears: tau, liquidQuotes: chain.legs, asof: fairCtx.nowTs, }); return r === null ? null : { value: r.value, note: `b76 nLiq=${r.receipt.nLiquid}` }; } /** `HHMM` → `HH:MM` (the day runner's tape labels are colon-less; the frame convention carries a colon). */ function colonize(hhmm) { return hhmm.length === 4 ? `${hhmm.slice(0, 2)}:${hhmm.slice(2)}` : hhmm; } /** The width (minutes) this driver's market pane serves tape rows at — CONSTANT for a session (each * `af.tape` sample is one 1-minute single-sample bucket). Named ONCE so `marketPaneOf` (which stamps it * onto every frame's `market.tapeBucketMin`) and the schedule-time View validator (`scheduleTimeViewDefect`, * which checks a `tape <window>` arg against it via `windowServableBy`) read the SAME width — the two can * never drift (kestrel-wa0j.19 §1). */ export const SERVED_TAPE_BUCKET_MIN = 1; export function marketPaneOf(af, instrument, options, // kestrel-121: the fair-pricing context, when the driver can source it. Absent ⇒ the chain `fair` // column stays `—` (byte-identical to a pre-fair frame) — every other `marketPaneOf` caller is safe. fairCtx) { // kestrel-rs4: carry the spot's AGE onto the levels the author reads. A feed that stopped printing // and a market that stopped moving hand the pane the same `spot`; without this the pane can only // render the dead one with the confidence of the live one. Absent age ⇒ omitted (byte-identical). const levels = { spot: af.spot, ...(af.spotAgeMs !== undefined ? { spotStaleSeconds: Math.floor(af.spotAgeMs / 1000) } : {}), priorClose: af.priorClose, hod: af.hod, lod: af.lod, vwap: af.vwap, // kestrel-wa0j.58: carry the frozen opening range onto the levels the author reads. The state computes // it, but this seam used to omit it, so `failed-breaks` rendered all-dashes on every one of the 11,809 // corpus frames (the ORB fake-out level was blinded). `null` (pre-anchor) ⇒ the pane fails closed to `—`. orHigh: af.orHigh, orLow: af.orLow, }; // Each tape sample is a single-sample bucket (O=H=L=C) — honest (no invented OHLC); a finer bucketer is later. const tape = af.tape.map((t) => ({ clock: colonize(t.label), open: t.px, high: t.px, low: t.px, close: t.px })); const chainSrc = af.chain; const chain = chainSrc === null ? [] : chainSrc.legs.map((l) => { // kestrel-121: populate `fair` (+ its `b76` receipt note) from the SAME `executionFair` the fill // engine prices `@fair` through; `null`/absent (unbuildable) ⇒ the render shows `—` (fail-closed). const f = fairCtx === undefined ? null : chainLegFair(chainSrc, l, fairCtx); return { strike: l.strike, right: l.right, bid: l.bid, ask: l.ask, ...(f !== null ? { fair: f.value, fairNote: f.note } : {}) }; }); // The options-analytics projection (kestrel-4gl.13.13) is populated ONLY for the perception arm (an // overlay was supplied + folded to this cutoff); absent, `market.options` stays undefined and the // options panes (off-default) never render — byte-identical to the minimal arm. // kestrel-wa0j.19 §4: thread the RELATIVE days-to-expiry the machine channel already carries // (`AuthorFrame.chain.dte`) onto the frame, so the chain-pane header regains the `0dte` the // one-renderer swap dropped. `null` (unresolvable expiry) / no chain ⇒ omitted — byte-identical. const chainDte = chainSrc !== null && chainSrc.dte !== null ? { chainDte: chainSrc.dte } : {}; return { instrument, levels, tape, tapeBucketMin: SERVED_TAPE_BUCKET_MIN, chain, ...chainDte, ...(options !== undefined ? { options } : {}) }; } // ───────────────────────────────────────────────────────────────────────────── // Options-analytics overlay fold (kestrel-4gl.13.13) — the perception arm's live `market.options` // // The driver (the impure orchestrator) folds the OPRA overlay tape causally to each frame cutoff and // hands the pure frame builders the finished, date-blind {@link OptionsAnalytics}. Kept OUT of the // pure `projectWakeFrame`/`briefingInputOf` seams (they receive the built projection, never the tape), // so those stay pure functions of their inputs. // ───────────────────────────────────────────────────────────────────────────── /** Stream `events` up to and including `tsMax` — the causal T-5m cutoff (no look-ahead). A lazy * generator over a lazy source reads only the prefix (the fold stops at the first later event). */ function* untilTs(events, tsMax) { for (const e of events) { if (e.ts > tsMax) return; yield e; } } /** Relabel a projection's expiry DATE labels (`2024-03-05`) to relative-day tags (`+0d`, `+1d`, from * `daysToExpiry`) so NO calendar token reaches the date-blind agent frame (the iso-date fence, * `scanDateLeak`). The panes read `tau`/`dte` + front-first ordering, never the label, so the * rendered pane text is byte-identical to the raw-label projection. Pure. */ function dateBlindOptions(o) { const expiries = o.expiries.map((e, i) => ({ ...e, expiry: e.daysToExpiry === null ? `exp${i}` : `+${e.daysToExpiry}d`, })); return { ...o, expiries }; } /** Fold the options overlay to `cutoffTs` (causal, no look-ahead) and build the date-blind * options-analytics projection for one live frame. Returns `undefined` when nothing folds (an empty / * pre-open cutoff) — the pane then fails closed on its own. Pure over (overlay events ≤ cutoffTs, * config): no wall clock, no RNG, and the MODEL honesty (stale-spot → UNKNOWN) rides in * {@link buildOptionsAnalytics}. */ export function buildFrameOptions(cfg, cutoffTs) { const surface = reduceOptionSurface(untilTs(cfg.events(), cutoffTs)); if (surface.cutoffSeq < 0) return undefined; // nothing folded (cutoff before the overlay's first event) const raw = buildOptionsAnalytics(surface, { expiryCloseTs: cfg.expiryCloseTs, openInterest: cfg.openInterest, ...(cfg.multiplier !== undefined ? { multiplier: cfg.multiplier } : {}), ...(cfg.staleMs !== undefined ? { staleMs: cfg.staleMs } : {}), }); return dateBlindOptions(raw); }