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.
135 lines (134 loc) • 7.83 kB
JavaScript
/**
* # fair — ExecutionFair: the honest `@fair` anchor (ARCHITECTURE §4, RUNTIME §4)
*
* ExecutionFair is the price at which a market maker will actually **fill** you — not the
* midpoint of a possibly-fictional book. It is **underlying-anchored intrinsic + a vol read
* backed out of the *liquid* quotes, floored at intrinsic, and receipt-gated**: the value
* carries whether it can be trusted (model version, how many liquid strikes backed it, the
* IV it was priced at), not just a number. The observed mid is a health signal (thin/dark
* book fingerprint), never a value.
*
* This module is **pure** — no I/O, no clock, no RNG (RUNTIME §0). It is the one place that
* knows how `fair` is computed; every surface that resolves `@fair` reads through here.
*
* ## The contract (RUNTIME §4)
* `executionFair(...)` returns `{ value, receipt }` or **`null`**. It returns `null` in
* exactly two cases — **no usable underlying**, or **zero liquid strikes** to back a vol out
* of. `null` is *not* an error: it is the signal for the price resolver to fall back to an
* **annotated** book value (`fair=fallback(mid)` for a BUY, `fair=fallback(max(mid,
* intrinsic))` for a SELL) — a silent mid is forbidden. This module never invents that
* fallback; it reports honestly that fair is unbuildable and lets the caller annotate.
*
* When it *does* return a value, that value is **always floored at intrinsic** — the price a
* maker will never sell you below (RUNTIME §4 SELL floor, and the honest lower bound on any
* option's worth).
*
* ## Freshness is NOT this module's job
* A pure valuation cannot know whether the quotes it priced are stale — staleness is a fact
* about the clock and the book's last update, and this module reads neither. So it does not
* gate on freshness; it **carries** the observation time instead. The caller may inject an
* `asof` timestamp (epoch ms) on {@link ExecutionFairInput}; it is stamped verbatim onto the
* {@link FairReceipt} as {@link FairReceipt.asof}. **Applying** a staleness gate (comparing
* `asof` against `now`, de-arming or annotating a too-old fair) is the **engine's** job,
* downstream — the receipt merely makes the input observable so that gate can exist. Absent
* with a reason, now documented.
*/
import { black76, intrinsic } from "./black76.js";
import { buildSurface, impliedForward, interpIv } from "./surface.js";
export { black76, impliedVol, intrinsic, tauYearsToClose, erf, normCdf, MS_PER_YEAR, } from "./black76.js";
export { buildSurface, impliedForward, interpIv, FORWARD_NO_ARB_BAND, PARITY_TOTAL_VOL_TOLERANCE, } from "./surface.js";
/** `@fair` for a spot instrument (equity/crypto, ADR-0017): its own two-sided quote mid with a
* receipt (`exec-fair-quote-v1`), or `null` when one-sided/dark — never Black-76. */
export { executionFairSpot, EXEC_FAIR_QUOTE_MODEL, } from "./spot.js";
/** The versioned model tag stamped on every receipt. EVs across model versions are not
* comparable and are distinguished by this string (ARCHITECTURE §4). */
export const EXEC_FAIR_MODEL = "exec-fair-b76-v1";
/** Narrow a {@link FairOutcome} to the refusal arm. */
export function isFairRefusal(o) {
return "refused" in o;
}
/** Rounding slack for the fair-vs-book comparison. The bound exists to catch a value that is
* WRONG (the SPY-752 incident sat 28% above the ask), never one a float-ulp outside the quote. */
const BOOK_BOUND_EPS = 1e-9;
/**
* Resolve `@fair` for one option, reporting a machine-readable reason when it refuses.
*
* The forward is READ from the options by parity (kestrel-ukwz, ADR-0037 §2) and used for BOTH
* the IV inversion and the final pricing; the intrinsic floor stays on spot. A parity strike
* whose call/put IVs disagree contributes no vol point (kestrel-ku99). A value outside the valued
* leg's own real+fresh book is REFUSED, never clamped (ADR-0037 §3/§5).
*
* {@link executionFair} is the reason-free wrapper over this; prefer THIS when you can put the
* reason in an audit trail.
*/
export function executionFairOutcome(input) {
const { underlyingSpot, strike, right, tauYears, liquidQuotes, asof } = input;
if (!Number.isFinite(underlyingSpot) || underlyingSpot <= 0) {
return { refused: "unbuildable", reason: "no-underlying" };
}
// THE FORWARD IS DERIVED, NEVER ASSUMED (ADR-0037 §2, kestrel-ukwz). It is threaded into BOTH
// the IV inversion (buildSurface) and the Black-76 price — the two must agree on the forward or
// the vol read is inverted against one number and re-priced against another.
const fwd = impliedForward(liquidQuotes, underlyingSpot);
const surface = buildSurface(liquidQuotes, fwd.forward, tauYears);
const nLiquid = surface.length;
if (nLiquid === 0)
return { refused: "unbuildable", reason: "no-liquid-strikes" };
const ivAtStrike = interpIv(surface, strike);
if (ivAtStrike === null)
return { refused: "unbuildable", reason: "no-surface-at-strike" };
// The worst parity disagreement any backing strike was accepted under — the honest fit-quality
// signal (kestrel-ku99). Max (not mean): one inconsistent strike is not excused by many clean ones.
let parityResidual;
for (const p of surface) {
if (p.dispersion === undefined)
continue;
if (parityResidual === undefined || p.dispersion > parityResidual)
parityResidual = p.dispersion;
}
const modeled = black76({ forward: fwd.forward, strike, tauYears, sigma: ivAtStrike, right });
// The floor stays SPOT-anchored (ADR-0037 §1): intrinsic is settlement truth — a fact about
// spot, not about the forward.
const value = Math.max(modeled, intrinsic(underlyingSpot, strike, right)); // floored, always
// The fair-vs-book bound (kestrel-ku99, ADR-0037 §3) — applied ONLY on the caller's freshness
// word, and only when the leg's own book is a REAL two-sided market. REFUSE, never clamp: a
// clamp would manufacture a plausible number and hide the broken vol read (ADR-0037 §5).
if (input.bookFresh === true) {
const bid = input.legBid ?? null;
const ask = input.legAsk ?? null;
if (bid !== null && ask !== null && bid < ask) {
if (value > ask + BOOK_BOUND_EPS)
return { refused: "tainted", reason: "fair-above-fresh-ask" };
if (value < bid - BOOK_BOUND_EPS)
return { refused: "tainted", reason: "fair-below-fresh-bid" };
}
}
return {
value,
// `asof` is carried verbatim (absent when the caller injected none); never gated here.
receipt: {
model: EXEC_FAIR_MODEL,
nLiquid,
ivAtStrike,
forward: fwd.forward,
forwardSource: fwd.source,
...(fwd.strike !== undefined ? { forwardStrike: fwd.strike } : {}),
...(fwd.residual !== undefined ? { forwardResidual: fwd.residual } : {}),
...(fwd.degraded !== undefined ? { forwardDegraded: fwd.degraded } : {}),
...(parityResidual !== undefined ? { parityResidual } : {}),
...(asof !== undefined ? { asof } : {}),
},
};
}
/**
* Resolve `@fair` for one option, or `null` when none can be served.
*
* The reason-free wrapper over {@link executionFairOutcome} — `null` collapses every refusal
* (unbuildable AND tainted) into the one signal the caller already handles: fall back to an
* annotated book value (RUNTIME §4). Callers that can carry the reason into an audit trail
* should call {@link executionFairOutcome} instead; `src/engine/pricing.ts` does.
*/
export function executionFair(input) {
const outcome = executionFairOutcome(input);
return isFairRefusal(outcome) ? null : outcome;
}