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
Markdown
# 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.