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 (123 loc) 10.7 kB
# The execution core is instrument-general: equity legs, quote-anchored fair, mark-to-market settle The wire (`src/protocol`), the bus envelope (`SessionInstrument.assetClass`, equity-only sessions), and the frame (empty chain) were built instrument-general from the start. The *execution core* — the language's order leg, `@fair`, the fill match, and settle — was not: it is Black-76 and intrinsic-at-expiry all the way down (generality audit, 2026-07-12). This ADR lands the first non-options cell — a **long-only, single-ticker, intraday equity** trade — by making the execution core instrument-general along four seams, additively, with the options path byte-identical. Owner decision: the platform is equities + equity options on Alpaca-tradeable universes; 0DTE index options is the internal app (the platform's product-scope canon). This is the smallest thing that proves the spine is instrument-general (audit §3 "minimal path", gap items 14, 7). ## The leg surface: `buy 100 shares` An equity/spot leg is `<side> <qty> shares` — `buy 100 shares`. The instrument is the ambient `USING exec <SYMBOL>` with **no expiry** (already legal — `Instrument.expiry` is optional); the leg carries only side, quantity, and the `shares` marker that distinguishes it from an option leg (`buy 2 +1 C`). No strike, no right — a spot instrument has neither. `shares` is a leg **unit word** in the register of the existing `Quantity` units (`R`/`c`/`%`/`bp`), not a new domain term, so CONTEXT.md needs no new glossary entry (the bus already says `assetClass: "equity"`); the AST names the node **EquityLeg** beside the existing **OptionLeg**, and `Leg` becomes their discriminated union (`kind: "leg" | "equity-leg"`). Keeping the option node's `kind: "leg"` byte-for-byte is what leaves every existing golden and AST fixture unchanged; the union forces every consumer to narrow, so the compiler — not a code review — enumerates the sites the equity path must handle. Consistent with SURFACES.md, which reserved "equity/underlying legs (options-only in v1)" as a *documented absence, not a stub* — this fills it. ## Doctrine mapping (the non-negotiables, re-expressed for a spot instrument) - **Bounded risk = cost basis at risk.** A long option's bounded risk is its premium; a long equity's is its **cost basis** (`qty × entry_px`, the premium analog). A cash buy is inherently bounded, so the first cell needs no new risk primitive — `size × max_loss ≤ budget` reads `max_loss = entry_px` per share. - **Never-naked → long-only in this slice.** Equity SELL is admitted **only against held shares** — the covered-sell netting is reused, keyed on the instrument instead of strike+right; an uncovered equity SELL is a naked short (unbounded loss + a borrow/locate constraint) and is **refused loudly**, never submitted. No shorting yet (audit gap item 12: bounded risk for unbounded instruments is its own later structural item). TP/EXIT sells stay the separate clamped-to-held path. - **`@fair` for a spot instrument = its own two-sided quote mid, with a receipt — never Black-76.** There is no chain to back a vol out of and no strike to take an intrinsic of, so ExecutionFair-B76 does not apply. `@fair` resolves to `(bid + ask) / 2` of the instrument's own quote, carrying a receipt under a new model tag **`exec-fair-quote-v1`** (distinct from `exec-fair-b76-v1`; EVs across tags do not compare). When the quote is one-sided or dark, fair is **unbuildable → null**, and the price resolver fails closed exactly as it does for options (a silent mid is forbidden, RUNTIME §4): the author must price off `@bid`/`@ask`/`@mid`/`@last`. `@intrinsic`/`@basis` remain option anchors and are refused for a spot leg. - **Settle for no-expiry inventory = mark-to-final-spot.** There is no expiry and no strike, so intrinsic-at-expiry is undefined. A filled equity lot settles at the **final spot**: `pnl = (settleSpot − entry_px) × qty × multiplier` for a buy, the mirror for a sell (multiplier `1`). Per-lot mark-to-spot **telescopes** for a closed round-trip (buy earns `spot − buyPx`, the covering sell earns `sellPx − spot`; the spot cancels, leaving `sellPx − buyPx`) — exactly as the option intrinsic settle telescopes — so no new inventory-netting is needed at the settle seam. The final spot carries its source-series provenance; when it is **stale** the settle records a logged provenance note rather than silently marking to a dead print (related: kestrel-xwf staleness-taint — applying the gate stays the engine's job; this cell logs the taint). ## Fill: instrument-keyed strict-cross Strict-cross is already instrument-agnostic *in principle* (a BUY fills iff the ask crosses below the resting price); only the engine's match was keyed on strike+right. An equity leg matches on **instrument alone** against the instrument's own two-sided quote. The far-OTM directional guard, moneyness, and the covered-wing exemption are option concepts with no spot analogue — they simply do not fire for an equity order (the symmetric path). The equity quote rides an **additive `bid?`/`ask?` on the SPOT tick** (an equity NBBO print); an option tape sets neither, so option tapes stay byte-identical, and the driver reassesses equity resting orders on those SPOT ticks the way it reassesses option orders on BOOK events. `multiplier` is already the contract-size seam (equity `1`); no schema change. ## Considered and rejected - **Model an equity as a degenerate option** (strike 0, a sentinel right, tau→0). Rejected: it smuggles a fictional strike and reuses Black-76/intrinsic — the exact doctrine this ADR refuses — and would corrupt the honest `@fair`/settle semantics. - **A distinct equity quote *event* on the tape.** Rejected as structural where an additive `bid?`/`ask?` on SPOT is a small lift that keeps every option tape byte-identical. - **Shorting / short-fair / borrow-locate in this cell.** Deferred: unbounded risk is a separate structural item (audit gap 12); long-only sidesteps it entirely. ## Consequences `Leg` is a discriminated union, so the engine narrows at each leg site; the equity buy + mark-to-spot settle is wired end to end for the long-only cell, while the not-yet-built equity paths (shorting, and any equity sell not covered by held shares) **fail closed loudly** rather than mis-execute. `@fair` gains a spot resolver (`exec-fair-quote-v1`); the SPOT tick gains optional `bid`/`ask`; the fill engine gains an instrument-keyed match and a mark-to-spot settle branch. A synthetic single-ticker SPOT-only tape (generic ticker, ARCHITECTURE §7) with trend+chop segments drives a Simulate/Grade e2e that projects deterministically to a byte-stable graded record. The N-day equity swing then rides the existing `Instance = ordered sequence of Sessions` model (per-session mark-to-close → carry), deferring the single-bus-spans-weeks work (kestrel-5zl.16) until after this cell is real. ## Amendment (2026-07-18): never-naked is a TWO-TIER contract — static structures refuse at ARM, fire-time remains the backstop (kestrel-53gw) The accepted text above (§"Doctrine mapping""Never-naked → long-only in this slice", and §"Consequences") states never-naked as a single **fire-time** boundary: `#sellCovered` admits a SELL only against net-held inventory of that `(strike, right)` (equity: that instrument) at each fire, and refuses it loudly otherwise. That boundary is unchanged and remains authoritative for the shape it was written for. This amendment records that never-naked now enforces at **two tiers**, and reconciles the one shape where the fire-time-only reading minted a misleading proof — an **opening multi-leg structure** that co-authors an entry BUY with an entry SELL (the debit/credit spread, the single-entry covered call written as one ticket). - **ARM-TIME tier (new, kestrel-53gw).** A *statically-visible* opening-short structure — an entry `DO`/`ALSO` **SELL** leg co-authored with an entry `DO`/`ALSO` **BUY** leg, with no `ARM … foreach|any held leg` and no `EXIT`/`TP … foreach|any held leg` to adopt covering inventory — is refused **AS DATA at arm** (coded `opening-short-uncovered`; `parse --arm`, `/validate`, and a real run all fail closed identically, RUNTIME §8 fail-closed-at-arm). Why the fire-time boundary alone was insufficient here: at the plan's single entry fire the same-fire entry BUY has not filled yet (fills return as later bus events), and cross-strike STRUCTURAL coverage (the long +1 C bounding the short +3 C to a defined debit) needs ATOMIC multi-leg execution the engine does not yet do (the AST `atomic` keyword is reserved-but-refused, ADR-0005; whole-structure preflight + atomic adapter are kestrel-psn5). So the short was refused never-naked at *every* fire while the entry BUY fired ALONE — silently degrading the spread to a naked long whose graded proof is **byte-identical** to the single long leg, with the refusal buried on an `armed` lifecycle line the headline proof never surfaces. Pricing the author's mistake at arm — one frame, whole-document, LOUD — is what keeps that misleading proof from ever minting. - **FIRE-TIME tier (unchanged, this ADR's original boundary).** The `#sellCovered` never-naked check REMAINS the backstop for every shape the arm-time static analysis cannot see: a **DYNAMICALLY-naked** sell whose coverage was legitimately DEFERRED to a fill that never arrived. The canonical case is a **RELOAD** sell (it fires during MANAGEMENT, after the entry BUY *may* have filled, so it arms clean and is coverage-checked at each fire): if the covering long's fill never lands, the sell is naked at fire and refused there with the `uncovered sell refused: never naked` diagnostic. A **pure naked opening sell** (a `DO sell …` with NO opening buy) is likewise left to this tier — its proof is `order_count = 0`, already distinguishable from a long, so it is not the misleading-proof defect. The equity naked-short refusal described above is this same fire-time tier. Scope guard: the arm-time tier fires *only* on the static opening-buy-plus-opening-sell shape (the byte-identical-proof defect). It does not narrow the fire-time boundary, does not touch the inventory-adopting paths (`ARM … foreach held leg`, `EXIT`/`TP … held leg`), and does not change the equity naked-short surface. Recorded as an amendment per repo convention (ADR-0005 precedent); append-only, the accepted text above is unchanged. Refs: kestrel-53gw (this fix), kestrel-psn5 (atomic combo execution + structure-aware coverage follow-up).