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.

901 lines (800 loc) 41.7 kB
/** * # ast — the typed object model IS the language (ADR-0004) * * Kestrel's canonical representation is this discriminated-union family, not the text. * `parse(text) -> objects` (phase 3) and `print(objects) -> text` (this phase, in * `print.ts`) are inverse projections with a byte-stable round-trip. Every node carries a * literal `kind` tag; there are no classes (data is plain readonly objects) and no `any`. * * One language, four statement kinds (ADR-0001): {@link ViewStatement}, * {@link WakeStatement}, {@link PlanStatement}, {@link GradeStatement} — plus the org * nodes {@link PodStatement} / {@link BookStatement} (ADR-0002). All four surfaces share * one lexical core: the {@link Trigger} algebra, {@link SeriesRef} operands, and * {@link PriceExpr} price expressions. * * The AST is ALWAYS fully qualified — `USING` defaults are resolved at parse time, and * the printer elides whatever matches the ambient `USING` (ADR-0004; ARCHITECTURE §2). */ // ───────────────────────────────────────────────────────────────────────────── // Comment trivia — the byte-stable "why" that travels WITH the action (ADR-0033) // ───────────────────────────────────────────────────────────────────────────── // // Comments are NOT executable (they carry no runtime semantics); they are authored reasoning // that MUST survive `print(parse(text))` byte-for-byte (ADR-0004 round-trip; ADR-0013:63-65 // depends on it; ADR-0031 journals through it). They are modeled as a cross-cutting SIDECAR // on the block nodes that print as one or more lines — never fields on the semantic clause // nodes — so the object model of triggers/prices/legs stays comment-free and every existing // builder and deep-equality test is untouched (the sidecar is simply absent on comment-free // documents). // // Two kinds, by position: // • OWN-LINE (leading/tail): a line whose first non-space char is `#`. Printed on its own // line at the block/clause indentation, ABOVE the line it precedes (leading) or after the // block's last line (tail). // • TRAILING-INLINE: a `#…` after the statement/clause tokens on the same line. Printed // after the line content with the canonical single space before `#`. // // A comment's `text` is captured VERBATIM: everything after the opening `#` to end of line, // the `#` itself excluded (so `# why` stores " why" and reprints as `# why`, byte-identical). // The line INDENT and the single space before a trailing `#` are the only things canonicalized. /** One printed line's comment trivia. `at` is the line's ordinal WITHIN its owning block, in * canonical print order: `0` = the block's own header line; `1..N` = the block's direct * interior lines (USING/WHEN/BECAUSE/clauses/panes/risk/directives). Child blocks own their * own trivia, so they never consume an ordinal here. */ export interface LineComment { readonly at: number; /** Own-line comments printed on their own line(s) ABOVE line `at`, verbatim, `#` excluded. */ readonly leading?: readonly string[]; /** An inline comment printed after line `at`'s content, one canonical space before `#`. */ readonly trailing?: string; } /** The comment sidecar for a block node (a statement, or the module). Absent entirely on a * comment-free document, so it never perturbs deep-equality against builder output. */ export interface CommentLayer { /** Comments bound to specific printed lines of this block, by ordinal. */ readonly lines?: readonly LineComment[]; /** Own-line comments trailing the block's last line, at the block's interior indentation. */ readonly tail?: readonly string[]; } // ───────────────────────────────────────────────────────────────────────────── // Primitive lexical units // ───────────────────────────────────────────────────────────────────────────── /** Timescale unit for windows and durations. There is no absolute shock: magnitude is * judged at a window (CONTEXT: Window). */ export type TimeUnit = "s" | "m" | "h" | "d" | "w" | "mo" | "q" | "y"; /** The timescale parameter a rate/magnitude series carries — `velocity(1m)`. */ export interface Window { readonly kind: "window"; readonly value: number; readonly unit: TimeUnit; } /** A span of time used by `held`, `within`, `esc`, and relative `ttl`. */ export interface Duration { readonly kind: "duration"; readonly value: number; readonly unit: TimeUnit; } /** A wall-clock time of day (session calendar), `HH:MM`. */ export interface TimeOfDay { readonly kind: "time-of-day"; readonly hour: number; readonly minute: number; } /** A numeric quantity with an optional unit. Plain number, risk-fraction `R`, cents `c`, * percent `%`, or basis points `bp`. Used as a trigger operand and in offsets. */ export interface Quantity { readonly kind: "quantity"; readonly value: number; readonly unit?: "R" | "c" | "%" | "bp"; } /** A baseline-relative threshold — the portable, cross-instrument form of "big" * (CONTEXT: Window). `p99` = 99th percentile of the window's own trailing baseline; * `3sigma` = three standard deviations. */ export interface Baseline { readonly kind: "baseline"; readonly stat: "p" | "sigma"; readonly n: number; } // ───────────────────────────────────────────────────────────────────────────── // Series references — the universal operand (CONTEXT: Series, Registry) // ───────────────────────────────────────────────────────────────────────────── /** A selector on a path segment: an org quantifier (`children(any)`) or a named target * (`plan(chase-urgent)`). */ export type PathSelector = | { readonly kind: "sel-quant"; readonly q: "any" | "all" } | { readonly kind: "sel-name"; readonly name: string }; /** One dotted segment of a series path, optionally selected. */ export interface PathSegment { readonly name: string; readonly selector?: PathSelector; } /** * A named series whose value changes over time — the universal operand. * * Two kinds and only two (CONTEXT: Series): **market facts** (ambient per signal * instrument, spectator-visible: `spot`, `vwap`, `velocity(1m)`) and **org facts** * (path-scoped in the pod tree, acting-sessions only: `pnl`, `alpha.pnl`, * `fills.avg_px`, `plan(chase-urgent).state`). The market/org distinction is **registry * resolution metadata, not a syntactic property** — canonical text does not mark it (a * dotted path prints identically for both), so it is NOT a field on this node; the * registry (a later phase) resolves a path to its space. A bare symbolic value like a * plan-state literal (`armed`) is a single-segment series — indistinguishable in text * from any other name, and resolved by the registry (ADR-0004: the node is the syntactic * identity; round-trip is mechanical, so nothing un-printable lives here). */ export interface SeriesRef { readonly kind: "series"; readonly segments: readonly PathSegment[]; readonly window?: Window; } /** Any leaf value that a comparison or cross can stand on. A bare identifier operand (a * plan-state literal like `armed`) is a single-segment {@link SeriesRef} — text cannot * distinguish it from a series, and ADR-0004 forbids un-round-trippable distinctions. */ export type Operand = SeriesRef | Quantity | Baseline; // ───────────────────────────────────────────────────────────────────────────── // Trigger algebra — the shared WHEN language (CONTEXT: Trigger algebra) // ───────────────────────────────────────────────────────────────────────────── export type CompareOp = "gt" | "ge" | "lt" | "le" | "eq" | "ne"; /** `left OP right` — a level/series comparison. */ export interface Comparison { readonly kind: "cmp"; readonly op: CompareOp; readonly left: Operand; readonly right: Operand; } /** * `left crosses above|below right` — an edge event, not a level. * * Two hardening dials, both learned from a graded-sim failure where a bare cross phantom- * fired 10/12 on a single anomalous print: * * - **`touch`** picks the boundary predicate. Omitted (the default) is a **strict** cross: * the series must move strictly past `right` (`>`/`<`). Present (`touches above|below`) is * an **at-or-touch** cross: reaching *or* touching the level fires (`>=`/`<=`). * - **`band`** is a **re-arm** width. With a band, after the cross fires the series must * clear back past `right ∓ band` (below `right−band` for an `above` cross, above * `right+band` for `below`) and **re-arm** before it can fire again — so a lone spurious * tick cannot phantom-fire. Omitted = a bare, single-shot cross (the fragile form). A * positive magnitude; a zero/negative band cannot re-arm and is refused at parse time. * * Doctrine (fail-closed, all surfaces): a cross is an edge, not a level, so it may **not** * be wrapped in `held` — sustaining an edge for a duration is the anti-pattern that phantom- * fired; the parser rejects it, the builder refuses to construct it, the printer throws on * it. Robustness comes from `band` (re-arm), not from holding the edge. (`within` over a * cross stays legal: "the cross occurred inside this window" is a well-formed question.) */ export interface CrossEvent { readonly kind: "cross"; readonly left: Operand; readonly dir: "above" | "below"; readonly right: Operand; readonly touch?: boolean; readonly band?: Quantity; } /** Break-and-hold: `inner held <dur>` — the inner condition sustained for a duration. */ export interface BreakHold { readonly kind: "break-hold"; readonly inner: Trigger; readonly dur: Duration; } /** `inner within <dur>` — the inner event occurred inside a rolling window. */ export interface WithinWindow { readonly kind: "within"; readonly inner: Trigger; readonly dur: Duration; } /** `inner until <clock>` — the inner expectation is in force up to a wall-clock time; the * thesis temporal envelope (kestrel-rtf). A postfix combinator over the SAME predicate * surface as {@link WithinWindow} — `until`/`at` are siblings of `within`, not a second * predicate language — each carrying a {@link TimeOfDay}. */ export interface UntilWindow { readonly kind: "until"; readonly inner: Trigger; readonly at: TimeOfDay; } /** `inner at <clock>` — the inner expectation is evaluated at a wall-clock time; the thesis * temporal envelope (kestrel-rtf). A postfix combinator sibling of {@link WithinWindow}. */ export interface AtWindow { readonly kind: "at"; readonly inner: Trigger; readonly at: TimeOfDay; } /** * `held <dur>` in EXIT/lead position — a 0DTE **time-held stop**: the position has been HELD * for a duration (measured from the plan's first acquiring fill), independent of any market * predicate. This is the in-grammar form for "exit N minutes after entry" the FOMC-options * single-model runs reached for out-of-grammar (`EXIT held 90m`, `EXIT held 120m`) and * parse-escaped on, de-arming the whole plan (docs/results/fomc-options-axis/report.md). * * It is NOT a {@link BreakHold}: break-hold sustains an INNER predicate for a duration (any * flicker resets the clock); a `held-stop` has no inner predicate — it is a bare hold-clock on * the acquired inventory. As a general {@link Trigger} it only has a referent inside a clause * that manages a held position (EXIT); read in any other position it fails closed (UNKNOWN), * exactly like {@link UntilWindow}/{@link AtWindow}. The engine fires it at the leg's own basis, * respecting never-naked / covered-close, through the SAME {@link ExitClause} path. */ export interface TimeHeldStop { readonly kind: "held-stop"; readonly dur: Duration; } /** * `clockET <clock>` — a 0DTE **wall-clock time-stop**: fire at or after a wall-clock ET time * (the theta-into-the-close cut the FOMC-options runs authored out-of-grammar as * `EXIT clockET 15:40`). Semantically a sibling of `time after <clock>`, but named `clockET` * because that is the surface the single-model authors emitted (mirroring the watcher's * `scheduleWake at {kind:"atClockET"}` vocabulary). Carries a {@link TimeOfDay}. */ export interface ClockStop { readonly kind: "clock-stop"; readonly at: TimeOfDay; } /** The Nth occurrence of an event: `second failed-break of HOD`. */ export interface NthEvent { readonly kind: "nth"; readonly ordinal: number; readonly event: Trigger; } /** A named structural market event, optionally of a level: `failed-break of HOD`. */ export interface StructuralEvent { readonly kind: "event"; readonly name: string; readonly of?: Operand; } /** A session-calendar phase event: `phase open`. */ export interface PhaseEvent { readonly kind: "phase"; readonly phase: string; } /** A wall-clock window: `time 09:30..10:00`, `time after 15:45`, `time before 10:00`. * At least one of `from` / `to` must be present. */ export interface TimeWindowTrig { readonly kind: "time-window"; readonly from?: TimeOfDay; readonly to?: TimeOfDay; } /** A fill-lifecycle event (fill telemetry as a trigger). */ export interface FillEvent { readonly kind: "fill"; readonly event: "filled" | "partial-fill" | "unfilled" | "rejected" | "cancelled"; readonly leg?: number; } export interface And { readonly kind: "and"; readonly terms: readonly Trigger[]; } export interface Or { readonly kind: "or"; readonly terms: readonly Trigger[]; } export interface Not { readonly kind: "not"; readonly term: Trigger; } /** The shared WHEN expression, used by Wake, Plan, and Grade filters alike. */ export type Trigger = | Comparison | CrossEvent | BreakHold | WithinWindow | UntilWindow | AtWindow | TimeHeldStop | ClockStop | NthEvent | StructuralEvent | PhaseEvent | TimeWindowTrig | FillEvent | And | Or | Not; // ───────────────────────────────────────────────────────────────────────────── // Price expressions (CONTEXT / ARCHITECTURE: ExecutionFair, the @fair anchor) // ───────────────────────────────────────────────────────────────────────────── /** A price anchor. `fair` = ExecutionFair (the honest MM fill price); `mid` is an * authoring resting-price, never a fair-value source (ARCHITECTURE §4). */ export type AnchorName = | "fair" | "intrinsic" | "basis" | "bid" | "ask" | "mid" | "last" | "join" | "improve" | "stub" // The underlying spot as a PRICE anchor (emergent grammar, ADR-0030 / kestrel-ipc). `spot` is // already a first-class SERIES name (`WHEN spot > 499`); admitting it in the `@ price` position // makes the anchor set a strict superset — no previously-parsed form changes meaning. It resolves // to the canonical underlying spot for an equity/spot leg and is REFUSED for an option leg // (the mirror image of `intrinsic`/`basis`, which parse always but are refused on a spot leg — // ADR-0017). The two readings of `spot` (series vs anchor) read the SAME underlying value. | "spot"; export interface Anchor { readonly kind: "anchor"; readonly name: AnchorName; } /** An absolute limit price, e.g. `4.20`. */ export interface AbsolutePrice { readonly kind: "price-abs"; readonly value: number; } /** `base ± N(c|%)` — a tight offset from a base price, e.g. `fair-3c`. */ export interface PriceOffset { readonly kind: "price-offset"; readonly base: PriceExpr; readonly sign: "+" | "-"; readonly amount: number; readonly unit: "c" | "%"; } /** `lean(a, b, x)` — blend between two prices by fraction x (lean-to-MM). */ export interface Lean { readonly kind: "lean"; readonly a: PriceExpr; readonly b: PriceExpr; readonly x: number; } /** `min(...)` / `max(...)` over price expressions. */ export interface PriceMinMax { readonly kind: "price-fn"; readonly fn: "min" | "max"; readonly args: readonly PriceExpr[]; } export type PriceExpr = Anchor | AbsolutePrice | PriceOffset | Lean | PriceMinMax; /** One rung of an escalation ladder: escalate the resting price `to` after `after`. */ export interface EscStage { readonly kind: "esc-stage"; readonly to: PriceExpr; readonly after: Duration; } /** The resting-order execution policy: peg/fix, an esc ladder, cap/floor lists, a * per-order cancel-if guard, and gtc. Distinct from *which* price (that is a * {@link PriceExpr}). */ export interface OrderPolicy { readonly kind: "order-policy"; readonly pricing?: "peg" | "fix"; readonly esc?: readonly EscStage[]; readonly caps?: readonly PriceExpr[]; readonly floors?: readonly PriceExpr[]; readonly cancelIf?: Trigger; readonly gtc?: boolean; } // ───────────────────────────────────────────────────────────────────────────── // Order legs (CONTEXT: Book; ADR-0005 Plan v1) // ───────────────────────────────────────────────────────────────────────────── /** How a strike is chosen: relative to the anchor grid (`+1`), an absolute strike * (`450`), at-the-money (`atm`), or delta-targeted (`25d`). */ export type StrikeSpec = | { readonly kind: "strike-rel"; readonly steps: number } | { readonly kind: "strike-abs"; readonly strike: number } | { readonly kind: "strike-atm" } | { readonly kind: "strike-delta"; readonly delta: number }; /** One option leg: `buy 2 +1 C`. * * A SELL leg is legal ONLY as a **covered** close of inventory this plan actually holds — never-naked * applies to OPTION legs exactly as it does to an {@link EquityLeg} (ADR-0017). Coverage is enforced * per `(strike, right)` (`PlanEngine#sellCovered`: net-held-of-that-leg minus its resting sells) on each * surface that can emit a sell: entry, ALSO, RELOAD, flatten, **TAKE-PROFIT**, and **EXIT**. * * The last two are called out because they were the hole (kestrel-h5nx). Both sized off the PLAN-TOTAL * net-held while binding the sell to a SINGLE leg, so any plan holding more than one leg (a straddle, a * two-strike ladder) oversold that leg and went NET SHORT an uncovered option. On a one-leg plan the two * quantities coincide, which is why it survived for so long — and why a contaminated result had already * been pinned into the committed golden fixtures. Multi-leg sells now ALLOCATE across the held legs, each * capped by and named for the leg that covers it. */ export interface OptionLeg { readonly kind: "leg"; readonly side: "buy" | "sell"; readonly qty: number; readonly strike: StrikeSpec; readonly right: "C" | "P"; /** * The leg's own tenor (kestrel theta-cell seam a; AUTHORABLE since kestrel-ih5h seam 1). * * `undefined` means **inherit the ambient execution tenor** — the expiry authored once on the * execution instrument (`USING exec … 0dte`, {@link ExpirySelector} on {@link Instrument}). It is * never a silent 0dte: the inheritance is resolved at execution (`#execForLeg` in * `src/engine/plans.ts`), and an expiry that cannot be resolved fails closed rather than defaulting. * * Authored per-leg with the `exp` marker — `buy 2 +1 C exp 0dte`, `sell 1 atm P exp 2026-07-17` — * reusing the SAME selector vocabulary as the instrument form. The marker is required because a leg * is followed by `,` or `@` and a bare tag selector would swallow the continuation; the instrument * form can be positional only because a stop-set holds it apart. Legs written without `exp` print * byte-identically to before this syntax existed (ADR-0004; the tail is additive). * * It is also what lets a leg RECONSTRUCTED from a held/marked position (not authored — e.g. the * per-wake mark-to-model folding a held straddle back out of the tape's option book, whose * `BookState` carries one expiry) carry its own tenor with it. * * Note the execution BOOK is still keyed by SYMBOL alone (`#books` in `src/engine/plans.ts`, * `foldBook` in `src/bus/types.ts` keeps ONE expiry per instrument). A leg expiry therefore reaches * execution as a CHECK, not a selector: `#bookFor` refuses a book whose expiry contradicts the * authored one (fail-closed) rather than pricing against the wrong tenor. Re-keying the book by * `(symbol, expiry)` — what a genuine multi-expiry / calendar position needs — is the other half of * this seam and is deliberately NOT made here. */ readonly expiry?: ExpirySelector; } /** One equity/spot leg: `buy 100 shares` (ADR-0017). A spot instrument has neither a * strike nor a right, so an equity leg carries only side + quantity + the `shares` marker; * the tradeable symbol is the ambient `USING exec <SYMBOL>` with no expiry. `qty` is a share * count. Deliberately long-biased in v1: an uncovered equity SELL is a naked short (unbounded * risk) and is refused downstream (never-naked, ADR-0017). The SAME boundary governs an * {@link OptionLeg} sell — never-naked is not an equity-only rule (kestrel-h5nx). */ export interface EquityLeg { readonly kind: "equity-leg"; readonly side: "buy" | "sell"; readonly qty: number; } /** An order leg — an {@link OptionLeg} (`buy 2 +1 C`) or an {@link EquityLeg} (`buy 100 * shares`, ADR-0017). A discriminated union on `kind`; every consumer narrows, so the * compiler enumerates the sites the instrument-general execution core must handle. */ export type Leg = OptionLeg | EquityLeg; // ───────────────────────────────────────────────────────────────────────────── // Plan clauses (ADR-0005: WHEN / DO / ALSO / RELOAD / TP / EXIT / INVALIDATE / // CANCEL-IF / ARM; inventory binding) // ───────────────────────────────────────────────────────────────────────────── /** A held-inventory quantifier for clauses that act on pre-existing positions * (ADR-0005 inventory binding). */ export type HeldQuantifier = | { readonly kind: "held-foreach" } // FOREACH held leg | { readonly kind: "held-any" }; // any held leg /** `DO <legs> @ <price> <policy>` — the primary order ticket. `atomic` is reserved and * refused symmetrically on every surface (ADR-0005): the parser REJECTS the keyword, the * builders REFUSE to construct it, and the printer THROWS on it. The AST keeps the field * anyway — the parser needs the vocabulary to reject it loudly, and the type documents the * future surface (a whole-structure preflight + an atomic execution adapter) — but no valid * v1 ticket ever carries it, so `parse(print(x))` round-trips (ADR-0004). Never faked. */ export interface DoTicket { readonly kind: "do"; readonly legs: readonly Leg[]; readonly price: PriceExpr; readonly policy?: OrderPolicy; readonly atomic?: boolean; } /** `ALSO <legs> @ <price> <policy>` — an additional co-armed ticket. */ export interface AlsoTicket { readonly kind: "also"; readonly legs: readonly Leg[]; readonly price: PriceExpr; readonly policy?: OrderPolicy; readonly atomic?: boolean; } /** `RELOAD [WHEN <trig>] <legs> @ <price> <policy>` — buy more on adverse movement. */ export interface ReloadClause { readonly kind: "reload"; readonly when?: Trigger; readonly legs: readonly Leg[]; readonly price: PriceExpr; readonly policy?: OrderPolicy; readonly atomic?: boolean; } /** What target a take-profit rests against. */ export type TpTarget = | { readonly kind: "tp-pct"; readonly pct: number } // +100% | { readonly kind: "tp-mult"; readonly mult: number } // 2x | { readonly kind: "tp-price"; readonly price: PriceExpr }; /** `TP <target> [frac F] [<held-quant>] [@ <price>] <policy>` — resting take-profit. */ export interface TpClause { readonly kind: "tp"; readonly target: TpTarget; readonly frac?: number; readonly over?: HeldQuantifier; readonly price?: PriceExpr; readonly policy?: OrderPolicy; } /** `EXIT <trig> [<held-quant>] [@ <price>] <policy>` — get out when the thesis breaks. */ export interface ExitClause { readonly kind: "exit"; readonly when: Trigger; readonly over?: HeldQuantifier; readonly price?: PriceExpr; readonly policy?: OrderPolicy; } /** `INVALIDATE <trig>` — thesis dead: stop active management, ride the tail. */ export interface InvalidateClause { readonly kind: "invalidate"; readonly when: Trigger; } /** `CANCEL-IF <trig>` — cancel resting orders (plan-scope, vs the per-order guard). */ export interface CancelIfClause { readonly kind: "cancel-if"; readonly when: Trigger; } /** `ARM [WHEN <trig>] [basis <price>] [<held-quant>]` — the on-arm trigger + inventory * binding (ADR-0005): the basis price anchor and a held-leg quantifier. */ export interface ArmClause { readonly kind: "arm"; readonly when?: Trigger; readonly basis?: PriceExpr; readonly over?: HeldQuantifier; } export type PlanClause = | DoTicket | AlsoTicket | ReloadClause | TpClause | ExitClause | InvalidateClause | CancelIfClause | ArmClause; // ───────────────────────────────────────────────────────────────────────────── // Instruments, USING, provenance // ───────────────────────────────────────────────────────────────────────────── /** An expiry selector for an execution instrument. */ export type ExpirySelector = | { readonly kind: "expiry-dte"; readonly dte: number } // 0dte, 1dte | { readonly kind: "expiry-date"; readonly date: string } // 2026-07-17 | { readonly kind: "expiry-tag"; readonly tag: string }; // weekly, monthly /** A tradeable instrument, optionally with an expiry (execution side). */ export interface Instrument { readonly kind: "instrument"; readonly symbol: string; readonly expiry?: ExpirySelector; } /** Scoped defaults: a signal-space instrument (where WHEN reads) and an execution-space * instrument + expiry (where DO acts). The only v1 customization point for imports * (ADR-0003). */ export interface Using { readonly kind: "using"; readonly signal?: Instrument; readonly exec?: Instrument; } export type ProvenanceTier = "vetted" | "candidate" | "unvetted"; /** Authorship + trust metadata. A declared provenance can only *narrow* the channel's * authority (ARCHITECTURE §6 provenance ceiling). */ export interface Provenance { readonly kind: "provenance"; readonly tier?: ProvenanceTier; readonly author?: string; readonly origin?: string; readonly replay?: string; } // ───────────────────────────────────────────────────────────────────────────── // VIEW (CONTEXT: View, Pane) // ───────────────────────────────────────────────────────────────────────────── /** A pane argument, carrying only its **syntactic supersort** — the class the lexer/parser assigns * with ZERO catalog knowledge (ADR-0041 §1: the `wf` rung is catalog-independent, so the parser * never picks a pane's semantic sort). Five supersorts: a bare **ident** (`skyline`, `vwap`), a * **window** (`5m` — a numeral + a time unit), a bare **numeral count** (`12` — a numeral with * NO unit, e.g. `chain 12`), a **session ordinal** (`d-0`, `d-1` — the `d-` prefix + a numeral, * Train 1B), and an **expiry ordinal** (`e-0`, `e-1` — the `e-` prefix + a numeral, Train 1B). A * catalog `ParamSlot` REFINES a supersort to a semantic sort * (`Instrument`/`LevelName`/`Window`/`Count`/`SessionOrdinal`/`ExpiryOrdinal`) at materialization * (`src/frame/pane-catalog.ts`). Each supersort has ONE canonical printed form so `print(parse(text))` * stays byte-stable (ADR-0004): an ident prints its name, a window `<n><unit>`, a count its bare * numeral, a session ordinal `d-<n>`, an expiry ordinal `e-<n>`. Panes are references + args only — * a View never computes (no application node, ADR-0041 §1). */ export type PaneArg = | { readonly kind: "arg-ident"; readonly name: string } | { readonly kind: "arg-window"; readonly window: Window } | { readonly kind: "arg-count"; readonly count: number } | { readonly kind: "arg-ordinal"; readonly ordinal: number } | { readonly kind: "arg-expiry"; readonly expiry: number }; /** One named block of a View. */ export interface PaneRef { readonly kind: "pane"; readonly name: string; readonly args: readonly PaneArg[]; } export interface ViewStatement { readonly kind: "view"; readonly name: string; readonly budget?: number; // token budget readonly panes: readonly PaneRef[]; /** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */ readonly comments?: CommentLayer; } // ───────────────────────────────────────────────────────────────────────────── // Citation — the `because` pre-registration clause (kestrel-rtf) // ───────────────────────────────────────────────────────────────────────────── /** * A content-hash reference to a platform-side Thesis artifact — the `because` clause * (kestrel-rtf). It carries ONLY the digest of the fixed shape `sha256:<64 hex>`; the thesis * body lives platform-side, never inline (a `because` with an inline body is a parse refusal). * * Because the clause prints byte-stably like any other canonical text, it binds into the * armed-document hash (`sha256(print(module))`) automatically — so pre-registration is * tamper-evident once the plan is armed. It is an OPTIONAL field on {@link PlanStatement} and * {@link WakeStatement}, NOT a fifth statement kind: the statement algebra stays View/Wake/ * Plan/Grade (ADR-0001), and `because` attached to a View or Grade is a fail-closed refusal. */ export interface Citation { readonly kind: "citation"; readonly algo: "sha256"; /** The 64-lowercase-hex content hash of the platform-side Thesis (no inline body). */ readonly hash: string; } // ───────────────────────────────────────────────────────────────────────────── // WAKE (CONTEXT: Wake, Scan, Wake delivery) // ───────────────────────────────────────────────────────────────────────────── /** `DELIVER <view> [MANDATORY] [KEYFRAME]` — the wake decides *when*, the View *what*. * Delta frame by default; keyframe on request. */ export interface DeliverSpec { readonly kind: "deliver"; readonly view: string; readonly mandatory?: boolean; readonly keyframe?: boolean; } /** A wake's attention budget: wakes/day and/or tokens/day. */ export interface WakeBudget { readonly kind: "wake-budget"; readonly wakesPerDay?: number; readonly tokensPerDay?: number; } /** A Scan's universe scope (`all-listed`, `nyse`) — wide and slow, vs a narrow coverage * wake. */ export interface UniverseScope { readonly kind: "universe"; readonly universe: string; } export interface WakeStatement { readonly kind: "wake"; readonly name: string; readonly when: Trigger; /** Optional `because sha256:<64 hex>` pre-registration citation (kestrel-rtf). */ readonly because?: Citation; readonly deliver?: DeliverSpec; readonly priority?: number; readonly coalesce?: string; readonly budget?: WakeBudget; readonly universe?: UniverseScope; /** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */ readonly comments?: CommentLayer; } // ───────────────────────────────────────────────────────────────────────────── // PLAN (ARCHITECTURE §2; ADR-0005) // ───────────────────────────────────────────────────────────────────────────── /** Budget as a risk fraction, `0.2R`. `size × max_loss ≤ budget` (ARCHITECTURE §6). */ export interface Budget { readonly kind: "budget"; readonly value: number; readonly unit: "R"; } /** Time-to-live: relative (`+30m`) or an absolute clock (`16:00`, hold-to-close). */ export type Ttl = | { readonly kind: "ttl-rel"; readonly dur: Duration } | { readonly kind: "ttl-at"; readonly at: TimeOfDay }; /** One tag binding of a regime gate: `intraday: trend`. */ export interface RegimeTagBinding { readonly scope: string; readonly value: string; } /** `regime {intraday: trend}` — an open-vocabulary tag gate (ADR-0005). */ export interface RegimeGate { readonly kind: "regime-gate"; readonly tags: readonly RegimeTagBinding[]; } /** The lifecycle standing of a standing statement (CONTEXT: Standing). */ export type Standing = "authored" | "armed" | "versioned" | "superseded"; export interface PlanStatement { readonly kind: "plan"; readonly name: string; readonly budget?: Budget; readonly ttl?: Ttl; readonly regime?: RegimeGate; readonly priority?: number; readonly standing?: Standing; readonly provenance?: Provenance; readonly using?: Using; readonly when?: Trigger; /** Optional `because sha256:<64 hex>` pre-registration citation (kestrel-rtf). */ readonly because?: Citation; readonly clauses: readonly PlanClause[]; readonly atomic?: boolean; /** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */ readonly comments?: CommentLayer; } // ───────────────────────────────────────────────────────────────────────────── // GRADE (ADR-0006) // ───────────────────────────────────────────────────────────────────────────── /** What is being graded — everything authored is gradable (ADR-0006). */ export interface GradeSubject { readonly kind: "grade-subject"; readonly what: "plan" | "wake" | "view" | "pod" | "tag"; readonly name: string; } /** A counterfactual — syntax, not a bespoke script (ADR-0006). The structural null and * bracket cross the SAME fill model. */ export type Counterfactual = | { readonly kind: "cf-ungated" } | { readonly kind: "cf-null" } | { readonly kind: "cf-bracket" }; /** The corpus range a grade replays over: `2025-01..2026-06`. Opaque date tokens. */ export interface CorpusRange { readonly kind: "corpus-range"; readonly from: string; readonly to: string; } /** A `BY` stratification dimension — cells, not pools (ADR-0006). */ export type GradeDimension = | { readonly kind: "dim-series"; readonly series: SeriesRef } | { readonly kind: "dim-vehicle" } | { readonly kind: "dim-name" } | { readonly kind: "dim-lineage" }; export interface GradeStatement { readonly kind: "grade"; readonly subject: GradeSubject; readonly over?: CorpusRange; readonly fill?: string; // named, versioned fill model readonly versus: readonly Counterfactual[]; readonly by: readonly GradeDimension[]; /** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */ readonly comments?: CommentLayer; } // ───────────────────────────────────────────────────────────────────────────── // POD / BOOK / RISK (ADR-0002) // ───────────────────────────────────────────────────────────────────────────── /** Instruments assigned to a Trader PLUS the thesis for why (CONTEXT: Coverage). */ export interface Coverage { readonly kind: "coverage"; readonly instruments: readonly Instrument[]; readonly thesis: string; } /** How a Book's concurrent plans compete for its envelope (open-vocabulary policy). */ export interface Arbitration { readonly kind: "arbitration"; readonly policy: string; readonly concurrency?: number; } /** One risk line: `RISK day-loss 2R -> halt`. The L0 envelope may clamp/veto, never open * risk (ARCHITECTURE §3). */ export interface RiskLine { readonly kind: "risk"; readonly metric: string; readonly threshold: Quantity; readonly action: string; } /** The leaf of the org tree — the only place positions and orders live (ADR-0002). */ export interface BookStatement { readonly kind: "book"; readonly name: string; readonly budget?: Budget; readonly coverage?: Coverage; readonly arbitration?: Arbitration; readonly risk?: readonly RiskLine[]; /** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */ readonly comments?: CommentLayer; } /** The recursive org node: an allocating PM role + a risk envelope + children (each a * Book or another Pod). Depth is unbounded; the org is data (ADR-0002). */ export interface PodStatement { readonly kind: "pod"; readonly name: string; readonly risk: readonly RiskLine[]; readonly children: readonly (PodStatement | BookStatement)[]; /** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */ readonly comments?: CommentLayer; } // ───────────────────────────────────────────────────────────────────────────── // Module / Document (ADR-0003) // ───────────────────────────────────────────────────────────────────────────── /** An ESM-like import of named statements from another module (ADR-0003). */ export interface ImportDecl { readonly kind: "import"; readonly names: readonly string[]; readonly from: string; } /** Any top-level statement of a module. */ export type Statement = | ViewStatement | WakeStatement | PlanStatement | GradeStatement | PodStatement | BookStatement; /** A Kestrel document: imports + optional USING defaults + statements + provenance. */ export interface Module { readonly kind: "module"; readonly imports: readonly ImportDecl[]; readonly using?: Using; readonly statements: readonly Statement[]; readonly provenance?: Provenance; /** Byte-stable comment trivia (ADR-0033) for the module's own directive lines (IMPORT / * PROVENANCE / USING, ordinal 0..k-1) and its tail. Comments before/on a statement live on * that statement, not here. Absent on comment-free documents. */ readonly comments?: CommentLayer; } /** Any node the printer accepts at the top level. */ export type KestrelNode = Module | Statement;