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.

112 lines 6.04 kB
/** * # series/trigger — the tri-state, causal trigger evaluator (RUNTIME §3) * * Evaluates a `Trigger` (from `src/lang`) to `true | false | UNKNOWN`. **Only a definite * `true` fires** (RUNTIME §3). One {@link TriggerEvaluator} instance belongs to one armed * statement instance and owns that instance's **per-node causal memory** — the edge/anchor * state that makes crosses, break-holds, withins, and nth-events causal rather than * level-only. * * ## The exact tri-state algebra (RUNTIME §3) * - **AND**: `false` wins; else `UNKNOWN` if any term is UNKNOWN; else `true`. * - **OR**: `true` wins; else `UNKNOWN` if any term is UNKNOWN; else `false`. * - **NOT**: `UNKNOWN` maps to `UNKNOWN`; otherwise it negates. * - Every operand UNKNOWN propagates — a comparison/cross over an UNKNOWN operand is * UNKNOWN, never silently `false`. * * ## All subtrees evaluate every tick (causal-memory correctness) * AND/OR do **not** short-circuit their traversal: every child is evaluated in full each * tick *before* the tri-state is folded, so a `crosses`/`held`/`within`/`nth` buried under a * sibling that already decided the parent still advances its own edge memory. Short-circuit * would silently drop the transition sample that makes those predicates causal. * * ## Per-node causal memory (RUNTIME §3) * - `crosses above|below` fires only on an **observed transition** — it needs a prior sample * strictly on the *other* side; never true on first sight. Straddling equality holds the * last non-equal anchor (does not overwrite it). `touches above|below` is the at-or-touch * variant: reaching the level (`>=`/`<=`) counts as the target side, so an exact touch fires. * A `band` adds a **re-arm** gate: after a fire the cross is disarmed until the * series clears back past `right ∓ band`, so an in-band wiggle cannot phantom-fire. * - `held Ns` anchors continuity: the inner must be *continuously* `true` for the duration; * any tick where the inner is `false` **or UNKNOWN** resets the anchor (a flicker restarts * the clock). * - `within Nm` anchors at the qualifying event: once the inner was `true`, the window is * `true` until `N` elapses, then `false`. * - `nth` counts rising edges of its inner event and latches `true` once the count reaches * the ordinal. * * Memory is keyed by a **structural path** (a fixed tree ⇒ a fixed key per node), so it is * both deterministic and serializable — {@link TriggerEvaluator.dumpState} returns it for the * determinism tests; {@link TriggerEvaluator.reset} clears it. * * ## Detector / calendar / fill inputs are provider hooks * `failed-break`-style structural events, `time HH:MM` windows, and fill-lifecycle events * are inputs from other modules; they resolve through OPTIONAL {@link SeriesProvider} hooks * and read UNKNOWN (fail-closed, absent-with-reason) when the provider does not supply them. * `phase` is answered directly from canonical state. */ import type { Trigger } from "../lang/index.ts"; import { type TriState } from "./types.ts"; import type { SeriesProvider } from "./provider.ts"; type CrossMem = { readonly kind: "cross"; prevSide: "above" | "below" | null; /** Re-arm gate for a banded cross: a fire disarms until the series clears back * past `right ∓ band`. Always `true` for a bare (band-less) cross. */ armed: boolean; }; type HeldMem = { readonly kind: "held"; since: number | null; }; type WithinMem = { readonly kind: "within"; lastTrue: number | null; }; type NthMem = { readonly kind: "nth"; count: number; prevInner: boolean; }; /** The causal memory a single stateful trigger node carries. */ export type NodeMem = CrossMem | HeldMem | WithinMem | NthMem; /** A deterministic, serializable dump of the evaluator's memory: `[path, mem]` pairs sorted * by path. Two runs over the same bus + same armed document yield equal dumps. */ export type EvaluatorState = readonly (readonly [string, NodeMem])[]; export declare class TriggerEvaluator { private readonly mem; /** Evaluate `trigger` against `provider` at the injected `now` (RUNTIME §0). Returns * `true | false | UNKNOWN`; only `true` fires. Advances per-node causal memory. */ evaluate(trigger: Trigger, provider: SeriesProvider, now: number): TriState; /** Clear all per-node memory (determinism / re-arm). */ reset(): void; /** A deterministic dump of the causal memory (sorted, deep-copied). */ dumpState(): EvaluatorState; private evalNode; /** Resolve a comparison/cross operand pair, honoring a `Baseline` on either side (a * baseline resolves against its paired *series* operand's trailing distribution). */ private resolvePair; private operandValue; /** A bare single-segment series operand → its own name as a symbolic string literal; any * other operand → UNKNOWN. Used only in the eq/ne literal fallback. */ private literalOf; /** * Fold a comparison where exactly one operand is a **quantified** aggregate * (`children(any|all).<fact>`): resolve the operand to its per-member vector, apply the authored * comparator to EACH member (preserving operand order), then combine by the quantifier — * `any` = ∃ (tri-state OR), `all` = ∀ (tri-state AND). This makes the ∃/∀ meaning honor the * comparator direction, so a `<`/`<=` guard cannot silently invert (the fail-open the scalar fold * hid). An unresolvable vector (no member published, or an unknown quantified path) ⇒ UNKNOWN, * de-armed loudly by the provider (RUNTIME §8). Both operands quantified, or the counterpart * unresolved, ⇒ UNKNOWN (fail closed). `eq`/`ne` fold per member too; only `crosses` (an edge) * is excluded, handled in the `cross` node. */ private evalQuantifiedCompare; private compareResolved; private crossMem; private heldMem; private withinMem; private nthMem; } export {}; //# sourceMappingURL=trigger.d.ts.map