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.

610 lines (564 loc) 33.1 kB
/** * # 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 type { Right } from "../bus/index.ts"; import type { AnchorName, EscStage, OrderPolicy, PriceExpr } from "../lang/index.ts"; import { assertNever } from "../lang/index.ts"; import type { ExecutionFairInput } from "../fair/index.ts"; import { executionFairOutcome, executionFairSpot, isFairRefusal, EXEC_FAIR_QUOTE_MODEL, } from "../fair/index.ts"; import { SETTLE_MARK_STALE_AFTER_MS } from "../fill/index.ts"; import { durationMs } from "../series/index.ts"; // ───────────────────────────────────────────────────────────────────────────── // Public surface // ───────────────────────────────────────────────────────────────────────────── /** A parsed price line: which price ({@link PriceExpr}) and how it rests ({@link OrderPolicy} * — peg/fix, esc ladder, cap/floor lists). Extracted by the caller from any ticket clause * (`DO` / `ALSO` / `RELOAD` / `TP` / `EXIT`), all of which carry a `price` + optional * `policy`. */ export interface PriceLine { readonly price: PriceExpr; readonly policy?: OrderPolicy; } /** The injected global reprice rate limiter. A pegged reprice consumes a token; when the * bucket is empty the order holds at its prior price (RUNTIME §4). The caller owns refill and * scope — pricing only asks. */ export interface RepriceTokenBucket { /** Take one token, returning whether one was available. Called at most once per pegged * reprice, only after the >=1-tick hysteresis gate passes. */ tryTake(): boolean; } /** The prior resolution of the same order, supplied on re-resolution so peg hysteresis and * esc-boundary detection have something to diff against. Absent on the first placement. */ export interface PriorResolution { readonly px: number; readonly escStage: number; } /** The quote a price resolution reads — the structural subset of a bus `OptionQuote` (which * satisfies it as-is), and exactly the shape a spot instrument's own NBBO provides (ADR-0017: * no strike/right on a spot quote). A dark side is `null`. */ export interface LegQuote { readonly bid: number | null; readonly ask: number | null; /** Last trade price, when one has printed. */ readonly last?: number; } /** Everything a single price resolution needs. All time is injected (RUNTIME §0). */ export interface PriceCtx { /** The order side — decides fallback shape, join/improve direction, snap direction, and the * intrinsic floor. */ readonly side: "buy" | "sell"; /** The leg being priced (locates the quote on the book; carried for annotation/audit). * `strike`/`right` are absent for a spot instrument (ADR-0017 — a spot leg has neither). */ readonly leg: { readonly instrument: string; readonly strike?: number; readonly right?: Right }; /** The current quote for this leg (`bid`/`ask`/`last`). A dark side is `null`. */ readonly quote: LegQuote; /** Which price doctrine applies (ADR-0017). Absent or `"option"` ⇒ the options doctrine * (Black-76 `@fair` with annotated fallback, `@intrinsic`/`@basis` anchors, intrinsic SELL * floor) — byte-identical to before this field existed. `"spot"` ⇒ the spot doctrine: * `@fair` = the instrument's own two-sided quote mid with a receipt (`exec-fair-quote-v1`, * fail-closed to UNRESOLVABLE when one-sided/dark/crossed — never a silent mid, never * Black-76), and `@intrinsic`/`@basis` are refused (option anchors; a spot leg has no strike * to take an intrinsic of). */ readonly instrumentKind?: "option" | "spot"; /** The inputs to {@link executionFair} for this leg, or `null` when no fair can even be * attempted — either way `@fair` resolves through the annotated fallback when unbuildable. */ readonly fairInput: ExecutionFairInput | null; /** The held position's cost basis, or `null` when nothing is held — a `basis` anchor is then * unresolvable (the statement de-arms, RUNTIME §8). */ readonly basis: number | null; /** Per-contract intrinsic of this leg at the current spot (injected — the engine computes it * from canonical state). The `intrinsic` anchor and the mandatory SELL floor read it. */ readonly intrinsic: number; /** The canonical underlying spot — the SAME value the `spot` SERIES reads (`CanonicalState.spot`), * NOT the quote mid (that would be a two-truths divergence). The `spot` PRICE anchor (ADR-0030 / * kestrel-ipc) reads it, and ONLY for an equity/spot instrument. `null` (UNKNOWN — a gapped/dead * feed) ⇒ the `spot` anchor is unresolvable and the statement de-arms fail-closed (ven.4); a * silent fallback to mid/last is forbidden. Absent for an option leg — the anchor is refused * there before this field is ever read. */ readonly spot?: number | null; /** The instrument tick size (dollars). BUY floor-snaps to it, SELL ceil-snaps; `improve` * steps one of these; peg hysteresis is measured in it. */ readonly tickSize: number; /** The current event timestamp (epoch ms, the bus clock in sim — RUNTIME §0). */ readonly now: number; /** The order's placement timestamp (epoch ms). `esc` stages are ABSOLUTE-FROM-PLACEMENT, so * elapsed = `now - placedAt`. Defaults to `now` (fresh placement ⇒ stage 0). */ readonly placedAt?: number; /** The prior resolution, for peg hysteresis + esc-boundary reprice. Absent on first * placement. */ readonly prior?: PriorResolution; /** The injected reprice token bucket (peg only). Absent ⇒ no rate limit. */ readonly tokenBucket?: RepriceTokenBucket; } /** A resolved order intent, or an unresolvable line. The resolved shape always carries a * `sourceAnnotation` — the audit trail RUNTIME §4 requires (a fallback is never silent). When * `repriceOf` is present the caller should cancel/replace the prior resting order at that * price; when absent the order holds (first placement, sub-tick hysteresis, or a denied * token). */ export type PriceResolution = | { readonly px: number; readonly sourceAnnotation: string; readonly capped: boolean; readonly floored: boolean; /** 0 = the base price line; N>0 = the Nth `esc` stage (1-based) is active. */ readonly escStage: number; /** The prior resting price this intent cancel-replaces, when it is a reprice. */ readonly repriceOf?: number; } | { readonly unresolvable: string }; /** Is this resolution an unresolvable line? */ export function isUnresolvable( r: PriceResolution, ): r is { readonly unresolvable: string } { return "unresolvable" in r; } // ───────────────────────────────────────────────────────────────────────────── // Internal resolution result (a value + its annotation, or a de-arm reason) // ───────────────────────────────────────────────────────────────────────────── type Val = { readonly px: number; readonly ann: string }; type Bad = { readonly bad: string }; type Res = Val | Bad; function isBad(r: Res): r is Bad { 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: number): number { 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: number, tick: number, dir: "down" | "up"): number { 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: LegQuote): number | null { 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: ExecutionFairInput, ctx: PriceCtx): ExecutionFairInput { 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: PriceCtx): Res { // 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: AnchorName, ctx: PriceCtx): Res { 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: PriceExpr, ctx: PriceCtx): Res { 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: number[] = []; const anns: string[] = []; 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"); } } // ───────────────────────────────────────────────────────────────────────────── // esc — absolute-from-placement stage selection // ───────────────────────────────────────────────────────────────────────────── interface EscActive { /** 0 = the base line; N = the Nth stage (1-based) is active. */ readonly stage: number; /** The price expression to resolve at this stage. */ readonly expr: PriceExpr; } /** * 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: PriceExpr, esc: readonly EscStage[] | undefined, ctx: PriceCtx): EscActive { 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 }; } // ───────────────────────────────────────────────────────────────────────────── // Caps + floors (compose conservatively; SELL always floored at intrinsic) // ───────────────────────────────────────────────────────────────────────────── interface Bound { readonly px: number; readonly ann: string; readonly applied: boolean; } /** 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: number, ann: string, line: PriceLine, ctx: PriceCtx): Bound | Bad { const explicit = line.policy?.caps ?? []; const caps: Array<{ px: number; ann: string }> = []; 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: number, ann: string, line: PriceLine, ctx: PriceCtx): Bound | Bad { const explicit = line.policy?.floors ?? []; const floors: Array<{ px: number; ann: string }> = []; 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: PriceLine, ctx: PriceCtx): PriceResolution { 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: number, ann: string, capped: boolean, floored: boolean, escStage: number, line: PriceLine, ctx: PriceCtx, tick: number, ): PriceResolution { 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 }; }