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.

522 lines (469 loc) 21.2 kB
/** * # builders — the programmatic authoring surface (ADR-0004) * * Ergonomic constructors for the {@link ../ast.ts} object model. An ESM module composing * these objects and a `.kestrel` file are the same kind of module (ADR-0004): programs * compose here, agents/humans write text, neither is second-class. * * Every helper returns a plain readonly AST node with its `kind` tag filled in. Optional * fields are omitted (never set to `undefined`) so the result satisfies * `exactOptionalPropertyTypes`. */ import type { AbsolutePrice, Anchor, AnchorName, Arbitration, ArmClause, Baseline, BookStatement, Budget, CancelIfClause, Citation, Comparison, CompareOp, Coverage, Counterfactual, CrossEvent, DeliverSpec, DoTicket, Duration, ExitClause, ExpirySelector, FillEvent, GradeDimension, GradeStatement, GradeSubject, HeldQuantifier, ImportDecl, InvalidateClause, Instrument, Leg, Lean, Module, Operand, OrderPolicy, PaneArg, PaneRef, PhaseEvent, PlanClause, PlanStatement, PodStatement, PriceExpr, PriceMinMax, PriceOffset, Provenance, Quantity, RegimeGate, RegimeTagBinding, ReloadClause, RiskLine, SeriesRef, Standing, Statement, StrikeSpec, StructuralEvent, TimeOfDay, TimeUnit, TimeWindowTrig, TpClause, TpTarget, Trigger, Ttl, Using, ViewStatement, WakeBudget, WakeStatement, Window, } from "./ast.ts"; import { refuseAtomic, refuseBadCitation, refuseExitOnMark, refuseHeldOverCross, refuseNonPositiveBand, refuseNonPositiveBudget, refuseProvenanceElevation, } from "./validate.ts"; // Strip keys whose value is undefined, so optional AST fields stay absent rather than // present-and-undefined (exactOptionalPropertyTypes). The narrowing back to the AST type // happens at each call site's return. function compact<T extends Record<string, unknown>>(o: T): Partial<T> { const r: Record<string, unknown> = {}; for (const [k, v] of Object.entries(o)) if (v !== undefined) r[k] = v; return r as Partial<T>; } // ── Lexical units ──────────────────────────────────────────────────────────── export const win = (value: number, unit: TimeUnit): Window => ({ kind: "window", value, unit }); export const dur = (value: number, unit: TimeUnit): Duration => ({ kind: "duration", value, unit }); export const tod = (hour: number, minute: number): TimeOfDay => ({ kind: "time-of-day", hour, minute, }); export const num = (value: number, unit?: Quantity["unit"]): Quantity => ({ kind: "quantity", ...compact({ value, unit }) }) as Quantity; export const p = (n: number): Baseline => ({ kind: "baseline", stat: "p", n }); export const sigma = (n: number): Baseline => ({ kind: "baseline", stat: "sigma", n }); // ── Series ─────────────────────────────────────────────────────────────────── /** A series reference: `series("spot")`, `series("velocity", { window: win(1,"m") })`. * Dotted paths pass segments: `series(["regime","intraday"])`. The market/org distinction * is registry metadata, not syntax (ADR-0004), so it is not encoded here. */ export function series(path: string | readonly string[], opts: { window?: Window } = {}): SeriesRef { const names = typeof path === "string" ? [path] : path; return { kind: "series", segments: names.map((name) => ({ name })), ...compact({ window: opts.window }), } as SeriesRef; } /** An org-aggregate path with a quantified segment: `children(any).drawdown`. */ export const childrenAny = (field: string): SeriesRef => ({ kind: "series", segments: [{ name: "children", selector: { kind: "sel-quant", q: "any" } }, { name: field }], }); /** A named plan-state path: `plan(chase-urgent).state`. */ export const planState = (planName: string, field = "state"): SeriesRef => ({ kind: "series", segments: [{ name: "plan", selector: { kind: "sel-name", name: planName } }, { name: field }], }); // ── Triggers ─────────────────────────────────────────────────────────────── export const cmp = (left: Operand, op: CompareOp, right: Operand): Comparison => ({ kind: "cmp", op, left, right, }); export const gt = (l: Operand, r: Operand): Comparison => cmp(l, "gt", r); export const ge = (l: Operand, r: Operand): Comparison => cmp(l, "ge", r); export const lt = (l: Operand, r: Operand): Comparison => cmp(l, "lt", r); export const le = (l: Operand, r: Operand): Comparison => cmp(l, "le", r); export const eq = (l: Operand, r: Operand): Comparison => cmp(l, "eq", r); export const ne = (l: Operand, r: Operand): Comparison => cmp(l, "ne", r); /** A cross edge event. `opts.touch` picks the at-or-touch predicate (`touches`, `>=`/`<=`); * `opts.band` is a positive re-arm width so a lone spurious tick cannot phantom-fire. * Both are omitted (a bare strict cross) by default. The band>0 doctrine invariant is enforced * at construction (fail closed early) via ./validate.ts, symmetric with parse/print. */ export const cross = ( left: Operand, direction: "above" | "below", right: Operand, opts: { touch?: boolean; band?: Quantity } = {}, ): CrossEvent => { refuseNonPositiveBand(opts.band); return { kind: "cross", left, dir: direction, right, ...compact(opts) } as CrossEvent; }; /** Refuse to CONSTRUCT held-over-cross (the sfg8 anti-pattern; ./validate.ts). `within` over a * cross stays legal. */ export const held = (inner: Trigger, d: Duration): Trigger => { refuseHeldOverCross(inner); return { kind: "break-hold", inner, dur: d }; }; export const within = (inner: Trigger, d: Duration): Trigger => ({ kind: "within", inner, dur: d }); /** `held <dur>` in lead position — a 0DTE time-held stop (`EXIT held 90m`); a bare hold-clock on * the acquired position, NOT the `held(inner, dur)` break-hold postfix (docs/results/fomc-options).*/ export const heldStop = (d: Duration): Trigger => ({ kind: "held-stop", dur: d }); /** `clockET <clock>` — a 0DTE wall-clock time-stop (`EXIT clockET 15:40`). */ export const clockStop = (time: TimeOfDay): Trigger => ({ kind: "clock-stop", at: time }); /** `inner until <clock>` — the thesis temporal envelope (kestrel-rtf), a postfix sibling of * `within` over the same predicate surface (no second predicate language). */ export const until = (inner: Trigger, time: TimeOfDay): Trigger => ({ kind: "until", inner, at: time }); /** `inner at <clock>` — the thesis temporal envelope (kestrel-rtf), a postfix sibling of `within`. */ export const at = (inner: Trigger, time: TimeOfDay): Trigger => ({ kind: "at", inner, at: time }); export const nth = (ordinal: number, event: Trigger): Trigger => { // Ordinals are 1-based (`first`, `second`, …) — the parser rejects 0/negatives and the // printer cannot render them lexably. Refuse to construct one that could never round-trip. if (!Number.isInteger(ordinal) || ordinal < 1) { throw new Error(`kestrel/builders: nth ordinal must be a positive integer (>= 1); got ${String(ordinal)}`); } return { kind: "nth", ordinal, event }; }; export const event = (name: string, of?: Operand): StructuralEvent => ({ kind: "event", name, ...compact({ of }) }) as StructuralEvent; export const phase = (name: string): PhaseEvent => ({ kind: "phase", phase: name }); export const timeWindow = (opts: { from?: TimeOfDay; to?: TimeOfDay }): TimeWindowTrig => ({ kind: "time-window", ...compact(opts) }) as TimeWindowTrig; export const fill = (evt: FillEvent["event"], leg?: number): FillEvent => ({ kind: "fill", event: evt, ...compact({ leg }) }) as FillEvent; export const and = (...terms: Trigger[]): Trigger => ({ kind: "and", terms }); export const or = (...terms: Trigger[]): Trigger => ({ kind: "or", terms }); export const not = (term: Trigger): Trigger => ({ kind: "not", term }); // ── Prices ─────────────────────────────────────────────────────────────────── export const anchor = (name: AnchorName): Anchor => ({ kind: "anchor", name }); export const fair: Anchor = anchor("fair"); export const intrinsic: Anchor = anchor("intrinsic"); export const basis: Anchor = anchor("basis"); export const bid: Anchor = anchor("bid"); export const ask: Anchor = anchor("ask"); export const mid: Anchor = anchor("mid"); export const last: Anchor = anchor("last"); export const join: Anchor = anchor("join"); export const improve: Anchor = anchor("improve"); export const stub: Anchor = anchor("stub"); // The `spot` PRICE anchor (ADR-0030 / kestrel-ipc). No collision with the `spot` SERIES: that is // the function-call `series("spot")`, this is the anchor-value node. export const spot: Anchor = anchor("spot"); export const px = (value: number): AbsolutePrice => ({ kind: "price-abs", value }); export const minus = (base: PriceExpr, amount: number, unit: "c" | "%" = "c"): PriceOffset => ({ kind: "price-offset", base, sign: "-", amount, unit, }); export const plus = (base: PriceExpr, amount: number, unit: "c" | "%" = "c"): PriceOffset => ({ kind: "price-offset", base, sign: "+", amount, unit, }); export const lean = (a: PriceExpr, b: PriceExpr, x: number): Lean => ({ kind: "lean", a, b, x }); export const pmin = (...args: PriceExpr[]): PriceMinMax => ({ kind: "price-fn", fn: "min", args }); export const pmax = (...args: PriceExpr[]): PriceMinMax => ({ kind: "price-fn", fn: "max", args }); // ── Legs & order policy ────────────────────────────────────────────────────── export const rel = (steps: number): StrikeSpec => ({ kind: "strike-rel", steps }); export const abs = (strike: number): StrikeSpec => ({ kind: "strike-abs", strike }); export const atm: StrikeSpec = { kind: "strike-atm" }; export const delta = (d: number): StrikeSpec => ({ kind: "strike-delta", delta: d }); /** An option leg `buy <qty> <strike> <right>`, optionally with its OWN expiry (`buy 2 +1 C exp 0dte`, * kestrel-ih5h seam 1). Omit `expiry` and the leg inherits the ambient `USING exec` tenor — the object * form of leaving `exp` off the text, so the two authoring surfaces (ADR-0004: objects and text are * peers) stay mechanically equivalent. */ export const buy = (qty: number, strike: StrikeSpec, right: "C" | "P", expiry?: ExpirySelector): Leg => ({ kind: "leg", side: "buy", qty, strike, right, ...compact({ expiry }) }) as Leg; export const sell = (qty: number, strike: StrikeSpec, right: "C" | "P", expiry?: ExpirySelector): Leg => ({ kind: "leg", side: "sell", qty, strike, right, ...compact({ expiry }) }) as Leg; /** An equity/spot leg `buy <qty> shares` (ADR-0017) — no strike/right; the symbol is the * ambient `USING exec`. */ export const buyShares = (qty: number): Leg => ({ kind: "equity-leg", side: "buy", qty }); export const sellShares = (qty: number): Leg => ({ kind: "equity-leg", side: "sell", qty }); export const esc = (to: PriceExpr, after: Duration): { kind: "esc-stage"; to: PriceExpr; after: Duration } => ({ kind: "esc-stage", to, after, }); export const policy = (opts: { pricing?: "peg" | "fix"; esc?: readonly ReturnType<typeof esc>[]; caps?: readonly PriceExpr[]; floors?: readonly PriceExpr[]; cancelIf?: Trigger; gtc?: boolean; }): OrderPolicy => ({ kind: "order-policy", ...compact(opts) }) as OrderPolicy; export const foreachHeld: HeldQuantifier = { kind: "held-foreach" }; export const anyHeld: HeldQuantifier = { kind: "held-any" }; // ── Plan clauses ───────────────────────────────────────────────────────────── export const doTicket = (opts: { legs: readonly Leg[]; price: PriceExpr; policy?: OrderPolicy; atomic?: boolean; }): DoTicket => (refuseAtomic(opts.atomic), { kind: "do", ...compact(opts) } as DoTicket); export const also = (opts: { legs: readonly Leg[]; price: PriceExpr; policy?: OrderPolicy; atomic?: boolean; }): PlanClause => (refuseAtomic(opts.atomic), { kind: "also", ...compact(opts) } as PlanClause); export const reload = (opts: { when?: Trigger; legs: readonly Leg[]; price: PriceExpr; policy?: OrderPolicy; atomic?: boolean; }): ReloadClause => (refuseAtomic(opts.atomic), { kind: "reload", ...compact(opts) } as ReloadClause); export const tpPct = (pct: number): TpTarget => ({ kind: "tp-pct", pct }); export const tpMult = (mult: number): TpTarget => ({ kind: "tp-mult", mult }); export const tpPrice = (price: PriceExpr): TpTarget => ({ kind: "tp-price", price }); export const tp = (opts: { target: TpTarget; frac?: number; over?: HeldQuantifier; price?: PriceExpr; policy?: OrderPolicy; }): TpClause => ({ kind: "tp", ...compact(opts) }) as TpClause; export const exit = (opts: { when: Trigger; over?: HeldQuantifier; price?: PriceExpr; policy?: OrderPolicy; }): ExitClause => ( // marks lie (ADR-0005): refuse an EXIT conditioned on a mark at construction, symmetric with // parse/print, so the object never exists to break `parse(print(x))`. refuseExitOnMark(opts.when), { kind: "exit", ...compact(opts) } as ExitClause ); export const invalidate = (when: Trigger): InvalidateClause => ({ kind: "invalidate", when }); export const cancelIf = (when: Trigger): CancelIfClause => ({ kind: "cancel-if", when }); export const arm = (opts: { when?: Trigger; basis?: PriceExpr; over?: HeldQuantifier } = {}): ArmClause => ({ kind: "arm", ...compact(opts) }) as ArmClause; // ── Instruments, USING, provenance ─────────────────────────────────────────── export const dte = (n: number): ExpirySelector => ({ kind: "expiry-dte", dte: n }); export const expDate = (date: string): ExpirySelector => ({ kind: "expiry-date", date }); export const expTag = (tag: string): ExpirySelector => ({ kind: "expiry-tag", tag }); export const instrument = (symbol: string, expiry?: ExpirySelector): Instrument => ({ kind: "instrument", symbol, ...compact({ expiry }) }) as Instrument; export const using = (opts: { signal?: Instrument; exec?: Instrument }): Using => ({ kind: "using", ...compact(opts) }) as Using; export const provenance = (opts: { tier?: Provenance["tier"]; author?: string; origin?: string; replay?: string; }): Provenance => ({ kind: "provenance", ...compact(opts) }) as Provenance; /** A `because` pre-registration citation (kestrel-rtf): a content hash of the fixed shape * `sha256:<64 hex>`, no inline body. Refuses a malformed digest at construction, symmetric with * parse/print (./validate.ts), so the object never exists to break `parse(print(x))`. */ export const citation = (hash: string, algo: Citation["algo"] = "sha256"): Citation => { const c: Citation = { kind: "citation", algo, hash }; refuseBadCitation(c); return c; }; // ── Statement helpers ──────────────────────────────────────────────────────── export const budget = (value: number): Budget => ( refuseNonPositiveBudget(value), { kind: "budget", value, unit: "R" } ); export const ttlIn = (d: Duration): Ttl => ({ kind: "ttl-rel", dur: d }); export const ttlAt = (at: TimeOfDay): Ttl => ({ kind: "ttl-at", at }); export const regime = (...tags: RegimeTagBinding[]): RegimeGate => ({ kind: "regime-gate", tags }); export const tag = (scope: string, value: string): RegimeTagBinding => ({ scope, value }); export const argIdent = (name: string): PaneArg => ({ kind: "arg-ident", name }); export const argWindow = (w: Window): PaneArg => ({ kind: "arg-window", window: w }); export const argCount = (count: number): PaneArg => ({ kind: "arg-count", count }); /** A SessionOrdinal literal `d-<n>` (Train 1B / ADR-0041 §1): `d-0` current session, `d-1` prior… */ export const argOrdinal = (ordinal: number): PaneArg => ({ kind: "arg-ordinal", ordinal }); /** An ExpiryOrdinal literal `e-<n>` (Train 1B / ADR-0041 §1): `e-0` nearest expiry, `e-1` next out… */ export const argExpiry = (expiry: number): PaneArg => ({ kind: "arg-expiry", expiry }); export const pane = (name: string, ...args: PaneArg[]): PaneRef => ({ kind: "pane", name, args }); export const view = ( name: string, panes: readonly PaneRef[], opts: { budget?: number } = {}, ): ViewStatement => ({ kind: "view", name, panes, ...compact(opts) }) as ViewStatement; export const deliver = ( viewName: string, opts: { mandatory?: boolean; keyframe?: boolean } = {}, ): DeliverSpec => ({ kind: "deliver", view: viewName, ...compact(opts) }) as DeliverSpec; export const wakeBudget = (opts: { wakesPerDay?: number; tokensPerDay?: number }): WakeBudget => ({ kind: "wake-budget", ...compact(opts) }) as WakeBudget; export const wake = ( name: string, when: Trigger, opts: { because?: Citation; deliver?: DeliverSpec; priority?: number; coalesce?: string; budget?: WakeBudget; universe?: { kind: "universe"; universe: string }; } = {}, ): WakeStatement => ({ kind: "wake", name, when, ...compact(opts) }) as WakeStatement; export const universe = (name: string): { kind: "universe"; universe: string } => ({ kind: "universe", universe: name, }); export const plan = ( name: string, opts: { budget?: Budget; ttl?: Ttl; regime?: RegimeGate; priority?: number; standing?: Standing; provenance?: Provenance; using?: Using; when?: Trigger; because?: Citation; clauses?: readonly PlanClause[]; atomic?: boolean; } = {}, ): PlanStatement => ( refuseAtomic(opts.atomic), ({ kind: "plan", name, clauses: opts.clauses ?? [], ...compact({ ...opts, clauses: undefined }) }) as PlanStatement ); export const subject = (what: GradeSubject["what"], name: string): GradeSubject => ({ kind: "grade-subject", what, name, }); export const over = (from: string, to: string): { kind: "corpus-range"; from: string; to: string } => ({ kind: "corpus-range", from, to, }); export const vsUngated: Counterfactual = { kind: "cf-ungated" }; export const vsNull: Counterfactual = { kind: "cf-null" }; export const vsBracket: Counterfactual = { kind: "cf-bracket" }; export const bySeries = (s: SeriesRef): GradeDimension => ({ kind: "dim-series", series: s }); export const byVehicle: GradeDimension = { kind: "dim-vehicle" }; export const byName: GradeDimension = { kind: "dim-name" }; export const byLineage: GradeDimension = { kind: "dim-lineage" }; export const grade = ( gradeSubject: GradeSubject, opts: { over?: { kind: "corpus-range"; from: string; to: string }; fill?: string; versus?: readonly Counterfactual[]; by?: readonly GradeDimension[]; } = {}, ): GradeStatement => ({ kind: "grade", subject: gradeSubject, versus: opts.versus ?? [], by: opts.by ?? [], ...compact({ over: opts.over, fill: opts.fill }), }) as GradeStatement; export const coverage = (instruments: readonly Instrument[], thesis: string): Coverage => ({ kind: "coverage", instruments, thesis, }); export const arbitration = (policy: string, concurrency?: number): Arbitration => ({ kind: "arbitration", policy, ...compact({ concurrency }) }) as Arbitration; export const risk = (metric: string, threshold: Quantity, action: string): RiskLine => ({ kind: "risk", metric, threshold, action, }); export const book = ( name: string, opts: { budget?: Budget; coverage?: Coverage; arbitration?: Arbitration; risk?: readonly RiskLine[]; } = {}, ): BookStatement => ({ kind: "book", name, ...compact(opts) }) as BookStatement; export const pod = ( name: string, opts: { risk?: readonly RiskLine[]; children?: readonly (PodStatement | BookStatement)[] } = {}, ): PodStatement => ({ kind: "pod", name, risk: opts.risk ?? [], children: opts.children ?? [], }); export const importFrom = (names: readonly string[], from: string): ImportDecl => ({ kind: "import", names, from, }); export const module = (opts: { imports?: readonly ImportDecl[]; using?: Using; statements?: readonly Statement[]; provenance?: Provenance; }): Module => { const statements = opts.statements ?? []; // Provenance ceiling (ARCHITECTURE §6): refuse a module whose statement elevates above its // declared tier at construction, symmetric with parse/print (./validate.ts). refuseProvenanceElevation(opts.provenance?.tier, statements); return { kind: "module", imports: opts.imports ?? [], statements, ...compact({ using: opts.using, provenance: opts.provenance }), } as Module; };