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.

727 lines 52 kB
# The CATALYST Series — the calendar (scheduled) and the news (unscheduled), and the anticipatory Scans over them

> **Scope note (file name is historical).** This ADR was authored as the *calendar* ADR and was
> **extended on 2026-07-14 (owner decision, same day, after the design workflow ran)** to cover
> **BOTH kinds of catalyst**: **scheduled** ones (the calendar — earnings, FOMC, expiry) and
> **unscheduled** ones (the news — breaking headlines). The file name is kept to avoid churn in the
> many places that already cite `adr/0036`. **Every calendar decision below stands unchanged**; the
> news half is added as §(k)–§(o), and it re-uses the calendar's machinery rather than inventing
> any.

**Status:** **Proposed — design for owner review (2026-07-14).** Design only; introduces no new
determinism-core plumbing and lands no code. It is the concrete content of owner-approved
decision **D** from the 2026-07-14 business/growth grill: *the forward-looking calendar Scan is
in scope now, and it is the highest-ROI Scan* — **plus the owner's follow-on decision the same day
that NEWS completes the catalyst surface** (§(k)).

**The catalyst surface, in one line:**

| Kind | Scan | Timing | Source | Cost to us |
|---|---|---|---|---|
| **Scheduled** — earnings, FOMC, expiry | **calendar Scan** | *anticipatory* — fires **before** the catalyst | an as-of events table | ~free (a table join, §(d)) |
| **Unscheduled** — breaking headlines | **news Scan** | *immediate* — fires **when it just happened** | **Benzinga via Alpaca**, the user's own broker entitlement | **free** — no purchase, nothing redistributed (§(l)) |

**Together they are the whole catalyst surface: *what you know is coming* + *what just happened*.**
The calendar alone covers only half of it, and the half it misses is the half that moves the tape
without warning.

