Task: Give every fixing index name an oresmd identity
Table of Contents
- 1. Goal
- 2. Status
- 3. Acceptance
- 4. The index names, and what each class needs
- 5. The decision that gates the rest
- 6. Decided
- 7. The power contract-hour shape, against ORE's own grammar
- 8. The remaining classes, and the two rows that are market data
- 9. The generic index, and the two keys that are not index names
- 10. The last three classes, and the allowlist they close
- 11. Plan
- 12. Measurements
- 13. Notes
- 14. Test Scenarios
- 15. PRs
- 16. Review
- 17. Result
This page documents a task in the Clean ores.marketdata to the component clean standard story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Every distinct fixing index name in external/ore/examples resolves to an oresmd identifier, so a market_fixing row can carry an oresmd_uri and the identity cutover has a way back for the fixing half of the corpus.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Clean ores.marketdata to the component clean standard |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-30 |
3. Acceptance
- from_index_name names every distinct index name the corpus carries, asserted by a corpus-driven test whose allowlist is empty. Met: the class list is empty and the census reads 156 of 156 index names.
- Each asset class the corpus puts in the fixing space has a stated mapping, or a recorded reason it has none. Met: the two rows that are not index names are the only ones with a reason, and it is recorded.
- The corpus fixing import creates a series for every fixing row, with no skipped-name warnings. Not met, and not this task's: the import still writes the index name into the series qualifier and never calls from_index_name, which is the identity cutover's work. Review row 11 records it.
4. The index names, and what each class needs
Measured over external/ore/examples (every *fixing*.txt and *fixing*.csv
outside ExpectedOutput): 158 distinct index names. They fall into seven shapes
as first read, and only the first was nameable at that point. COMM- and POWER-
are one shape up to the prefix but two classes in the census below, because the
prefix is what the class tag is: 25 and 48 names.
| class | count | shape | nameable now | what it needs |
|---|---|---|---|---|
| ir | 57 | CCY-FAMILY[-TENOR] |
34 | 23 families the enum does not hold |
| inflation | 7 | a bare index code | 0 | the inflation class already models it |
| equity | 8 | EQ-<ticker-or-ric> |
0 | a currency the name does not carry |
| fx | 7 | FX-<source>-<CCY1>-<CCY2> |
0 | a source field |
| commodity | 25 | COMM-<EXCHANGE>:<CONTRACT>-<YYYY-MM> |
0 | a currency, a delivery period |
| power | 48 | POWER-<EXCHANGE>:<CONTRACT>[-<YYYY-MM-DD>[-<SEC>-<SEC>[-DST]]] |
0 | a delivery window, and the class |
| security | 2 | BOND-ISIN:<isin>[-<month>] |
0 | the security class already models it |
| (unclassified) | 4 | GENERIC-... |
0 | split below: generic for the two index names, a refusal for the two keys |
The interest-rate 57 split two ways. The ones whose family the enum holds
are nameable; 22 of the rest are real benchmark families it does not: BBSW,
CAMARA, REPOFIX, CZEONIA, PRIBOR, CIBOR, DKKOIS, CMS, EONIA,
FedFunds, NIBOR, SIOR, STIBOR, SIFMA, HIBOR. The twenty-third,
JPY-TONAR, is the spelling ORE writes for tona. The identifier already carries
index_spelling for exactly this – "the spelling that arrived is carried beside
the family so the key reads back as it came" – so a spelling needs an alias beside
its family, and the missing families need adding. AONIA is neither: the family
token is matched case-insensitively, so it has always resolved to aonia.
Two of the four GENERIC names are not fixings at all. They are
GENERIC-MD/EQUITY_OPTION/PRICE/RIC:.SPX/USD/2025-10-03/3300/C and its sibling:
market-data keys that appear inside a fixings file, read by the whole-line parser
as fixings because that is the file they are in. Whether the reader should refuse
a key that is not an index name is part of this task. The other two,
GENERIC-JuniorNote and GENERIC-PortfolioBasket#1, are a curve name and a
basket name, and need classifying rather than mapping.
5. The decision that gates the rest
Four of the six classes need a field the index name does not carry:
- an equity fixing (
EQ-SP5) names a ticker and no currency, and the equity identifier requires one; - a commodity or power fixing (
COMM-NYMEX:CL-2024-12,POWER-ICE:PDQ-2021-01-04-14400-18000) names a contract and no currency, and the commodity identifier requires one; - an FX fixing (
FX-ECB-EUR-USD) names a source as well as a pair, and the FX identifier has no field for one.
Two readings, and they produce different permanent URIs:
- A fixing's identity does not include a currency. For
type=fixingthe currency is a property of where the instrument is quoted, not of which index it is, so the grammar should not require it.oresmd://equity/sp5?type=fixingandoresmd://commodity/nymex:cl-2024-12?type=fixing, each carrying a contract as its point where there is one. This is the reading the IR case already follows in reverse – there the currency is in the name, so the identifier has it – and it needs no field the corpus cannot supply. - The caller supplies the missing fields. The fixing boundary would look the
currency up (refdata knows a ticker's currency) and add
ccyto the URI. This keeps the identifier's invariants intact and moves the problem to the import, but it makes the URI depend on reference data: the same file would produce different URIs in two databases that disagree.
The recommendation was the first, with the FX source carried as a spelling beside
the pair the way index_spelling carries one. The owner took the first and kept
the source as a field rather than a spelling, because a spelling is orthographic
and the two sources are two different rates; the decision is recorded under
* Decided.
6. Decided
Requiredness follows the entity kind: a fixing and a quote do not share a set of required fields, because they do not answer the same question. A quote says what a thing is worth and in what currency; a fixing says what an index was on a date. So the currency is required of every type that is not a fixing, and not of a fixing – which is what the parser now enforces, with the type carrying it as optional rather than mandatory.
This settles the equity and commodity half of the gap. Still open, and separate:
- the interest-rate families the enum does not hold –
EONIA,CIBOR,NIBOR,STIBOR,SIOR,FedFunds,CMS,SIFMA,BBSW,AONIA,CAMARA,REPOFIXand the rest – which is data rather than grammar; - the FX source (
FX-ECB-EUR-USDagainstFX-TR20H-EUR-USD), which is a field the FX identifier does not have; and - the power contract-hour shape, which is a shape the grammar does not have.
The FX fixing carries its source, and the pair as it was quoted. The source
is a field, not a spelling: FX-ECB-EUR-USD and FX-TR20H-EUR-USD are two
different rates, where a spelling difference is two ways of writing one. So
source joins the FX identifier, lower-cased, with source_spelling carrying the
token as ORE wrote it when the two differ, and both are meaningful only when
type=fixing – ORE's FX quote keys carry no source at all.
The pair is never put into a canonical order. A fixing's value is only meaningful
in the direction it was published, and the corpus writes FX-TR20H-EUR-GBP and
FX-TR20H-GBP-EUR as one rate both ways, on 122 shared dates, reciprocal to about
three decimal places and no more. The model brief carries the measured pair of
figures. Canonicalising would put two values under one date, and nothing in the
import inverts a value.
7. The power contract-hour shape, against ORE's own grammar
The last grammar decision. POWER-ICE:PDQ-2021-01-04-14400-18000 arrives as an
index name, and ORE writes the grammar for that name down rather than leaving it to
be inferred from the corpus.
ORE states the format. OREData/ored/utilities/indexparser.cpp carries a parser
of its own for this family, parseIntradayPowerIndex(), standing beside
parseCommodityIndex() rather than inside it, and its failure message is the
grammar:
Expected format: POWER-COMMNAME[-YYYY-MM-DD[-DELIVERYSTART-DELIVERYEND[-DST]]]
It builds a QuantExt::IntradayPowerIndex, a type of its own, and parseIndex()
gives the power parser its own place in the dispatch order. The parser splits the
remainder after POWER- on dashes and reads tokens[0] as the commodity name, so
the name's boundary is the first dash and a power commodity name carries no dash –
ICE:PDQ is whole. It then accepts the remainder only at three token counts: one
(the commodity name alone, with no date), four (a name and a date, with no time), and
six or seven (a name, a date, a start, an end, and an optional DST, matched
case-insensitively). Every power name the corpus carries is the six-token form but
for one four-token name, and no name has a shape the grammar does not admit.
The two numbers are seconds from midnight, and the window they name is half-open: the corpus's windows are all 3600 seconds wide and tile a day without a gap.
The date is the delivery date. Every power row carries the same date in the
file's first column and in the name, so the power payload alone cannot say which the
name's date is. Its sibling settles it: COMM-NYMEX:CL-2025-04 is observed on
2024-12-02, so a period in a name of this family is the delivery period. ORE's own
types agree – IntradayPowerIndex takes a deliveryDate, and the trade that
consumes it, IntradayPowerForwardData, carries DeliveryDate, DeliveryStart
and DeliveryEnd as three separate fields, the last two in seconds.
The window is not an hour. The corpus samples 3600-second windows only, but the
trade model carries DeliveryStart 2700 and DeliveryEnd 4500 for the forward it
calls "0.45am to 1.15am". The coordinate is therefore a second-resolution window,
and "the contract-hour shape" names the corpus's own sampling rather than a rule the
model may hardcode.
What the corpus carries. 48 distinct names, every one of them on ICE:PDQ:
| shape | names | example | carried |
|---|---|---|---|
POWER-COMMNAME |
0 | POWER-ICE:PDQ |
no |
POWER-COMMNAME-YYYY-MM-DD |
1 | POWER-ICE:PDQ-2021-01-04 |
yes |
POWER-COMMNAME-YYYY-MM-DD-START-END |
47 | POWER-ICE:PDQ-2021-01-04-14400-18000 |
yes |
POWER-COMMNAME-YYYY-MM-DD-START-END-DST |
0 | POWER-ICE:PDQ-2021-01-04-14400-18000-DST |
no |
The one date-only name is the whole-day contract for 2021-01-04, at 38.75 against 25 to 38 for that day's 23 hourly windows. That day carries 24 rows and 2021-01-01 to 2021-01-03 carry 8 each, so the corpus is not a complete tiling of a day and nothing may assume one window per hour of a delivery date.
The name is an index name and nothing else. Every other class with an
index-name prefix in the corpus also appears as a market quote key – FX/RATE on
1372 rows, EQUITY/PRICE on 105, COMMODITY_FWD/PRICE on 2803, BOND/YIELD_SPREAD
on 92 – so each of them has a quote key to project as well as a fixing key.
POWER does not: the string occurs on the 48 fixing rows and nowhere else, and an
intraday power price is quoted as SHAPE_PROFILE/SHAPE_FACTOR/PROFILE/DATE/SECOND/UNIT,
which the shape_profile class already owns. So the index name is the only key this
class has, and the class has no quote type at all.
7.0.1. Worked example
The four forms the grammar admits, and the identifier proposed for each:
| ORE index name | proposed oresmd URI |
|---|---|
POWER-ICE:PDQ |
oresmd://power/ice:pdq?type=fixing |
POWER-ICE:PDQ-2021-01-04 |
oresmd://power/ice:pdq?type=fixing&delivery=2021-01-04 |
POWER-ICE:PDQ-2021-01-04-14400-18000 |
oresmd://power/ice:pdq?type=fixing&delivery=2021-01-04-14400-18000 |
POWER-ICE:PDQ-2021-01-04-14400-18000-DST |
oresmd://power/ice:pdq?type=fixing&delivery=2021-01-04-14400-18000-DST |
to_index_name() renders POWER-, the commodity code upper-cased, and the
delivery with the same dashes the field arrived under; from_index_name() strips
the prefix, takes the first dash-delimited token as the commodity code, and reads
the tail by token count exactly as ORE does – one token for no delivery, four for a
date, six or seven for a date and a window – refusing every other length. A date
that is not the canonical YYYY-MM-DD is refused rather than reshaped, because a
name that would not read back as it arrived is not an identity.
7.0.2. Why the coordinate is a fixing field and not point
point is defined as the coordinate a quote or a vol URI names, and the model's
own is_scalar() already draws the line: a fixing whose coordinate is a field of its
own is scalar – oresmd://ir/usd?type=fixing&index=libor&tenor=3m is, and a case
asserts it – while an identifier that carries a point is not. The precedent for
"the coordinate that says which index a fixing is" is therefore a dedicated field
– tenor for interest rates, source for FX – and both leave the fixing scalar.
The delivery window is not a per-observation coordinate either: every observation of
POWER-ICE:PDQ-2021-01-04-14400-18000 carries the same window, and it is the series
that is specific to it.
The sibling shape_profile class does put this same intraday coordinate – a
delivery date, a second of the day, a unit and a DST flag – in point, but it
does so for a quote, which is exactly the case point is for. One coordinate, two
slots, because ORE itself splits them: the coordinate as a quote is
SHAPE_PROFILE/SHAPE_FACTOR/..., and as an index it is POWER-....
One delivery field carries the tail whole, because the grammar's own nesting is
already "a date, then optionally a window, then optionally DST, each level
available only under the one before it", and one key keeps the query grammar small –
the same reason credit folds its seniority and tenor into point and shape_profile
folds four parts into it. Typed delivery_date, delivery_start, delivery_end and
dst keys remain available if the coordinate should be queryable per component.
7.0.3. Why power is its own class and not commodity
They are the same shape up to the prefix – COMM-COMMNAME-YYYY-MM and
POWER-COMMNAME-YYYY-MM-DD[-START-END] – so folding power into commodity is the
obvious alternative, and the shared token argues for it: ICE:PDQ is a commodity
code, and the daily-average curve the intraday power curve is built on is quoted as
COMMODITY_FWD/PRICE/ICE:PDQ/USD/<date>.
The prefix cannot be derived, and that is what decides it. ORE's power parser accepts
a bare POWER-ICE:PDQ, and its commodity parser accepts a bare COMM-ICE:PDQ; under
one class both would be the identifier commodity/ice:pdq?type=fixing with no
coordinate, and one of the two names could not be written back. A discriminator field
would be needed to tell them apart, and the URI's class tag is already that
discriminator. So power joins shape_profile and rating as a class the corpus's
own prefix names, and the class_of() census, which already counts power separately,
stays true.
8. The remaining classes, and the two rows that are market data
The last open question is settled. The four GENERIC names split two ways.
GENERIC- is a prefix ORE resolves in a parser of its own, so GENERIC-JuniorNote
and GENERIC-PortfolioBasket#1 are index names like any other, and a generic
index's identity is its own name. GENERIC-MD is not the same prefix: it is the
market-data wrapper whose inner type and metric name the series, and it is already
one of the coverage gate's two recorded exemptions. Its two rows in
Products/Input/fixings.csv carry SPX call prices – 2862.29 at the 3300 strike
and 2767.41 at 3400, about 95 apart for a 100 strike step – so they are market
data in a file whose header declares fixingId for its second column. The owner
reads them as errors rather than as fixings to model.
So the target is 156 of 158: the two GENERIC-MD rows are errors, and the
other nineteen names are named.
| class | names | what it needs |
|---|---|---|
| inflation | 7 | a known index-code list: the corpus's own header words are dash-less too, and ORE carries such a list in its parser |
| equity | 8 | nothing new: ccy_optional_for_fixing already settles the currency, so the projection is EQ-<ticker-or-ric> |
| security | 2 | delivery reused for the bond future's expiry month, the shape commodity's contract month already uses |
| generic | 2 | the name as the entity, with the case it arrived in as a spelling |
The names are AUCPI, EUHICP, EUHICPXT, FRHICP, UKRPI, USCPI and ZACPI
for inflation; EQ-SP5 and seven EQ-RIC: or EQ-FIGI: tickers for equity;
BOND-ISIN:IE00BH3SQ895 and its -2025-08 future for security; and
GENERIC-JuniorNote and GENERIC-PortfolioBasket#1 for generic.
One row stays unexplained and is not part of this: GENERIC-JuniorNote is valued
at 4000000 in Legacy/Example_46/Input/fixings.txt, alone after a blank line among
ordinary index fixings. Four million is not a rate or a price, so it should be
checked against Example_46's portfolio rather than assumed either way.
9. The generic index, and the two keys that are not index names
GENERIC- is a prefix ORE resolves rather than a spelling of another class, so it
gets a class of its own. ORE's parser resolves the prefix, and the name is all the
identity there is: a generic index is whatever its producer called it, the corpus
carries no other key for either series, and ORE has no market-data key for one. The
corpus carries two, GENERIC-JuniorNote in
Legacy/Example_46/Input/fixings.txt and GENERIC-PortfolioBasket#1 in
Products/Input/fixings.csv, both on fixing rows. So generic joins power as a
fixing-only class: it carries no quote type, and quote_key_generic() returns
nothing.
9.0.1. Worked example
The three names, and the identifier each projects to:
| ORE index name | oresmd identifier |
|---|---|
GENERIC-JuniorNote |
oresmd://generic/juniornote?type=fixing&name_spelling=JuniorNote |
GENERIC-PortfolioBasket#1 |
oresmd://generic/portfoliobasket%231?type=fixing&name_spelling=PortfolioBasket%231 |
GENERIC-SOMEINDEX |
oresmd://generic/someindex?type=fixing |
to_index_name() renders GENERIC- and the name; from_index_name() strips the
prefix and takes the tail whole. A tail that carries a slash is refused, because the
slash is the market-data key grammar's separator and not an index name's: that is
what keeps GENERIC-MD/EQUITY_OPTION/PRICE/RIC:.SPX/USD/2025-10-03/3300/C and its
3400 sibling out of the fixing space. The two are SPX call prices – 2862.29 and
2767.41, about 95 apart for a 100 strike step – in a file whose header declares
fixingId for the column they sit in, and the owner reads them as errors rather than
as fixings to model.
9.0.2. Why the name is a spelling, not the entity's own case
The identifier holds the name upper-cased, as every other class's entity field
does, and to_uri() lower-cases it into the path segment as it does for every
class. The case the corpus wrote sits beside it in name_spelling. Both corpus
names arrive in mixed case, so without the field neither would read back as ORE
wrote it. That is the FX source_spelling precedent, and the same rule the
interest-rate family spells under: the identifier holds the normalised token, and
the name keeps the spelling it arrived under.
Carrying the name verbatim as the entity is the alternative, and it is the only one
that needs a special case: the parser's entity assignment and to_uri()'s path
segment are generated from the Fields table for every class alike, and both
case-fold what they carry. The spelling is a query key whose value the parser
carries raw, which is what the FX source already is, so the class needed one guard
slot in the parser's field list and no new shape of its own.
The character set is ORE's and not the URI's. # is a fragment delimiter, so it is
percent-encoded in the path segment and in the spelling, and the parser decodes
both. A case reads GENERIC-PortfolioBasket#1 out to a URI and back to prove it.
10. The last three classes, and the allowlist they close
Inflation, equity and security are closed, which leaves no class the corpus puts in the fixing space unnamed: 156 of its 158 names in a fixing column resolve, and the other two are the market-data keys.
Inflation is the one class whose index name has no prefix. UKRPI is the whole
of the name: there is no INFLATION-, no separator, and nothing in the string says
which class the token belongs to. So the class carries the codes it knows, in an
Index codes table in inflation_quote_type.org, and the index-name projection names
a dash-less token only when it is one of them. A stray token or a header word keeps
its name instead of being given an inflation identity, and a code written in another
case is refused rather than reshaped – the rule the power delivery date follows,
and why the seven codes the corpus carries are all upper-case. The list is data: a
code ORE adds is a row.
Equity needs no new field. A fixing carries no currency, which
ccy_optional_for_fixing already settles, so the whole projection is
EQ-<ticker>. The ticker is the tail ORE writes, its identifier scheme included:
the corpus carries EQ-RIC:.SPX and EQ-FIGI:BBG00R251JN8 as fixings beside
EQUITY_OPTION/RATE_LNVOL/RIC:.SPX/USD/... as the quote key for the same
instrument, so the scheme belongs in the name rather than in a field beside it.
Security reuses delivery. The corpus writes BOND-ISIN:IE00BH3SQ895 for the
bond and BOND-ISIN:IE00BH3SQ895-2025-08 for the future that expires in August
2025, so the trailing period is a contract month: the shape commodity already gives
its delivery. delivery joins security's Fields table and leaves its Reject keys
table, and the shared shape helper becomes contract_month_is_the_shape now that it
has a second caller. A quote carries that information inside the name it is quoted
under, which is why nothing reads the field for one.
The allowlist is empty and the population is still whole. The two
GENERIC-MD/EQUITY_OPTION/PRICE/... rows are not index names, so the census counts
them in a bucket of their own, pins them by name and asserts that the projection
refuses both. That case replaces the recorded-list entry that used to excuse them,
so nothing was dropped to make the list empty.
11. Plan
- Measure first. Walk every fixing payload with the real fixing reader, report the yield per index-name class, and pin the classes that name nothing. The instrument landed with the interest-rate work rather than after it: the branch this task descends from skipped three quarters of the distinct series with a warning, and no test asked about the names it skipped.
- Interest rates are data, not grammar. The families and the spellings ORE
writes for them go in the Index family table of
ir_quote_type.org. The tenor rule moves into that table too, so a family cannot join the enum without being classified, and the hand-written header that held a copy of the rule goes. - Then the grammar, one class at a time. The FX source and the power contract-hour shape each need a field or a shape the identifier does not have. Each is a decision taken with a worked example against the corpus, and each shrinks the pinned list by one class.
(Implementation strategy. Written when work starts; key decisions
are distilled into the parent story's * Decisions at close, but the
plan itself stays — it is the historical record of what we did.)
12. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
| Fixing payloads walked | 66 files, 158 distinct index names, 8 classes | ores.marketdata.core.tests "[fixing]" |
495dc8d2e3 |
| Interest-rate names that name, before | 34 of 57 | same | 495dc8d2e3 |
| Interest-rate names that name, after | 57 of 57 | same | 495dc8d2e3 |
| Interest-rate names that round trip, after | 57 of 57 | same | 495dc8d2e3 |
| Fixing payloads the reader refuses, before | 1: CurveBuilding/Input/fixings_bondyieldshifted.csv |
same | 495dc8d2e3 |
| Fixing payloads the reader refuses, after | 0 | same | 6fa10a87f4 |
ores.marketdata.core suite |
1393 assertions in 333 cases | ores.marketdata.core.tests |
6fa10a87f4 |
ores.ore.core suite |
429839 assertions in 552 cases | ores.ore.core.tests |
6fa10a87f4 |
| codegen suite | 955 passed | pytest projects/ores.codegen/tests |
6fa10a87f4 |
| Regeneration is idempotent | git status empty on a second run |
./projects/ores.codegen/codegen.sh generate --model projects/ores.marketdata/modeling/oresmd/model.org --address ores.cpp.oresmd |
6fa10a87f4 |
| FX names that name, before | 0 of 7 | ores.marketdata.core.tests "[fixing]" |
fb3c5a4f31 |
| FX names that name, after | 7 of 7 | same | fb3c5a4f31 |
| FX names that round trip, after | 7 of 7 | same | fb3c5a4f31 |
| Corpus names that name, after | 64 of 158 (interest rates 57, FX 7) | same | fb3c5a4f31 |
ores.marketdata.core suite, after the FX work |
1472 assertions in 346 cases | ores.marketdata.core.tests |
fb3c5a4f31 |
| codegen suite, after the FX work | 961 passed | pytest projects/ores.codegen/tests |
fb3c5a4f31 |
| The two GBP-EUR directions | 122 shared dates; product 0.9997 median, 0.9964 worst | a walk of external/ore/examples pairing FX-TR20H-EUR-GBP with FX-TR20H-GBP-EUR |
fb3c5a4f31 |
| Power index names the corpus carries | 48: 47 with a delivery window, 1 date-only, 0 DST, on one commodity code |
a walk of external/ore/examples/MarketRisk/Input/Intradaypower/fixings.txt |
d3325300d5 |
| Power names that name, after | 48 of 48, all round-tripped | ores.marketdata.core.tests "[fixing]" |
2a0d8afef6 |
| Commodity names that name, after | 25 of 25, all round-tripped | same | 2a0d8afef6 |
| Corpus names that name, after the power and commodity work | 137 of 158 (interest rates 57, power 48, commodity 25, FX 7) | same | 2a0d8afef6 |
ores.marketdata.core suite, after |
1556 assertions in 350 cases | ores.marketdata.core.tests |
2a0d8afef6 |
| Instrument-key coverage, after | 485606 of 485606 keys, unchanged | ores.marketdata.core.tests "[oresmd]" |
2a0d8afef6 |
| Power index names ORE's grammar refuses | 0 of 48 | the same walk, reading the tail by token count as parseIntradayPowerIndex() does |
d3325300d5 |
| Power windows that are one hour wide | 47 of 47, contiguous and half-open, 01:00 to 24:00 at the widest | the same walk | d3325300d5 |
| Commodity fixing rows whose period is not the observation month | 711 of 928 | a walk of external/ore/examples/Products/Input/fixings.csv |
d3325300d5 |
| Generic names that name, after | 2 of 2, both round-tripped through a URI and back | ores.marketdata.core.tests "[fixing]" |
7b3918b6fe |
| Corpus names that name, after the generic work | 139 of 158 (interest rates 57, power 48, commodity 25, FX 7, generic 2) | same | 7b3918b6fe |
ores.marketdata.core suite, after the generic work |
1641 assertions in 378 cases | ores.marketdata.core.tests |
7b3918b6fe |
| codegen suite, after the generic work | 990 passed | pytest projects/ores.codegen/tests |
7b3918b6fe |
| codegen suite, after the declared-case check | 992 passed | same | 9edae76203 |
| Declared oresmd test cases no template emitted, before | 22 of 198 (power 11, generic 11) | pytest projects/ores.codegen/tests/test_oresmd_spec_test_cases.py, run against the same generated files at 7b3918b6fe~1 |
9edae76203 |
| Inflation, equity and security names that name, after | 17 of 17 (inflation 7, equity 8, security 2), all round-tripped | ores.marketdata.core.tests "[fixing]" |
38f63cb9b6 |
| Corpus index names that name, after the last three classes | 156 of 156, with the 2 market-data keys counted apart and refused | same | 48eb0e27aa |
ores.marketdata.core suite, after the last three classes |
1764 assertions in 394 cases | ores.marketdata.core.tests |
48eb0e27aa |
| codegen suite, after the last three classes | 992 passed | pytest projects/ores.codegen/tests |
48eb0e27aa |
| Component file lists, the marketdata parts and codegen | up to date, rc 0 for each | regenerate_cmake_component_files.py --component <name> --check |
48eb0e27aa |
ores.marketdata.core suite, after the review fixes |
1770 assertions in 398 cases | ores.marketdata.core.tests |
509bb244d7 |
ores.marketdata.core suite, after the pull request review |
1771 assertions in 398 cases | same | a4fb8923f8 |
| Corpus names, after the review fixes | 156 of 156 index names, 2 keys pinned apart | ores.marketdata.core.tests "[fixing]" |
509bb244d7 |
The test binaries are under
build/output/linux-clang-debug-make/publish/bin/. The per-class table is in the
WARN output of the [fixing] cases, printed on every run rather than asserted
in prose.
13. Notes
The curve-configuration CHECK is a different rule from the index-name one.
requires_tenor says whether every index name ORE writes for a family carries a
tenor, so that the family is never written bare. The CHECK constraint
on ores_synthetic_ir_curve_generation_configs_tbl says which (family, tenor)
pairs a synthetic curve may be configured with, and the two disagree for shibor,
taibor and tiie: the CHECK wants tenor = '' for them, and the corpus writes
CNY-SHIBOR-3M, TWD-TAIBOR-3M and MXN-TIIE-28D. The old is_overnight()
kept both rules in one predicate, which is why the hand-kept family list in the
parser tests called those three overnight and failed once the corpus was measured.
A family in the enum is not a family synthetic can configure. The same list is
written into
projects/ores.synthetic/modeling/ores.synthetic.ir_curve_generation_config.org
three times – the membership CHECK, the tenor-combo CHECK and a static_combo –
and it generates the SQL from there. None of the fifteen families this task adds
is in it, so a synthetic curve cannot be configured for them. That is a decision
for that component, not a consequence of this table.
ftiie is in the enum and in a migration but not in create/.
git grep ftiie finds it in the marketdata enum and in
synthetic_ir_curve_generation_configs_add_ftiie_migration.sql, and finds nothing
under projects/ores.sql/create/ or in the synthetic model. A database created
from create/ therefore rejects a family a migrated one accepts. Recorded here
because this task walked the same list; it belongs to ores.synthetic.
The refused fixing file was invisible to every count. Its two names,
USD-FedFunds and USD-LIBOR-3M, also appear in files the reader could read, so
refusing it changed no total. Only a test that asks about the file can see it,
which is why the coverage test asserts on refused payloads and not only on names.
The semicolon separator is read for market data too. tokenize() is shared by
parse_fixings() and parse_market_data(), and it normalises separators rather
than detecting them, so adding ; widens both readers. Measured: none of the 66
market payloads the coverage test selects contains a semicolon, so the corpus
measures the same either way; the widening is the design's own shape rather than a
concession to one file, and it is the shape a producer's semicolon-delimited
market file would need.
One line of the PlantUML diagram is not this task's. The index_family enum
in ores.marketdata.puml was missing ftiie, which the enum has held since an
earlier change, and the file lists the enum, so the gap was closed while the table
was being extended. Measured: the enum declares 38 values and the diagram now
lists the same 38.
A type-gated key is gated by the table now, not by the template. The section
took only a key and the template hard-coded quote. It takes a type column; a row
that names none is gated on quote, which is what the interest-rate table's one row
means.
Three classes do not emit the Reject keys table their model carries. equity
(delegate_function), credit and commodity (inline_delegate) route their
query validation through the shared check instead, so the rows added to their
tables are documentation and the shared check is what refuses the keys. That is
pre-existing – their other rows are equally unenforced – and it is why the two
new keys went into the shared check as well as the six tables that do emit.
The reciprocal figure took two attempts. The corpus's two GBP-EUR directions were first recorded as reciprocal to six decimal places, from a single sampled date; the correction then read the median of the absolute deviation as a product and said 1.0003. Over all 122 shared dates the median product is 0.9997 and the worst 0.9964, so the two agree to three decimal places and not more, and slightly below one. The decision not to canonicalise does not rest on the precision: two values still land on one date.
A test case a model declares is invisible until a template block emits it. The
two fixing-only classes each had eleven: power declared its Projections, Round-trip
and Rejection rows in the identity change and no {{#power_*}} block existed to
render them, and generic's rows were written beside them in the same state. Neither
the C++ suite, the drift check nor the coverage gate said so, because nothing that
runs reads a spec's * Test cases table; the table is input to mustache and to
nothing else. test_oresmd_spec_test_cases.py now loads every model through the
generator's own loader and requires each declared description to appear as a
TEST_CASE in the generated sources. Run against the tree before the wiring it
names all 22 rows.
The delivery key is gated in the template, because the table cannot reach it.
The Type-gated keys table is emitted only for the classes that build a
validate_<class>(), so a class that validates inline gets no gate from it, which
is the codegen gap the models record for delivery. The gate therefore lives in the
shared {{#has_delivery}} block, where every class that names the field passes
through it, so commodity, power and security all refuse a delivery off a fixing and
the claim that no class accepts the key and drops it is true of all three.
One component file list is stale on main, and it is not this task's.
regenerate_cmake_component_files.py --all --check reports
projects/ores.workflow/api/src/component_files.cmake stale at 48eb0e27aa, and
git diff origin/main shows this branch does not touch ores.workflow at all, so
the drift arrives with main. Every component this task edits is clean: the four
marketdata parts and ores.codegen each report rc 0 on the same check. Fixing
another component's generated list here would put an unrelated regeneration in this
pull request, so it is recorded rather than fixed.
14. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
15. PRs
| PR | Title |
|---|---|
| #2355 | [marketdata,codegen] Name the last three fixing classes |
| #2353 | [marketdata,codegen] Name the generic fixing index names |
| #2346 | [marketdata,codegen] Name the power and commodity fixings |
| #2340 | [marketdata] Carry an FX fixing's source and the pair as quoted |
| #2336 | [marketdata] Give every interest-rate fixing index name an identity |
16. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Six added comments narrate what the code used to be rather than what it is | templates, tests, market_data_parser.cpp |
Accepted | Fixed in 0d8b0bdc8a; each states the present rule |
| 2 | The first commit's subject is 82 characters, over the documented 72 | -- | Accepted | Rebuilt at 70 characters |
| 3 | record_index_spelling and record_index_name_spelling are one shape with the comparison swapped |
oresmd_projections.cpp |
Declined | The two key spaces spell differently on purpose: an ORE key segment is case-folded, an index name is compared exactly. A shared helper needs a mode flag, which hides the difference rather than removing it |
| 4 | aliases is a comma-joined cell split in the loader |
org_loader.py |
Declined | A second table for two aliases is more structure than the data holds |
| 5 | index_name_ir() carried eleven lines of comment |
oresmd_projections.cpp |
Accepted | Trimmed to four in 0d8b0bdc8a |
| 6 | is_fixing_payload and named_as_fixings duplicate a predicate |
corpus_files.hpp, classification_series_classifier_tests.cpp |
Declined | They select different sets: a payload a reader feeds, against a census of the whole tree that includes ORE's own outputs. Both say which |
| 7 | Untracked .misspell-fixer.3* byproducts are in the tree |
-- | No action | None remain. The only .misspell-fixer* file is the tracked ignore file; the numbered ones are transient within a run |
| 8 | "Five fixing index classes remain", and then lists seven | task_clean_marketdata.org |
Accepted | Corrected to seven |
| 9 | The interest-rate class is called "half" of the key space; it is 36% of the names | this task | Accepted | Both places say "third" |
| 10 | Acceptance "a corpus-driven test whose allowlist is empty" is not met | oresmd_fixing_coverage_tests.cpp |
Accepted | The task stays STARTED and the per-class list is the record; the analysis carries the update. Only interest rates are closed |
| 11 | The import creating a series for every fixing row is neither delivered nor tested | import_service.cpp |
Accepted | Recorded in Result: the import still stores the index name in the series qualifier and never calls from_index_name, which is the identity cutover's work |
| 12 | The spec said the two spelling mismatches were AONIA and TONAR | this task | Accepted | Corrected: AONIA has always resolved, the family token being matched case-insensitively; the second spelling is ESTER |
| 13 | The Notes paraphrase inverted requires_tenor |
this task | Accepted | Corrected |
| 14 | ; widens the shared tokenize(), so market data reads it too |
market_data_parser.cpp |
Accepted | Measured: no selected market payload carries a semicolon, so the corpus measures the same. Recorded in Notes and in the pull request's limitations |
| 15 | The PlantUML edit adds ftiie, which is not this task's |
ores.marketdata.puml |
Accepted | Disclosed in Notes. The file lists the enum, and the enum declares 38 values; leaving one out keeps the diagram wrong |
| 16 | The commit message counted five lost round trips where there are six | -- | Accepted | Corrected in the rebuilt commit |
| 17 | source is accepted and dropped by equity, credit and commodity, whose Reject keys tables are not emitted |
equity/credit/commodity_quote_type.org, ores.cpp.oresmd.parser.org |
Accepted | Found by both axes. The keys joined the shared check those three do run, which was renamed validate_no_foreign_keys for it; a case covers the three. The underlying codegen gap is recorded in Notes |
| 18 | from_index_name re-implemented split_index_name's tokenizer |
oresmd_projections.cpp |
Accepted | Extracted split_on_dash, which both use |
| 19 | The loader comment narrated how the section was first used | org_loader.py |
Accepted | Present tense |
| 20 | The spelling rule was stated three times | parser, projections, model | Accepted | One statement per site |
| 21 | fx_fixing_has_no_quote_key is negative-only |
oresmd_oresmd_projections_tests.cpp |
Declined | The same table's positive rows fail if to_quote_key ever returned nullopt, so the pair is meaningful |
| 22 | A positive assertion sat inside the FX rejection case | oresmd_oresmd_projections_tests.cpp |
Accepted | Moved into the case that owns the direction |
| 23 | The reciprocal figure was six decimal places | this task, fx_quote_type.org |
Accepted, then corrected again | The first correction read the median of the absolute deviation as a product and reported 1.0003; the median product is 0.9997. Caught by the second Spec pass, which also found the figure restated in three places |
| 24 | The review ran while the tree was still being written | -- | Accepted | Process: the axis pinned a revision and re-verified, but the worktree should have been still. Findings re-checked by hand |
| 25 | The FX round-trip boilerplate justified having no key-readback comparison with a quote's reason, which is false for a fixing | ores.cpp.oresmd.parser_tests.org |
Accepted | The block's note now reads true for both, and says the exact readback is the coverage tests' claim |
| 26 | The new class-refusal loop named six own-table classes and tested two of them | same | Accepted | The loop covers every class but FX, ten assertions |
| 27 | validate_no_foreign_keys' comment called index and metric keys "one asset class owns" |
ores.cpp.oresmd.parser.org |
Accepted | Reworded: the six belong to other classes, and for the three that delegate this is their whole query validation |
| 28 | split_index_name's comment ended up above split_on_dash |
oresmd_projections.cpp |
Accepted | Moved back above the function it describes |
| 29 | Two model-driven reject rows duplicated the hand-written case | fx_quote_type.org |
Accepted | Rows removed; the type-gated table still declares the rule and the case keeps the positive assertion. Case count 346 to 344 |
| 30 | The type-gated template line had grown to 111 columns | ores.cpp.oresmd.parser.org |
Accepted | Split |
| 31 | Both commit bodies run past 72 columns | -- | Recorded, not fixed | Measured: 22 of main's last 40 non-merge commits also exceed it, and correcting these means force-pushing a branch whose reviewed revisions are already recorded. Titles are 71 and 67 and the trailers are complete |
| 32 | source and the pair are checked for shape, not against a currency list |
-- | Recorded as a limit | The library has no reference-data dependency by design, as its own header states; the model brief says what is checked. Tightening it belongs to the migration that gives the library one |
| 33 | Edit-history narration: "four ways now", and a list of which classes are absent | oresmd_fixing_coverage_tests.cpp |
accept | Reworded to present state. The reviewer cited a template as the source; no such template exists and the test is hand-written, so the fix landed in the file itself. |
| 34 | Two delivery-shape helpers differing only in how they count parts | ores.cpp.oresmd.projections.org |
accept | A shared is_number() is extracted. The two predicates stay two, because a power window and a commodity month are different grammars and are not the same predicate called twice. |
| 35 | is_digits duplicates one in ores.utility |
same | decline | That copy is file-local in a .cpp, so the projections translation unit cannot reach it. |
| 36 | An assertion whose comment claims more than it pins | ores.cpp.oresmd.projection_tests.org |
accept | The comment now says what the assertion covers. |
| 37 | A ragged 143-column comment wrap in the template and its output | ores.cpp.oresmd.projections.org |
accept | Caused by an earlier edit of mine joining two lines; the wrap is restored. |
| 38 | model.org keeps a stale #+updated after gaining a row |
model.org |
accept | Bumped, and commodity's model with it. |
| 39 | delivery accepted and silently dropped by every class but commodity and power |
every oresmd model and the parser template | accept | The real defect, and the same one the FX source work closed. delivery joins validate_no_foreign_keys and the nine other Reject keys tables, a case walks every class but the two that name it, and commodity moves onto its own reject list so the shared check stops refusing the key commodity owns. |
| 40 | The task's Result section is stale and contradicts its own Status | this task | accept | Superseded by this PR's result row below. |
| 41 | analysis.org carries no update for the power and commodity commits |
analysis.org |
accept | Recorded as the next step before the PR, since the section still counts power 48 as unnameable. |
| 42 | Both variant doc comments still say "seven" | identifiers and requirements templates | accept | The count is dropped rather than raised to eleven, so it cannot go stale again. |
| 43 | The DST flag is refused in a spelling ORE matches case-insensitively |
projections and the power model | decline | Deliberate: a flag written any other way would not read back as it arrived, which is the test the rest of the class's refusals are held to. Already recorded in the model, and the fix if the corpus ever writes another spelling is a delivery_spelling field, the FX source_spelling precedent. |
| 44 | inbox.org gained index rows for two captures unrelated to this work |
inbox.org |
decline | The regenerator rewrites a bucket index from the directory, so it lists captures the index had drifted from. No capture file is touched by this branch. |
| 45 | The explicit DST check in the power arm overlaps the shape check |
ores.cpp.oresmd.projections.org |
accept | A comment now says why it is deliberate: the shape check runs on the lower-cased tail, so it cannot tell ORE's spelling from an invented one. |
| 46 | parse_power accepts type=quote, which then projects nothing |
the power arm and its cases | decline | The behaviour is already pinned by from_index_name_rejects_power_names_that_are_not_the_shape, and IR has the same property: its default type is quote and a quote with no quote key names nothing. Rejecting the type would need a gate no class has. |
| 47 | reject_if_present("power", "model", ...) sits below the id setup |
ores.cpp.oresmd.parser.org |
decline | Cosmetic, and generated that way for every inline class rather than for power: the model reject comes from the shape-slot branch that follows the type parse. |
| 48 | Commodity's reject list is a hand copy of the shared check minus delivery |
the commodity model and the parser template | decline | It is the codegen gap the task already records, now confirmed by the reviewer. A shared helper taking an allowed set is the fix, and it belongs with the rest of the validation-mode work rather than here. |
| 49 | The shape checks admit a calendar-invalid date and an out-of-day window | ores.cpp.oresmd.projection_tests.org |
accept | Deliberate and already in the PR's limitations; a comment on the case now says so, so a reader is not surprised by what passes. |
| 50 | The fixes made after the two-axis review were not re-reviewed | the branch | accept | Agreed and already stated in the PR's testing limitations. The two comment changes here are the only code touched since, and they are comments. |
| 51 | Stale #+updated: on the four archetype sources this branch edits, and on analysis.org |
the four oresmd templates, analysis.org |
accept | All five bumped to 2026-09-28. The field is read by no template, so nothing regenerates from it; the nine models with the same defect were fixed in a7bfbc2f91. |
| 52 | generic_quote_type.org's #+description: is 124 characters, over the documented 120 |
generic_quote_type.org |
accept | Shortened to 108. Siblings exceed it too, and no tooling enforces it. |
| 53 | The power and generic template blocks are byte-identical; one shared key would carry both | ores.cpp.oresmd.parser_tests.org, projection_tests.org |
decline | The per-class block is the file's idiom for all twelve classes, and each block's position reproduces the hand-crafted file it replaced. A shared list would need a class discriminator and would stop a reader finding one class's cases by name. |
| 54 | The name-spelling case re-lists the same eleven class URIs as the delivery case | ores.cpp.oresmd.parser_tests.org |
decline | The refusal is per key and each case reads as the key it pins. The list is data in a test rather than logic, and neither list can drift without a class being added, which the corpus census already fails on. |
| 55 | The new check has no scripts/check_*.py entry point |
test_oresmd_spec_test_cases.py |
decline | It runs in the codegen suite, which the pull-request checks run. The sibling scripts are invoked by path because they are gates over the tree rather than pytest cases. |
| 56 | ORE is claimed to build the GenericIndex from the rest of the name, four times |
this task, generic_quote_type.org, analysis.org, the projections template and its case |
accept | The claim is not verifiable here – ORE's source is not vendored – and it is removed. The prose now states what the corpus shows: the name is the identity, and no other key exists for either series. |
| 57 | "The path segment is upper-cased" is backwards | this task | accept | Reversed: the identifier's field holds the name upper-cased, and to_uri() lower-cases the segment as it does for every class. |
| 58 | "The class adds no template branch of its own" is false | this task, Result | accept | It adds one guard slot to the parser's field list for a query key the parser carries raw. The sentence now says that. |
| 59 | The codegen-suite measurement cites 992 at a commit whose suite is 990 | this task | accept | Split: 990 at 7b3918b6fe and 992 at 9edae76203, the commit that added the two-case file. |
| 60 | The census table's "seven shapes" and "(unclassified) 4" contradict the new section | this task, oresmd_fixing_coverage_tests.cpp |
accept | The table is marked as the first reading and its last row points at the split. The coverage test no longer states a shape count, so it cannot go stale again. |
| 61 | Acceptance 1 (an empty allowlist) and 3 (the import creates a series) remain unmet | this task | no action | Acknowledged and unchanged: the Status stays STARTED, rows 10 and 11 record both, and the three remaining classes are the next step. |
| 62 | The diff also emits power's eleven declared cases and adds the cross-model check | the branch | no action | Disclosed in the commit bodies and the pull request's changes. The check is what keeps the repair from being undone. |
| 63 | name_spelling is accepted with a value that is not a case variant of the name |
the parser template | accept | The parser refuses it now: the identifier would otherwise hold two identities, the one its entity names and the one its index name reads back as. A case covers the refusal. FX's source_spelling is not checked and is left as merged, so the two differ until the cutover. |
| 64 | type=quote is accepted on a generic URI and projects nothing |
the parser template | decline | The same property power has, and the same one the previous review declined in row 46: a class with no quote key names nothing, and rejecting the type would need a gate no class has. The codegen gap is recorded in the Notes. |
| 65 | from_index_name hard-codes substr(8) for the prefix length |
the projections template | accept | A named generic_prefix carries the prefix, and the test and the offset both read from it. |
| 66 | to_upper on a non-ASCII name depends on the shared helper's behaviour |
the projections template | recorded as a limit | detail::to_upper is the helper every class's entity already passes through, and the corpus is ASCII. Widening the input belongs to the cutover. |
| 67 | The check matches the literal TEST_CASE("<description>" |
test_oresmd_spec_test_cases.py |
accept | A comment states the coupling: a template that reformats the call breaks the check rather than passing it. |
| 68 | The commit bodies run past the documented 72 columns | this branch | accept | The three original messages are re-wrapped by a reflow over the branch; the two written after this round are wrapped as they are written. |
| 69 | The loader's new comment claimed a last-item marker nothing emits | org_loader.py |
accept | The comment now says what the template does: one code per row, comma included. |
| 70 | The prefix-and-tail shape is duplicated between commodity and security | the projections template | accept | split_code_and_month and index_name_code_and_month carry it, and both classes' arms and emitters call them. |
| 71 | market_data_key names a class label, while the vector holds keys |
the coverage test | accept | Renamed market_data_key_class. |
| 72 | "The one index-name prefix pair that is a bare token" names nothing | the projections template | accept | The comment now says the class has no prefix to strip. |
| 73 | The inflation rule holds on the read side only | the parser and projections templates | accept | Both sides use the same generated table: a fixing URI whose code the model does not carry is refused as it is parsed, and an identifier holding one has no name to write. |
| 74 | A rejection case also asserts a positive routing | the projections tests | accept | The assertion moved into the inflation case that owns the routing. |
| 75 | delivery is accepted on a security quote and dropped |
the parser template and the three models | accept | The same defect class the previous review closed elsewhere, and it is closed for every class that names a delivery: the gate is in the shared block, since the Type-gated keys table reaches only the classes that build a validator. Each of the three models declares the rejection. |
| 76 | The inflation table is bypassed where an identity is minted | the parser and projections templates | accept | Both directions are gated on the same table, so neither mints an identity the other refuses. |
| 77 | The census reports its population without asserting it | the coverage test | accept | It now asserts that the classes and the pinned keys add up to the corpus's 158 names, so a payload that leaves the walk fails instead of shrinking the total quietly. |
| 78 | The acceptance cites the wrong review rows | this task | accept | Row 11 alone records the import gap; row 10 records the allowlist, which is met. |
| 79 | The classifier is narrower than the projection for a security name | the coverage test | accept | It matches any BOND- tail, as the projection accepts one. |
| 80 | The emitters write names the reader would refuse, a code carrying a dash and a ticker carrying a slash | the projections template | accept | Both are refused now, which is the rule this round applied to inflation: the write side refuses what the read side refuses. |
| 81 | The parser is stricter than before about a delivery on a non-fixing URI | the parser template | no action | Intended. Every delivery URI the models and the tests carry is a fixing, and no oresmd URI is stored yet, so nothing depends on the old behaviour. |
| 82 | The inflation code list is emitted in two translation units | the parser and projections templates | decline | One model table, two generated arrays. They cannot drift, and a shared header for one list is more machinery than the list holds. |
| 83 | The delivery refusal is emitted once per class | the parser template | decline | The template holds the check once; what repeats is generated output, and a runtime helper for three generated lines adds indirection rather than removing it. |
| 84 | No case covers a delivery month the parser accepted and the writer refuses | the projections tests | accept | The security shape case now pins that a month which is not a contract month projects no name; the lower-case inflation code was already pinned by the dash-less case. |
17. Result
The interest-rate third of the fixing key space is closed. from_index_name names
all 57 distinct interest-rate index names the corpus carries, and each projects
back to the name it arrived as; before this work it named 34 and round-tripped 28.
That is 57 of the corpus's 158 names, so most of the key space is still open, and
none of it is wired: import_service still stores the index name in the series
qualifier and never calls from_index_name, so the acceptance criterion about the
import creating a series for every fixing row belongs to the identity cutover.
Fifteen families joined the enum: BBSW, CIBOR, CMS, HIBOR, NIBOR, PRIBOR,
REPOFIX, STIBOR, CAMARA, CZEONIA, DKKOIS, EONIA, FedFunds, SIFMA and SIOR. The
two spellings ORE writes differently, ESTER for estr and TONAR for tona,
became aliases in the model, and the identifier now carries the spelling a name
arrived under so it reads back as it came. The tenor rule left the hand-written
oresmd_index_family_utils.hpp for the Index family table, which the generated
requires_tenor() and both suite's family lists are built from.
A curve identifier drops the tenor of an overnight family, because the tenor
belongs to the curve key; a fixing identifier keeps the tenor the index was
published at, because USD-SOFR and USD-SOFR-3M are two series. role is what
tells the two apart, and both readings are pinned in the projections tests.
The FX class is closed with it. An FX fixing carries source, lower-cased, and
source_spelling for the token as ORE wrote it, both meaningful only when
type=fixing; the pair stays the pair as published. All seven FX names the corpus
carries name and round-trip, from none, and every other asset class refuses the two
keys rather than dropping them. The corpus total is 64 of 158.
The new coverage test walks every fixing payload with the real reader and reports
the table per class. It found a fixing file the reader refused:
CurveBuilding/Input/fixings_bondyieldshifted.csv is semicolon-separated, is the
fixingDataFile its example names, and is a file ORE reads. The reader now reads
it, and every one of the 66 payloads is walked.
The power and commodity halves are closed with the next change. ORE states the
intraday power index grammar in its own parser rather than leaving it to be
inferred, and its commodity parser reads a contract month the same way, so a
fixing's delivery period becomes a delivery field of its own rather than a
point: point is what a quote or a vol names, and a fixing whose coordinate is a
field stays scalar, as the interest-rate tenor already does. Power becomes a class
of its own because ORE accepts a bare POWER-ICE:PDQ and a bare COMM-ICE:PDQ,
and one class could not write both names back. Power is 48 of 48 and commodity 25
of 25, all round-tripped, which takes the corpus total to 137 of 158. delivery
joins the keys the other classes refuse, so no class accepts it and drops it.
Still open, each its own decision: inflation (7 names), equity (8) and security (2).
The two GENERIC-MD/EQUITY_OPTION/... rows the corpus puts in a fixing column are
errors rather than fixings to model, so the target is 156 of 158 rather than 158,
and the census counts them as market-data-key so that the population it walks
stays whole.
The generic class is closed with the next change. ORE states the prefix in a parser
of its own, so generic joins power as a class the corpus's prefix names: the
identifier carries the index name upper-cased and the case ORE wrote it in beside it
in name_spelling, which is the FX source_spelling rule. Both names the corpus
carries name and round-trip, the second of them through the %23 its # becomes in
a URI, and 2 of 2 takes the corpus total to 139 of 158.
The fixing key space is closed with the last three classes. Every class the corpus puts in it resolves and round-trips: interest rates 57, intraday power 48, commodity 25, equity 8, inflation 7, FX 7, security 2 and generic 2, which is 156 of the 158 names in a fixing column. The other two are market-data keys in a fixing column, and the projection refuses them, so the target is 156 of 158 rather than 158: the owner reads them as errors rather than as fixings to model.
The corpus-driven coverage test now names the classes with an empty allowlist, so a class that stops resolving fails it, and the two keys are pinned in a bucket of their own and asserted to project to nothing. Nothing was dropped to make the list empty: the fixing space and the keys misfiled inside it still add up to every distinct name the corpus carries.
What remains is not this task's. The import still stores the index name in the
series qualifier and never calls from_index_name, so the acceptance criterion
about the import creating a series for every fixing row belongs to the identity
cutover, as Review row 11 records.