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.

1,547 lines (1,541 loc) 104 kB
import { __export } from "./bin-wckvcay0.js"; // src/protocol/attestation.ts var GRADE_SIGN_PREFIX = "kgrade1"; var ROOT_HASH_PREFIX = "sha256:"; var VERIFY_KEYS_PATH = "/.well-known/kestrel-markets"; var SIGNATURE_COVERED_GRADE_FIELDS = Object.freeze([ "kind", "result", "evView", "supportPartition", "pinnedDataRef", "pinnedProvenanceRef", "pinnedAuthorProvenanceRef", "pinnedFillModelVersion", "judge", "issuedAt", "boundRoots", "counterfactual", "kid", "epoch", "replayable" ]); function canonicalize(value) { if (value === null) return null; if (Array.isArray(value)) return value.map(canonicalize); if (typeof value === "object") { const src = value; const out = {}; for (const key of Object.keys(src).sort()) { const v = src[key]; if (v === undefined) continue; out[key] = canonicalize(v); } return out; } return value; } function canonicalizeJson(value) { return JSON.stringify(canonicalize(value)); } function certifiedPayload(fields) { return { kind: fields.kind, result: fields.result, evView: fields.evView, supportPartition: fields.supportPartition, pinnedDataRef: fields.pinnedDataRef, ...fields.pinnedProvenanceRef !== undefined ? { pinnedProvenanceRef: fields.pinnedProvenanceRef } : {}, ...fields.pinnedAuthorProvenanceRef !== undefined ? { pinnedAuthorProvenanceRef: fields.pinnedAuthorProvenanceRef } : {}, pinnedFillModelVersion: fields.pinnedFillModelVersion, judge: fields.judge, issuedAt: fields.issuedAt, boundRoots: [...fields.boundRoots], ...fields.counterfactual !== undefined ? { counterfactual: fields.counterfactual } : {}, kid: fields.kid, epoch: fields.epoch, replayable: fields.replayable }; } async function sha256Hex(text) { const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); const bytes = new Uint8Array(digest); let hex = ""; for (const byte of bytes) hex += byte.toString(16).padStart(2, "0"); return hex; } async function certifiedRoot(fields) { return sha256Hex(canonicalizeJson(certifiedPayload(fields))); } // src/lang/lex.ts class KestrelParseError extends Error { line; col; name = "KestrelParseError"; constructor(message, line, col) { super(`${message} (line ${line}, col ${col})`); this.line = line; this.col = col; } } var PUNCT2 = ["..", "->", ">=", "<=", "==", "!="]; var PUNCT1 = [">", "<", "+", "-", "%", ":", ",", ".", "(", ")", "{", "}", "@", "/"]; function isDigit(ch) { return ch >= "0" && ch <= "9"; } function isWordStart(ch) { return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_"; } function isWordPart(ch) { return isWordStart(ch) || isDigit(ch); } function tokenizeLine(content, lineNo) { const toks = []; let i = 0; const n = content.length; while (i < n) { const ch = content[i]; if (ch === " " || ch === "\t") { i++; continue; } if (ch === "#") return { tokens: toks, trailing: content.slice(i + 1) }; const col = i + 1; if (ch === '"') { let j = i + 1; let out = ""; while (j < n) { const c = content[j]; if (c === "\\") { const nx = content[j + 1]; if (nx === undefined) { throw new KestrelParseError("unterminated string escape", lineNo, j + 1); } out += nx; j += 2; continue; } if (c === '"') break; out += c; j++; } if (j >= n || content[j] !== '"') { throw new KestrelParseError('unterminated string: expected a closing "', lineNo, col); } j++; toks.push({ type: "string", text: out, line: lineNo, col, end: j + 1 }); i = j; continue; } if (isDigit(ch)) { let j = i + 1; while (j < n && isDigit(content[j])) j++; if (content[j] === "." && j + 1 < n && isDigit(content[j + 1])) { j += 2; while (j < n && isDigit(content[j])) j++; } toks.push({ type: "number", text: content.slice(i, j), line: lineNo, col, end: j + 1 }); i = j; continue; } if (isWordStart(ch)) { let j = i + 1; while (j < n) { const c = content[j]; if (isWordPart(c)) { j++; continue; } if (c === "-" && j + 1 < n && isWordStart(content[j + 1])) { j += 2; continue; } break; } toks.push({ type: "word", text: content.slice(i, j), line: lineNo, col, end: j + 1 }); i = j; continue; } const two = content.slice(i, i + 2); if (PUNCT2.includes(two)) { toks.push({ type: "punct", text: two, line: lineNo, col, end: i + 3 }); i += 2; continue; } if (PUNCT1.includes(ch)) { toks.push({ type: "punct", text: ch, line: lineNo, col, end: i + 2 }); i++; continue; } throw new KestrelParseError(`unexpected character ${JSON.stringify(ch)}`, lineNo, col); } return { tokens: toks }; } function leadingIndent(content, lineNo) { let i = 0; while (i < content.length && content[i] === " ") i++; if (content[i] === "\t") { throw new KestrelParseError("tab in indentation: Kestrel indents with spaces", lineNo, i + 1); } return i; } function lex(src) { const physical = src.split(` `); const roots = []; const stack = []; let pending = []; const moduleTail = []; const attachTail = (c) => { for (let s = stack.length - 1;s >= 0; s--) { const node = stack[s]; if (node.indent < c.indent) { node.tail = [...node.tail ?? [], c.text]; return; } } moduleTail.push(c.text); }; for (let p = 0;p < physical.length; p++) { const content = physical[p]; const lineNo = p + 1; const trimmed = content.trim(); if (trimmed === "") continue; if (trimmed.startsWith("#")) { const indent2 = leadingIndent(content, lineNo); pending.push({ indent: indent2, text: content.slice(content.indexOf("#") + 1) }); continue; } const indent = leadingIndent(content, lineNo); const { tokens, trailing } = tokenizeLine(content, lineNo); if (tokens.length === 0) continue; const leading = []; for (const c of pending) { if (c.indent > indent) attachTail(c); else leading.push(c.text); } pending = []; const node = { indent, line: lineNo, col: tokens[0].col, tokens, children: [] }; if (leading.length > 0) node.leading = leading; if (trailing !== undefined) node.trailing = trailing; while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop(); if (stack.length === 0) roots.push(node); else stack[stack.length - 1].children.push(node); stack.push(node); } for (const c of pending) attachTail(c); return { roots, tail: moduleTail }; } function flatten(node) { const out = [...node.tokens]; for (const child of node.children) out.push(...flatten(child)); return out; } // src/lang/vocab.ts function keysOf(table) { return Object.keys(table); } var TIME_UNIT_TABLE = { s: true, m: true, h: true, d: true, w: true, mo: true, q: true, y: true }; var TIME_UNITS_LIST = keysOf(TIME_UNIT_TABLE); var TIME_UNITS = new Set(TIME_UNITS_LIST); var ORDINAL_PREFIX = "d"; var EXPIRY_PREFIX = "e"; var ANCHOR_TABLE = { fair: true, intrinsic: true, basis: true, bid: true, ask: true, mid: true, last: true, join: true, improve: true, stub: true, spot: true }; var ANCHOR_NAMES = keysOf(ANCHOR_TABLE); var ANCHORS = new Set(ANCHOR_NAMES); var TIME_STOP_TABLE = { "held-stop": "held", "clock-stop": "clockET" }; var HELD_STOP_KEYWORD = TIME_STOP_TABLE["held-stop"]; var CLOCK_STOP_KEYWORD = TIME_STOP_TABLE["clock-stop"]; var FILL_EVENT_TABLE = { filled: true, "partial-fill": true, unfilled: true, rejected: true, cancelled: true }; var FILL_EVENTS = new Set(keysOf(FILL_EVENT_TABLE)); var STANDING_TABLE = { authored: true, armed: true, versioned: true, superseded: true }; var STANDINGS = new Set(keysOf(STANDING_TABLE)); var GRADE_WHAT_TABLE = { plan: true, wake: true, view: true, pod: true, tag: true }; var GRADE_WHATS = new Set(keysOf(GRADE_WHAT_TABLE)); var CITATION_ALGO_TABLE = { sha256: true }; var CITATION_ALGOS = new Set(keysOf(CITATION_ALGO_TABLE)); var PROV_RANK = { unvetted: 0, candidate: 1, vetted: 2 }; var PROV_TIERS = new Set(keysOf(PROV_RANK)); var CMP_TO_PUNCT = { gt: ">", ge: ">=", lt: "<", le: "<=", eq: "==", ne: "!=" }; var CMP_FROM_PUNCT = Object.fromEntries(Object.entries(CMP_TO_PUNCT).map(([op, punct]) => [punct, op])); var ORDINALS = [ "zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth" ]; // src/lang/validate.ts function assertNever(x, ctx) { throw new Error(`kestrel/validate: unhandled variant in ${ctx}: ${JSON.stringify(x)}`); } var ATOMIC_MESSAGE = "the `atomic` keyword is reserved and refused in v1: an atomic multi-leg structure " + "needs a whole-structure preflight and an atomic execution adapter, which do not exist " + "yet (ADR-0005). Kestrel refuses rather than silently leg a named structure sequentially " + "(text that parses but lies). Author the legs as separate single-leg tickets, or wait for " + "atomic support"; var ATOMIC_RESERVED = "atomic is reserved: refused until whole-structure preflight + atomic execution adapter exist"; function refuseAtomic(atomic) { if (atomic === true) throw new Error(ATOMIC_RESERVED); } var HELD_CROSS_MESSAGE = "a cross cannot be `held`: a cross is an edge event, not a level, so sustaining it for a " + "duration is the anti-pattern that phantom-fired 10/12 in graded sim on a single anomalous " + "print (xrearm-1). To reject a lone spurious tick, give the cross a re-arm band " + "(`spot crosses above hod band 5c`); to require persistence, hold a level comparison instead " + "(`spot > hod held 30s`). `within` over a cross stays legal (the cross occurred inside a window)"; var HELD_CROSS_RESERVED = "a cross cannot be held: a cross is an edge event, not a level — use a re-arm band instead of holding the edge (xrearm-1)"; function refuseHeldOverCross(inner) { if (inner.kind === "cross") throw new Error(HELD_CROSS_RESERVED); } var BAND_POSITIVE_RESERVED = "a cross re-arm band must be a positive width: a zero/negative band can never re-arm — the cross would never clear its band to fire again (xrearm-1)"; function bandMessage(widthText) { return `a cross re-arm band must be a positive width (got ${widthText}); a zero/negative ` + `band cannot re-arm — the cross would never clear its band to fire again`; } function refuseNonPositiveBand(band) { if (band !== undefined && !(band.value > 0)) throw new Error(BAND_POSITIVE_RESERVED); } function budgetMessage(value) { return `a risk budget must be a positive risk fraction (got ${value}R); \`size × max_loss ≤ budget\` requires budget > 0`; } function refuseNonPositiveBudget(value) { if (!(value > 0)) throw new Error(budgetMessage(value)); } var MARK_ANCHORS = new Set(["mid", "bid", "ask", "last", "mark", "premium"]); function isSimpleSeries(op) { return op.kind === "series" && op.segments.length === 1 && op.segments[0].selector === undefined && op.window === undefined; } function markOperand(op) { return isSimpleSeries(op) && MARK_ANCHORS.has(op.segments[0].name) ? op.segments[0].name : undefined; } function findMarkInTrigger(t) { switch (t.kind) { case "cmp": case "cross": return markOperand(t.left) ?? markOperand(t.right); case "event": return t.of === undefined ? undefined : markOperand(t.of); case "break-hold": case "within": case "until": case "at": return findMarkInTrigger(t.inner); case "nth": return findMarkInTrigger(t.event); case "not": return findMarkInTrigger(t.term); case "and": case "or": for (const term of t.terms) { const m = findMarkInTrigger(term); if (m !== undefined) return m; } return; case "phase": case "time-window": case "fill": case "held-stop": case "clock-stop": return; default: return assertNever(t, "findMarkInTrigger"); } } function markMessage(mark) { return `EXIT may not condition on the mark \`${mark}\`: marks lie (the observed ${mark} is a ` + `health signal, never a value; ADR-0005). Condition the exit on \`fair\`, \`spot\`, a ` + `structural level, or an org fact instead`; } function refuseExitOnMark(when) { const mark = findMarkInTrigger(when); if (mark !== undefined) throw new Error(markMessage(mark)); } function provenanceMessage(planTier, moduleTier) { return `provenance may only narrow downward (ARCHITECTURE §6): a \`${planTier}\` plan cannot ` + `sit under a \`${moduleTier}\` module — authority narrows, it never elevates`; } function elevatesProvenance(s, moduleRank) { if (s.kind === "plan" && s.provenance?.tier !== undefined && PROV_RANK[s.provenance.tier] > moduleRank) { return s.provenance.tier; } return; } function refuseProvenanceElevation(moduleTier, statements) { if (moduleTier === undefined) return; const modRank = PROV_RANK[moduleTier]; for (const s of statements) { const bad = elevatesProvenance(s, modRank); if (bad !== undefined) throw new Error(provenanceMessage(bad, moduleTier)); } } var CITATION_HASH_HEX = /^[0-9a-f]{64}$/; function isCitationHash(hash) { return CITATION_HASH_HEX.test(hash); } function citationBadHashMessage(got) { return `a \`because\` citation needs a content hash of the fixed shape \`sha256:<64 hex>\` ` + `(got \`${got}\`): a Thesis digest is 64 lowercase hex chars`; } var CITATION_INLINE_BODY_MESSAGE = "a `because` citation carries a content hash only — no inline body: the Thesis prose lives " + "platform-side (referenced by `sha256:<64 hex>`), so drop the inline text after the digest"; function citationOnMessage(kind) { return `a \`because\` citation may only pre-register a Plan or Wake, not a ${kind}: pre-registration ` + `binds an executable expectation to its thesis, which a ${kind} does not author`; } var CITATION_BAD_HASH_RESERVED = "a `because` citation needs a content hash of the fixed shape `sha256:<64 hex>`: a Thesis digest is 64 lowercase hex chars"; function refuseBadCitation(c) { if (c.algo !== "sha256" || !isCitationHash(c.hash)) throw new Error(CITATION_BAD_HASH_RESERVED); } // src/lang/parse.ts var CLAUSE_KEYWORDS = "WHEN, USING, DO, ALSO, RELOAD, TP, EXIT, INVALIDATE, CANCEL-IF, or ARM"; var PLAN_CLAUSE_KEYWORD_SET = new Set([ "WHEN", "USING", "DO", "ALSO", "RELOAD", "TP", "EXIT", "INVALIDATE", "CANCEL-IF", "ARM" ]); var TRIGGER_VERB_SYNONYMS = new Set([ "breaks", "break", "dips", "dip", "rises", "rise", "falls", "fall", "moves", "move", "drops", "drop", "climbs", "climb", "jumps", "jump", "hits", "hit", "reaches", "reach", "exceeds", "exceed" ]); function triggerVerbSynonymMessage(verb) { return `unexpected \`${verb}\` in a trigger — Kestrel has no \`${verb}\` trigger verb. ` + `Write a threshold event as \`crosses above|below\` (a strict crossing) or ` + `\`touches above|below\` (at-or-touch), e.g. \`spot crosses below 499.15\` — ` + `or a plain comparison with \`<\`/\`>\`, e.g. \`spot < 499.15\`.`; } var PRICE_ANCHOR_SYNONYMS = new Set(["ceiling", "floor", "limit"]); function priceAnchorSynonymMessage(tok) { return `unexpected \`${tok}\` — Kestrel has no \`${tok}\` price anchor. ` + `To bound a price use \`min(...)\` to cap it or \`max(...)\` to floor it, ` + `or \`lean(a,b,x)\` to lean a fraction between two anchors — ` + `e.g. \`min(fair, 0.95)\` caps the price at 0.95.`; } function describe(t) { if (t === undefined) return "end of line"; if (t.type === "string") return "a string"; if (t.type === "number") return `number ${t.text}`; return `\`${t.text}\``; } function lineCommentOf(node, at) { const leading = node.leading; const trailing = node.trailing; const hasLeading = leading !== undefined && leading.length > 0; if (!hasLeading && trailing === undefined) return; return { at, ...hasLeading ? { leading } : {}, ...trailing !== undefined ? { trailing } : {} }; } function commentLayer(header, interior) { const lines = []; const h = lineCommentOf(header, 0); if (h !== undefined) lines.push(h); interior.forEach((n, i) => { const lc = lineCommentOf(n, i + 1); if (lc !== undefined) lines.push(lc); }); const tail = header.tail; const hasTail = tail !== undefined && tail.length > 0; if (lines.length === 0 && !hasTail) return; return { ...lines.length > 0 ? { lines } : {}, ...hasTail ? { tail } : {} }; } function withComments(layer) { return layer === undefined ? {} : { comments: layer }; } class Cursor { toks; fallbackLine; i = 0; constructor(toks, fallbackLine) { this.toks = toks; this.fallbackLine = fallbackLine; } peek(k = 0) { return this.toks[this.i + k]; } wordAt(k = 0) { const t = this.toks[this.i + k]; return t !== undefined && t.type === "word" ? t.text : undefined; } atEnd() { return this.i >= this.toks.length; } here() { const t = this.toks[this.i]; if (t !== undefined) return { line: t.line, col: t.col }; const last = this.toks[this.toks.length - 1]; return last !== undefined ? { line: last.line, col: last.end } : { line: this.fallbackLine, col: 1 }; } fail(message) { const { line, col } = this.here(); throw new KestrelParseError(message, line, col); } advance() { const t = this.toks[this.i]; if (t === undefined) this.fail("unexpected end of line"); this.i++; return t; } matchWord(text) { return this.wordAt() === text; } optWord(text) { if (this.matchWord(text)) { this.i++; return true; } return false; } expectWord(text, ctx) { if (!this.matchWord(text)) this.fail(`expected \`${text}\` ${ctx}, got ${describe(this.peek())}`); this.i++; } matchPunct(text) { const t = this.peek(); return t !== undefined && t.type === "punct" && t.text === text; } optPunct(text) { if (this.matchPunct(text)) { this.i++; return true; } return false; } expectPunct(text, ctx) { if (!this.matchPunct(text)) this.fail(`expected \`${text}\` ${ctx}, got ${describe(this.peek())}`); this.i++; } expectString(ctx) { const t = this.peek(); if (t === undefined || t.type !== "string") this.fail(`expected a quoted string ${ctx}, got ${describe(t)}`); this.i++; return t.text; } expectNumber(ctx) { const t = this.peek(); if (t === undefined || t.type !== "number") this.fail(`expected a number ${ctx}, got ${describe(t)}`); this.i++; return Number(t.text); } expectEnd(ctx) { if (!this.atEnd()) this.fail(`unexpected ${describe(this.peek())} after ${ctx}`); } readName(what) { const first = this.peek(); if (first === undefined || first.type !== "word") this.fail(`expected ${what}, got ${describe(first)}`); let text = first.text; let end = first.end; this.i++; for (;; ) { const t = this.peek(); if (t === undefined || t.col !== end) break; if (t.type === "punct" && (t.text === "-" || t.text === "_")) { const after = this.peek(1); if (after !== undefined && after.col === t.end && (after.type === "word" || after.type === "number")) { text += t.text + after.text; end = after.end; this.i += 2; continue; } break; } if (t.type === "word" || t.type === "number") { text += t.text; end = t.end; this.i++; continue; } break; } return text; } } function expectClauseEnd(c, clauseCtx) { const t = c.peek(); if (t !== undefined && t.type === "word" && PLAN_CLAUSE_KEYWORD_SET.has(t.text)) { c.fail(`unexpected \`${t.text}\` after ${clauseCtx} — this looks like two clauses on one line. ` + `Each PLAN clause goes on its OWN line, indented two spaces beneath the \`PLAN <name>\` header: ` + `write \`WHEN …\` and \`DO …\` as SEPARATE lines, never \`WHEN … DO …\` together.`); } c.expectEnd(clauseCtx); } function readTimeUnit(c, ctx) { const units = TIME_UNITS_LIST.join(", "); const u = c.readName(`a time unit (${units}) ${ctx}`); if (!TIME_UNITS.has(u)) c.fail(`unknown time unit \`${u}\`; expected one of ${units}`); return u; } function readWindow(c) { const value = c.expectNumber("for a window size"); const unit = readTimeUnit(c, "for a window"); return { kind: "window", value, unit }; } function readDuration(c, ctx) { const value = c.expectNumber(`for a duration ${ctx}`); const unit = readTimeUnit(c, ctx); return { kind: "duration", value, unit }; } function readTimeOfDay(c) { const hour = c.expectNumber("for the hour of a clock time"); c.expectPunct(":", "in a clock time (HH:MM)"); const minute = c.expectNumber("for the minute of a clock time"); if (!Number.isInteger(hour) || hour < 0 || hour > 23) c.fail(`bad clock hour ${hour}; expected 0..23`); if (!Number.isInteger(minute) || minute < 0 || minute > 59) c.fail(`bad clock minute ${minute}; expected 0..59`); return { kind: "time-of-day", hour, minute }; } function readQuantity(c, ctx) { let sign = 1; if (c.matchPunct("+")) c.advance(); else if (c.matchPunct("-")) { c.advance(); sign = -1; } const value = sign * c.expectNumber(ctx); const unit = readOptUnit(c); return unit === undefined ? { kind: "quantity", value } : { kind: "quantity", value, unit }; } function readBand(c) { const q = readQuantity(c, "for a cross re-arm band width (`band 5c`)"); if (!(q.value > 0)) c.fail(bandMessage(quantityText(q))); return q; } function quantityText(q) { return q.unit === undefined ? String(q.value) : `${q.value}${q.unit}`; } function readOptUnit(c) { if (c.matchPunct("%")) { c.advance(); return "%"; } const w = c.wordAt(); if (w === "R" || w === "c" || w === "bp") { c.advance(); return w; } return; } function parseSeries(c) { const segments = []; for (;; ) { const name = c.readName("a series name"); if (c.matchPunct("(") && c.peek(1)?.type === "word") { c.advance(); const q = c.wordAt(); let selector; if (q === "any" || q === "all") { c.advance(); selector = { kind: "sel-quant", q }; } else { selector = { kind: "sel-name", name: c.readName("a selector name inside `(...)`") }; } c.expectPunct(")", "to close a series selector"); segments.push({ name, selector }); } else { segments.push({ name }); } if (c.optPunct(".")) continue; break; } let window; if (c.matchPunct("(") && c.peek(1)?.type === "number") { c.advance(); window = readWindow(c); c.expectPunct(")", "to close a series window"); } return window === undefined ? { kind: "series", segments } : { kind: "series", segments, window }; } function parseOperand(c) { const t = c.peek(); if (t !== undefined && (t.type === "number" || t.type === "punct" && (t.text === "+" || t.text === "-"))) { let sign = 1; if (c.matchPunct("+")) c.advance(); else if (c.matchPunct("-")) { c.advance(); sign = -1; } const value = c.expectNumber("for an operand"); if (c.matchWord("sigma")) { c.advance(); return { kind: "baseline", stat: "sigma", n: value }; } const unit = readOptUnit(c); return unit === undefined ? { kind: "quantity", value: sign * value } : { kind: "quantity", value: sign * value, unit }; } const w = c.wordAt(); if (w !== undefined && /^p\d+$/.test(w)) { c.advance(); return { kind: "baseline", stat: "p", n: Number(w.slice(1)) }; } if (w !== undefined) return parseSeries(c); c.fail(`expected an operand (a series name, a number, or a baseline like p99/3sigma), got ${describe(t)}`); } function parseTrigger(c) { return parseOr(c); } function parseOr(c) { const terms = [parseAnd(c)]; while (c.optWord("OR")) terms.push(parseAnd(c)); return terms.length === 1 ? terms[0] : { kind: "or", terms }; } function parseAnd(c) { const terms = [parseNot(c)]; while (c.optWord("AND")) terms.push(parseNot(c)); return terms.length === 1 ? terms[0] : { kind: "and", terms }; } function parseNot(c) { if (c.optWord("NOT")) return { kind: "not", term: parseNot(c) }; return parsePostfix(c); } function parsePostfix(c) { let inner = parseAtom(c); for (;; ) { if (c.matchWord("held")) { if (inner.kind === "cross") c.fail(HELD_CROSS_MESSAGE); c.advance(); inner = { kind: "break-hold", inner, dur: readDuration(c, "after `held`") }; } else if (c.matchWord("within")) { c.advance(); inner = { kind: "within", inner, dur: readDuration(c, "after `within`") }; } else if (c.matchWord("until")) { c.advance(); inner = { kind: "until", inner, at: readTimeOfDay(c) }; } else if (c.matchWord("at")) { c.advance(); inner = { kind: "at", inner, at: readTimeOfDay(c) }; } else break; } return inner; } function ordinalValue(c) { const w = c.wordAt(); if (w !== undefined) { const idx = ORDINALS.indexOf(w); if (idx >= 1) return idx; } const t = c.peek(); if (t !== undefined && t.type === "number" && c.wordAt(1) === "th") return Number(t.text); return; } function parseAtom(c) { if (c.optPunct("(")) { const t = parseTrigger(c); c.expectPunct(")", "to close a parenthesized trigger"); return t; } const ord = ordinalValue(c); if (ord !== undefined) { if (c.wordAt() !== undefined) c.advance(); else { c.advance(); c.advance(); } return { kind: "nth", ordinal: ord, event: parseAtom(c) }; } if (c.matchWord("phase")) { c.advance(); return { kind: "phase", phase: c.readName("a phase name after `phase`") }; } if (c.matchWord("time")) { c.advance(); return parseTimeWindow(c); } if (c.matchWord(HELD_STOP_KEYWORD) && c.peek(1)?.type === "number") { c.advance(); return { kind: "held-stop", dur: readDuration(c, "after `held` (a 0DTE time-held stop, `held 90m`)") }; } if (c.matchWord(CLOCK_STOP_KEYWORD)) { c.advance(); return { kind: "clock-stop", at: readTimeOfDay(c) }; } const w = c.wordAt(); if (w !== undefined && FILL_EVENTS.has(w)) { c.advance(); const event = w; if (c.optWord("leg")) { const leg = c.expectNumber("for a fill-event leg index"); return { kind: "fill", event, leg }; } return { kind: "fill", event }; } return parseOperandLed(c); } function parseTimeWindow(c) { if (c.optWord("after")) return { kind: "time-window", from: readTimeOfDay(c) }; if (c.optWord("before")) return { kind: "time-window", to: readTimeOfDay(c) }; const from = readTimeOfDay(c); c.expectPunct("..", "in a time window (`time HH:MM..HH:MM`)"); const to = readTimeOfDay(c); return { kind: "time-window", from, to }; } function parseOperandLed(c) { const left = parseOperand(c); if (c.matchWord("crosses") || c.matchWord("touches")) { const touch = c.advance().text === "touches"; const kw = touch ? "touches" : "crosses"; if (c.optWord("outside")) { const lo = parseOperand(c); c.expectPunct("-", "between the low and high of a breakout range (`crosses outside LO-HI`)"); const hi = parseOperand(c); const touchOpt = touch ? { touch: true } : {}; const above = { kind: "cross", left, dir: "above", right: hi, ...touchOpt }; const below = { kind: "cross", left, dir: "below", right: lo, ...touchOpt }; return { kind: "or", terms: [above, below] }; } let dir; if (c.optWord("above")) dir = "above"; else if (c.optWord("below")) dir = "below"; else c.fail(`expected \`above\`, \`below\`, or \`outside LO-HI\` (a breakout) after \`${kw}\`, got ${describe(c.peek())}`); const right = parseOperand(c); let band; if (c.optWord("band")) band = readBand(c); return { kind: "cross", left, dir, right, ...touch ? { touch } : {}, ...band !== undefined ? { band } : {} }; } const opTok = c.peek(); if (opTok !== undefined && opTok.type === "punct" && opTok.text in CMP_FROM_PUNCT) { c.advance(); const op = CMP_FROM_PUNCT[opTok.text]; return { kind: "cmp", op, left, right: parseOperand(c) }; } const infix = c.wordAt(); if (infix !== undefined && TRIGGER_VERB_SYNONYMS.has(infix)) c.fail(triggerVerbSynonymMessage(infix)); if (isSimpleSeries(left)) { const name = left.segments[0].name; if (c.optWord("of")) return { kind: "event", name, of: parseOperand(c) }; return { kind: "event", name }; } c.fail(`expected a comparison (>, >=, <, <=, ==, !=), \`crosses above/below\`, or \`of\` ` + `after this operand, got ${describe(c.peek())}`); } function parsePriceAtom(c) { const t = c.peek(); if (t !== undefined && t.type === "word") { if (ANCHORS.has(t.text)) { c.advance(); return { kind: "anchor", name: t.text }; } if (t.text === "min" || t.text === "max") { c.advance(); c.expectPunct("(", `after \`${t.text}\``); const args = [parsePrice(c)]; while (c.optPunct(",")) args.push(parsePrice(c)); c.expectPunct(")", `to close \`${t.text}(...)\``); return { kind: "price-fn", fn: t.text, args }; } if (t.text === "cap") { c.advance(); const args = [parsePrice(c)]; while (c.optPunct(",")) args.push(parsePrice(c)); if (args.length < 2) { c.fail("`cap` needs a ceiling — write `cap <price>, <ceiling>` (it normalizes to " + "`min(<price>, <ceiling>)`), e.g. `cap fair, 0.95`"); } return { kind: "price-fn", fn: "min", args }; } if (t.text === "lean") { c.advance(); c.expectPunct("(", "after `lean`"); const a = parsePrice(c); c.expectPunct(",", "between lean arguments"); const b = parsePrice(c); c.expectPunct(",", "between lean arguments"); const x = c.expectNumber("for the lean fraction"); c.expectPunct(")", "to close `lean(...)`"); return { kind: "lean", a, b, x }; } } if (t !== undefined && t.type === "number") { c.advance(); return { kind: "price-abs", value: Number(t.text) }; } if (t !== undefined && t.type === "word" && PRICE_ANCHOR_SYNONYMS.has(t.text)) { c.fail(priceAnchorSynonymMessage(t.text)); } c.fail(`expected a price: an anchor (${ANCHOR_NAMES.join(", ")}), a number, or min/max/lean(...), ` + `got ${describe(t)}`); } function parsePrice(c) { let base = parsePriceAtom(c); for (;; ) { const t = c.peek(); const next = c.peek(1); if (t !== undefined && t.type === "punct" && (t.text === "+" || t.text === "-") && next?.type === "number") { c.advance(); const amount = c.expectNumber("for a price offset"); let unit; if (c.optPunct("%")) unit = "%"; else if (c.matchWord("c")) { c.advance(); unit = "c"; } else { c.fail(`expected \`c\` or \`%\` after a price offset amount, got ${describe(c.peek())}`); } base = { kind: "price-offset", base, sign: t.text, amount, unit }; continue; } break; } return base; } function parseStrike(c) { if (c.optWord("atm")) return { kind: "strike-atm" }; if (c.matchPunct("+") || c.matchPunct("-")) { const sign = c.advance().text === "-" ? -1 : 1; return { kind: "strike-rel", steps: sign * c.expectNumber("for a relative strike") }; } const t = c.peek(); if (t !== undefined && t.type === "number") { c.advance(); if (c.matchWord("d")) { c.advance(); return { kind: "strike-delta", delta: Number(t.text) }; } return { kind: "strike-abs", strike: Number(t.text) }; } c.fail(`expected a strike (+N/-N relative, N absolute, atm, or Nd delta) for an option leg, ` + `or \`shares\` for an equity leg (\`buy N shares\`, ADR-0017), got ${describe(t)}`); } function parseLeg(c) { let side; if (c.optWord("buy")) side = "buy"; else if (c.optWord("sell")) side = "sell"; else c.fail(`expected \`buy\` or \`sell\` to start a leg, got ${describe(c.peek())}`); const qty = c.expectNumber("for a leg quantity"); if (c.optWord("shares")) { const t = c.peek(); const w = c.wordAt(); if (t !== undefined && t.type === "number" || w === "C" || w === "P" || w === "atm" || c.matchPunct("+") || c.matchPunct("-")) { c.fail(`an equity/spot leg carries no strike or right — \`${qty} shares\` is complete (ADR-0017); drop the strike/right`); } return { kind: "equity-leg", side, qty }; } const strike = parseStrike(c); let right; if (c.optWord("C")) right = "C"; else if (c.optWord("P")) right = "P"; else c.fail(`expected \`C\` or \`P\` (option right) after a strike, got ${describe(c.peek())}`); let expiry; if (c.optWord("exp")) { const t = c.peek(); if (t === undefined || t.type === "punct") { c.fail(`\`exp\` needs an expiry selector after it — \`0dte\`, a date (\`2026-07-17\`), or a tag (\`weekly\`) — got ${describe(t)}`); } expiry = parseExpiry(c); if (expiry === undefined) c.fail("`exp` needs an expiry selector after it (`0dte`, a date like `2026-07-17`, or a tag like `weekly`)"); } return { kind: "leg", side, qty, strike, right, ...expiry !== undefined ? { expiry } : {} }; } function parseLegs(c) { const legs = [parseLeg(c)]; while (c.optPunct(",")) legs.push(parseLeg(c)); return legs; } function parseHeldQuantifier(c) { if (c.matchWord("foreach")) { c.advance(); c.expectWord("held", "in a held-leg quantifier (`foreach held leg`)"); c.expectWord("leg", "in a held-leg quantifier (`foreach held leg`)"); return { kind: "held-foreach" }; } if (c.matchWord("any")) { c.advance(); c.expectWord("held", "in a held-leg quantifier (`any held leg`)"); c.expectWord("leg", "in a held-leg quantifier (`any held leg`)"); return { kind: "held-any" }; } if (c.wordAt() === "held" && c.wordAt(1) === "leg") { c.fail("a held-leg quantifier needs a `foreach` or `any` head — `held leg` alone is not a quantifier. " + "Did you mean `foreach held leg` (every held leg) or `any held leg`?"); } return; } function parseOrderPolicy(c) { let pricing; const esc = []; const caps = []; const floors = []; let cancelIf; let gtc = false; for (;; ) { if (c.matchWord("peg") || c.matchWord("fix")) { if (pricing !== undefined) c.fail("a ticket may declare `peg` or `fix` only once"); pricing = c.advance().text; } else if (c.optWord("esc")) { const to = parsePrice(c); esc.push({ kind: "esc-stage", to, after: readDuration(c, "for an esc stage") }); } else if (c.optWord("cap")) { caps.push(parsePrice(c)); } else if (c.optWord("floor")) { floors.push(parsePrice(c)); } else if (c.optWord("cancel-if")) { cancelIf = parseTrigger(c); } else if (c.optWord("gtc")) { gtc = true; } else break; } if (pricing === undefined && esc.length === 0 && caps.length === 0 && floors.length === 0 && cancelIf === undefined && !gtc) { return; } return { kind: "order-policy", ...pricing !== undefined ? { pricing } : {}, ...esc.length > 0 ? { esc } : {}, ...caps.length > 0 ? { caps } : {}, ...floors.length > 0 ? { floors } : {}, ...cancelIf !== undefined ? { cancelIf } : {}, ...gtc ? { gtc } : {} }; } function rejectAtomicIfPresent(c) { if (c.matchWord("atomic")) c.fail(ATOMIC_MESSAGE); } function parseTpTarget(c) { if (c.matchPunct("+") || c.matchPunct("-")) { const sign = c.advance().text === "-" ? -1 : 1; const pct = sign * c.expectNumber("for a TP percentage"); c.expectPunct("%", "after a TP percentage (`+100%`)"); return { kind: "tp-pct", pct }; } const t = c.peek(); if (t !== undefined && t.type === "number") { if (c.peek(1)?.type === "punct" && c.peek(1).text === "%") { c.advance(); c.advance(); return { kind: "tp-pct", pct: Number(t.text) }; } if (c.wordAt(1) === "x") { c.advance(); c.advance(); return { kind: "tp-mult", mult: Number(t.text) }; } } return { kind: "tp-price", price: parsePrice(c) }; } function parsePlanClause(c) { const kw = c.readName(`a clause keyword (${CLAUSE_KEYWORDS})`); switch (kw) { case "DO": case "ALSO": { rejectAtomicIfPresent(c); const legs = parseLegs(c); c.expectPunct("@", "before the ticket price"); const price = parsePrice(c); const policy = parseOrderPolicy(c); expectClauseEnd(c, `the ${kw} ticket`); const base = { legs, price, ...policy !== undefined ? { policy } : {} }; return kw === "DO" ? { kind: "do", ...base } : { kind: "also", ...base }; } case "RELOAD": { let when; if (c.optWord("WHEN")) when = parseTrigger(c); rejectAtomicIfPresent(c); const legs = parseLegs(c); c.expectPunct("@", "before the RELOAD price"); const price = parsePrice(c); const policy = parseOrderPolicy(c); expectClauseEnd(c, "the RELOAD ticket"); return { kind: "reload", ...when !== undefined ? { when } : {}, legs, price, ...policy !== undefined ? { policy } : {} }; } case "TP": { const target = parseTpTarget(c); let frac; if (c.optWord("frac")) frac = c.expectNumber("for a TP fraction"); const over = parseHeldQuantifier(c); let price; if (c.optPunct("@")) price = parsePrice(c); const policy = parseOrderPolicy(c); expectClauseEnd(c, "the TP clause"); return { kind: "tp", target, ...frac !== undefined ? { frac } : {}, ...over !== undefined ? { over } : {}, ...price !== undefined ? { price } : {}, ...policy !== undefined ? { policy } : {} }; } case "EXIT": { const when = parseTrigger(c); const mark = findMarkInTrigger(when); if (mark !== undefined) c.fail(markMessage(mark)); const over = parseHeldQuantifier(c); let price; if (c.optPunct("@")) price = parsePrice(c); const policy = parseOrderPolicy(c); expectClauseEnd(c, "the EXIT clause"); return { kind: "exit", when, ...over !== undefined ? { over } : {}, ...price !== undefined ? { price } : {}, ...policy !== undefined ? { policy } : {} }; } case "INVALIDATE": { const when = parseTrigger(c); expectClauseEnd(c, "the INVALIDATE clause"); return { kind: "invalidate", when }; } case "CANCEL-IF": { const when = parseTrigger(c); expectClauseEnd(c, "the CANCEL-IF clause"); return { kind: "cancel-if", when }; } case "ARM": { let when; if (c.optWord("WHEN")) when = parseTrigger(c); let basis; if (c.optWord("basis")) basis = parsePrice(c); const over = parseHeldQuantifier(c); if (when === undefined && over === undefined) { c.fail(basis === undefined ? "bare `ARM` binds nothing — an ARM clause must adopt a held leg or chain a plan. " + "Did you mean `ARM foreach held leg` (adopt a superseded plan's held leg — the exit " + "escape hatch) or `ARM WHEN <trigger>` (chain plans on another's state)?" : "`ARM basis <px>` carries an adoption basis but binds no leg — a basis only anchors an " + "ADOPTED held leg (RUNTIME §4). Did you mean `ARM basis <px> foreach held leg` (adopt " + "the superseded plan's held leg at that basis)?"); } expectClauseEnd(c, "the ARM clause"); return { kind: "arm", ...when !== undefined ? { when } : {}, ...basis !== undefined ? { basis } : {}, ...over !== undefined ? { over } : {} }; } default: c.fail(`unknown clause \`${kw}\`; expected one of ${CLAUSE_KEYWORDS}`); } } function parseExpiry(c) { const t = c.peek(); if (t === undefined) return; if (t.type === "number") { c.advance(); if (c.matchWord("dte")) { c.advance(); return { kind: "expiry-dte", dte: Number(t.text) }; } if (c.matchPunct("-")) { let s = t.text; while (c.optPunct("-")) { const seg = c.peek(); if (seg === undefined || seg.type !== "number") c.fail(`expected a number after \`-\` in an expiry date, got ${describe(seg)}`); s += "-" + seg.text; c.advance(); } return { kind: "expiry-date", date: s }; } c.fail(`expected \`dte\` or a date after ${t.text} in an expiry, got ${describe(c.peek())}`); } return { kind: "expiry-tag", tag: c.readName("an expiry tag") }; } function parseInstrument(c, opts) { const symbol = c.readName("an instrument symbol"); if (!opts.expiry) return { kind: "instrument", symbol }; const w = c.wordAt(); const nextIsStop = w !== undefined && opts.stop.has(w); const hasExpiry = !c.atEnd() && !nextIsStop; if (!hasExpiry) return { kind: "instrument", symbol }; const expiry = parseExpiry(c); return expiry === undefined ? { kind: "instrument", symbol } : { kind: "instrument", symbol, expiry }; } var USING_STOP = new Set(["signal", "exec"]); function parseUsingBody(c) { let signal; let exec; for (;; ) { if (c.optWord("signal")) signal = parseInstrument(c, { expiry: true, stop: USING_STOP }); else if (c.optWord("exec")) exec = parseInstrument(c, { expiry: true, stop: USING_STOP }); else break; } if (signal === undefined && exec === undefined) { c.fail("`USING` needs at least a `signal` or an `exec` instrument"); } return { kind: "using", ...signal !== undefined ? { signal } : {}, ...exec !== undefined ? { exec } : {} }; } function parseProvenanceBrace(c) { c.expectPunct("{", "to open a provenance map"); let tier; let author; let origin; let replay; if (!c.matchPunct("}")) { for (;; ) { const key = c.readName("a provenance field (tier, author, origin, or replay)"); c.expectPunct(":", "after a provenance field name"); switch (key) { case "tier": { const v = c.readName("a provenance tier (vetted, candidate, or unvetted)"); if (!PROV_TIERS.has(v)) c.fail(`unknown provenance tier \`${v}\`; expected vetted, candidate, or unvetted`); tier = v; break; } case "author": author = c.expectString("for a provenance author"); break; case "origin": origin = c.expectString("for a provenance origin"); break; case "replay": replay = c.expectString("for a provenance replay record"); break; default: c.fail(`unknown provenance field \`${key}\`; expected tier, author, origin, or replay`); } if (!c.optPunct(",")) break; } } c.expectPunct("}", "to close a provenance map"); return { kind: "provenance", ...tier !== undefined ? { tier } : {}, ...author !== undefined ? { author } : {}, ...origin !== undefined ? { origin } : {}, ...replay !== undefined ? { replay } : {} }; } function readContentHash(c) { const first = c.peek(); if (first === undefined || first.type !== "word" && first.type !== "number") { c.fail(citationBadHashMessage(describe(first))); } let text = first.text; let end = first.end; c.advance(); for (;; ) { const t = c.peek(); if (t === undefined || t.col !== end) break; if (t.type !== "word" && t.type !== "number") break; text += t.text; end = t.end; c.advance(); } return text; } function parseBecause(c) { const algo = c.readName("a content-hash algorithm in a `because` citation (`sha256:<64 hex>`)"); if (!CITATION_ALGOS.has(algo)) c.fail(citationBadHashMessage(`${algo}:…`)); c.expectPunct(":", "between `sha256` and its digest in a `because` citation (`sha256:<64 hex>`)"); const hash = readContentHash(c); if (!isCitationHash(hash)) c.fail(citationBadHashMessage(hash)); if (!c.atEnd()) c.fail(CITATION_INLINE_BODY_MESSAGE); return { kind: "citation", algo, hash }; } function mergeUsing(ambient, explicit) { if (ambient === undefined && explicit === undefined) return; const signal = explicit?.signal ?? ambient?.signal; const exec = explicit?.exec ?? ambient?.exec; return { kind: "using", ...signal !== undefined ? { signal } : {}, ...exec !== undefined ? { exec } : {} }; } function headerCursor(node, keyword) { const c = new Cursor(node.tokens, node.line); c.expectWord(keyword, "to open the statement"); return c; } function parseView(node) { const c = headerCursor(node, "VIEW"); const name = c.readName("a view name"); let budget; if (c.optWord("budget")) budget = c.expectNumber("for a view token budget"); c.expectEnd("the VIEW header"); for (const child of node.children) { if (child.tokens[0]?.type === "word" && child.tokens[0].text === "BECAUSE") { throw new KestrelParseError(citationOnMessage("View"), child.line, child.col); } } const panes = node.children.map(parsePane); return { kind: "view", name, panes, ...budget !== undefined ? { budget } : {}, ...withComments(commentLayer(node, node.children)) }; } function parsePane(node) { const c = new Cursor(flatten(node), node.line); const name = c.readName("a pane name"); const args = []; while (!c.atEnd()) { const t = c.peek(); if (t.type === "punct" && (t.text === ":" || t.text === "{" || t.text === ",")) { c.fail(`a pane is a NAME followed by plain space-separated arguments (e.g. \`chain fair realness\`, ` + `\`tape skyline 5m vwap\`) — not \`${t.text}\`. Drop any \`:\`/\`{…}\`/\`,\` and write one pane per ` + `indented line beneath the \`VIEW <name>\` header.`); } if (t.type === "word" && (t.text === ORDINAL_PREFIX || t.text === EXPIRY_PREFIX)) { const dash = c.peek(1); const numTok = c.peek(2); if (dash !== undefined && dash.type === "punct" && dash.text === "-" && dash.col === t.end && numTok !== undefined && numTok.type === "number" && numTok.col === dash.end) { c.advance(); c.advance(); const value = c.expectNumber(`for a ${t.text === ORDINAL_PREFIX ? "session ordinal (`d-<n>`)" : "expiry ordinal (`e-<n>`)"}`); if (t.text === ORDINAL_PREFIX) args.push({ kind: "arg-ordinal", ordinal: value }); else args.push({ kind: "arg-expiry", expiry: value }); continue; } } if (t.type === "number") { const value = c.expectNumber("for a pane numeral argument"); const next = c.peek(); if (next !== undefined && next.type === "word" && TIME_UNITS.has(next.text)) { const unit = readTimeUnit(c, "for a pane window argument"); args.push({ kind: "arg-window", window: { kind: "window", value, unit } }); } else { args.push({ kind: "arg-count", count: value }); } } else args.push({ kind: "arg-ident", name: c.readName("a pane argument") }); } return { kind: "pane", name, args }; } function parseWake(node) { const c = headerCursor(node, "WAKE"); const name = c.readName("a wake name"); c.expectEnd("the WAKE header"); let when; let because; let deliver; let priority; let coalesce; let budget; let universe; for (const child of node.children) { const cc = new Cursor(flatten(child), child.line); const kw = cc.readName("a WAKE clause (WHEN, BECAUSE, DELIVER, PRIORITY, COALESCE, BUDGET, or UNIVERSE)"); switch (kw) { case "WHEN": when = parseTrigger(cc); break; case "BECAUSE": if (because !== undefined) cc.fail("a WAKE carries at most one `because` citation"); because = parseBecause(cc); break; case "DELIVER": { const view = cc.readName("a view name after DELIVER"); const mandatory = cc.optWord("MANDATORY"); const keyframe = cc.optWord("KEYFRAME"); deliver = { kind: "deliver", view, ...mandatory ? { mandatory } : {}, ...keyframe ? { keyframe } : {} }; break; } case "PRIORITY": priority = cc.expectNumber("for a wake priority"); break; case "COALESCE": coalesce = cc.readName("a coalesce key"); break; case "BUDGET": budget = parseWakeBudget(cc); break; case "UNIVERSE": universe = { kind: "universe", universe: cc.readName("a universe name") }; break; default: cc.fail(`unknown WAKE clause \`${kw}\`; expected WHEN, BECAUSE, DELIVER, PRIORITY, COALESCE, BUDGET, or UNIVERSE`); } cc.expectEnd(`the ${kw} clause`); } if (when === undefined) throw new KestrelParseError("a WAKE requires a WHEN clause", node.line, node.col); return { kind: "wake", name, when, ...because !== undefined ? { because } : {}, ...deliver !== undefined ? { deliver } : {}, ...priority !== undefined ? { priority } : {}, ...coalesce !== undefined ? { coalesce } : {}, ...budget !== undefined ? { budget } : {}, ...universe !== undefined ? { universe } : {}, ...withComments(commentLaye