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 oresmd URI with only the fields that requirement actually constrains — oresmd://ir/eur?role=discount names "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=curve names 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 of ir, 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 direct oresmd counterpart below. A boost::urls::url implementation 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 from entity (equities, credit, commodities — an FX pair already encodes both currencies in entity, and IR's entity already 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 of discount, projection, self_discounting (the degenerate single-curve case current synthetic data uses, where one curve serves both roles) — IR only.
    • type: instrument type, one of fixing (a rate fixing/index), curve (a whole curve), quote (one raw quote/point), or vol (a volatility surface point) — used by every asset class.
    • quote: only meaningful when type=quote — the ORE quote TYPE (e.g. ir_swap, mm, cds, fwd), the first segment of ORE's TYPE/METRIC/... quote key. A per-asset-class enum, independent of the metric column: for an IR ir_swap the metric is RATE, for a basis_swap it is BASIS_SPREAD. Absent, it defaults to the asset class's canonical quote type (spot for FX/equity/commodity, cds for credit), and the projection yields no quote key where no default exists (IR and inflation, whose quote is required for a type=quote projection); correlation never uses it, since its only quote type is hardcoded CORRELATION/RATE.
    • metric: only meaningful when type=quote — disambiguates what kind of quote point refers 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 from tenor=/=point rather than folded into either. A per-asset-class enum (e.g. basis_spread, ratio, price, yield_spread); absent, it defaults from the quote type's own metric (an ir_swap defaults to rate, a basis_swap to basis_spread).
    • model: only meaningful when type=vol — the volatility model subtype, one of rate_lnvol, rate_nvol, rate_slnvol, shift, price. Defaults to rate_lnvol.
    • point: the term-structure or surface coordinate for a quote=/=vol URI (a swap tenor, a curve point, or a comma-separated surface coordinate like 5y,2y,atm — expiry, tenor, strike for type=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=6mtenor=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> where curve_id is <entity><tenor> with the tenor's unit letter and any separators stripped (e.g. USD3M, USD1D, EUR6M), per ORE's Yield/<CCY>/<CURVE_ID> convention.
  • Quote key (type=quote or type=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 an oresmd field alone: IR_SWAP/RATE's settlement/spot-lag dimension (2D in the worked USD/EUR examples below) is a per-currency market convention (2 business days for USD/EUR, but not universal), looked up from entity rather than carried by any oresmd query 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's ICAP=/=GFI broker 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 approach external_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&quote=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&quote=ir_swap&metric=rate&point=5y IR_SWAP/RATE/USD/2D/3M/5Y
oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=quote&quote=discount&metric=rate&point=6m DISCOUNT/RATE/USD/USD3M/6M
oresmd://ir/eur?index=euribor&tenor=3m&type=quote&quote=mm&metric=rate&point=1m MM/RATE/EUR/EURIBOR/3M/1M
oresmd://ir/eur?index=euribor&tenor=3m&type=quote&quote=fra&metric=rate&point=6m FRA/RATE/EUR/EURIBOR/3M/6M
oresmd://ir/usd?index=libor&tenor=3m&type=quote&quote=imm_fra&metric=rate&point=5y IMM_FRA/RATE/USD/LIBOR/3M/5Y
oresmd://ir/eur?index=euribor&tenor=3m&type=quote&quote=basis_swap&metric=basis_spread&point=5y BASIS_SWAP/BASIS_SPREAD/EUR/EURIBOR/3M/5Y
oresmd://ir/eur?tenor=3m&type=quote&quote=cc_basis_swap&metric=basis_spread&point=5y CC_BASIS_SWAP/BASIS_SPREAD/EUR/3M/5Y
oresmd://ir/usd?tenor=3m&type=quote&quote=cc_fix_float_swap&metric=rate&point=5y CC_FIX_FLOAT_SWAP/RATE/USD/3M/5Y
oresmd://ir/usd?tenor=3m&type=quote&quote=bma_swap&metric=ratio&point=5y BMA_SWAP/RATIO/USD/3M/5Y
oresmd://ir/eur?index=euribor&tenor=3m&type=quote&quote=zero&metric=rate&point=5y ZERO/RATE/EUR/EURIBOR/3M/5Y
oresmd://ir/eur?index=euribor&tenor=3m&type=quote&quote=zero&metric=yield_spread&point=5y ZERO/YIELD_SPREAD/EUR/EURIBOR/3M/5Y
oresmd://ir/eur?index=euribor&tenor=3m&type=quote&quote=mm_future&metric=price&point=cme MM_FUTURE/PRICE/EUR/EURIBOR/3M/CME
oresmd://ir/usd?index=sofr&tenor=3m&type=quote&quote=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&quote=dividend&point=1y EQUITY_DIVIDEND/RATE/AAPL/USD/1Y
oresmd://equity/lufthansa?ccy=eur&type=quote&quote=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&quote=hazard_rate&point=sr,5y HAZARD_RATE/RATE/VOD/SR/EUR/5Y
oresmd://credit/vod?ccy=eur&type=quote&quote=recovery_rate&point=sr RECOVERY_RATE/RATE/VOD/SR/EUR
oresmd://credit/cdx-na-ig?ccy=usd&type=quote&quote=cds_index&point=5y,0.1 CDS_INDEX/BASE_CORRELATION/CDX-NA-IG/5Y/0.1
oresmd://credit/2i65byeg6?ccy=usd&type=quote&quote=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&quote=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&quote=zc_swap&point=5y ZC_INFLATIONSWAP/RATE/UKRPI/5Y
oresmd://inflation/ukrpi?type=quote&quote=yy_swap&point=5y YY_INFLATIONSWAP/RATE/UKRPI/5Y
oresmd://inflation/ukrpi?type=quote&quote=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&quote=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_requirement variant (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-resolved ir_market_data_identifier with index=/=tenor=/=role all absent, because those fields simply do not apply to a type=vol instrument. index=/=tenor=/=role=/ =metric=/=point are therefore std::optional on both the identifier and the requirement — the fields that differ are which of them resolve(...) 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 by type (a curve needs index=/=tenor=/=role settled; a vol URI does not). Only ccy and type are unconditionally mandatory on ir_market_data_identifier, since every IR instrument has both.
  • Controlled vocabularies (asset_class, instrument_type, curve_role, metric, and — closing the same free-text gap index_name has 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, and point are 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 generic point type IR's swap-tenor/curve-point case uses (carrying seniority+tenor for credit, or a forward tenor for commodity), exactly mirroring how their URI-level point query key already absorbs those dimensions rather than getting dedicated fields of their own.

market_data_urn_types.png

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

Emacs 29.3 (Org mode 9.6.15)