Public publishers publish daily fixings through a supplier session

Table of Contents

1. Summary

Central banks and other public bodies publish rates every day. Each of them is a publisher, with its own ids, credit and terms. A supplier is who we connect to, and Frankfurter is one: it delivers many publishers' daily rates over one endpoint. A session is one configured adapter at a supplier, and a feed binds one publisher to one session and one service — here the end_of_day service.

One process, ores.marketdata.official, hosts one specialised adapter for each access route: Frankfurter is the first, and each publisher with its own API, such as the New York Fed, gets an adapter of its own. Each fetches what its publisher publishes and sends it as ticks over the adapter contract, as the simulator does. The values are official fixings, so they are durable: the ingest keeps them in a tick class of their own, with no age gate and no expiry. When a synthetic feed starts, it reads the latest fixing on or before the date of the market clock, so a feed that stops and starts again takes the most recent published value. The catalogue starts wide, with many publishers that publish the same pairs, so the case of one pair from several publishers is exercised from the first day.

2. Detail

2.1. The shape

publishers (ECB, Bank of England, Bank of Canada, FRED, IMF, ...)
       |  one feed each, on a supplier session and the end-of-day service
       v
suppliers (Frankfurter, ECB Data Portal, FRED, BoE, Treasury)
       |
sessions (one configured adapter each, e.g. frankfurter-eu)
       |
the public sources adapter  -- announce, demand, tick, heartbeat -->  ores.marketdata
       (backfill on start, then daily)                              | map supplier id -> item
                                                                    | durable class: no age gate
                                                                    v
                                                    durable tick table (kept for good)
                                                                    ^
                                                                    |  latest on or before, window
                                                    synthetic feed at start (calibration)
  • The adapter knows nothing of stochastic processes. It publishes what it fetched, in the supplier's own notation.
  • The service knows nothing of HTTP. It maps, checks, stores and delivers, as for any adapter.
  • A feed never fetches. It reads the store. When no fixing is stored, the feed keeps its fixed level and the screen says so.

2.2. Each publisher is a publisher, delivered by a supplier

A publisher is a row in the publisher catalogue, of kind reference. It owns its ids and its terms; it is not a delivery path. Frankfurter is a supplier: one endpoint, one session (frankfurter-eu), many feeds. A feed such as ecb-frankfurter names the publisher ecb, the session, the end_of_day service and the provider key the supplier needs. A publisher can also be reached through another supplier, and the two feeds are two paths to the same publisher's items.

The supplier owns its vendor ids: a market_data_vendor_instrument names each value in the supplier's own notation, and a market_data_mapping maps it to an oresmd item. The mapping belongs to the publisher, because the same value can arrive through more than one supplier.

The design is generic. The catalogue is data, so a new publisher on an existing route is a row and a set of vendor ids, not code. A new route is a fetcher, and one fetcher can serve many publishers: Frankfurter alone reaches about a hundred. The counts are in "Coverage at the start".

2.3. The item is an FX fixing

A central bank's daily rate is an official fixing taken once a day, not a quote of the live market. Its item is an FX index, the ORE index FX-SOURCE-CCY1-CCY2:

oresmd://fx/EUR?type=fixing&index=fx&source=ECB&ccy=USD     (FX-ECB-EUR-USD)
oresmd://fx/EUR?type=fixing&index=fx&source=BOE&ccy=USD     (FX-BOE-EUR-USD)

The publisher is part of the identity, because two banks' fixings of one pair are two numbers taken at two times. So EUR/USD from the ECB, the Bank of England, the Bank of Canada and the IMF are four items. Each is a series of its own publisher. A pair covered by several publishers is the duplicate case: the calibration has several fixings for one pair and chooses by a rank, and the screens and the demand show several providers for one underlying.

The item being a fixing also keeps the durable rule apart from live FX spot. The tick class is keyed by the instrument type, so the fixings can be durable while the spot quotes stay perishable.

2.4. A durable class with its own table

A tick is a quote or a fixing, and the item says which: a fixing's item is an index, which oresmd_uri_codec::read_index reads. The two classes meet different rules.

  • The ingest drops a quote whose vendor time is older than its age limit, twice: in the translator and again in the worker that drains the quotes' queue. A fixing skips the age check, and never enters that queue.
  • A fixing takes a path of its own: a queue that never sheds and one thread that stores what it holds. A daily volume is small, and a backfill waits in the queue rather than being lost.
  • The table of the class already exists. market_fixing is a hypertable keyed by the party, the series and the fixing date, with no retention, and a correction is a new version. A fixing is stored there, in the series of its index for each consumer, created as the ingest creates a quote's series. The tick table, with its seven days, never sees a fixing.
  • A backfill arrives in order, oldest first, for each vendor id, so the order rule still holds.
  • A fixing whose series already holds that value on that date is skipped, so a backfill that overlaps the store, after a restart, adds nothing. A different value for a stored date is a correction and adds one version.
  • A fixing whose item names no index can never be stored, so the store rejects and counts it, and the others of its batch are stored. A failure of the database is retried: the batch goes back to the front of the queue, after a wait of two seconds that doubles to a minute, five times at most. While a batch waits, the fixings of every source wait behind it, which suits a daily volume.
  • A fixing is a value of a date, so the vendor time gives only its date. The time of day a provider publishes matters to when the adapter asks, not to what the store keeps.

2.5. The public sources adapters

