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
JavaScript
/**
* # 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 }),
};
};