oresmd: ORE Studio Market Data URI
Table of Contents
Summary
oresmd is a proposed, structured ORE-Studio-native market data
identifier — a real RFC 3986
URI (oresmd://<asset_class>/<entity>?...),
not a bespoke string format — designed to close every gap
the gap analysis task found:
no structured tenor field (so same-currency, different-tenor indices
collide), no curve-role field (so discount and projection curves for
the same currency can't be told apart), and no single field ORE Studio's
own schemas key off that projects deterministically into ORE's index
name, curve key, and quote key. Because its optional metadata (tenor,
curve role, index family, …) lives in the URI's query string rather
than fixed path positions, the same grammar covers every asset class —
FX, interest rates, swaptions, equities, credit, commodities, inflation,
correlation — without per-asset-class variants or NONE placeholders.
It generalises the one
place ORE Studio already gets this right
(fx_spot_generation_config.ore_key)
and supersedes the free-text qualifier in
the existing market_series
key by giving it a structured source to be deterministically generated
from, rather than typed by hand.
Logical vs. physical: oresmd spans both, by how fully specified it is
Market Data Requirement
draws a distinction between a logical requirement ("the EUR discount
curve", stated independently of any particular instance that satisfies
it) and the physical instance it resolves to (one concrete,
bootstrapped curve object). Because oresmd's query parameters are
already optional and conditional on asset class, the same URI can
express both ends of that distinction — not two different schemes, but
two different points on one specificity spectrum:
- A requirement is an
oresmdURI with only the fields that requirement actually constrains —oresmd://ir/eur?role=discountnames "the EUR discount curve" without yet committing to which benchmark index builds it or what kind of thing (fixing/curve/quote) is being asked for. - A resolved instance is the same URI with every field its asset
class expects filled in —
oresmd://ir/eur?index=estr&tenor=1d&role=discount&type=curvenames one concrete curve.
Resolving a requirement to an instance, in these terms, is filling in
the fields the partial URI left unspecified — exactly the step
Market Data Requirements and
Resolution already describes in the abstract, given a concrete,
structured shape here for the IR/curve-role case the gap analysis
flagged, where today's free-text index_name gives it none. A
requirement-level URI can still resolve to more than one instance-level
URI over time (a different vintage, a different source) — resolution
picks one, it does not collapse the distinction between the two levels.
Worked resolution examples: one requirement, several possible instances
A single requirement typically under-specifies more than one field at once, so more than one resolved instance can legitimately satisfy it — resolution is a one-to-many relationship until the consumer's context narrows it down to one.
Requirement: oresmd://ir/usd?index=libor&tenor=3m&role=projection
"The USD 3M-LIBOR projection curve" — silent on what kind of thing is being asked for, so it resolves to any of:
| Resolved instance | What this resolution is | ORE Studio config it corresponds to |
|---|---|---|
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=fixing |
The index/fixing itself — projects to index name USD-LIBOR-3M. |
Implicit in an ir_curve_generation_config row's index_name field, not a standalone config today. |
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=curve |
The whole curve — projects to curve key Yield/USD/USD3M. |
The ir_curve_generation_config row itself (currency_code=USD, index_name=USD-LIBOR-3M) — the role this row lacks a field for today. |
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=quote&metric=par_rate&point=5y |
A 5Y par swap rate quote on that curve — projects to quote key IR_SWAP/RATE/USD/2D/3M/5Y. |
A market_series row (series_type=IR_SWAP, metric=RATE, qualifier=USD/2D/3M, point_id=5Y) — not a generation config, a published market observation. |
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=quote&metric=discount_factor&point=6m |
A directly-quoted discount factor sampled off that curve at 6M — projects to quote key DISCOUNT/RATE/USD/USD3M/6M. |
A market_series row (series_type=DISCOUNT, metric=RATE, qualifier=USD3M, point_id=6M). |
Requirement: oresmd://ir/eur?role=discount
"The EUR discount curve" — silent on both the benchmark and the kind of thing, so it resolves to any of:
| Resolved instance | What this resolution is | ORE Studio config it corresponds to |
|---|---|---|
oresmd://ir/eur?index=estr&tenor=1d&role=discount&type=fixing |
The EUR-ESTR fixing itself. |
Implicit in an ir_curve_generation_config row's index_name field. |
oresmd://ir/eur?index=estr&tenor=1d&role=discount&type=curve |
The whole discount curve — curve key Yield/EUR/EUR1D. |
The ir_curve_generation_config row (currency_code=EUR, index_name=EUR-ESTR) — again, role is a field this row has no equivalent of yet. |
oresmd://ir/eur?index=estr&tenor=1d&role=discount&type=quote&metric=discount_factor&point=2y |
A discount factor at the 2Y point — quote key DISCOUNT/RATE/EUR/EUR1D/2Y. |
A market_series row (series_type=DISCOUNT, metric=RATE, qualifier=EUR1D, point_id=2Y). |
Each resolved instance keeps every field the requirement already
specified unchanged (usd=/=eur, index, tenor, role) and adds
only the fields the requirement left open — resolution is strictly
additive, never a field the requirement already pinned down being
overwritten.
Why a URI, and why query parameters
URI, not URN
An earlier draft of this design called itself a "URN" while actually
proposing a flat, colon-delimited positional string
(mdurn:<scope>:<index_family>:...) with no real hierarchical
structure — that was a mislabel. A URN (urn:isbn:...) names a
resource with no authority/path semantics at all; what this design
actually needs is a real RFC 3986 URI with a genuine
scheme://authority/path?query structure, so it can be parsed and
built with a standard URI library (Boost.URL,
boost::urls::url, despite the library's name, parses any RFC 3986 URI
generically — not just HTTP URLs) instead of a bespoke parser.
The conditional-grammar problem, and why query parameters solve it
The instrument set this URI must cover is not shaped the same way
across asset classes: an interest-rate fixing needs a currency, an
index family, a tenor, and a curve role; an FX spot needs only a
currency pair; an equity needs a ticker and a currency; a credit
instrument needs a reference entity, a currency, a seniority, and a
tenor. A single fixed-position grammar (field 1, field 2, field 3, …)
cannot represent this without either a different grammar per asset
class (defeating the point of one canonical scheme) or padding every
inapplicable position with a placeholder like NONE (verbose, and a
magic string rather than a structural fact).
Approach considered: pure hierarchical (path-only) grammar
oresmd://<asset_class>/<entity>/<ccy>/<index_family>/<tenor>/<curve_role>/<instrument_type>[/<point>...]
| Pros | Compact; every field is visible at a glance in one string; prefix-matching/globbing works naturally (oresmd://ir/usd/libor/3m/* matches every USD 3M LIBOR record), which would map cleanly onto this codebase's existing dot-delimited NATS subject convention (see Market Data Identifier's / → . mapping); no ambiguity about parameter order, since position is the key; "rhymes" with the slash-delimited shape already familiar from ORE's own canonical key and market_series's series_type/metric/qualifier[/point_id]. |
| Cons | Fixed position count does not survive asset-class variation — equities need (entity, currency), credit needs (entity, currency, seniority, tenor), IR needs (currency, index family, tenor, curve role) — forcing NONE padding for every position an asset class doesn't use, or silently drifting into per-asset-class variant grammars; inserting a new optional field later renumbers every downstream position (a breaking change); "this field does not apply" and "this field is empty" are both spelled the same way (NONE), which is not type-safe. |
Approach adopted: asset-class authority, entity path, everything else as query parameters
oresmd://<asset_class>/<entity>?ccy=<CCY>&index=<FAMILY>&tenor=<TENOR>&role=<ROLE>&type=<TYPE>&point=<POINT>
| Pros | Naturally conditional — each asset class simply documents which query keys it uses (see the per-asset-class table below); no NONE placeholders, no renumbering when a field is added, no per-asset-class variant grammar; order-independent (two URIs differing only in query-key order are trivially equivalent, unlike a positional scheme where order is meaning); maps directly onto boost::urls::url's .params() accessor — parsing and validating "does this asset class expect a tenor key?" is a per-asset-class allow-list check, not a bespoke positional parser; unknown query keys can be ignored by older consumers without breaking, so the scheme is extensible. |
| Cons | Longer than the pure hierarchical form; two semantically-identical URIs can still differ in query-key ordering unless generation follows a documented canonical key order (parsing must still accept any order); loses the pure hierarchical scheme's "glob the whole identifier" property, since curve role/tenor/index family are query parameters, not path segments — a NATS-subject-style projection of oresmd would need its own flattening rule (query params sorted and dot-joined after the path), separate from the identifier grammar itself, the same way market_data_identifier.org's existing ORE-key-to-NATS-subject mapping is already a separate rule from the key format it maps. |
The query-parameter approach is adopted for this design, on the
strength of the conditional-grammar argument — it is the direct fix
for exactly the problem the hierarchical approach cannot solve without
either NONE padding or per-asset-class grammars.
Grammar
oresmd://<asset_class>/<entity>[?<query>]
asset_class(the URI's authority/host component, not a path segment — the//before it makes it so): one ofir,fx,equity,credit,commodity,inflation,correlation— matches the asset-class sections of External market data identifiers exactly, so every worked example in that document has a directoresmdcounterpart below. Aboost::urls::urlimplementation reads this via.authority()=/.host()=, not.segments().entity(path, mandatory): the free-form subject — a currency (usd) for IR, a 6-character currency pair (eurusd) for FX, a ticker or index code (aapl,sp5) for equities, a reference-entity name (itraxx-europe,vod) for credit, a commodity code (gold,wti) for commodities, an inflation index code (ukrpi) for inflation, and a hyphen-separated factor pair (ccy-eur-usd) for correlation.- Query parameters (all optional; each asset class documents which it
uses):
ccy: settlement/quote currency, when distinct fromentity(equities, credit, commodities — an FX pair already encodes both currencies inentity, and IR'sentityalready is the currency).index: benchmark index family (libor,euribor,sofr,estr,sonia,tona, …) — IR only.tenor: the index's own tenor (3m,6m,1d, …) — IR only.role: curve role, one ofdiscount,projection,self_discounting(the degenerate single-curve case current synthetic data uses, where one curve serves both roles) — IR only.type: instrument type, one offixing(a rate fixing/index),curve(a whole curve),quote(one raw quote/point), orvol(a volatility surface point) — used by every asset class.quote: only meaningful whentype=quote— the ORE quote TYPE (e.g.ir_swap,mm,cds,fwd), the first segment of ORE'sTYPE/METRIC/...quote key. A per-asset-class enum, independent of themetriccolumn: for an IRir_swapthe metric isRATE, for abasis_swapit isBASIS_SPREAD. Absent, it defaults to the asset class's canonical quote type (spotfor FX/equity/commodity,cdsfor credit), and the projection yields no quote key where no default exists (IR and inflation, whosequoteis required for atype=quoteprojection); correlation never uses it, since its only quote type is hardcodedCORRELATION/RATE.metric: only meaningful whentype=quote— disambiguates what kind of quotepointrefers to, since a coordinate on an IR curve is ambiguous between a par rate observed in the market (par_rate) and a directly-quoted discount factor sampled off the curve (discount_factor) — see below for why this is a separate field fromtenor=/=pointrather than folded into either. A per-asset-class enum (e.g.basis_spread,ratio,price,yield_spread); absent, it defaults from thequotetype's own metric (anir_swapdefaults torate, abasis_swaptobasis_spread).model: only meaningful whentype=vol— the volatility model subtype, one ofrate_lnvol,rate_nvol,rate_slnvol,shift,price. Defaults torate_lnvol.point: the term-structure or surface coordinate for aquote=/=volURI (a swap tenor, a curve point, or a comma-separated surface coordinate like5y,2y,atm— expiry, tenor, strike fortype=vol) — absent for scalar quotes (FX spot, equity spot, correlation, recovery rate).
Tenor vs. point, and the par-rate/discount-factor ambiguity
tenor and point are easy to conflate but answer different
questions: tenor identifies which curve (USD-LIBOR-3M and
USD-LIBOR-6M are structurally different curves, each built from its
own instruments), while point is a coordinate along whichever curve
tenor identified. Asking for "the 6M point on the USD 3M-LIBOR
projection curve" is therefore
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=quote&metric=discount_factor&point=6m
— tenor=3m (which curve) and point=6m (where on it) are independent
query keys, not a collision.
That still leaves one genuine ambiguity a point on an IR curve does
not resolve by itself: per the
ORE market data catalogue,
a term-structure coordinate can be either a par rate actually observed
in the market at that point (IR_SWAP/RATE, a par swap rate) or a
directly-quoted discount factor sampled off an already-built curve
(DISCOUNT/RATE, documented as "a curve construction alternative to
bootstrapping") — two different ORE quote types for two genuinely
different kinds of market observation, both of which are legitimately
"a point on a curve". metric is the field that disambiguates them;
neither tenor nor point alone can, since both quote types can share
the same tenor=/=point pair while meaning different things.
Field justification
Every field is justified by a specific finding in the gap analysis — no speculative fields:
| Field | Gap it closes |
|---|---|
asset_class |
New, but not speculative — needed the moment the design covers equities/credit/commodities, since those have no currency-shaped "scope" the way IR/FX do; without it there is no way to know whether entity=gold means an FX pair, a ticker, or a commodity code. |
entity (currency/pair/ticker/reference-entity/commodity code) |
Carries forward what is already present today (currency_code, or the equivalent identifying field per asset class) — not a new concept, just a name broad enough to cover every asset class's "the thing this is about". |
index |
Closes: "index_name is free text … nothing stops a typo'd index_name from silently colliding" — index is a fixed benchmark-family token, not free text. |
tenor |
Closes the tenor-index gap directly: "two configs sharing currency_code… can only be told apart if their free-text index_name strings happen to differ" — tenor is now a distinct, structured query key, so usd/libor/3m and usd/libor/6m are structurally distinct URIs, not string-lookalikes. |
role |
Closes the discount-vs-projection gap directly: "nothing records that a USD-LIBOR-3M curve is a projection curve, not the discount curve" — role is an explicit query key with no default that could be silently wrong. |
type |
Closes: curve_feed_controller's qualifier "has no concept of curve family… or of curve role, so it cannot express 'these two curves legitimately coexist'" — by separating what kind of thing (fixing/curve/quote/vol) from the coordinates, two records can now share every other field while differing only in type without being mistaken for a collision. |
quote |
A gap surfaced while applying this design: a type=quote URI with only type=/=metric=/=point does not name which ORE quote TYPE it is — the same point on the same metric can be quoted as different TYPE/METRIC pairs (IR_SWAP/RATE vs MM/RATE vs ZERO/RATE). quote names that TYPE (the first segment of ORE's quote key) as a per-asset-class enum, so a market observation is pinned to exactly one catalogue quote type; without it, projection would have to guess. |
point |
Closes: market_series's "qualifier is a single free-text column, not a per-type fixed-dimension tuple" — point is the structured slot for exactly the tenor/coordinate market_series currently has no dedicated field for. |
metric |
Closes a gap surfaced while applying this design, not the original gap analysis directly: without it, tenor=+=point alone cannot distinguish a par-rate quote (IR_SWAP/RATE) from a curve-sampled discount factor (DISCOUNT/RATE) at the same coordinate — two different ORE quote types the catalogue documents as alternatives to each other, not variants of the same thing. |
model |
A gap surfaced while applying the volatility-surface sub-schema: a type=vol URI's point coordinate (expiry, tenor, strike) does not name the model whose surface it samples — RATE_LNVOL, RATE_NVOL, RATE_SLNVOL, SHIFT, or PRICE — and the parser must not silently default it. model names that subtype as an enum, defaulting to rate_lnvol. |
Projection rules
Deterministic, worked for each field combination:
- Index name (
type=fixing):<entity>-<index>-<tenor>for indexed benchmarks (e.g.USD-LIBOR-3M);<entity>-<index>for overnight benchmarks with no tenor suffix (e.g.USD-SOFR,EUR-ESTR) — mirrors ORE's own index-name convention exactly. - Curve key (
type=curve):Yield/<entity>/<curve_id>wherecurve_idis<entity><tenor>with the tenor's unit letter and any separators stripped (e.g.USD3M,USD1D,EUR6M), per ORE'sYield/<CCY>/<CURVE_ID>convention. - Quote key (
type=quoteortype=vol): per the ORE market data catalogue's documented per-type shapes — see the worked examples below, one per asset class, for the exact mapping each asset class uses. Not every dimension in every catalogue shape is derivable from anoresmdfield alone:IR_SWAP/RATE's settlement/spot-lag dimension (2Din the worked USD/EUR examples below) is a per-currency market convention (2 business days for USD/EUR, but not universal), looked up fromentityrather than carried by anyoresmdquery parameter — the same kind of external-convention lookup the Bloomberg/RIC projection below needs, just for a dimension internal to ORE's own quote key rather than an external vendor notation. - Bloomberg/RIC (best-effort only, per the survey — these are
vendor-licensed and not fully systematic): a lookup table keyed on
(
asset_class,entity,index,tenor) is required rather than a pure string transform, since — per External market data identifiers — neither vendor's convention is a deterministic function of these fields alone (e.g. RIC'sICAP=/=GFIbroker suffixes, Bloomberg's per-asset-class ticker shape). The URI does not attempt to generate a valid RIC/Bloomberg string; it only records which one a given URI corresponds to, where known, the same reference-table approachexternal_market_data_identifiers.org's worked examples already use.
Worked examples
One per asset class, matching
External market data
identifiers's own worked instruments exactly, so every row in that
comparison table has a direct oresmd counterpart here.
FX
| oresmd URI | Quote key |
|---|---|
oresmd://fx/eurusd?type=quote |
FX/RATE/EUR/USD |
oresmd://fx/eurusd?type=quote"e=fwd&point=6m |
FXFWD/RATE/EUR/USD/6M |
quote defaults to spot; fwd (forward points) is tenor-shaped and
needs point.
Interest rates
| oresmd URI | Index name | Curve key | Quote key |
|---|---|---|---|
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=fixing |
USD-LIBOR-3M |
Yield/USD/USD3M |
— |
oresmd://ir/usd?index=libor&tenor=6m&role=projection&type=fixing |
USD-LIBOR-6M |
Yield/USD/USD6M |
— |
oresmd://ir/usd?index=sofr&tenor=1d&role=discount&type=fixing |
USD-SOFR |
Yield/USD/USD1D |
— |
oresmd://ir/eur?index=euribor&tenor=6m&role=projection&type=fixing |
EUR-EURIBOR-6M |
Yield/EUR/EUR6M |
— |
oresmd://ir/eur?index=estr&tenor=1d&role=discount&type=fixing |
EUR-ESTR |
Yield/EUR/EUR1D |
— |
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=quote"e=ir_swap&metric=rate&point=5y |
— | — | IR_SWAP/RATE/USD/2D/3M/5Y |
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=quote"e=discount&metric=rate&point=6m |
— | — | DISCOUNT/RATE/USD/USD3M/6M |
oresmd://ir/eur?index=euribor&tenor=3m&type=quote"e=mm&metric=rate&point=1m |
— | — | MM/RATE/EUR/EURIBOR/3M/1M |
oresmd://ir/eur?index=euribor&tenor=3m&type=quote"e=fra&metric=rate&point=6m |
— | — | FRA/RATE/EUR/EURIBOR/3M/6M |
oresmd://ir/usd?index=libor&tenor=3m&type=quote"e=imm_fra&metric=rate&point=5y |
— | — | IMM_FRA/RATE/USD/LIBOR/3M/5Y |
oresmd://ir/eur?index=euribor&tenor=3m&type=quote"e=basis_swap&metric=basis_spread&point=5y |
— | — | BASIS_SWAP/BASIS_SPREAD/EUR/EURIBOR/3M/5Y |
oresmd://ir/eur?tenor=3m&type=quote"e=cc_basis_swap&metric=basis_spread&point=5y |
— | — | CC_BASIS_SWAP/BASIS_SPREAD/EUR/3M/5Y |
oresmd://ir/usd?tenor=3m&type=quote"e=cc_fix_float_swap&metric=rate&point=5y |
— | — | CC_FIX_FLOAT_SWAP/RATE/USD/3M/5Y |
oresmd://ir/usd?tenor=3m&type=quote"e=bma_swap&metric=ratio&point=5y |
— | — | BMA_SWAP/RATIO/USD/3M/5Y |
oresmd://ir/eur?index=euribor&tenor=3m&type=quote"e=zero&metric=rate&point=5y |
— | — | ZERO/RATE/EUR/EURIBOR/3M/5Y |
oresmd://ir/eur?index=euribor&tenor=3m&type=quote"e=zero&metric=yield_spread&point=5y |
— | — | ZERO/YIELD_SPREAD/EUR/EURIBOR/3M/5Y |
oresmd://ir/eur?index=euribor&tenor=3m&type=quote"e=mm_future&metric=price&point=cme |
— | — | MM_FUTURE/PRICE/EUR/EURIBOR/3M/CME |
oresmd://ir/usd?index=sofr&tenor=3m&type=quote"e=oi_future&metric=price&point=cme |
— | — | OI_FUTURE/PRICE/USD/SOFR/3M/CME |
Tenor-index gap, resolved
The libor/3m and libor/6m rows above are the exact same-currency,
different-tenor-index pair the gap analysis flagged as
indistinguishable today (USD-LIBOR-3M vs a hypothetical
USD-LIBOR-6M, both only distinguished by string content of a free-text
index_name). Under oresmd, tenor is a distinct query key: the two
URIs differ structurally at that key, not just in string content, and
project to distinct curve keys (USD3M vs USD6M) automatically.
Discount-vs-projection gap, resolved
The EUR-EURIBOR-6M and EUR-ESTR rows are the same-currency
discount-vs-projection pair the gap analysis flagged: both are EUR,
but EUR-EURIBOR-6M's URI has role=projection while EUR-ESTR's has
role=discount. curve_feed_controller (or its eventual
oresmd-aware successor) can now express "these two curves legitimately
coexist because one discounts and one projects" as a structural fact
about the two URIs, rather than an unstated assumption about what two
similar-looking index_name strings happen to mean.
Swaptions
| oresmd URI | Quote key |
|---|---|
oresmd://ir/eur?type=vol&point=5y,2y,atm |
SWAPTION/RATE_LNVOL/EUR/5Y/2Y/ATM |
oresmd://ir/eur?type=vol&model=rate_nvol&point=5y,2y,atm |
SWAPTION/RATE_NVOL/EUR/5Y/2Y/ATM |
(index=/=tenor=/=role are absent here — a swaption vol surface point
is about the underlying swap's expiry/tenor/strike coordinate, carried
entirely in point, not about a benchmark index the way a fixing or
curve is; this is exactly the per-asset-class conditionality the
query-parameter grammar exists to express. model names the volatility
model subtype — rate_lnvol by default, or rate_nvol=/=rate_slnvol=/
=shift=/=price — since the same coordinate can be quoted under
different models. The shared volatility_surface_point struct carries
the three point dimensions (expiry, tenor, strike) plus model_subtype
as typed fields.)
Equities and indices
| oresmd URI | Quote key |
|---|---|
oresmd://equity/aapl?ccy=usd&type=quote |
EQUITY/PRICE/AAPL/USD |
oresmd://equity/aapl?ccy=usd&type=quote"e=dividend&point=1y |
EQUITY_DIVIDEND/RATE/AAPL/USD/1Y |
oresmd://equity/lufthansa?ccy=eur&type=quote"e=fwd&point=6m |
EQUITY_FWD/PRICE/LUFTHANSA/EUR/6M |
(ccy is required here since, unlike IR where entity already is the
currency, an equity's entity is a ticker — the quote currency is
independent information. quote defaults to spot; dividend and
fwd are tenor-shaped and need point.)
Credit
| oresmd URI | Quote key |
|---|---|
oresmd://credit/itraxx-europe?ccy=eur&type=quote&point=sr,5y |
CDS/CREDIT_SPREAD/ITRAXX-EUROPE/SR/EUR/5Y |
oresmd://credit/vod?ccy=eur&type=quote"e=hazard_rate&point=sr,5y |
HAZARD_RATE/RATE/VOD/SR/EUR/5Y |
oresmd://credit/vod?ccy=eur&type=quote"e=recovery_rate&point=sr |
RECOVERY_RATE/RATE/VOD/SR/EUR |
oresmd://credit/cdx-na-ig?ccy=usd&type=quote"e=cds_index&point=5y,0.1 |
CDS_INDEX/BASE_CORRELATION/CDX-NA-IG/5Y/0.1 |
oresmd://credit/2i65byeg6?ccy=usd&type=quote"e=index_cds_tranche&point=5y,0.07 |
INDEX_CDS_TRANCHE/BASE_CORRELATION/2I65BYEG6/5Y/0.07 |
(point carries both the seniority and tenor dimensions here, since
credit's quote-key shape has one more dimension than IR's — again, the
query-parameter grammar absorbs this without needing a dedicated
seniority key for every asset class that happens not to need one.
quote defaults to cds; the base-correlation types (cds_index,
index_cds_tranche) drop the ccy segment from their quote key,
carrying only the index/tranche + point.)
Commodities
| oresmd URI | Quote key |
|---|---|
oresmd://commodity/gold?ccy=usd&type=quote |
COMMODITY/PRICE/GOLD/USD |
oresmd://commodity/wti?ccy=usd&type=quote"e=fwd&point=6m |
COMMODITY_FWD/PRICE/WTI/USD/6M |
quote defaults to spot; fwd and cpr are tenor-shaped and need
point.
Inflation
| oresmd URI | Quote key |
|---|---|
oresmd://inflation/ukrpi?type=quote"e=zc_swap&point=5y |
ZC_INFLATIONSWAP/RATE/UKRPI/5Y |
oresmd://inflation/ukrpi?type=quote"e=yy_swap&point=5y |
YY_INFLATIONSWAP/RATE/UKRPI/5Y |
oresmd://inflation/ukrpi?type=quote"e=seasonality&point=jan |
SEASONALITY/RATE/MULT/UKRPI/JAN |
The entity is the inflation index code (e.g. ukrpi); there is no
ccy — the index code already identifies the index. quote is
required (no default). The seasonality shape is the one 5-segment
exception in the family, with a literal MULT segment.
Correlation
| oresmd URI | Quote key |
|---|---|
oresmd://correlation/ccy-eur-usd?type=quote"e=pairwise |
CORRELATION/RATE/CCY-EUR-USD |
The entity is a hyphen-separated factor pair (ccy-eur-usd = EUR vs
USD), with the factor-class prefix (ccy) as its first segment; the
quote key is the entity itself, uppercase, after CORRELATION/RATE.
quote is never used (pairwise is the only quote type); point is
rejected.
Data model
A structured URI is only half the design — code needs typed objects to
build/parse/validate it against, not raw strings. The type system below
keeps the same logical/physical distinction as the rest of this
document at the type level, not just the value level: a
..._requirement type and a ..._identifier type exist per asset
class, so a partially-specified requirement and a fully-resolved
instance are never the same type — a function that needs a resolved
instance simply cannot be called with a requirement, a compile-time
guarantee the string-based oresmd URI alone cannot give.
Deliberately no abstract base class or virtual dispatch: one
independent, strongly-typed, plain struct per asset class
(fx_market_data_identifier, ir_market_data_identifier,
equity_market_data_identifier, credit_market_data_identifier,
commodity_market_data_identifier, inflation_market_data_identifier,
correlation_market_data_identifier), each exposing only the fields
that asset class actually uses (an fx_market_data_identifier has no
index_family=/=tenor=/=curve_role members at all, rather than
NONE-valued ones), mirroring the query-parameter grammar's own
conditionality at the type level. Every identifier struct also carries
a volatility_surface_point (expiry, strike, volatility_model_subtype,
the model whose surface the point samples) for type=vol instruments,
though only swaption vol projects a vol quote key today. The seven
identifier structs are tied together only as a std::variant<...>
(aliased market_data_identifier for readability), never as siblings
of a common base — since the URI's asset_class authority component
already tells a consumer which concrete struct it is looking at, a base
class/virtual interface would buy nothing an ordinary tagged union does
not already provide, and it would come at a real cost: reflection-based
serialisation libraries (e.g. rfl=/reflect-cpp, a natural fit for
round-tripping these plain structs to/from JSON or the database)
generally handle plain structs and =std::variant well but do not
handle polymorphic class hierarchies gracefully. Three free functions
do the asset-class dispatch instead of a virtual call:
parse_oresmd(uri) -> market_data_identifier inspects the URI's
asset_class component and constructs the matching concrete struct
inside the variant; to_uri(market_data_identifier) -> oresmd_uri does
the reverse via std::visit; resolve(market_data_requirement, ...) ->
market_data_identifier takes a requirement plus whatever additional
information narrows it (a source, a vintage, an explicit choice among
the fields still open) and, via std::visit over the requirement
variant, either produces the matching identifier variant member or
fails if fields remain unresolved.
- A parallel
market_data_requirementvariant (same seven asset classes, same "no base class" shape) mirrors the identifier variant field-for-field. Note that "resolved" does not mean "every field on the identifier class is non-null" — the swaption worked example above is a fully-resolvedir_market_data_identifierwithindex=/=tenor=/=roleall absent, because those fields simply do not apply to atype=volinstrument.index=/=tenor=/=role=/ =metric=/=pointare thereforestd::optionalon both the identifier and the requirement — the fields that differ are which of themresolve(...)is obliged to have settled (one way or the other, including "confirmed not applicable") before it will produce an identifier at all, a check that varies bytype(acurveneedsindex=/=tenor=/=rolesettled; avolURI does not). Onlyccyandtypeare unconditionally mandatory onir_market_data_identifier, since every IR instrument has both. - Controlled vocabularies (
asset_class,instrument_type,curve_role,metric, and — closing the same free-text gapindex_namehas today —index_family) are enums, not strings, so an invalid value is a compile error or a construction-time validation failure, not a silently-accepted typo. tenor,currency, andpointare not reinvented here — a real implementation would bind these to whatever this codebase's own domain types for tenor and currency already are, rather than introduce competing ones; credit and commodity reuse the same genericpointtype IR's swap-tenor/curve-point case uses (carrying seniority+tenor for credit, or a forward tenor for commodity), exactly mirroring how their URI-levelpointquery key already absorbs those dimensions rather than getting dedicated fields of their own.
Figure 1: Proposed oresmd type system: seven independent, non-inheriting structs per side (identifier/requirement), tied together only as a std::variant per side; free functions (parse_oresmd, to_uri, resolve) dispatch on the URI's asset_class rather than using virtual calls.
Source: market_data_urn_types.puml.
Regenerate with plantuml -tpng doc/knowledge/domain/market_data_urn_types.puml.
Relationship to existing schemes
oresmd is designed as ORE Studio's single identifier scheme, not one
more scheme coexisting alongside fx_spot_generation_config.ore_key,
market_series's free-text qualifier, and ir_curve_generation_config's
(currency_code, index_name) pair — those are all superseded, not
wrapped. The per-field migration plan (what each legacy field maps to,
and what gets deleted once migration is complete) is tracked as
follow-on implementation work in
Migrate to oresmd, delete
market_series qualifier and ore_key, rather than in this design
document.
See also
- Gap analysis: ORE Studio identifiers vs ORE's real addressing scheme — the gap table every field above is justified against.
- Survey ORE, Bloomberg, and Reuters market data notations and External market data identifiers — the survey material the projection rules (especially the Bloomberg/RIC best-effort mapping) are grounded in, and the worked instruments this document's examples are matched to.
- ORE market data catalogue — the authoritative per-type quote-key shapes the quote-key projection rule is built from.
- Market Data Identifier — the existing
market_serieskeyoresmdsupersedes. - Market Data Requirement — the logical side of the logical/physical distinction
oresmdsits on the physical side of. - Dual-curve discount+projection model — the still-BACKLOG task whose data model should agree with this URI's
rolequery key on one representation. - Migrate to oresmd, delete market_series qualifier and ore_key — the follow-on implementation capture tracking the per-field migration and deletion plan.