One process speaks the adapter contract for every public source. It hosts one adapter for each access route, and each adapter is specialised: it reads one API in one format under one set of terms. Two publishers share an adapter only when their API and format are the same, as the Frankfurter providers are.

  • The split. An adapter is a fetcher, which is the specialised half, and the shared half, which is the same for every publisher. The fetcher names its adapter code and the notation of its ids, reads its publisher's universe, and reads what it published over a window. The shared half announces, answers the demand set, sends a heartbeat, backfills, and publishes ticks. A source names the adapter that reads it, as <source name>:<provider code>:<adapter code>:<key>.
  • Fetchers. Frankfurter is the first. SOFR from the New York Fed, €STR from the ECB Data Portal, SONIA from the Bank of England and CORRA from the Bank of Canada follow, for the overnight rates. A fetcher takes its transport as an interface, so its tests run on stored responses with no network.
  • One publisher a request. The Frankfurter fetcher asks for one provider at a time, /v2/providers/<key>/rates, and never for the blended rate. Frankfurter rebases every provider's rates to the euro unless the request names a base, so the fetcher names the provider's own pivot, base=<pivot>, and refuses a row in any other base: an item names the pivot, and a row in another base would be filed under the wrong item. For a provider whose pivot is not the euro, the digits are Frankfurter's division back to the pivot, which may differ from the publisher's in the last place. The blended rate is Frankfurter's own sum and hides which bank the number came from.
  • Backfill, then daily. On start the adapter fetches the window that the calibration needs for each demanded item, oldest first. After that it polls each publisher once a day, after its publication time, from the last day it sent. It does not remember what it sent across a restart; the store does, and skips what it already holds.
  • HTTPS. The only outbound HTTP client today, in ores.storage, takes http:// alone. The adapter gets an HTTPS GET built on the Boost.Beast and OpenSSL that the build already links.
  • The address is a setting. The hosted Frankfurter is a convenience. A team can point the adapter at its own Frankfurter in Docker without a change of code.

2.6. Terms

Every provider has terms, Bloomberg and LSEG included, so the terms are columns of provider: the terms page, the credit it asks for, whether it allows the values to be stored, when the terms were accepted, by whom, and the version of the terms. The system administrator accepts or declines each public provider in the first run, and again when its terms change. See the task Show the public sources' terms in the first run.

The adapter never fetches for a provider whose terms are not accepted. A provider with no terms page of its own is not offered until someone reads its own site and records what it says.

When a provider's terms change after its values are stored, the acceptance clears and the adapter stops fetching it until the system administrator accepts again. The stored values stay, because they were taken under the terms in force when they were taken, and each value's provider and time say which version that was. If the new terms withdraw permission to store, the system administrator deletes that provider's fixings from the durable table, which records who did it.

2.7. Demand

The adapter answers a demand set like any adapter. The system party subscribes to each public provider and routes to its source, so the items it calibrates from are in the demand. A tenant may subscribe too, and reads the fixings through its own series and its entitlement, as for any source.

2.8. The reader

The reader has two questions: the latest fixing of an item on or before a date, and the fixings of an item over a window. Each answer names the provider, the vendor id, the vendor time and the time it was received. It follows the snapshot reader that already returns the latest tick at or before an instant, on the durable table.

The calibration is its own task: a feed's config names the fixing items it may start from, with a rank, and a policy (latest_on_or_before_clock or exact:<date>). See Calibrate the synthetic ticks from free public sources.

2.9. Coverage at the start

Frankfurter lists 106 providers, 41 of them with a terms page. Among those 41, USD, EUR, GBP, JPY, CHF and CAD are each quoted by six or more: the ECB, the Bank of England, the Bank of Canada, FRED (H.10), the IMF, the Bank of Japan, Norges Bank, the Czech National Bank and others. The first catalogue takes every daily provider of the 41 whose terms allow storage, and so covers the main pairs several times over. The list was read from /v2/providers on 2026-10-28.

Three of the providers are not daily: the BIS is monthly, the US Treasury reporting rate quarterly, and Japan Customs weekly. They wait for the class policy to say how stale a monthly fixing may be.

2.10. Secrets

A credential is never in a table, a log or a stored URL. A provider row holds a credential_ref, such as env:ORES_MARKETDATA_FRED_API_KEY, and a secret_source interface resolves it. The first implementation reads the service environment, as the database passwords are read. Frankfurter needs no key.

2.11. How this design changed

  • 2026-10-23: a table of its own. The first design loaded the public values into a new table, published_quote, with a publisher catalogue, a series mapping and a fetch and load provenance: five new entities beside the market data service.
  • 2026-10-28: the existing tables. Before any of it was built, the question was asked whether the existing tables carry it. The catalogue, the vendor mapping and the DQ pipeline held four of the five. An H.10 dataset already travels the DQ path into market_observation and the feeds read it as a vintage.
  • 2026-10-28: an adapter. DQ is for canned data, a snapshot that stays fixed. A public source publishes every day, and a feed that starts again should take the newest value. So a public source is an adapter that publishes ticks, the publisher is a provider, and the values are durable fixings. The new parts are the durable class, the adapter, the provider terms and the reader. DQ keeps the canned vintages.
  • 2026-11-09: one adapter for each route. The overnight rates come from publishers with APIs of their own, so the adapter split into a shared half and one fetcher for each route, all hosted in the one process.

3. See also

Emacs 29.3 (Org mode 9.6.15)