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.
450 lines (449 loc) • 25.4 kB
JavaScript
/**
* # engine/pricing — price resolution + order intents (RUNTIME §4)
*
* The **pure** layer between a parsed price line (a {@link PriceExpr} + its
* {@link OrderPolicy}, both from `src/lang`) and a concrete resting price with its audit
* annotation and reprice intent. It is the one place that knows what `fair`, `mid`,
* `lean(a,b,x)`, `join`, `improve`, `cap`, `floor`, `esc`, and `peg` mean when an order
* rests (ARCHITECTURE §4: "exactly one place knows"). The lifecycle engine (next phase)
* calls this to turn a plan clause's price into an order it can place / cancel-replace.
*
* No I/O, no clock, no RNG (RUNTIME §0): every timestamp (`now`, `placedAt`) is injected, the
* only fair value comes from the pure {@link executionFair}, and the reprice rate limiter is an
* injected {@link RepriceTokenBucket} the caller owns. Same inputs ⇒ same result.
*
* ## The contract it enforces (RUNTIME §4)
* - **`fair` = ExecutionFair with receipts.** When fair is unbuildable (`fairInput` null, or
* {@link executionFair} returns null) the resolution falls back to an **annotated** book
* value — `fair=fallback(mid)` for a BUY, `fair=fallback(max(mid,intrinsic))` for a SELL —
* and the annotation rides on the result. **A silent mid is forbidden**: the fallback is
* never invisible.
* - **`basis` with no held position is UNRESOLVABLE** (the engine de-arms the statement,
* RUNTIME §8) — never a guessed cost.
* - **BOOK anchors** (`bid ask mid last join improve`) read the leg quote; a dark side they
* need makes the whole line unresolvable (fail-closed, never a fabricated level).
* - **BUY caps compose to the TIGHTEST** (min upper bound); a pegged BUY carries an implicit
* default `fair` cap (heritage) so a chase never bids above fair.
* - **SELL floors compose to the HIGHEST** (max lower bound) and **always include intrinsic**;
* every resolved SELL price is `>= intrinsic`, ceil-snapped, always.
* - **Tick snapping:** BUY floor-snaps, SELL ceil-snaps — and a SELL never lands below
* intrinsic after the snap.
* - **`esc` stages are ABSOLUTE-FROM-PLACEMENT:** the stage clock starts at `placedAt`, not at
* the last reprice; the active stage's target replaces the base price, and crossing a stage
* boundary is a reprice.
* - **`peg` re-resolves on data change** but only emits a reprice intent on **>= 1-tick drift**
* (hysteresis) and only if the injected token bucket grants a token; sub-tick drift or a
* denied token holds the order at its prior price.
*/
import { assertNever } from "../lang/index.js";
import { executionFairOutcome, executionFairSpot, isFairRefusal, EXEC_FAIR_QUOTE_MODEL, } from "../fair/index.js";
import { SETTLE_MARK_STALE_AFTER_MS } from "../fill/index.js";
import { durationMs } from "../series/index.js";
/** Is this resolution an unresolvable line? */
export function isUnresolvable(r) {
return "unresolvable" in r;
}
function isBad(r) {
return "bad" in r;
}
// ─────────────────────────────────────────────────────────────────────────────
// Number hygiene + tick snapping
// ─────────────────────────────────────────────────────────────────────────────
const EPS = 1e-9;
/** Round away floating-point fuzz to a deterministic 1e-8 grid so snapped prices are
* byte-stable (RUNTIME §0). Prices live well above 1e-8, so no real precision is lost. */
function cleanNum(x) {
return Math.round(x * 1e8) / 1e8;
}
/** Snap a price to the tick grid: `down` floors (BUY), `up` ceils (SELL). A non-positive tick
* is a no-op (just cleaned). */
function snap(px, tick, dir) {
if (!(tick > 0))
return cleanNum(px);
const t = px / tick;
const n = dir === "down" ? Math.floor(t + EPS) : Math.ceil(t - EPS);
return cleanNum(n * tick);
}
// ─────────────────────────────────────────────────────────────────────────────
// Book helpers
// ─────────────────────────────────────────────────────────────────────────────
/** The two-sided mid, or `null` when either side is dark (a one-sided book has no honest mid —
* its "mid" is a health signal, never a price, ARCHITECTURE §4). */
function midOf(q) {
if (q.bid === null || q.ask === null)
return null;
return cleanNum(0.5 * (q.bid + q.ask));
}
// ─────────────────────────────────────────────────────────────────────────────
// fair — ExecutionFair with the annotated fallback (RUNTIME §4)
// ─────────────────────────────────────────────────────────────────────────────
/**
* The horizon inside which a book is FRESH enough for the fair-vs-book bound to have authority
* (kestrel-ku99). Deliberately bound to the SAME declared staleness horizon the settle-mark gate
* uses (kestrel-xwf) rather than forking a second tunable: both answer one question — "is this
* quote still a live fact?" — and its rationale carries over verbatim (comfortably coarser than
* the tape's event cadence, orders of magnitude finer than the hours-frozen-feed failure it
* exists to catch). One declared staleness horizon, not two that silently drift apart.
*/
export const BOOK_FRESH_WITHIN_MS = SETTLE_MARK_STALE_AFTER_MS;
/**
* Decide whether the valued leg's book is FRESH, and hand {@link executionFairOutcome} that
* verdict along with the leg's own quote. Freshness lives HERE because `src/fair` is pure and
* clockless (RUNTIME §0) — it reads no clock, so it cannot judge staleness; the engine holds
* both `now` and the book's `asof` and is the only layer that can.
*
* An ABSENT `asof` ⇒ NOT fresh. We cannot vouch for a book whose age is unknown, and per ADR
* ltrf an unvouched book carries no authority over the underlying-anchored surface — so the
* bound simply does not apply. (This is the ltrf-correct reading, not a loophole: the bound is a
* diagnostic that bites only on books we KNOW are live, never a clamp we apply by default.)
*/
function fairInputWithBook(fairInput, ctx) {
const { asof } = fairInput;
const age = asof === undefined ? undefined : ctx.now - asof;
// A future-dated book (age < 0) is not a book we can vouch for either.
const bookFresh = age !== undefined && age >= 0 && age <= BOOK_FRESH_WITHIN_MS;
return { ...fairInput, legBid: ctx.quote.bid, legAsk: ctx.quote.ask, bookFresh };
}
/**
* Resolve `@fair`. Prefers {@link executionFairOutcome}; on a refusal it falls back to an
* **annotated** book value — the annotation ALWAYS names the fallback, so a silent mid can never
* sneak through (RUNTIME §4). A BUY with a dark book (no mid) is genuinely unresolvable; a SELL
* degrades to intrinsic (its honest floor).
*
* A **tainted** refusal (a fair that WAS computed and then refused — e.g. it landed outside a
* real+fresh book) additionally names its machine-readable reason in the annotation, e.g.
* `fair=fallback(mid;tainted:fair-above-fresh-ask)`. An ordinary `unbuildable` fair annotates
* exactly as it always has: a fair we never had is the expected path, not a corruption signal,
* and only the latter earns a place in the audit trail.
*/
function resolveFair(ctx) {
// Spot doctrine (ADR-0017): `@fair` for a spot instrument is its OWN two-sided quote mid with a
// receipt (exec-fair-quote-v1) — never Black-76 (no chain, no strike). One-sided/dark/crossed ⇒
// fair is unbuildable ⇒ the line is UNRESOLVABLE (fail-closed; the author prices off
// @bid/@ask/@mid/@last). No annotated-mid fallback here: for a spot instrument the fair IS the
// mid, so "falling back to mid" would be the silent mid RUNTIME §4 forbids.
if (ctx.instrumentKind === "spot") {
const f = executionFairSpot({ bid: ctx.quote.bid, ask: ctx.quote.ask, asof: ctx.now });
if (f === null) {
return { bad: "fair unbuildable for a spot instrument: quote one-sided/dark/crossed — price off @bid/@ask/@mid/@last (ADR-0017)" };
}
return { px: f.value, ann: `fair(${EXEC_FAIR_QUOTE_MODEL})` };
}
// A tainted refusal's reason rides into the fallback annotation; `unbuildable` adds nothing.
let taint = "";
if (ctx.fairInput !== null) {
const f = executionFairOutcome(fairInputWithBook(ctx.fairInput, ctx));
if (!isFairRefusal(f)) {
// A parity-derived forward is the trusted read and annotates exactly as before ("fair").
// A DEGRADED forward (parity underivable — ADR-0037 §2 keeps the value alive at
// `forward = spot` rather than killing `@fair` on ~92% of real strikes) may never pass
// silently: the annotation NAMES the degradation and its machine-readable reason, so the
// audit trail shows a fair that was priced off spot and why (RUNTIME §4 — a fallback is
// never silent).
const r = f.receipt;
return r.forwardSource === "parity"
? { px: f.value, ann: "fair" }
: { px: f.value, ann: `fair(fwd=spot;degraded:${r.forwardDegraded ?? "unspecified"})` };
}
if (f.refused === "tainted")
taint = `;tainted:${f.reason}`;
}
const mid = midOf(ctx.quote);
if (ctx.side === "buy") {
if (mid === null) {
return { bad: "fair fallback needs a two-sided mid (BUY) but the book is one-sided/dark" };
}
return { px: mid, ann: `fair=fallback(mid${taint})` };
}
// SELL: max(mid, intrinsic) — and if the book is dark, intrinsic is the honest floor.
if (mid === null) {
return { px: ctx.intrinsic, ann: `fair=fallback(intrinsic;mid-dark${taint})` };
}
return { px: Math.max(mid, ctx.intrinsic), ann: `fair=fallback(max(mid,intrinsic)${taint})` };
}
// ─────────────────────────────────────────────────────────────────────────────
// Anchors
// ─────────────────────────────────────────────────────────────────────────────
function resolveAnchor(name, ctx) {
const q = ctx.quote;
switch (name) {
case "fair":
return resolveFair(ctx);
case "intrinsic":
// Option anchor: intrinsic(spot, strike, right) needs a strike — a spot leg has none (ADR-0017).
return ctx.instrumentKind === "spot"
? { bad: "intrinsic anchor is an option concept — refused for a spot instrument (ADR-0017)" }
: { px: ctx.intrinsic, ann: "intrinsic" };
case "basis":
if (ctx.instrumentKind === "spot") {
return { bad: "basis anchor is an option anchor in v1 — refused for a spot instrument (ADR-0017)" };
}
return ctx.basis === null
? { bad: "basis anchor referenced with no held position" }
: { px: ctx.basis, ann: "basis" };
case "bid":
return q.bid === null ? { bad: "bid anchor but the bid is dark" } : { px: q.bid, ann: "bid" };
case "ask":
return q.ask === null ? { bad: "ask anchor but the ask is dark" } : { px: q.ask, ann: "ask" };
case "mid": {
const m = midOf(q);
return m === null ? { bad: "mid anchor but the book is one-sided/dark" } : { px: m, ann: "mid" };
}
case "last":
return q.last === undefined
? { bad: "last anchor but no trade has printed for this leg" }
: { px: q.last, ann: "last" };
case "join":
// Rest at the best price on our own side of the book.
if (ctx.side === "buy") {
return q.bid === null ? { bad: "join anchor but the bid is dark" } : { px: q.bid, ann: "join(bid)" };
}
return q.ask === null ? { bad: "join anchor but the ask is dark" } : { px: q.ask, ann: "join(ask)" };
case "improve":
// One tick better than the best price on our own side (more aggressive).
if (ctx.side === "buy") {
return q.bid === null
? { bad: "improve anchor but the bid is dark" }
: { px: cleanNum(q.bid + ctx.tickSize), ann: "improve(bid+1t)" };
}
return q.ask === null
? { bad: "improve anchor but the ask is dark" }
: { px: cleanNum(q.ask - ctx.tickSize), ann: "improve(ask-1t)" };
case "stub":
// A minimal placeholder resting price — one tick, the smallest representable level.
return { px: ctx.tickSize, ann: "stub" };
case "spot": {
// The `spot` PRICE anchor (emergent grammar, ADR-0030 / kestrel-ipc) is the mirror image of
// `intrinsic`/`basis`: those parse always but are REFUSED on a spot leg (ADR-0017); `spot`
// parses always but is REFUSED on an OPTION leg. An option's resting price is not the
// underlying (a $3 option priced at spot 5000 is a category error — a buy is absurdly
// marketable, a sell never fills), so we steer rather than silently remap to option mid/fair.
if (ctx.instrumentKind !== "spot") {
return {
bad: "spot anchor is the underlying price — refused for an option leg; use @fair/@mid/@bid/@ask/@last (ADR-0017)",
};
}
// Equity/spot leg: resolve to the canonical underlying spot — the SAME value the `spot` SERIES
// reads (`CanonicalState.spot`), never the quote mid (a two-truths divergence). UNKNOWN spot
// (null/absent — a gapped/dead feed) ⇒ unresolvable ⇒ the line de-arms fail-closed (ven.4),
// NEVER a silent fallback to mid/last.
if (ctx.spot === undefined || ctx.spot === null) {
return { bad: "spot anchor but the underlying spot is UNKNOWN (gapped/dead feed) — the line de-arms (fail-closed, ven.4)" };
}
return { px: ctx.spot, ann: "spot" };
}
default:
return assertNever(name, "price-anchor");
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Expression evaluation (anchors · absolute · offsets · lean · min/max)
// ─────────────────────────────────────────────────────────────────────────────
function resolveExpr(expr, ctx) {
switch (expr.kind) {
case "anchor":
return resolveAnchor(expr.name, ctx);
case "price-abs":
return { px: expr.value, ann: String(expr.value) };
case "price-offset": {
const base = resolveExpr(expr.base, ctx);
if (isBad(base))
return base;
// `c` = cents ($0.01 each); `%` = a fraction of the base.
const delta = expr.unit === "c" ? expr.amount * 0.01 : base.px * (expr.amount / 100);
const px = expr.sign === "+" ? base.px + delta : base.px - delta;
return { px: cleanNum(px), ann: `${base.ann}${expr.sign}${expr.amount}${expr.unit}` };
}
case "lean": {
const a = resolveExpr(expr.a, ctx);
if (isBad(a))
return a;
const b = resolveExpr(expr.b, ctx);
if (isBad(b))
return b;
// Blend a→b by fraction x (lean-to-MM).
const px = a.px + (b.px - a.px) * expr.x;
return { px: cleanNum(px), ann: `lean(${a.ann},${b.ann},${expr.x})` };
}
case "price-fn": {
const vals = [];
const anns = [];
for (const arg of expr.args) {
const r = resolveExpr(arg, ctx);
if (isBad(r))
return r;
vals.push(r.px);
anns.push(r.ann);
}
if (vals.length === 0)
return { bad: `${expr.fn}() with no arguments` };
const px = expr.fn === "min" ? Math.min(...vals) : Math.max(...vals);
return { px: cleanNum(px), ann: `${expr.fn}(${anns.join(",")})` };
}
default:
return assertNever(expr, "price-expr");
}
}
/**
* Select the active esc stage, ABSOLUTE-FROM-PLACEMENT (RUNTIME §4): each stage's `after` is
* measured from `placedAt`, never from the previous reprice. The active stage is the one with
* the largest elapsed-by `after` that has passed; ties resolve to the later-authored stage.
* Before any stage's clock, the base price line is active (stage 0).
*/
function activeEsc(base, esc, ctx) {
if (esc === undefined || esc.length === 0)
return { stage: 0, expr: base };
const placedAt = ctx.placedAt ?? ctx.now;
const elapsed = Math.max(0, ctx.now - placedAt);
let bestIdx = -1;
let bestAfter = -1;
for (let i = 0; i < esc.length; i++) {
const s = esc[i];
if (s === undefined)
continue;
const afterMs = durationMs(s.after.value, s.after.unit);
if (afterMs <= elapsed && afterMs >= bestAfter) {
bestAfter = afterMs;
bestIdx = i;
}
}
if (bestIdx < 0)
return { stage: 0, expr: base };
const chosen = esc[bestIdx];
if (chosen === undefined)
return { stage: 0, expr: base };
return { stage: bestIdx + 1, expr: chosen.to };
}
/** Compose BUY caps to the TIGHTEST (min upper bound) and clamp. A pegged BUY carries an
* implicit default `fair` cap (heritage) so a chase never bids above fair; a default cap that
* cannot be built is skipped (an explicit cap that cannot be built fails closed, upstream). */
function applyCaps(px, ann, line, ctx) {
const explicit = line.policy?.caps ?? [];
const caps = [];
for (const c of explicit) {
const r = resolveExpr(c, ctx);
if (isBad(r))
return r; // an authored cap you cannot build ⇒ de-arm (fail-closed)
caps.push({ px: r.px, ann: r.ann });
}
// Default cap: a pegged BUY caps at fair (heritage). Skip silently if fair is unbuildable —
// it is an implicit convenience, not an authored constraint.
if (ctx.side === "buy" && line.policy?.pricing === "peg") {
const f = resolveFair(ctx);
if (!isBad(f))
caps.push({ px: f.px, ann: `${f.ann}(default-peg-cap)` });
}
if (caps.length === 0)
return { px, ann, applied: false };
let bound = caps[0];
for (const c of caps)
if (c.px < bound.px)
bound = c;
if (px > bound.px + EPS) {
return { px: bound.px, ann: `${ann} cap(${bound.ann})`, applied: true };
}
return { px, ann, applied: false };
}
/** Compose floors to the HIGHEST (max lower bound) and clamp. A SELL ALWAYS includes intrinsic
* (RUNTIME §4) even with no authored floors. */
function applyFloors(px, ann, line, ctx) {
const explicit = line.policy?.floors ?? [];
const floors = [];
for (const f of explicit) {
const r = resolveExpr(f, ctx);
if (isBad(r))
return r; // an authored floor you cannot build ⇒ de-arm (fail-closed)
floors.push({ px: r.px, ann: r.ann });
}
if (ctx.side === "sell")
floors.push({ px: ctx.intrinsic, ann: "intrinsic" });
if (floors.length === 0)
return { px, ann, applied: false };
let bound = floors[0];
for (const f of floors)
if (f.px > bound.px)
bound = f;
if (px < bound.px - EPS) {
return { px: bound.px, ann: `${ann} floor(${bound.ann})`, applied: true };
}
return { px, ann, applied: false };
}
// ─────────────────────────────────────────────────────────────────────────────
// resolvePrice — the one entry point
// ─────────────────────────────────────────────────────────────────────────────
/**
* Resolve a price line to a concrete resting price + order intent (RUNTIME §4). Pure and
* total: any unbuildable anchor / missing basis / dark side needed returns
* `{ unresolvable }` (the engine de-arms, RUNTIME §8) rather than throwing or guessing.
*
* Pipeline: pick the active `esc` stage (absolute-from-placement) → resolve its expression
* (anchors, offsets, lean, min/max) → apply BUY caps (tightest) → apply floors (highest; SELL
* always intrinsic) → snap to tick (BUY down, SELL up, SELL never below intrinsic) → decide the
* reprice intent (esc boundary always reprices; peg reprices on >=1-tick drift under the token
* bucket; fix and sub-tick hold).
*/
export function resolvePrice(line, ctx) {
const tick = ctx.tickSize;
// 1. Active esc stage (or the base line) — absolute-from-placement.
const esc = activeEsc(line.price, line.policy?.esc, ctx);
// 2. Resolve the effective price expression.
const r0 = resolveExpr(esc.expr, ctx);
if (isBad(r0))
return { unresolvable: r0.bad };
let px = r0.px;
let ann = esc.stage > 0 ? `esc#${esc.stage}:${r0.ann}` : r0.ann;
// 3. Caps (BUY: tightest). 4. Floors (highest; SELL always intrinsic). Caps before floors so
// the intrinsic floor has the final say (a SELL can never end below intrinsic).
const capped = applyCaps(px, ann, line, ctx);
if (isBad(capped))
return { unresolvable: capped.bad };
px = capped.px;
ann = capped.ann;
const floored = applyFloors(px, ann, line, ctx);
if (isBad(floored))
return { unresolvable: floored.bad };
px = floored.px;
ann = floored.ann;
// 5. Snap to tick — BUY floors down, SELL ceils up.
px = snap(px, tick, ctx.side === "buy" ? "down" : "up");
// SELL guard: never below intrinsic after the snap (RUNTIME §4).
if (ctx.side === "sell") {
const floorSnapped = snap(ctx.intrinsic, tick, "up");
if (px < floorSnapped)
px = floorSnapped;
}
// 6. Reprice intent.
return decideReprice(px, ann, capped.applied, floored.applied, esc.stage, line, ctx, tick);
}
/**
* Decide whether this resolution is a reprice (cancel/replace the prior order) or a hold.
* - First placement (no prior): the resolved price, no `repriceOf`.
* - Crossed an esc stage boundary: a scheduled reprice, always (the ladder is deliberate).
* - `peg`, same stage: reprice iff drift >= 1 tick AND the token bucket grants a token;
* otherwise HOLD at the prior price (sub-tick hysteresis, or a denied token).
* - `fix`, same stage: never moves — hold at the prior price.
*/
function decideReprice(px, ann, capped, floored, escStage, line, ctx, tick) {
const prior = ctx.prior;
if (prior === undefined) {
return { px, sourceAnnotation: ann, capped, floored, escStage };
}
if (escStage !== prior.escStage) {
// Scheduled esc-boundary reprice — not hysteresis-gated, not token-gated.
return { px, sourceAnnotation: `${ann} [esc:boundary]`, capped, floored, escStage, repriceOf: prior.px };
}
if (line.policy?.pricing === "peg") {
const drift = Math.abs(px - prior.px);
if (drift < tick - EPS) {
// Sub-tick drift ⇒ hysteresis hold: the order stays put at its prior price.
return { px: prior.px, sourceAnnotation: `${ann} [peg:hold<1tick]`, capped, floored, escStage };
}
if (ctx.tokenBucket !== undefined && !ctx.tokenBucket.tryTake()) {
// Drift qualifies, but no reprice token ⇒ hold at the prior price (RUNTIME §4).
return { px: prior.px, sourceAnnotation: `${ann} [peg:denied]`, capped, floored, escStage };
}
return { px, sourceAnnotation: `${ann} [peg:reprice]`, capped, floored, escStage, repriceOf: prior.px };
}
// fix (or unspecified), same stage: a static order never moves.
return { px: prior.px, sourceAnnotation: `${ann} [fix]`, capped, floored, escStage };
}