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
TypeScript
/**
* # 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