Extends **ADR-0032** (the tier cascade — the Scan is the cascade's free, deterministic first
layer, and the layer that makes a wide candidate universe affordable) and **ADR-0035** (the human
oversight surface — a Scan's candidates surface in the cockpit like any other wake). It builds on,
and does **not** re-decide, the platform's serving-and-pricing canon already accepted:
derived-only serving, the practice/served/sealed tier split, and pricing-is-data. It rests
entirely on primitives CONTEXT.md already names —
**Scan**, **Series**, **Registry**, **Trigger algebra**, **Window**, **Wake** — and invents no new
one.

**Nothing here weakens a determinism or fail-closed non-negotiable.** The headline is the
opposite: the calendar Series is *forced into* the relative encoding by an invariant the runtime
already enforces (`SourceWatermark`'s "no date ever rides a Field"), and the date-blindness that
makes the holdback tier honest is therefore preserved **by construction**, not by discipline.

## Context

CONTEXT.md already has the word and the shape:

> **Scan**: A Wake whose scope is a **universe** rather than a coverage — wide and slow (all
> NYSE/NASDAQ at `move(1d) > p99`) where a coverage Wake is narrow and fast. **The discovery
> layer**: a Scan fires → the PM evaluates → may author a new leaf (Book + Coverage + thesis +
> budget) into the pod. **Breadth × window-length is the attention economics** that keeps a wide
> universe affordable. *Avoid: screener (the retail noun; Scan is the Wake kind).*

Every Scan the glossary illustrates is **reactive**: `move(1d) > p99` fires *after* the move. That
is a structurally late entry — the discovery layer discovers the thing once the market has already
paid for it. The trader's actual question is the opposite one: **where should I be positioned
*before* the catalyst?**

That question is answered by a calendar, and the platform has no calendar Series. What it has
today is:

- a **session** calendar (`time HH:MM`, `ttl-at`) — an intraday clock hook, resolved through an
  optional provider hook and reading UNKNOWN when the session injects none
  (`src/series/trigger.ts` §"Detector / calendar / fill inputs are provider hooks";
  `src/series/provider.ts` `timeOfDayMinutes`);
- a platform market-fact vocabulary that is entirely **price-derived** — the seven scalars
  (`spot`, `vwap`, `hod`, `lod`, `prior_close`, `or_high`, `or_low`) and the three window metrics
  (`velocity`, `move`, `range`) in their `$`/`%` framings (`src/series/registry.ts`
  `PLATFORM_SERIES`).

Nothing in that vocabulary can express *"an earnings print lands tomorrow."* So the discovery layer
can only look backward, and the Scan portfolio — which decision (A) establishes as **the standing,
compounding asset** (Plans are ephemeral, TTL-bounded; Scans are standing) — is missing its
cheapest and most predictive member.

There is a second, sharper reason this is urgent. Under ADR-0032's cascade the Scan is layer 1: a
free deterministic screen, high-recall/low-precision by design, whose survivors are triaged by the
cheap **watcher-gate** before any frontier strategist token is spent (decision A). **Scan breadth
is what the gate exists to make affordable** — and a Scan that must read tick data to evaluate a
10k-ticker universe is *not* free, because the tape it reads is the platform's most expensive
input (Databento). A calendar Scan is a **table join**. It is the one Scan whose evaluation cost
is genuinely ~zero at 10k breadth, and it happens to also be the one that surfaces the best
candidates. That coincidence is the whole ADR.

The tension, and the reason this needs an ADR rather than a commit: a calendar is *made of dates*,
and **dates are precisely the thing the runtime refuses to carry**. `SourceWatermark`'s *when* is
`asOfSeq` — *"an ordinal, never a wall clock — the determinism and replay-stability key… **no date
ever rides a Field**"* (CONTEXT.md). The Frame renderer enforces it (`src/frame/render.ts`: *"No
date/day/epoch token is ever emitted — only relative time and the HH:MM ET clock"*), and the
benchmark treats a breach as **contamination**: the shipped capability catalog records that the
agent-day evidence is practice-tier *because* **"its author boundary leaked calendar identity and
the contamination fence is conjunctive, so no run is certified"** (`src/catalog/catalog.ts:52`,
`src/catalog/status.ts:94`). Leaking calendar identity is already a de-certifying event in this
codebase. A calendar Series that hands the agent a date would burn the holdback tier — the only
tier whose Grades can honestly *mean* anything (CONTEXT.md, Practice/Holdback tier).

The resolution is not a compromise. It is that the honest encoding of a calendar for an agent is
**the relative one anyway**.

### Context (added 2026-07-14, owner decision) — the calendar covers only HALF the catalyst space

A calendar answers *"what do I know is coming?"* It is silent on the other half of every trading
day: **what just happened that nobody had on a calendar.** A halt, a guidance cut, an approval, a
lawsuit, a takeover rumour, an ETF decision — these are **unscheduled catalysts**, and they are
where the largest unanticipated repricings live. A discovery layer that sees only scheduled events
is a discovery layer that is *structurally blind on precisely the days the tape moves most*.

**News is available for free, through the same sidestep the platform already uses for realtime
market data.** Alpaca carries the **Benzinga** news feed, and a user's Alpaca key is *their own
entitlement*. The platform's rule is already written: *"Realtime comes from the user's own broker
entitlement (their Alpaca/IBKR key) — Alpaca prohibits redistribution, so its data is used **only
via the customer's own account**"* (PRD §3.1, `adr/0011:35-38`). **News rides that exact rule.**
There is **no Databento-class purchase here, and nothing to buy** — unlike the events feed the
calendar half needs (§(i)).

Two strategic points make this more than a second data source. They are the reason it matters:

**1. NEWS IS WHERE THE LLM's EDGE IS MOST DEFENSIBLE.** Everywhere else in this system we are
teaching a language model to read **markets** — prices, ranges, velocities, books. That is a thing
quantitative funds already do superbly, with decades of machinery, and where an LLM is at best a
novel competitor on someone else's home ground. **News is the inverse.** It is unstructured natural
language, and a quant/systematic pipeline **chokes on it**: it must be rendered, tagged, entity-
resolved, sentiment-scored, feature-engineered — a lossy compression into numbers *before* the
system can think about it at all. An LLM reads it **natively**. No rendering step, no feature
engineering, no lossy projection: the headline *is* the percept. **On news the LLM plays its home
game.** If there is a durable edge for an agentic trader over a systematic fund — and the whole
product thesis needs there to be one — **news judgment is where it lives.**

**2. NEWS IS THE FLAGSHIP APPLICATION OF THE WATCHER-GATE, AND ITS RICHEST RL CORPUS.** The gate's
one question (ADR-0032 §10.3) is *"is this worth spending strategist tokens on, or stand down?"* —
a **value-of-information** call. A headline stream poses that question **thousands of times a day at
the worst signal-to-noise ratio in the entire system**: ~99% of headlines are immaterial to any
given Book. *"Is this headline material enough to spend strategist tokens on?"* **is** the gate's
question, verbatim, at maximum volume and minimum precision. Two consequences:

- **It is the killer demo.** The gate discards 990 headlines and escalates the 10 that mattered —
  legibly, on the Bus, with the strategist's token bill shown gated-vs-ungated. Nothing else in the
  system shows the gate's economics that vividly, because nothing else has that fan-in.
- **It is the densest, cheapest-to-label, fastest-to-score RL corpus in the product.** ADR-0032 §11
  argues the gate is the best RL target in the system (dense, cheap, fast, self-labeling reward).
  **News is where that argument is *most* true**: thousands of labeled decisions per day, each with
  an observable outcome (did the tape move? did the strategist, when it looked, find a Plan?), at a
  marginal data cost of zero.

## Decision

### (a) A calendar is JUST ANOTHER SERIES — the trigger algebra needs NO GRAMMAR CHANGE

A Series is *"anything with a name whose value changes over time — the universal operand of the
language"* (CONTEXT.md, Series). `earnings.in_days` is exactly that: a name, whose value changes
over time, that a trigger can compare against a number. The **Trigger algebra** is *"named series ×
predicates (comparison, cross, break-and-hold, distance, velocity, state, window, fill events) ×
AND/OR/NOT + held/within"* — and a calendar countdown is consumed by the plainest predicate it
has, a `Comparison` (`src/lang/ast.ts`).

So the following is **already grammatical today**, and needs zero parser, printer, or golden-fixture
work:

```kestrel
WHEN earnings.in_days <= 1 AND range_pct(20d) < p20
```

That is the whole surface change: **there isn't one.** No new statement kind, no new predicate, no
new operand form, no new keyword. The Registry's own doctrine anticipated this — *"Register once →
visible to every surface (trigger, pane, grade column) by construction"* (CONTEXT.md, Registry) —
and the registry module states the rule that keeps it honest: *"There is no author-facing `SERIES`
statement in v1 — the phonebook is populated by the platform in code"* (`src/series/registry.ts`).
A calendar Series is a **platform declaration**, not a language feature.

(The illustrative `iv_rank > p80` from the design session is **not** grounded: `iv_rank` is not in
`PLATFORM_SERIES` today. It would be its own registration, on its own merits, in its own issue.
This ADR registers the calendar facts and nothing else — the example above uses only names that
exist.)

### (b) THE KEY INVARIANT — DATE-BLIND BY CONSTRUCTION: the value is a countdown, never a date

**The calendar Series resolves to a RELATIVE integer — a countdown — and the platform has no
surface on which it can resolve to an absolute date.** The agent sees *"earnings in 1 day"*; it
never sees *"2026-07-15."*

This is the same move the Frame already makes with intraday time: the wake deadline renders as
`T-92m to close`, *"a date-blind DURATION (never a wall-clock/absolute time)"* (`src/frame/render.ts`).
`earnings.in_days = 1` is that rule extended from the session clock to the session **calendar** —
one axis out, identical discipline.

Three properties fall out, and all three are load-bearing:

1. **It is forced, not chosen.** A Field's `SourceWatermark` carries `asOfSeq` — an ordinal — and
   *"no date ever rides a Field"* (CONTEXT.md). A calendar Series that resolved to a date could not
   be rendered into a Frame without breaking an invariant that is already enforced in code and
   grepped in tests (`tests/frame.test.ts`, `tests/session.relative-day.test.ts`). The relative
   encoding is not a blinding *trick* bolted onto a calendar; it is **the only encoding the runtime
   will accept.**

2. **It preserves the blinding that makes the holdback tier work.** The holdback tier is *"the same
   Cell shape run as a sealed forward window after the model's training cutoff, blinded, and
   therefore the only tier that is ranking-eligible"* (CONTEXT.md). An absolute date is the single
   most efficient way to **de-blind** a Cell: hand a model trained past that date the string
   `2026-07-15` beside a ticker and it can recall how the print resolved. `earnings.in_days = 1`
   leaks nothing — it is true of thousands of (ticker, day) pairs across market history and
   identifies none of them. The contamination fence (`src/catalog/status.ts:94`) stays intact, and
   a calendar Scan can therefore run **inside a ranking-eligible sealed season** (decision F) — which
   a date-carrying one never could.

3. **It is what the trader actually reasons over.** "Earnings in 1 day" is the decision-relevant
   quantity; the date is an implementation detail of the decision-relevant quantity. The blinding
   costs the agent **nothing**, which is why this invariant is free rather than a tax. (Contrast the
   holdback tier's other blinds, which genuinely do cost the agent information.)

**The fence, stated as a testable clause:** no calendar Series may resolve to, and no Rendering may
emit, a date/day/epoch token. The existing date-blind grep is the enforcement; a calendar fact that
breaches it is a fail-closed construction error, exactly like a `MODEL` Field without its receipt.

### (c) Registration — three new platform market facts, ONE new unit, ONE new binding, and NO new SeriesKind

There are **two-and-only-two** kinds of Series (CONTEXT.md; `SeriesKind = "market" | "org"`), and
this ADR **does not add a third**. A calendar fact is a **market fact**: *"ambient per signal
instrument, visible even to spectators"* — which is exactly right, since a spectator Frame may
honestly show "earnings in 1 day" and nothing is at stake in doing so.

The registration is three names, declared in `PLATFORM_SERIES` alongside the price scalars:

| Name | Meaning | Scope |
|---|---|---|
| `earnings.in_days` | sessions until the instrument's next scheduled earnings print | signal (per instrument) |
| `fomc.in_days` | sessions until the next FOMC decision | signal (resolves identically for every instrument — a market-wide event needs no new scope) |
| `expiry.in_days` | sessions until the instrument's front option expiry | signal (per instrument) |

Each is `windowing: "scalar"` — a countdown carries no Window, because *"there is no absolute
shock: magnitude is judged at a window"* applies to **rate/magnitude** series (CONTEXT.md, Window),
and a countdown is neither. It is judged against an absolute threshold, and that is legal
(*"absolute thresholds stay legal"*).

Two small, additive extensions to the registration record (`src/series/registry.ts`), both of which
already have precedent-shaped holes waiting for them:

- **One new `SeriesUnit`: `days`.** The unit vocabulary today is `price | price_delta | fraction`,
  with the stated policy *"Kept small; extend as real facts need it, never stubbed ahead."* A real
  fact now needs it.
- **One new `MarketBinding`: `{ via: "calendar", event: "earnings" | "fomc" | "expiry" }`.** The
  binding union today is `{via:"scalar"} | {via:"window"}` — how the provider dials the fact from
  canonical state. A calendar fact dials a third way: an **events hook** on the provider, in the
  same shape as the session-calendar hook that already exists (`timeOfDayMinutes`) and reads UNKNOWN
  when the session injects none. `src/series/trigger.ts` already names this seam: *"Detector /
  calendar / fill inputs are provider hooks."* The event calendar is the same seam, one axis out.

### (d) The observability class is `session` — and THAT is why the Scan is nearly free

Every platform market fact carries an `ObservabilityClass` — *"the finest data granularity at which
the series is faithfully observable"* — over the lattice `tick → second → minute → session`
(`src/series/registry.ts`, kestrel-cza.3). The price scalars are `tick` (`spot`, `vwap`) or
`minute` (`hod`, `or_high`); only `prior_close` is `session`, *"one fact per session… faithful with
session-level data alone."*

**A calendar fact is `session`-observable** — the coarsest rung. It changes at most once per
session, by construction.

This is not bookkeeping trivia; it is the economic argument, and it is *already enforced by shipped
code*. `SeriesRegistry.admitAtFidelity` refuses any series **finer** than the session's grading
fidelity — *"you cannot grade a tick-sensitive series honestly on minute bars."* A `session`-class
fact is admissible at **every** fidelity in the lattice. Therefore:

- A calendar Scan can be evaluated, and **graded**, on the **coarsest data the lake holds** — no
  tick data, no minute bars, no Databento spend at all beyond a symbol list. It is a join between a
  ticker universe and an events table.
- The reactive Scans the glossary illustrates (`move(1d) > p99`) are built on `tick`-class window
  metrics and are refused at anything below tick fidelity. **The calendar Scan is the only Scan
  whose evaluation cost does not scale with the price of the tape.**

CONTEXT.md's Scan entry says *"breadth × window-length is the attention economics that keeps a wide
universe affordable."* The calendar Scan's data economics is the same sentence one layer down:
**the calendar Scan is the one whose breadth is affordable at 10k tickers because its observability
class is the cheapest rung on the lattice.**

### (e) Fail-closed on absent coverage — UNKNOWN, never a sentinel

A ticker with no scheduled event, or no calendar coverage at all, resolves `earnings.in_days` to
**UNKNOWN** (`src/series/types.ts`). A trigger over an UNKNOWN operand evaluates **UNKNOWN, never
silently false** (RUNTIME §3); only a definite `true` fires; the referencing statement de-arms
cleanly with a logged reason (CONTEXT.md, Registry).

**The failure mode this clause exists to forbid, named explicitly:** a calendar provider that
returns a large sentinel (`999`, `+Infinity`, "no event soon") for an uncovered ticker. That is a
**silent default**, and it is a *dangerous* one — it makes `earnings.in_days <= 1` evaluate to a
confident `false` for every ticker the events feed simply does not know about, so the Scan reports
"no catalyst" where the truth is "I cannot see." That is the exact shape of the thing
`isFinerThanFidelity` and `admitAtFidelity` already refuse elsewhere, and the calendar hook refuses
it the same way: **an unresolvable calendar fact is UNKNOWN, and an UNKNOWN operand never fires.**

Corollary for the Scan's *reporting* surface: a Scan run must be able to say **how much of its
universe read UNKNOWN**. A 10k-ticker Scan with 40% calendar coverage is not a 10k-ticker Scan; it
is a 6k-ticker Scan with a 4k blind spot, and the Grade must say so. (Absent-not-hidden — CONTEXT.md,
Kernel.)

### (f) Point-in-time calendars — the look-ahead fence

The events table is read **as of the Bus `seq`**, never as a current snapshot. Earnings dates get
moved; a company that pre-announced on Monday and printed on Thursday has *two* histories, and only
the one that was **knowable at that seq** may be resolved into a Field.

This is not a new rule; it is `asOfSeq` (CONTEXT.md, SourceWatermark) applied to a table instead of
a tick, and it is what ADR-0018's ranking guarantee already depends on — a sealed forward window is
only "look-ahead is impossible" (`adr/0018:36-48`) if **every** input is point-in-time. A calendar
back-filled with the revised date is a look-ahead channel that would silently contaminate the one
ranking-eligible tier. **The events feed must be historised (as-of), and a feed that cannot supply
as-of dates is a practice-tier-only feed.**

### (g) The Scan is a Wake — it spends attention, never risk

A Scan **is** a Wake (CONTEXT.md), and a Wake is *"a standing subscription over the trigger algebra
that spends **attention** (tokens, wakes), **never risk**. A Wake is not a Plan."*

So the calendar Scan **cannot place an order.** It produces *candidates*. Under the ADR-0032
cascade as extended by decision (A), what a candidate can do is precisely bounded:

    calendar Scan (free, deterministic, high-recall)
        → watcher-GATE (cheap model, one binary value-of-information call: spend strategist tokens, or stand down?)
            → STRATEGIST (frontier, expensive — only ever sees what the gate passed) authors the Plan
                → watcher-MANAGER (cheap, ADR-0032's watcher) manages the ARMED Plan
                    → HUMAN (ADR-0035)

Every rung below the Scan is unchanged by this ADR, and the safety keystone is untouched: the Gate
admits, never trusts (ADR-0032 §3). The Scan adds **breadth**, and breadth is exactly the thing the
gate was introduced to make affordable. The calendar Scan is therefore the cascade's **cheapest
possible top** — free at the widest breadth the system will ever run.

Wake delivery discipline applies unchanged: past its attention budget a Scan's wakes **coalesce and
downgrade — never silently drop**, and the squelch is visible in the kernel (CONTEXT.md, Wake
delivery). A 10k-ticker Scan on an earnings-heavy day will hit that budget, and it must degrade
legibly rather than lie by omission.

### (h) It ships FIRST — cheapest scan, best candidates

The ordering argument, stated so it can be falsified:

- **Cheapest to evaluate.** A table join, at `session` observability, over a symbol list (d). No
  tick data. The marginal cost of adding a ticker to the universe is a row.
- **Best candidates.** It is the only Scan that fires **before** the catalyst rather than after it.
  Every price-derived Scan in the vocabulary (`move`, `range`, `velocity`) is definitionally
  backward-looking — it fires once the market has already moved. `earnings.in_days <= 1` fires while
  the position can still be *taken*.
- **Highest gate-precision, therefore cheapest cascade.** The gate's cost is `strategist tokens ×
  gate pass-rate` (decision A). A Scan whose candidates are pre-catalyst is a Scan whose passed
  candidates are worth a strategist call. Cheap at the top **and** cheap at the gate.
- **Nothing blocks it.** No grammar change (a), no new SeriesKind (c), no new determinism plumbing,
  no tick lake. It needs a symbol→events table.

That combination — **cheapest to run, best candidates, nothing blocking** — is what makes it the
first Scan and not the fifth.

### (i) What it requires — an events feed, served derived-only

**One new dependency: a calendar/events feed** (earnings dates, FOMC dates, expiry calendars),
historised as-of (f). This is trivial next to Databento: it is a table of a few million rows, not a
tick lake, and its acquisition cost is not in the same order of magnitude as
`plans/2026-07-13-databento-sourcing-and-cost.md`'s.

**It is served derived-only, and date-blindness IS that rule.** Platform `adr/0011:30-34` already
decides that we serve derived outputs — Blotter, Grade, percept — *"never the raw ticks — the
'can't resell the bits' line."* The calendar's derived output is **`in_days`, an integer**; the raw
input is **the date**. So clause (b)'s date-blind fence and the platform's derived-only serving rule
are **the same fence seen from two sides**: the agent gets the countdown, the wire never carries the
date, and the redistribution question mostly evaporates because the redistributable artifact is
never transmitted. (Whatever the events feed's licence says about redistributing *dates* is
therefore largely moot for the served surface — but it must still be read before purchase; see Open
questions.)

### (j) Scans are STANDING and GRADED — the detector leaderboard

Decision (A)'s product insight lands here as a build consequence. **Plans are ephemeral** (a Plan
carries a TTL — CONTEXT.md, Plan). **Scans are standing** (a Wake is Standing — *"authored, armed,
versioned, and superseded"*). The compounding asset is therefore the **portfolio of Scans**, not the
portfolio of Plans — and an asset that compounds is an asset that must be **graded**.

The machinery already exists and needs no extension:

- **Lineage.** *"Names are data… the name is the leaderboard key"* (CONTEXT.md). A Scan authored
  under a recurring name — `pre-earnings-compression` armed fresh on 200 days — is one detector with
  200 instances, and its graded record is a first-class aggregation.
- **Grade.** *"Every run, however produced, carries a Grade"* — including the runs a Scan sourced.
- **The attribution ladder gains its top rung.** ADR-0032's ladder is runtime → watcher →
  strategist → human. Decision (A) inserts the **gate**, and this ADR makes the **Scan** the thing
  the gate is fed by. The natural question a graded Scan answers is *"which screens actually find
  money"* — a **leaderboard of detectors**, which is a more valuable and more forkable artifact than
  a P&L leaderboard, because a detector is a *reusable* claim and a P&L is not.
- **The Armory is the fork registry.** *"A module of vetted, reusable Plan templates… trust
  graduated by provenance"* (CONTEXT.md). A Scan with an earned record graduates into it exactly as
  a Plan name-family does.

**A graded Scan is falsifiable in the only way that matters:** a Scan whose passed candidates do not
beat a random-sample-of-the-universe counterfactual is **refuted-as-posed** — the same cull the
Regime tag already takes (*"Grade culls them (a gate that doesn't beat its ungated counterfactual is
refuted-as-posed)"*, CONTEXT.md). The detector leaderboard is not a vanity surface; it is the cull.

---

## Decision — the UNSCHEDULED half (added 2026-07-14, owner decision)

### (k) NEWS is the second Scan, and it COMPLETES the catalyst space

**The Scan portfolio's first two members are the two halves of one surface:**

- **CALENDAR Scan — scheduled catalysts.** *Anticipatory.* It fires **before** the event, while the
  position can still be taken. Nearly free: a table join at `session` observability (§(d)).
- **NEWS Scan — unscheduled catalysts.** *Immediate.* It fires **when the thing just happened**, on
  a headline nobody had on a calendar. Free: the user's own Alpaca/Benzinga entitlement (§(l)).

Neither subsumes the other, and **neither alone is the catalyst layer**. The calendar cannot see a
halt; news cannot get you positioned ahead of a print. *What you know is coming* + *what just
happened* is the whole of it, and the discovery layer is complete only with both.

The ordering in §(h) is unchanged — **the calendar still ships first** (it needs no new realtime
plumbing, and its economics argument is airtight). News ships **second, and close behind**, because
its cost is also ~zero and its strategic value (Context, points 1–2) is the highest in the portfolio.

### (l) THE SOURCE — Benzinga via Alpaca: the user's OWN entitlement, free to us, redistributed NEVER

**News arrives through the user's own broker credential, exactly as realtime market data already
does.** Alpaca carries Benzinga news; the user's Alpaca key is the user's own entitlement to it.

**The rule, stated so it is testable and so nobody weakens it later:**

> **A news item fetched under a user's broker entitlement is used ONLY within that user's own
> Session. It is never pooled across users, never cached into a shared corpus, never served to a
> second user, and never redistributed in any Rendering that leaves that user's session.**

This is not a new policy. It is `adr/0011:35-38` — *"Alpaca prohibits redistribution, so its data is
used **only via the customer's own account**"* — applied to a second feed on the same wire. Three
consequences follow, and all three are load-bearing:

1. **Zero acquisition cost, zero licence negotiation.** There is **no Databento-class purchase for
   news, and no purchase of any kind.** (Contrast §(i): the calendar half genuinely does need an
   events feed we must buy and licence-review. **News does not.**) The one dependency this ADR's
   news half adds is *a provider hook*, not a vendor.
2. **The pooling prohibition is an ARCHITECTURAL constraint, not a policy note.** A news cache keyed
   by headline-id and shared across Books/users would be the obvious engineering optimisation and it
   is **forbidden**. The provider hook is **per-Session**, dialed through the user's own credential,
   and its output dies with the Session. A shared news store is a redistribution channel wearing a
   cache's clothes.
3. **It composes with derived-only serving.** What may leave the session is what always could: the
   **derived** artifacts (Blotter, Grade, Frame, the gate's verdict and its *reason*) — never the
   headline corpus. The same fence as the tape (`adr/0011:30-34`).

### (m) A news item is a SERIES — no grammar change, exactly as the calendar needed none

§(a)'s argument transfers wholesale. A Series is *"anything with a name whose value changes over
time — the universal operand of the language"* (CONTEXT.md). **News facts are that.** They are
ordinary `PLATFORM_SERIES` registrations, consumed by the plainest predicates the **Trigger algebra**
already has, and they are therefore **grammatical today**:

```kestrel
WHEN news.since_mins <= 5 AND move_pct(5m) > p95
```

**Zero parser work, zero printer work, zero golden-fixture churn.** No new statement kind, no new
predicate, no new keyword — the same "there isn't one" surface change as the calendar. The Registry
doctrine holds: *"Register once → visible to every surface (trigger, pane, grade column) by
construction"*, and *"there is no author-facing `SERIES` statement in v1 — the phonebook is populated
by the platform in code"* (`src/series/registry.ts`).

**No new `SeriesKind` either.** §(c)'s two-and-only-two rule survives: a news fact is a **market
fact** — ambient per signal instrument, spectator-visible. And it needs **no new `SeriesUnit`
beyond §(c)'s relative-time one** (a recency is a duration; see §(n)).

**One new `MarketBinding`: `{ via: "news" }`** — a provider hook, the third way a fact is dialed
from canonical state, in the same shape as the calendar hook (§(c)) and the session-clock hook
(`timeOfDayMinutes`) that already exist. `src/series/trigger.ts` already names this seam:
*"Detector / calendar / fill inputs are provider hooks."*

**Fail-closed, identically to §(e):** no news coverage for an instrument, or a provider that cannot
answer, resolves **UNKNOWN — never a sentinel.** The forbidden failure is the same one, and it is
*worse* here: a provider that returns "no news" (a confident `false`) when the truth is "I cannot
see the feed" makes `news.since_mins <= 5` silently false for every instrument the feed does not
cover, and the Scan reports *"nothing happened"* where the truth is *"I am blind."* **UNKNOWN, and an
UNKNOWN operand never fires** (RUNTIME §3). The Scan's coverage report (§(e), corollary) applies
unchanged: a news Scan must say how much of its universe read UNKNOWN.

### (n) DATE-BLINDNESS — a headline carries RELATIVE time, never an absolute date

**§(b) is not relaxed for news; it is the same fence.** A headline is the single most
de-blinding artifact imaginable if it carries a date — and it is *also* the most de-blinding if it
carries an **identifying dateline, wire timestamp, or a body naming the day**. So:

> **No news Series may resolve to, and no Rendering may emit, a date/day/epoch token — including one
> embedded in headline text.** Recency is a **countdown/duration** (`news.since_mins`), exactly as
> the wake deadline renders `T-92m to close` — *"a date-blind DURATION (never a wall-clock/absolute
> time)"* (`src/frame/render.ts`).

This is *forced*, not chosen (§(b), property 1): `SourceWatermark`'s *when* is `asOfSeq`, an ordinal,
and **no date ever rides a Field**. The existing date-blind grep (`tests/frame.test.ts`,
`tests/session.relative-day.test.ts`) is the enforcement, and it applies to a headline pane the
moment one exists. A calendar-identity leak is **already** a de-certifying event in this codebase
(`src/catalog/status.ts:94`); a **headline** leak would be the same event with a much bigger blast
radius, since a dated headline hands a cutoff-aware model the answer key to the *whole session*, not
merely to one print.

**This is a stronger requirement for news than for the calendar, and it must be said plainly:** the
calendar's value is an integer that structurally *cannot* carry a date. **A headline is text, and
text can carry anything.** So the news half's date-blind fence is a **scrubbing obligation on the
provider hook** — the hook is where datelines/timestamps are stripped, and the Field it produces
carries only relative recency. Rejected alternative 7 below records why scrubbing at the Rendering
layer is not acceptable.

### (o) **THE ARCHITECTURAL RULE — THE GATE FILTERS NEWS *BEFORE* IT ENTERS THE STRATEGIST'S PERCEPT**

**This is the load-bearing clause of the news half.**

> **Unfiltered news never reaches the Frame. The watcher-GATE (ADR-0032 §10.3) triages the headline
> stream, and ONLY headlines the gate escalates may be rendered into the strategist's percept.**

The reason is economic and it is not close. **Headlines are tokens.** A raw news pane — even one
scoped to a Book's Coverage — would be *the single most expensive pane in the system*, and on a busy
day it would dwarf every other percept surface combined, spending frontier tokens at the highest
volume and the worst signal-to-noise the platform has. Rendering the stream into the Frame and
asking the **strategist** to ignore 99% of it is **paying frontier prices for a triage job**, which
is exactly the bad economics ADR-0032 §10.4 exists to refuse:

    ungated:  strategist_cost × (every headline)              ← the most expensive pane in the system
    gated:    gate_cost × (every headline) + strategist_cost × (ESCALATED)
              └──── ~0 ─────────────────┘   └── the only line that grows ──┘

**The gate is precisely the thing that prevents the expensive pane from existing.** So the news
pipeline is, and must be, in this order:

    news feed (user's own Alpaca/Benzinga entitlement)
        → news Series / news Scan  (deterministic, free, high-recall — "a headline touched a covered name")
            → watcher-GATE  (cheap model; ONE binary call per headline: material enough to spend strategist tokens, or stand down?)
                → STRATEGIST percept  (frontier — the Frame renders ONLY escalated headlines)
                    → watcher-MANAGER → HUMAN

**Assertable, not aspirational:** the strategist **never sees a dropped headline**, and that is a
property of the **Bus**, exactly as the gate's existing acceptance criterion states it
(kestrel-58fi: *"THE STRATEGIST NEVER SEES A DROPPED CANDIDATE (assertable on the Bus)"*). The
Frame's headline pane is fed from the gate's **escalations**, not from the feed.

The safety keystone is untouched (ADR-0032 §3, §10.4): the gate sits **below the authoring line** —
it emits a verdict, never an `AgentTurn`. It cannot arm, size, or place. **A weak news gate costs
money in attention and in opportunity; it can never cost money in risk.** And a gate verdict that is
unparseable or UNKNOWN → **STAND DOWN with a logged reason**, with the headline entering the
**sampled audit** (ADR-0032 §11.2) so the miss is eventually priced.

**The audit is what keeps the drop honest.** A gate that silently discards 990 headlines/day is only
trustworthy because a random `p` of the drops is handed to the strategist anyway, and the `(1/p)`
inverse-propensity term makes the false-negative estimate **unbiased**. On news — the highest-volume
drop stream in the system — this is not a nicety; it is the only reason the 99% discard rate is a
*decision* rather than a *hope*.

## Consequences

- **The discovery layer can look forward.** The Scan stops being a post-hoc mover-list and becomes
  an anticipatory screen. This is the first Scan that fires while the trade is still available.
- **Zero grammar change, zero golden-fixture churn.** `print(parse(text))` is untouched; the round-trip
  contract is not renegotiated. The entire feature is a Registry declaration plus a provider hook.
- **Zero new SeriesKind.** The two-and-only-two rule survives. A calendar fact is a market fact —
  ambient, spectator-visible, signal-scoped.
- **Date-blindness is preserved by construction, not by discipline.** The relative encoding is forced
  by `SourceWatermark`'s no-date-on-a-Field rule and enforced by the existing date-blind grep. The
  contamination fence that already de-certifies calendar-identity leaks (`src/catalog/status.ts:94`)
  needs no new clause — a countdown cannot breach it.
- **Sealed seasons stay ranking-eligible with calendar Scans running inside them.** This is the
  commercially load-bearing consequence: decision (F) makes the sealed season **the only
  ranking-eligible tier**, and a date-carrying calendar would have burned it. A countdown does not.
- **The cascade gets a free, wide top layer** — which is precisely the layer ADR-0032's economics
  needed, because the gate exists to make breadth affordable and the calendar Scan is the cheapest
  breadth there is.
- **The graded corpus gains its most tractable RL target's input.** The gate is the RL target
  (decision A); its features are the Scan's candidate rows. A `session`-observable, ~free, wide Scan
  produces **dense** candidate rows at negligible cost — the exact data shape the gate's training
  program (dense, cheap, fast reward) is starved of otherwise.
- **New surfaces to build (all additive; none in this ADR):** the `days` unit and the `calendar`
  MarketBinding in the Registry; the events provider hook (same shape as `timeOfDayMinutes`); the
  as-of events store; the Scan runner over a universe; the Scan's UNKNOWN-coverage report; the
  detector-leaderboard projection over the Ledger. No determinism-core change.
- **One dependency to buy:** an as-of events feed. Small next to the tape, and served derived-only.

### From the news half (§(k)–§(o))

- **The discovery layer becomes COMPLETE.** Scheduled + unscheduled = the whole catalyst surface.
  With the calendar alone, the Scan portfolio is blind on exactly the days the tape moves most.
- **Zero acquisition cost and zero new vendor.** News is free via the user's own Alpaca/Benzinga
  entitlement. **No Databento purchase, no news purchase.** The calendar half's events feed remains
  the only thing this ADR asks anyone to buy.
- **A hard architectural constraint lands: NO SHARED NEWS STORE.** The obvious cache — headline-id →
  text, pooled across users — is **forbidden**, because it is a redistribution channel. The news
  provider hook is per-Session, dialed through the user's own credential, and its output dies with
  the Session (§(l)). Anyone who builds the cache has broken the licence rule, not merely a
  convention.
- **The gate acquires its flagship application and its richest RL corpus** (Context, point 2). The
  news stream is the highest-volume, worst-SNR value-of-information problem in the system —
  simultaneously the most vivid demo of the gate's economics (990 discarded, 10 escalated, with the
  token bill shown both ways) and the densest, cheapest-to-label training data the RL program
  (kestrel-y77 / ADR-0032 §11) will ever get.
- **The most expensive possible pane is prevented BY CONSTRUCTION** (§(o)). An unfiltered news pane
  would be the largest token line in the Frame. Because the gate filters *before* the percept, that
  pane never exists — the Frame renders only escalations.
- **The strategic claim the product rests on gets its sharpest form** (Context, point 1). Everywhere
  else we teach an LLM to read markets — a quant's home ground. On news the LLM reads **natively**,
  and a systematic pipeline must first destroy the text to use it. **If the agentic trader has a
  durable edge over a systematic fund, news judgment is where it lives.**
- **The date-blind fence gets a harder job and a named owner.** A countdown *cannot* carry a date; a
  **headline can**. So the fence becomes a **scrubbing obligation on the news provider hook** (§(n)),
  and the existing date-blind grep must extend to any headline surface. This is the single largest
  new risk the news half introduces, and it is the one to test hardest.
- **New surfaces to build (all additive):** the `news` MarketBinding and the news recency/materiality
  Series in the Registry; the Alpaca/Benzinga provider hook (per-Session, scrubbing, UNKNOWN-never-
  sentinel); the news Scan; the gate-on-news tracer bullet (gated-vs-ungated strategist token cost +
  sampled-audit recall). No determinism-core change and **no grammar change**.
- **Existing work to reuse, not rebuild:** the in-flight news worktree
  `kestrel-4gl13-news-fundamentals`, and the news cells already in the tape corpus —
  `CRYPTO-DAY-ETFNEWS` (spot-ETF approval news day) and `EQ-DAY-EARN` (post-earnings gap day), both
  specified in `docs/plans/2026-07-13-benchmark-matrix-design.md`. **The corpus for grading a news
  gate already exists.**

## Rejected alternatives

1. **A calendar Series that resolves to a DATE (`earnings.date = "2026-07-15"`), blinded at the
   Rendering layer.** Rejected, and it is the important rejection. It would put the leak *inside the
   Field*, one config flag away from every surface, and rely on every Rendering to keep the secret —
   the exact "blinding by discipline" the platform refuses everywhere else. It also breaks
   `SourceWatermark`'s no-date-on-a-Field invariant outright. Blinding must be **structural**: if the
   date is never *in* the value, no surface can leak it.
2. **A dedicated `CALENDAR` statement kind / a fifth surface.** Rejected: it re-litigates ADR-0001's
   four-statement-kinds decision to buy nothing. A calendar is a Series; the trigger algebra already
   consumes Series; the Registry already exists to declare them. Adding a surface here would fork the
   language for a table join.
3. **A third `SeriesKind` (`calendar`) beside `market` and `org`.** Rejected: CONTEXT.md's
   two-and-only-two rule is load-bearing for the market-vs-org routing that every consumer (trigger,
   pane, grade column) dials through one place (`series/provider`). A calendar fact is ambient and
   spectator-visible — the definition of a market fact. A new kind would buy a taxonomy and cost a
   routing seam.
4. **Sentinel values for uncovered tickers (`999` / `+Infinity` / "far away").** Rejected,
   non-negotiable: it converts "I cannot see" into a confident "no catalyst," which is the precise
   silent-false the fail-closed rule exists to forbid. UNKNOWN, and an UNKNOWN operand never fires.
5. **Ship a reactive price Scan first (`move(1d) > p99`), calendar later.** Rejected on the two
   arguments in (h): the reactive Scan is *more* expensive to evaluate (tick-class observability ⇒ it
   needs the tape at 10k breadth) and produces *worse* candidates (it fires after the move). It is
   strictly dominated as a first Scan.
6. **A current-snapshot (non-historised) events table, "fixed later."** Rejected: it is a look-ahead
   channel straight into the sealed season, and a contaminated ranking tier cannot be
   de-contaminated retroactively. Practice-tier-only until the feed is as-of.

### Added by the news half (§(k)–§(o))

7. **Render the raw headline stream into the strategist's Frame and let the frontier model ignore
   the noise.** Rejected, and it is the important rejection of the news half. It pays **frontier
   prices for triage**, making the news pane the largest token line in the system on exactly the
   days the system is busiest — the same bad economics ADR-0032 §10.4 refuses at the discovery end
   and §1 refuses at the management end. **The gate filters before the percept** (§(o)); the Frame
   renders escalations, never the feed.
8. **Scrub datelines/timestamps at the Rendering layer instead of at the provider hook.** Rejected
   for the same reason alternative 1 was: it puts the leak *inside the Field*, one surface away from
   every consumer, and relies on every Rendering to keep the secret. Blinding must be **structural**
   — if the date is never *in* the value, no surface can leak it. The hook is the fence (§(n)).
9. **A pooled/shared news store (headline-id → text, cached across users and Books).** Rejected,
   non-negotiable: it is a **redistribution channel**, and the entitlement is the *user's*, not
   ours (`adr/0011:35-38`). Per-Session, credential-scoped, dies with the Session (§(l)). This is the
   one place where the obvious engineering optimisation is the licence violation.
10. **Buy a news feed (a Databento-class purchase for text).** Rejected as unnecessary: Benzinga via
    Alpaca is already in the user's own entitlement, so the feed is **free and the redistribution
    question never arises**. Paying for news would buy us a licence problem we currently do not have.
11. **A dedicated `NEWS` statement kind / a news-specific predicate.** Rejected for exactly
    alternative 2's reason: a news fact is a Series, the trigger algebra already consumes Series, and
    forking the language for a provider hook buys nothing (§(m)).
12. **A "no news" sentinel for instruments the feed does not cover.** Rejected, non-negotiable, and
    it is alternative 4 with a bigger blast radius: it converts *"I am blind to the feed"* into a
    confident *"nothing happened"* across the whole uncovered universe. UNKNOWN, and an UNKNOWN
    operand never fires (§(m)).

## Open questions

1. **Which expiry does `expiry.in_days` mean?** The front monthly, the front *listed* (which for a
   same-day-expiry-heavy name is today), or the expiry of the chain the Coverage names? Three defensible
   answers, and the 0DTE application makes the choice non-obvious. Resolve before registration —
   a series' meaning is not a runtime option.
2. **`in_days` counts what — calendar days or SESSIONS?** Friday-with-earnings-Monday is `1` in
   sessions and `3` in calendar days, and the trader means sessions. Recommend **sessions** (it is
   the unit every other relative-time surface uses), but it must be stated on the registration, not
   assumed — and the `days` unit name should probably be `sessions`.
3. **Signed or floored?** Does the countdown go **negative** after the event (`earnings.in_days =
   -1` = "printed yesterday" — a real and tradeable post-catalyst regime), or does it reset to the
   *next* event and lose the recency? A signed encoding is strictly more expressive and costs
   nothing; the risk is that `<= 1` then also matches everything post-print, which is a **footgun in
   the trigger algebra**. Likely answer: keep `in_days` non-negative (next event) and register a
   *separate* `earnings.since_days` if the post-catalyst regime earns it — but this is a real design
   choice, not a detail.
4. **The events feed's licence.** Derived-only serving (i) makes the redistribution question mostly
   moot for the served surface, but "mostly" is not a licence review. Read it before purchase — and
   note that unlike the Databento question (`adr/0011:57-59`), this one is not currently blocking
   anything.
5. **Universe definition.** "~10k tickers" is a placeholder. Which universe — all US listed, a
   liquidity floor, an optionable filter? This is a Scan-authoring question (the Scan names its
   universe), not a Registry one, but the first Scan has to pick.
6. **Does the Scan register as a Series itself?** A Scan's *output* — "candidates today" — is a
   thing whose value changes over time, and the detector leaderboard (j) wants to grade it. Whether
   that makes it an org fact registered on first authorized write (`noteOrgWrite`) or a projection
   over the Bus is an open modelling question, and it is the one that decides how `gate_alpha` is
   computed.

### Added by the news half (§(k)–§(o))

7. **What, exactly, are the news Series?** `news.since_mins` (recency of the last headline touching
   the instrument) is the obvious first registration and the one §(m)'s example uses. Is there a
   second — a **count** (`news.count(30m)`), or a provider-supplied **materiality/importance** tier?
   A provider-supplied score is tempting and is **half a gate in the vendor's hands**; taking it
   uncritically would be trusting Benzinga's triage instead of learning our own, which is the exact
   judgment the RL corpus exists to earn. Recommend registering **recency and count** (facts the
   provider cannot get wrong) and treating any vendor score as a *feature the gate may read*, never
   as a Series a trigger fires on. Not decided.
8. **Where does the headline TEXT live between the gate and the strategist?** The gate reads text
   (that is its whole job); the strategist reads escalated text. Both are in-Session (§(l)), so
   there is no licence question — but there *is* a Bus question: is an escalated headline a **typed
   Bus event** (replayable, seq-ordered, content-addressed — as `kestrel-hnw4.1` does for oversight
   messages) or a percept-only artifact? Determinism says **Bus event**, and byte-identical replay
   of a news session is the test that decides it. Unresolved, and it is the news half's biggest
   open plumbing question.
9. **Replay determinism for a live news feed.** A recorded tape replays byte-identically; a *live*
   Benzinga stream is wall-clock-arrival-ordered and therefore is **not** a deterministic input
   unless it is anchored to `seq` on arrival. The recorded news cells (`CRYPTO-DAY-ETFNEWS`,
   `EQ-DAY-EARN`) are fine. **The live path needs the same as-of discipline §(f) imposes on the
   calendar**, and it is not obviously the same mechanism. Resolve before any live news session.
10. **What is the gate's news unit of decision — the headline, or the (headline × Book) pair?** The
    same headline is material to one Book and noise to another, so the honest unit is the pair — but
    that multiplies the gate's call volume by the number of Books, and the demo's arithmetic (§(o))
    is stated per-headline. This decides what the RL corpus's rows actually *are*, so it must be
    settled before the corpus is generated, not after.
11. **How much of the ~99% discard rate is real?** The "990 of 1000 discarded" figure is the
    *expectation*, not a measurement. The sampled audit (ADR-0032 §11.2) is what turns it into a
    number, and the tracer bullet's job is to produce that number honestly on a real stream.