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
Markdown
# 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 1–4, 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).