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.

172 lines (148 loc) 12.9 kB
# Multi-session horizon — the Instance is a sequence of carried Sessions **Status:** Accepted (owner, 2026-07-16 — implemented/extracted; kestrel-5zl.16). Extends ADR-0012 (the wake-driven Simulator) — this is the "named later child" that ADR-0012 §Consequences deferred. Honours CONTEXT.md's *Instance* / *Session* / *Blotter* vocabulary verbatim; proposes the new terms **SessionCarry**, **carry-mark**, **InstanceRunId**, **session-ordinal (relative-day) coordinate**, **timescale band**, and **InstanceBlotter**. ## Context The runtime grades one intraday session. `runSimSession` / `runDaySession` / `runSimulateSession` each drive **one** `SessionCore` over **one** bus with **one** `settle()` into **one** Blotter. Every axis of the P0 platform benchmark that is not a day trade — swing, position, multi-day options income, crypto — is blocked on a horizon longer than one session (benchmark-matrix-design §4.2, §4.3; generality-audit §6). Five intraday assumptions gate it: a single `session_date` on META, same-day-16:00 tau, settle-to-intrinsic at end-of-bus, `atClockET` re-anchoring only to `session_date`, and the date-blind fence tripping at ~11.57 relative days (`\b\d{10,}\b` against a monotonic `sinceOpenMs`). **The composition model is already decided — this ADR ratifies and specifies it, it does not re-open it.** CONTEXT.md ("Instances, sessions, blotters") states: *an Instance's execution divides into **Sessions** (one per market session), each yielding a **Blotter**; its history is that ordered sequence*, and *a Session owns exactly **one Bus** and yields exactly **one Blotter**.* ADR-0012 §Consequences ("Phase-1 scope is one Session, stated plainly") names overnight carry, cross-Session standing-Plan re-arm, and *Blotter-sequence → Instance history* as a later child. So the "one bus spanning days" alternative the audit weighed against is **already foreclosed by the domain model**: the Session is the atom, the Bus is per-session, and a multi-day horizon is an *ordered sequence of single-session buses with carried state*. This ADR specifies what carries and how the sequence stays deterministic, date-blind, and fail-closed. ## Decision ### 1. Composition — an Instance runs an ordered sequence of Sessions; a typed **SessionCarry** crosses each boundary A multi-day sim is **N single-session runs**, each a fresh `SessionCore` over its own bus, finalised to its own Blotter, chained by a **SessionCarry** — the one typed object handed from Session *k*'s finalize to Session *k+1*'s open. No bus spans days; no `SessionCore` is reused across a boundary (each keeps its single `settle()` and single Blotter — zero churn to the single-session contract). What the SessionCarry carries, and its fail-closed rules: - **Positions** — carried at their **carry-mark** (Decision 3), never force-settled to intrinsic. Session *k+1* opens with this inventory as its starting book and the carry-mark as its basis. - **Cash / realised P&L** — carries as a running Instance figure. The **`R` budget re-anchors per session** (the Session/day is the independent risk unit — m9i.3; generality-audit §6): a session gets a fresh `R` allotment, but capital already committed in carried positions persists and is visible against it. - **Standing document** — the agent's authored book (View/Wake/Plan) carries and is **re-armed fresh at each Session open**, minting **new Plan-instance ids per session** (one Plan instance = one Session, per CONTEXT/ADR-0012). Consequence, fail-closed: a Plan's `ttl` / `within` window **re-anchors to session open and does not silently span sessions**. Carried *inventory* rides its TP/EXIT inside the new session's re-armed Plans; an agent that wants a Plan to persist re-authors/supersedes it — the runtime never invents cross-session Plan lifetime. - **Pending wake frontier** — carries across the boundary. `inMinutes` offsets already span any horizon natively (`+1440` reaches tomorrow); `atClockET` targets re-anchor to the *target session's* date (Decision 3 / b3l router). - **Journal / agent memory** — carries as Instance-level author context (JOURNAL), so the agent reasons across days. It is **author metadata, never an engine input** (a57.11), so it cannot perturb determinism. New term **SessionCarry**: the typed hand-off object `{ positions@carry-mark, cash, standing document, pending wake frontier, journal }`. It is the *only* channel between sessions — anything not on it does not survive a boundary (fail-closed by omission). ### 2. Overnight physics — mark across the gap, settle expiry, refuse or receipt the rest - **Gaps (next open ≠ prior close).** Positions re-mark at each session's **opening carry-mark** = that session's first admissible print (bounded by the same T-5 author cutoff, no look-ahead). The overnight gap P&L is realised into the new session's mark; the carry-mark handed *out* of a session is its **session-close mark**. Close-mark → next open-mark is exactly the gap. - **Options expiry mid-sequence.** Settle is **polymorphic by instrument** (Decision 3): an expiry-terminal instrument (a weekly option that expires on day 5 of `EQOPT-SWING-INCOME`) **settles to intrinsic at its expiry session's close** via the *existing* 0DTE settle path — expiry is a genuine terminal settle for that leg — and does **not** carry past expiry. Assignment/roll of a held short option (position → underlier conversion) is P1, tracked separately. - **Corporate actions.** Deferred (P2) and **fail-closed until modelled**: a tape carrying a corporate-action event kind (split, dividend, symbol change) is **refused loudly** ("corporate actions not modelled"), never silently mis-marked. First-wave benchmark cells are chosen to avoid them (benchmark-matrix-design §2). - **Financing / borrow.** Deferred (P2). Every cross-session boundary stamps an explicit **"financing not modelled" receipt** so a multi-day grade is honest about the omission. Long-only cash cells (crypto, long equity — the first-wave multi-day cells) do not need it; short/margin tapes are **refused until financing is modelled** (this also keeps the never-naked / bounded-risk non-negotiable intact, since unbounded shorts stay out). ### 3. Determinism + identity — the Instance is a pure function of its ordered Sessions - **Settle becomes carry-aware but stays a per-session `settle()`.** A **carry-mark** settle (mark held inventory to the session-close spot, `pnl = (mark − basis)·qty·multiplier`, position survives) sits beside the existing **intrinsic** settle (expiry-terminal, position dies). Which one runs is a property of the instrument, not a new session mode. RUNTIME §6 already anticipates this ("physical-style instruments mark at last"). - **Identity chains.** Per-session **SimRunId** = `sha256(graded bus)` is unchanged. New **InstanceRunId** = `sha256` of the *ordered* per-session SimRunIds (a hash of ordered hashes) — the Instance identity is a pure function of its Sessions **and their order**. The grid's tape axis extends to an **ordered sequence of session tapes** (a *sequence cell*); an existing single-session cell is the length-1 degenerate case. - **Replay.** The SessionCarry into session *k+1* is a **pure function of session *k*'s graded bus**, so the whole sequence replays byte-identically: same tapes + same agent turns ⇒ byte-identical per-session buses ⇒ identical SimRunIds ⇒ identical InstanceRunId. No wall clock, no RNG crosses a boundary (RUNTIME §0). - **The date-blind fence generalises by *coordinate*, not by widening the threshold.** The cross-session author coordinate is the pair **`(session-ordinal d{N}, session-relative sinceOpenMs)`** — a relative day index (`d0, d1, d2, …`, pinning no calendar date) plus a `sinceOpenMs` that **resets at each session's own open**. Because `sinceOpenMs` never accumulates past one session (≤ 8 digits for a full RTH day), it **structurally cannot reach the 10-digit epoch signature** no matter how many weeks the horizon spans. The fence pattern stays `\b\d{10,}\b`**unchanged and un-weakened**; the ~11.57-day limit disappears because the horizon is no longer expressed as a single monotonic millisecond count. This is the principled fix the bead demanded, not a threshold-widening hack. ### 4. Crypto 24/7 — a UTC-day session by convention (design-only, split out as future work, P2) Crypto has no RTH boundary, so the *same* sequence-of-Sessions carry model applies with the **session boundary redefined as UTC midnight**: a crypto "session" is one UTC calendar day, the tape is continuous (no pre/open/close phases), and the carry-mark is taken at the **daily UTC close (00:00Z)**. Long-only / no-short / cash-only is enforced at the runtime (a crypto SELL floors at zero position — benchmark-matrix-design §4.3). This is **future work**, split into its own child bead — it needs a non-RTH phase model and a UTC clock beside the ET clock, both larger than the equity/option critical path and not gating it. ### 5. Wake cadence scales with the horizon via a per-cell **timescale band** The intraday defaults (30-min spacing, minutes-scale staleness backstop, structural close ramp) are wrong for a position trader who wakes *daily*. The wake floor is parameterised by a **timescale band**`scalp | day | swing | position` (benchmark-matrix-design Axis A) — declared **per cell** and applied as the session's default staleness backstop, `MIN_WAKE_SPACING`, and `MAX_WAKES`; a per-session override is allowed. A `position` cell gets an hours-to-a-day backstop and a daily-mark cadence; a `scalp` cell keeps seconds-scale spacing. The staleness backstop stays non-removable while holding inventory (fail-closed), only its *magnitude* scales with the band. ### 6. Grade — per-session Blotters plus a sequence-level **InstanceBlotter** Each Session still projects to **one Blotter** from its own bus (unchanged, ADR-0011). New **InstanceBlotter**: a pure aggregation over the *ordered* per-session Blotters — chained carried P&L, an Instance-level EV / max-drawdown, and the **benchmark-relative alpha hook** (`alpha = Instance EV − buy-and-hold baseline over the full horizon`, benchmark-matrix-design §4.1). `buildGrid` extends so a **sequence cell**'s independent evidence unit is the whole InstanceBlotter (a swing "run" is the N-day sequence, not one day); `CellAggregate`'s per-axis `Dist` and the no-omnibus discipline are unchanged. A single-session cell is the length-1 InstanceBlotter — the degenerate case, so existing grids are untouched. ## Consequences - **The single-session contract is preserved, not rewritten.** `SessionCore`, its one `settle()`, and its one Blotter are unchanged; the Instance runner is a *new* orchestration layer above them threading SessionCarry. This is why the lift is bounded — the sequence runner + carry + carry-mark + relative-day coordinate is the whole P0 critical path. - **Fail-closed is structural at the new seam.** Corporate-action tapes, short/margin tapes without financing, and any state not on the SessionCarry are refused or dropped loudly — never silently mis-modelled. A cross-session `ttl` never silently persists. - **Determinism holds across the horizon** because the carry is a pure function of the prior session's graded bus and the date-blind coordinate cannot manufacture an epoch integer. - **Crypto, financing, corporate actions, assignment/roll, and the sequence-level grade are deferrable** (P1/P2) behind the equity/option carry critical path — the first swing and option-income cells need only P0 + the timescale band. - **New modules (indicative):** `src/session/instance.ts` (the sequence runner + SessionCarry), a carry-mark branch in `src/fill/engine.ts` settle, a session-ordinal coordinate in `src/session/day.ts`'s author projection, `InstanceRunId` in `src/session/run-identity.ts`, and an `InstanceBlotter` aggregation in `src/session/grid.ts`. Consolidate onto the extracted date-blind fence once kestrel-7dv.2 lands (do not fork it). ## Alternatives considered - **One bus spanning days** (the audit's foil). Rejected — it contradicts CONTEXT.md ("a Session owns exactly one Bus") and ADR-0012's Session atom, forces the date-blind fence to swallow multi-week millisecond counts (the widening hack), and re-marks nothing at the overnight gap. The sequence-of-Sessions model is both the smaller lift and the one the domain already mandates. - **Cross-session Plan instances / spanning `ttl`.** Rejected — breaks the "one Plan instance = one Session" identity and makes lifecycle non-deterministic across a boundary. Re-arm fresh per session; carried inventory rides the re-armed Plans.