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.
105 lines • 6.83 kB
TypeScript
/**
* # src/fill/mark-to-model — the PER-WAKE mark-to-model of held option legs (kestrel theta-cell seam b)
*
* ## The gap this closes
* The sim marks an option position ONCE: {@link import("./engine.ts").SimFillEngine} cash-settles filled
* inventory at **intrinsic at the close**. So a held straddle's loss lands only at the terminal settle — there is NO
* intra-session mark between wakes, and therefore no dense signal that punishes STANDING DOWN on a
* position that is bleeding theta. On the FOMC-OPTIONS WHIPSAW day that terminal floor is already −$186,
* but a watcher graded per-wake sees a FLAT position at every interim wake and pays no running cost for
* doing nothing.
*
* This module adds that missing intra-session mark: at each wake it re-marks the held legs against the
* **tape's own option book** (the bid-side / sellable value folded as-of that wake ts) and reports the
* running mark-to-market versus the premium basis. A held long straddle whose premium is decaying shows
* a progressively NEGATIVE mark between wakes — so standing down becomes natively costly, which is the
* whole point of the theta cell (it structurally dissolves the "doing-nothing-wins" degenerate reward).
*
* ## Gated, additive, byte-identical when OFF
* The entire pass is behind {@link MarkToModelInput.markToModel}. When it is `false` (the default for
* every existing cell — their pinned floors do not move) the function returns an EMPTY mark series and a
* `standDownFloorUsd` of exactly `0`: nothing is marked, nothing downstream changes. Only a cell that
* opts in (`markToModel: true` on its frame) gets the per-wake bleed.
*
* ## What it is NOT (the clean boundary)
* This is a MARKING seam, not a settlement rewrite. It does not touch the τ provider
* ({@link import("../session/clock.ts").expiryTauYears}), does not touch `SimFillEngine.settle`, and does
* not compute position greeks (delta/gamma/theta as first-class quantities — that is the watcher-pareto
* peer's percept pane). It marks against the observed book, the one honest closeable value; a Black-76
* `@fair` mark is a deliberate alternative NOT taken here.
*
* ## Why kestrel-wcnd (τ-from-expiry) does not touch this file's MATH
* The τ bug mispriced every model-derived value. This seam derives NO model value: `sellablePerContract`
* reads the tape's own posted bid/ask, which needs no τ, no forward, and no vol surface. So the marking
* is unaffected by the τ fix BY CONSTRUCTION — it is exactly the property that made an observed-book mark
* the honest choice here. What DOES change is the leg-tenor assertion below: it now guards a `book.expiry`
* that genuinely drives pricing elsewhere, so a multi-tenor position (the ONE case where a single
* `book.expiry` cannot serve every leg) must keep surfacing loudly rather than mark a leg against another
* contract's book.
*/
import { type BusEvent } from "../bus/types.ts";
import type { ExpirySelector } from "../lang/ast.ts";
/** The standard listed-option contract multiplier (premiums + P&L are per-contract-×-100 dollars),
* matching `src/session/sim.ts` execSpec for an option underlier. */
export declare const OPTION_CONTRACT_MULTIPLIER = 100;
/** One held option leg to mark — a reconstructed (not authored) position, so it carries its OWN tenor
* via {@link ExpirySelector} (the {@link import("../lang/ast.ts").OptionLeg} `expiry` seam). `basisPremium`
* is the per-contract premium the leg was opened at (the arm-time ask for a long leg). */
export interface HeldOptionLeg {
readonly side: "buy" | "sell";
/** Contracts held (positive). */
readonly qty: number;
readonly strike: number;
readonly right: "C" | "P";
/** Per-contract premium basis (arm-time ask for a long leg; the credit for a short leg). */
readonly basisPremium: number;
/** The leg's preserved tenor (seam a). Optional; when present it is asserted to match the book's
* single expiry, so a multi-tenor position would surface the (unbuilt) re-keying boundary loudly. */
readonly expiry?: ExpirySelector;
}
/** One per-wake mark of the whole held position. */
export interface WakeMark {
readonly ts: number;
/** The book state's `asof_ts` the mark was folded from (≤ `ts`); `null` when no book exists yet. */
readonly bookAsOfTs: number | null;
/** The position's per-contract sellable value at this wake (Σ leg bid-side, a dark bid ⇒ 0). */
readonly sellablePerContract: number;
/** The running mark-to-market in USD vs the premium basis, summed over legs and SIGNED BY SIDE: a LONG
* leg contributes `(bid − basis) × qty × multiplier` (it paid the basis as a debit), a SHORT leg
* contributes `(−ask + basis) × qty × multiplier = (credit − ask) × qty × multiplier` (it received the
* basis as a credit). NEGATIVE = the held position has bled below its basis. */
readonly mtmUsd: number;
}
export interface MarkToModelInput {
/** The tape (BOOK events are folded into the running option book; everything else is ignored). */
readonly events: readonly BusEvent[];
/** The instrument whose book to fold (the option underlier symbol, e.g. `"SPY"`). */
readonly instrument: string;
/** The held legs to mark (the armed straddle, reconstructed from the arm). */
readonly legs: readonly HeldOptionLeg[];
/** The wake instants (UTC ms) to mark AT — typically the post-statement wakes where the position is held. */
readonly wakeTsList: readonly number[];
/** THE GATE. `false`/absent ⇒ no marking at all (empty series, `standDownFloorUsd = 0`) — byte-identical. */
readonly markToModel?: boolean;
/** Contract multiplier (default {@link OPTION_CONTRACT_MULTIPLIER}). */
readonly multiplier?: number;
}
export interface MarkToModelResult {
/** Whether the pass actually ran (mirrors the gate) — an inert seam reads `false` here. */
readonly enabled: boolean;
/** The per-wake mark series (empty when disabled). */
readonly marks: readonly WakeMark[];
/**
* The realized floor of a STAND-DOWN (hold-and-do-nothing) at the LAST marked wake — the intra-session
* theta-bled value the watcher is charged for standing pat. `0` when disabled (the position is carried
* at basis, no interim loss recognized) — which is exactly the OFF/ON contrast the theta seam exists to
* create: costly when ON, ~0 when OFF.
*/
readonly standDownFloorUsd: number;
}
/**
* Mark the held legs at each wake against the tape's option book. See the module header. Pure; no clock,
* no RNG, no lookahead (each wake folds the book only up to its own ts).
*/
export declare function markHeldLegsPerWake(input: MarkToModelInput): MarkToModelResult;
//# sourceMappingURL=mark-to-model.d.ts.map