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.

279 lines (278 loc) 15.1 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 { refuseAtomic, refuseBadCitation, refuseExitOnMark, refuseHeldOverCross, refuseNonPositiveBand, refuseNonPositiveBudget, refuseProvenanceElevation, } from "./validate.js"; // 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(o) { const r = {}; for (const [k, v] of Object.entries(o)) if (v !== undefined) r[k] = v; return r; } // ── Lexical units ──────────────────────────────────────────────────────────── export const win = (value, unit) => ({ kind: "window", value, unit }); export const dur = (value, unit) => ({ kind: "duration", value, unit }); export const tod = (hour, minute) => ({ kind: "time-of-day", hour, minute, }); export const num = (value, unit) => ({ kind: "quantity", ...compact({ value, unit }) }); export const p = (n) => ({ kind: "baseline", stat: "p", n }); export const sigma = (n) => ({ 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, opts = {}) { const names = typeof path === "string" ? [path] : path; return { kind: "series", segments: names.map((name) => ({ name })), ...compact({ window: opts.window }), }; } /** An org-aggregate path with a quantified segment: `children(any).drawdown`. */ export const childrenAny = (field) => ({ 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, field = "state") => ({ kind: "series", segments: [{ name: "plan", selector: { kind: "sel-name", name: planName } }, { name: field }], }); // ── Triggers ─────────────────────────────────────────────────────────────── export const cmp = (left, op, right) => ({ kind: "cmp", op, left, right, }); export const gt = (l, r) => cmp(l, "gt", r); export const ge = (l, r) => cmp(l, "ge", r); export const lt = (l, r) => cmp(l, "lt", r); export const le = (l, r) => cmp(l, "le", r); export const eq = (l, r) => cmp(l, "eq", r); export const ne = (l, r) => 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, direction, right, opts = {}) => { refuseNonPositiveBand(opts.band); return { kind: "cross", left, dir: direction, right, ...compact(opts) }; }; /** Refuse to CONSTRUCT held-over-cross (the sfg8 anti-pattern; ./validate.ts). `within` over a * cross stays legal. */ export const held = (inner, d) => { refuseHeldOverCross(inner); return { kind: "break-hold", inner, dur: d }; }; export const within = (inner, d) => ({ 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) => ({ kind: "held-stop", dur: d }); /** `clockET <clock>` — a 0DTE wall-clock time-stop (`EXIT clockET 15:40`). */ export const clockStop = (time) => ({ 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, time) => ({ kind: "until", inner, at: time }); /** `inner at <clock>` — the thesis temporal envelope (kestrel-rtf), a postfix sibling of `within`. */ export const at = (inner, time) => ({ kind: "at", inner, at: time }); export const nth = (ordinal, event) => { // 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, of) => ({ kind: "event", name, ...compact({ of }) }); export const phase = (name) => ({ kind: "phase", phase: name }); export const timeWindow = (opts) => ({ kind: "time-window", ...compact(opts) }); export const fill = (evt, leg) => ({ kind: "fill", event: evt, ...compact({ leg }) }); export const and = (...terms) => ({ kind: "and", terms }); export const or = (...terms) => ({ kind: "or", terms }); export const not = (term) => ({ kind: "not", term }); // ── Prices ─────────────────────────────────────────────────────────────────── export const anchor = (name) => ({ kind: "anchor", name }); export const fair = anchor("fair"); export const intrinsic = anchor("intrinsic"); export const basis = anchor("basis"); export const bid = anchor("bid"); export const ask = anchor("ask"); export const mid = anchor("mid"); export const last = anchor("last"); export const join = anchor("join"); export const improve = anchor("improve"); export const stub = 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("spot"); export const px = (value) => ({ kind: "price-abs", value }); export const minus = (base, amount, unit = "c") => ({ kind: "price-offset", base, sign: "-", amount, unit, }); export const plus = (base, amount, unit = "c") => ({ kind: "price-offset", base, sign: "+", amount, unit, }); export const lean = (a, b, x) => ({ kind: "lean", a, b, x }); export const pmin = (...args) => ({ kind: "price-fn", fn: "min", args }); export const pmax = (...args) => ({ kind: "price-fn", fn: "max", args }); // ── Legs & order policy ────────────────────────────────────────────────────── export const rel = (steps) => ({ kind: "strike-rel", steps }); export const abs = (strike) => ({ kind: "strike-abs", strike }); export const atm = { kind: "strike-atm" }; export const delta = (d) => ({ 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, strike, right, expiry) => ({ kind: "leg", side: "buy", qty, strike, right, ...compact({ expiry }) }); export const sell = (qty, strike, right, expiry) => ({ kind: "leg", side: "sell", qty, strike, right, ...compact({ expiry }) }); /** An equity/spot leg `buy <qty> shares` (ADR-0017) — no strike/right; the symbol is the * ambient `USING exec`. */ export const buyShares = (qty) => ({ kind: "equity-leg", side: "buy", qty }); export const sellShares = (qty) => ({ kind: "equity-leg", side: "sell", qty }); export const esc = (to, after) => ({ kind: "esc-stage", to, after, }); export const policy = (opts) => ({ kind: "order-policy", ...compact(opts) }); export const foreachHeld = { kind: "held-foreach" }; export const anyHeld = { kind: "held-any" }; // ── Plan clauses ───────────────────────────────────────────────────────────── export const doTicket = (opts) => (refuseAtomic(opts.atomic), { kind: "do", ...compact(opts) }); export const also = (opts) => (refuseAtomic(opts.atomic), { kind: "also", ...compact(opts) }); export const reload = (opts) => (refuseAtomic(opts.atomic), { kind: "reload", ...compact(opts) }); export const tpPct = (pct) => ({ kind: "tp-pct", pct }); export const tpMult = (mult) => ({ kind: "tp-mult", mult }); export const tpPrice = (price) => ({ kind: "tp-price", price }); export const tp = (opts) => ({ kind: "tp", ...compact(opts) }); export const exit = (opts) => ( // 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) }); export const invalidate = (when) => ({ kind: "invalidate", when }); export const cancelIf = (when) => ({ kind: "cancel-if", when }); export const arm = (opts = {}) => ({ kind: "arm", ...compact(opts) }); // ── Instruments, USING, provenance ─────────────────────────────────────────── export const dte = (n) => ({ kind: "expiry-dte", dte: n }); export const expDate = (date) => ({ kind: "expiry-date", date }); export const expTag = (tag) => ({ kind: "expiry-tag", tag }); export const instrument = (symbol, expiry) => ({ kind: "instrument", symbol, ...compact({ expiry }) }); export const using = (opts) => ({ kind: "using", ...compact(opts) }); export const provenance = (opts) => ({ kind: "provenance", ...compact(opts) }); /** 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, algo = "sha256") => { const c = { kind: "citation", algo, hash }; refuseBadCitation(c); return c; }; // ── Statement helpers ──────────────────────────────────────────────────────── export const budget = (value) => (refuseNonPositiveBudget(value), { kind: "budget", value, unit: "R" }); export const ttlIn = (d) => ({ kind: "ttl-rel", dur: d }); export const ttlAt = (at) => ({ kind: "ttl-at", at }); export const regime = (...tags) => ({ kind: "regime-gate", tags }); export const tag = (scope, value) => ({ scope, value }); export const argIdent = (name) => ({ kind: "arg-ident", name }); export const argWindow = (w) => ({ kind: "arg-window", window: w }); export const argCount = (count) => ({ 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) => ({ 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) => ({ kind: "arg-expiry", expiry }); export const pane = (name, ...args) => ({ kind: "pane", name, args }); export const view = (name, panes, opts = {}) => ({ kind: "view", name, panes, ...compact(opts) }); export const deliver = (viewName, opts = {}) => ({ kind: "deliver", view: viewName, ...compact(opts) }); export const wakeBudget = (opts) => ({ kind: "wake-budget", ...compact(opts) }); export const wake = (name, when, opts = {}) => ({ kind: "wake", name, when, ...compact(opts) }); export const universe = (name) => ({ kind: "universe", universe: name, }); export const plan = (name, opts = {}) => (refuseAtomic(opts.atomic), ({ kind: "plan", name, clauses: opts.clauses ?? [], ...compact({ ...opts, clauses: undefined }) })); export const subject = (what, name) => ({ kind: "grade-subject", what, name, }); export const over = (from, to) => ({ kind: "corpus-range", from, to, }); export const vsUngated = { kind: "cf-ungated" }; export const vsNull = { kind: "cf-null" }; export const vsBracket = { kind: "cf-bracket" }; export const bySeries = (s) => ({ kind: "dim-series", series: s }); export const byVehicle = { kind: "dim-vehicle" }; export const byName = { kind: "dim-name" }; export const byLineage = { kind: "dim-lineage" }; export const grade = (gradeSubject, opts = {}) => ({ kind: "grade", subject: gradeSubject, versus: opts.versus ?? [], by: opts.by ?? [], ...compact({ over: opts.over, fill: opts.fill }), }); export const coverage = (instruments, thesis) => ({ kind: "coverage", instruments, thesis, }); export const arbitration = (policy, concurrency) => ({ kind: "arbitration", policy, ...compact({ concurrency }) }); export const risk = (metric, threshold, action) => ({ kind: "risk", metric, threshold, action, }); export const book = (name, opts = {}) => ({ kind: "book", name, ...compact(opts) }); export const pod = (name, opts = {}) => ({ kind: "pod", name, risk: opts.risk ?? [], children: opts.children ?? [], }); export const importFrom = (names, from) => ({ kind: "import", names, from, }); export const module = (opts) => { 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 }), }; };