Skip to content

Record rates in a data plugin

This page is for the author of a data plugin for a rates publisher: a central bank's or a treasury's fixings, averages, index levels or curves. It records them with meridian.roles.data.rates and passes the data suite's rates cases. What a rate is, and how it is read, is in Rates. Start from Write a data plugin against its suite: this page adds what is different for rates.

You need SDK 0.36.0 and a deployment on chart 0.1.538, which reads a dataset's terms and pricing. meridian plugin new on CLI 0.1.60 writes a plugin on SDK 0.35.0, which meridian plugin migrate moves to 0.36.0.

Declare the dataset

A dataset of rates declares the data type meridian.v1.Rate:

DatasetDeclaration(
    key="ust_par",
    vendor="My publisher",
    data_types=["meridian.v1.Rate"],
    modes=["pull"],
    cadence=86_400,
    history=3650,
    terms_and_pricing=(
        "Public domain; free of charge. Cite as \"Source: My publisher\". "
        "Not affiliated with or endorsed by My publisher."
    ),
    day_time_zone="America/New_York",
    gap_recovery="backfill",
)

A publisher's terms often ask to be cited. Say so in terms_and_pricing, in the publisher's words: its copyright, whether it may be passed on, the attribution it asks for and any notice that the plugin is not affiliated with it, in at most 2,000 characters. Data sources shows the text to the deployment admin, whose licence is the one enforced (see Licences and entitlements).

Resolve the subjects

A rate's subject is a referential record the platform seeds: a reference rate, a curve or an index. Feed.subject resolves it by the identifiers the publisher states, the administrator's code as a symbol at its source and, where one exists, a FIGI or an ISIN, once per process. A record the deployment does not hold is reported missing once a day, and its rows are not recorded: a plugin never mints one. One missed is asked again the same day, after 5, then 15, then every 60 minutes, and its miss is still reported once a day.

from meridian.roles.data import rates

feed = rates.Feed(plugin, "ust", wants=wants, day_zone="America/New_York")
curve = await feed.subject(("symbol", "PAR_NOMINAL", "ust"), instrument_type="rate_curve")

Build the rows and record them

Feed.rate builds one rate; Feed.curve builds a curve's points for one date, one row per tenor the publisher published, and none for a tenor it left empty. A bare number is a percent; rates.index_level(value, base) is an index's level. Every rate carries its convention: name it as the publisher states it, with the constants for the common ones: MONEY_MARKET (actual/360, simple), BOND_EQUIVALENT (semi-annual compounding), DISCOUNT (actual/360, a bill's discount) and CONTINUOUS. An index level takes the convention of the rate it compounds, the SOFR Index MONEY_MARKET. Feed.rate refuses a rate with none, with a clear error, before the lake does.

rows = feed.curve(
    curve, "ust_par", "par_yield",
    {"1 Mo": "4.12", "3 Mo": "4.09", "10 Yr": "4.21", "20 Yr": None},
    business_date=day, published_ns=published_at, convention=rates.BOND_EQUIVALENT,
    raw_key=raw,
)
await feed.record("ust_par", rows)

rates.tenor reads a publisher's labels (1 Mo, 13 WEEKS, 10 Yr, 3M, ON) as the interface's tenor; a fraction of a month, such as 1.5 Month, is refused, for the plugin to name a maturity date or a whole tenor instead. Feed.record sends 500 rows a batch, against a want where they answer one, and through the plugin's Wants, which counts them and records their age at the edge.

Row keys and revisions. The row key names the series, kind, tenor or maturity, convention, business date and subject, never the published time, so a same-day revision is the next version of the same row and an identical repeat is answered unchanged. Always pass published_ns where the publisher states it: a read by publication time depends on it.

One point of another source's curve. A single-point series that is a curve's point, such as a 10-year yield, is recorded against the curve's record with its tenor (Feed.rate(..., tenor="10Y")), so it sits beside the curve's own publisher under the source priority.

Hold it to the suite

The data suite's rates cases are about: rates. Map each to the plugin's own conversion of its publisher's recorded answer with data.testing.rates_producers, and name everything a rates-only plugin never presents with data.testing.RATES_NOT_PRESENTED: the cases about prices, trades, quotes and outcomes (among them an-exact-price and a-want-recorded-against, about closes), and, from SDK 0.33.0, the chain's, the filings slice's and the quote-quality cases. The feed delay is required: map reports-its-feed-delay with data.testing.feed_delay, the plugin's roles.Latency reporting it.

A dataset's liveness. Changed in SDK 0.32.2: RATES_NOT_PRESENTED no longer names records-a-datasets-liveness. A polled plugin recording each poll its publisher answers as its dataset's liveness (Wants.liveness(key)) presents it; one recording none adds it to not_presented, with why, such as "it polls hourly and records no liveness".

from meridian.roles.data import testing

PRODUCERS = {
    **testing.rates_producers(MY_CASES, source="ust"),
    **MY_OTHER_PRODUCERS,                   # the cases every data plugin answers
}
report = run("data", PRODUCERS, not_presented=testing.RATES_NOT_PRESENTED,
             instance_id="reference-1")

MY_CASES maps each of data.testing.RATES_CASES to async def case(feed, instruments), which converts the publisher's answer for the case and records it through the feed given. A case your publisher never presents goes in not_presented too, with why.