ADR-0009: Market data and the risk-free rate
Status: Accepted Date: 2026-10-11 Stage: ADR-0002 stage 8 (roadmap 1.8), story 1.8.4; shapes 1.8.5 and 1.8.6 (now 1.8.7)
Context
The Bank of England's rate reaches the bank today as a float64 through
core.BaseRateSource: the simulation's RateSeries, parsed from a CSV
that cmd/boefetch pulls from the Bank's database, answers BaseRate(day);
the bank copies the answer into boeRate at each day roll, accrues
reserve interest from it, stores it in daily_snapshots.boe_rate as a
double and charts it on the products page. It is the last floating-point
rate in the bank after story 1.8.3, and the only input the bank reads
that no component owns and no record explains.
ADR-0006 planned to make it "a bank-scoped parameter the simulation writes". Two facts about the rate itself say that is the wrong owner:
- The Bank publishes Bank Rate at 12:00 London on a decision day, and it
is effective from that moment. The official series since 1694 is on
the Bank's page, and the same series is served as data (IADB series
IUDBEDR). It is a fact about the pound, not a decision of this bank. - Every currency has its own: the ECB's deposit facility rate for the euro, the Fed's target range for the dollar, each decided and published by a different process on a different calendar. A bank that holds more than one currency reads more than one.
And one fact about the bank: how it passes a change on, by how much and after what lag, is the bank's policy, which ADR-0007 puts in the bank's own module as code that may carry an algorithm. The contract must therefore not model lag and take as data; it only needs to let a rule read the market.
Decision
A currency's official risk-free rate is market data, read by the bank as a parameter at a scope of its own and written only by its feed.
| Term | Meaning |
|---|---|
| Market | The process that sets a currency's official risk-free rate and publishes it: for GBP the Bank of England, Bank Rate. One per currency. |
| Risk-free rate | The rate the market publishes, in basis points, signed. What a bank's policy derives its own rates from and what the treasury earns on reserves. |
| Source | Where the market publishes: a page for people, a feed for machines, a publication time. GBP: the Bank's Bank Rate page, the IADB series IUDBEDR, 12:00 London. Code in bank/market, one entry per currency. |
| Decision day | A day the market has said it will publish, from the calendar it issues ahead (the Bank's MPC dates are set a year out). Known in advance; what will be published is not, and often it is no change. |
| Publication | One change: the new rate, effective at the instant published, decided at that instant, by the market. A setting at the market scope, owner the currency, key risk_free_rate_bps. A decision day that leaves the rate as it was publishes nothing the bank records. |
| Feed | What writes publications into the bank: in the demo the simulation replaying the historical series; in a deployment a poller on the source's feed. Either goes through one core command. |
| Instant | A moment, not a day. A setting is effective from an instant; a rule's parameter for a day is its value at the end of that day. |
The scope
The contract (gobank-products) gains ScopeMarket. A declaration at
that scope names the key only; the owner is the currency of the product
whose rules read it, which the runner knows (Product.Currency). The
treasury reads the bank's own currency's rate the same way, through the
parameters component, with no declaration because it is not a rule.
| Scope | Owner | Written by | Read by |
|---|---|---|---|
| market | the currency | the feed, through core.MarketCommands.Publish |
the treasury; the policy (1.8.6, now 1.8.7); any version that tracks the market directly |
| bank | "bank" | staff; the policy's published values | the policy, versions |
| version | the product's ID | staff; the rules' published values | versions |
| account | the ledger account | the rules at open | versions |
Nothing but the feed writes at the market scope: the staff page does not offer it, and the policy's spread and lag are bank-scoped parameters of its own. A market setting is the record of what the market said, and the bank cannot say otherwise.
Instants
parameter_settings.effective_from is already a timestamp; the day
truncation in Set and Resolve goes. A rule still asks for a parameter
by day (Parameter(key, day)), and the runner resolves it as the value
in effect at the end of that day:
| Setting | Day asked for | Outcome |
|---|---|---|
| effective 12:00 on d | d | the new value: a publication on d counts for the whole of d's accrual, which is the Bank's own convention |
| effective 12:00 on d | d − 1 | the old value |
| effective 00:00 on d (a setting made by day, as the rules' account settings are) | d | the new value, as today |
| effective on d, decided after the moment asked at | any | invisible: a correction is a later decision (ADR-0006) |
The same rule serves the treasury's reserve accrual, so a cut at noon lowers the whole day's reserve interest. Publication times are stored in UTC; the source names its local time (12:00 London) and the feed converts.
Who writes, and when
The core gains MarketCommands.Publish(ctx, Publication), one command
for both feeds. The simulation replays the series: when the bank opens it
publishes everything up to the end of the day in progress, and thereafter
each day's publications at the day roll that closes the day, before the
bank reads the day's rate; until ADR-0010 the clock is the warped wall
clock of stage 4, which flat out never reaches noon, so a publication is
seen on the console the morning after. A publication the bank already has
is not recorded again, so a resumed run, or one upgraded from before the
market was a parameter, replays its whole past once. A deployment's
poller is named here and not built: it reads the feed, compares with the
rate resolved now, and publishes a difference.
A bank with no market setting for its currency on a day cannot start
that day: StartDay refuses before the pass and the console names the
silent market. ADR-0006's start-up refusal for a missing bank parameter
stands; a market parameter is refused at the day instead, because the
feed is a core command and so runs after the bank opens.
Storage
| Table | Owner | Change |
|---|---|---|
parameter_settings |
bank/parameters | none: the market scope is a value of scope; effective_from keeps its full precision |
daily_snapshots |
bank/history | risk_free_bps BIGINT added; rows from before it are read from boe_rate rounded; boe_rate is still written as the decimal for the older build of the drill's rollback, and goes a release later |
bank/market owns no table (ADR-0001): the sources are code and the
publications are settings. core.BaseRateSource, BaseRateFunc,
RatePoint.Rate and Position.BoERate go; RatePoint.Bps and
Position.RiskFreeBps replace them.
Choices made, and the alternative to each
| Choice | Alternative | Why this one |
|---|---|---|
| A scope of its own, owner the currency | the bank scope with a prefixed key (boe.base_rate_bps), as ADR-0006 had |
the market is not the bank: the bank cannot set it, and a second currency is a second owner, not a second key |
| Written only through one core command | the simulation writing settings directly, as it reads RateSeries today |
the feed and the simulation are the same thing to the bank; stage 9's tests publish a rate the same way |
| Lag and take left out of the contract | Derivation{Lag, Take} as data |
pass-through is the bank's policy, an algorithm in its own module (ADR-0007); data would be a product designer by another name |
| Effective at an instant; a day's value is its end-of-day value | effective by day, publications dated the day | the Bank publishes at noon and means that day; by-day storage could not say when, and the event clock (ADR-0010) needs the instant |
| Refuse the day, not the open, when the market is silent | refuse the open | the feed is a command on an open bank; refusing the open would need a second path into the settings before the bank exists |
| A rate column in basis points beside the double, both written for a release | convert the column in place | the drill's rollback runs the older build on the same database; it reads boe_rate and ignores the new column |
Consequences
- The treasury is the first reader of a parameter outside the runner:
bank/parametersgains a reader the bank itself uses, and the float-free rule (TestNoFloatMoneyStorage's family) covers the whole bank. - The sim's
RateSeriesbecomes a replay of publications; its CSV andcmd/boefetchstay as the series' source, now named bybank/market's GBP entry. - What is upcoming at the market scope is a decision day, never a value: the market's calendar says when it will speak, not what it will say, and a publication is effective when made. The calendar is data of the source, not built here; with the event clock (ADR-0010, story 1.8.5) a decision day is an event on the timeline, at which the feed publishes a change or nothing, and the products page can name the next one. Upcoming values on the products page are staff's settings, from the same story.
- A day the market has published no rate for is refused, and the console shows the refusal beside the day until a publication lets it start.
- A second currency is a second
Source, a second owner in the settings and, for the treasury, a question this ADR does not answer: which currencies' reserves it holds. Out of scope until a product is not GBP. - ADR-0006's resolution table is amended by this ADR: the bank-scope start-up refusal stands; a market-scope miss refuses the day.