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.
126 lines (114 loc) • 7.83 kB
text/typescript
/**
* # frame/session-scheme — the SessionScheme series-registry attribute (ADR-0041 §1/§3, kestrel-wa0j.44)
*
* A `SessionScheme` is a SERIES-REGISTRY attribute (the ADR-0036/0038 registry slot — the same
* surface ADR-0038 runs one axis over for Attribution): it declares an instrument's **monotone
* boundary stream** — the ordered `open` / `close` / session-split boundaries that `SessionOrdinal`'s
* `d-N` addresses index (ADR-0041 §1: scheme-relative ordinals). 24/7 and multi-session markets
* therefore extend the *value inventory* of the existing `SessionOrdinal` sort WITHOUT touching the
* closed roster — a perp's UTC-day convention and an equity RTH day are two schemes over one sort.
*
* ## Which boundaries a scheme carries is DATA (ADR-0041 §3)
* Each scheme declares WHICH boundary kinds it carries ({@link SchemeBoundaries}). A scheme with no
* `close` boundary (a perp's rolling UTC day) makes the `prior-context` pane's close address a
* first-class ABSENCE: "this instrument's scheme has no such boundary" is a rendering, never an
* invented midnight "close" (ADR-0041 §3 — absent-not-hidden reaches the address space). The
* scheme-conditioned paradigm cell (`src/frame/paradigm-ledger.ts`) reads THIS table to decide
* whether `(prior-context × SessionOrdinal)` is DEFECTIVE or ATTESTED-as-today for an instrument.
*
* ## Absent-default = `equity-rth` (no-churn)
* `InstrumentSpec.sessionScheme` is OPTIONAL; an absent value is {@link DEFAULT_SESSION_SCHEME}
* (`equity-rth`, which carries open+close). Every existing frame input — which declares no scheme —
* therefore resolves to `equity-rth` and renders BYTE-IDENTICALLY (the empty-ledger no-churn lesson).
*
* ## ABSENT is the default; an out-of-roster STRING fails closed (never a silent default)
* A DECLARED-but-out-of-roster scheme string is NOT the absent case: coercing it to `equity-rth` would
* be a silent default (the fail-closed doctrine's cardinal sin — a made-up boundary stream rendered as
* if declared). {@link resolveScheme} therefore REFUSES an out-of-roster string by name (naming the
* served value + the closed roster), throwing a {@link ../frame/types.ts KernelHonestyError}; only the
* ABSENT case (undefined/null) resolves to the default. The typed `InstrumentSpec.sessionScheme` path
* cannot reach the refusal (its values are members of the roster or absent) — this is defence-in-depth
* for an untyped caller, per the fail-closed non-negotiable.
*/
import { KernelHonestyError } from "./types.ts";
/** The three boundary KINDS a SessionScheme's monotone stream may carry (ADR-0041 §1). */
export const BOUNDARY_KINDS = ["open", "close", "session-split"] as const;
export type BoundaryKind = (typeof BOUNDARY_KINDS)[number];
/**
* The CLOSED v1 SessionScheme roster (a series-registry attribute — ADR-0036/0038 slot). Opened only
* by adding a scheme + its {@link SchemeBoundaries} declaration (the registry-attribute analogue of the
* sort-introduction ceremony). `equity-rth` is the absent-default.
*/
export const SESSION_SCHEMES = [
"equity-rth", // US equity/index Regular Trading Hours — open + close, one session/day (the default).
"cme-rth-overnight", // CME futures — an RTH session split from the overnight (globex) session.
"perp-utc-day", // crypto perpetual — a rolling UTC-day convention; NO daily close in the trading sense.
"fx-week", // spot FX — the Wellington-open → NY-Friday-close trading week, split into daily sessions.
] as const;
export type SessionScheme = (typeof SESSION_SCHEMES)[number];
/** The scheme an instrument that DECLARES none resolves to (ADR-0041 §3 no-churn: existing inputs are
* byte-identical). `equity-rth` carries both open and close, so every legacy equity frame is unchanged. */
export const DEFAULT_SESSION_SCHEME: SessionScheme = "equity-rth";
/**
* The boundary declaration for one scheme (ADR-0041 §3: which boundary kinds it carries is DATA). A
* scheme lacking `close` carries a {@link rollingAnchor} descriptor instead — the sanctioned
* periphrasis names it ("anchor to the declared rolling extreme") so the pane renders that declared
* anchor, never an invented midnight close.
*/
export interface SchemeBoundaries {
readonly open: boolean;
readonly close: boolean;
readonly sessionSplit: boolean;
/** The declared rolling extreme a no-close scheme anchors to instead of a close (e.g. a perp's
* `rolling 24h extreme`). Present iff `close` is false — the periphrasis has something to name. */
readonly rollingAnchor?: string;
}
/** The per-scheme boundary declarations (the DATA ADR-0041 §3 conditions the paradigm cell on). */
export const SESSION_SCHEME_BOUNDARIES: Readonly<Record<SessionScheme, SchemeBoundaries>> = {
"equity-rth": { open: true, close: true, sessionSplit: false },
"cme-rth-overnight": { open: true, close: true, sessionSplit: true },
"perp-utc-day": { open: false, close: false, sessionSplit: false, rollingAnchor: "rolling 24h extreme" },
"fx-week": { open: true, close: true, sessionSplit: true },
};
/** True iff `s` is a member of the closed {@link SESSION_SCHEMES} roster. */
export function isSessionScheme(s: string): s is SessionScheme {
return (SESSION_SCHEMES as readonly string[]).includes(s);
}
/**
* The typed refusal for a DECLARED-but-out-of-roster SessionScheme string (ADR-0041 §1/§3): it names
* the served value + the closed {@link SESSION_SCHEMES} roster (the unknown-value-naming-roster idiom),
* so the fix is legible — declare the scheme + its boundaries, or correct the typo. Fail-closed: an
* unknown scheme has no boundary declaration, so its `open`/`close`/split stream cannot be honestly
* rendered — refusing is the only truthful answer (never a silent `equity-rth`).
*/
export function unknownSchemeRefusal(served: string): KernelHonestyError {
return new KernelHonestyError(
`SessionScheme "${served}" is not in the closed roster {${SESSION_SCHEMES.join(", ")}} — a ` +
`series-registry attribute must name a DECLARED scheme (each carries its own boundary stream). ` +
`Judgment REFUSED (fail-closed, never a silent equity-rth default): an undeclared scheme has no ` +
`open/close/session-split declaration, so its boundaries cannot be honestly rendered. Add the ` +
`scheme + its SchemeBoundaries to open the roster, or correct the value (ADR-0041 §1/§3).`,
);
}
/**
* Resolve a declared-or-absent scheme to a concrete one. ABSENT (undefined/null) ⇒
* {@link DEFAULT_SESSION_SCHEME} (`equity-rth`, no-churn). A DECLARED value OUTSIDE the closed roster
* FAILS CLOSED — it throws {@link unknownSchemeRefusal} (naming the value + the roster) rather than
* fabricate an unknown scheme's boundaries or silently coerce to the default. ABSENT is not UNKNOWN:
* only an explicit out-of-roster string refuses; a missing declaration stays the default.
*/
export function resolveScheme(scheme: string | undefined | null): SessionScheme {
if (scheme === undefined || scheme === null) return DEFAULT_SESSION_SCHEME;
if (!isSessionScheme(scheme)) throw unknownSchemeRefusal(scheme);
return scheme;
}
/** True iff `scheme` carries the given boundary kind (ADR-0041 §3 — read from the declaration DATA). */
export function schemeHasBoundary(scheme: SessionScheme, kind: BoundaryKind): boolean {
const b = SESSION_SCHEME_BOUNDARIES[scheme];
return kind === "open" ? b.open : kind === "close" ? b.close : b.sessionSplit;
}
/** The declared rolling extreme a no-close scheme anchors to, or `undefined` when the scheme carries a
* close (the periphrasis is only reachable for a no-close scheme). */
export function rollingAnchorOf(scheme: SessionScheme): string | undefined {
return SESSION_SCHEME_BOUNDARIES[scheme].rollingAnchor;
}