Task: Move the rates family onto its header
Table of Contents
This page documents a task in the Rework the rates instruments to the instrument table rule story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
The rates family has a header: ores_trading_rate_instruments_tbl carries the
family's coverage check, the fields the products share, the trade-activity key
and the delete cascade that owns the family's rows. The eight product tables
become fact tables keyed by the same trade, and the import and the export read
and write the header beside them.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Rework the rates instruments to the instrument table rule |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-09 |
3. Acceptance
rate_instrumentsexists with the family's coverage check over its codes,start_date,maturity_date,description, the trade-activity key, the audit spine, tenant isolation and a* Delete cascadethat names the family's legs, leg children and fact tables.- The eight product tables are fact tables:
trade_idas a flat primary key,trade_activity_idflat, their own fields, noinstrument_identity, noparty_id, notrade_type_code, no shared dates, tenant isolation. - The trade-type catalogue routes the family's codes to
rate_instruments, so the header's check lists exactly them. - The import writes the header row and the fact row, and the export reads both;
ores.ore.core.testspasses at the new shape, including a delete that closes the family.
4. Plan
The shape is the bond pilot's, so each step is a copy of something that already works rather than a new mechanism.
- The header model.
ores.trading.rate_instrumentsmirrorsores.trading.bond_instrument::profile: trading-instrument, the identity groupores.trading.instrument_identity, the audit group,:routed_by:the trade-type catalogue with:routed_column: trade_type_code,:rls_tenant_isolation: true, the party and trade-type indexes, and a* Delete cascadesection namingswap_legs, the two leg children,callable_swap_call_datesand the eight fact tables, each with:table:and:column: trade_id. - A fact table's shape.
ores.trading.bond_optionis the template: flattrade_idas the primary key, flattrade_activity_id, the product's own fields, a* Foreign keyssection for the two the facts keep, and:rls_tenant_isolation: true. Each of the eight models drops its identity group, itsparty_id, itstrade_type_codeand the shared dates, and keeps its own fields. - The catalogue. The nine rates codes move from their product tables to
rate_instruments. That is what turns the header's check into the family's coverage check, and it is why the products must be converted in the same change:instrument_table_forstarts answeringrate_instrumentsfor every rates code, so the export reads the header for the instrument and the fact table for the fields. - The carrier and the mapper.
swap_instrument_datagains the header, so a rates instrument in memory is the header plus its leaf plus its children. The forward mappers fill the header's fields from the document, as they fill the leaf's today, and the reverse mappers take it. The import writes the header row before the fact row; the export service reads the header rows into the batch'srate_instrumentsarray and the facts into theirs, andrebuild_instrumentjoins them by trade id, exactly as it joins the legs. - Order of work. Author the header and the catalogue entry first, then convert one product end to end - FRA, which has the fewest own fields - with a build and the ORE round trip between each conversion. Each conversion is verifiable on its own, and the family is never half-routed: a product's codes move to the catalogue's header row in the same change that converts it.
5. Notes
- The story's acceptance says "each product fact table checks its own" trade
type. The pilot this family follows does not do that:
bond_instrumentscarries the family's whole check and the bond fact tables carry none, and they join the header by trade id. The catalogue can state one routing per trade type, so a fact table cannot hold a check of its own while the header holds the family's. This task follows the pilot and the design's entity table; the acceptance line is reconciled when the story closes. - The staging unit also carries two clauses that are already done and are not part of this task: the cross-currency swap's type (its own task) and, in part, the seven unwritten columns. What remains here is the header, the field move and the routing.
descriptionandtrade_activity_idare written by the handler andstamp_idsrather than by the mapper, so the field move touches them in the handler;start_dateandmaturity_dateare the mapper's (19 and 14 uses, the family's most-used fields) and are the bulk of the mechanical work.- The three unrouted codes (RateDigitalOption, SwaptionStraddle, ForwardVolatilityAgreement) have no table and are out of scope, as the design records.
6. 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 |
|---|---|---|---|
| Models converted to fact tables | 8, plus the catalogue | convert_rates_products_to_facts.py (evidence below) |
bb1127566a |
| Model diff | 234 insertions, 882 deletions across 9 models | git diff --numstat -- projects/ores.trading/modeling/ |
bb1127566a |
| Header coverage check | the family's ten codes | grep 'trade_type_code.*in (' trading_rate_instruments_create.sql |
bb1127566a |
| Cascade targets | 12 tables | grep 'delete from' trading_rate_instruments_create.sql |
bb1127566a |
| Party stamp | 2 in the header, 0 in a fact table | grep -c 'NEW.party_id' ... |
bb1127566a |
| Codegen suite | 1299 passed | pytest projects/ores.codegen/tests -q |
bb1127566a |
| Codegen drift | none | check_component_drift.py --component trading-cpp --dry-run |
bb1127566a |
| Build | exit 0, no errors, no warnings | ./compass.sh build --preset linux-clang-debug-make -j 2 |
bb1127566a |
ores.trading.api.tests |
370 assertions in 82 cases, all pass | the suite binary, after compass db recreate -y -k |
bb1127566a |
ores.trading.core.tests |
954 assertions in 141 cases, all pass | the suite binary | bb1127566a |
| The cascade delete | 25 assertions in 1 case, all pass | ./ores.trading.core.tests "[repository][rate_instrument]" |
bb1127566a |
ores.ore.core.tests |
482193 assertions in 624 cases, all pass | the suite binary | bb1127566a |
The raw output for every row above is in evidence/unit_3_verification.txt; the model conversion itself is the rerunnable script evidence/convert_rates_products_to_facts.py.
7. Notes
Decisions taken while doing the work, all following the pilot rather than inventing a mechanism:
- The header's tenant isolation is hand-written, not the generated
:rls_tenant_isolation:form. The header carriesparty_id, so it needs the restrictive party policy beside the tenant policy, and the generated form states the tenant policy alone;bond_instrumentsis wired the same way. The eight fact tables carry no party, so each takes the generated tenant policy, and the hand-written product blocks were removed fromtrading_rls_policies_create.sqlin the same change to avoid a second policy on every fact table. - The header's
start_dateandmaturity_datearestd::optional<std::chrono::year_month_day>, because the swaption's were nullable and the header is the union of the family's shapes. The table's check is the null-tolerant form the swaption carried. - The FRA's
end_datedoes not survive as a fact column: it is the header'smaturity_date. Finding R9 turns on one name for the date, andend_dateis one of the names it lists. netting_set_idwas dropped from the vanilla swap's facts (design decision D5).trade_bookingholds the same fact as a uuid foreign key, and nothing wrote the instrument's text copy; it joins the header with the rest of the field move rather than waiting for a separate clean-up.InflationSwaproutes torate_instrumentswith the rest of the family. A table cannot be both a routed instrument and a fact child, so convertinginflation_swap_instrumentsto a fact table forces the code to the header. The taxonomy still calls it an inflation product and it leaves with that family's rework, as the catalogue records.rates_instrument_variantbecamerates_fact_variant, because its alternatives are fact rows now, andswap_instrument_dataholds the header beside them, with the variant member namedfacts.
8. 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 |
|---|---|---|
9. PRs
10. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
11. Result
The rates family has a header. ores_trading_rate_instruments_tbl carries the
family's coverage check, the party and the party boundary, the trade-activity
key, the start and maturity dates, the description and the delete cascade that
closes the family's twelve tables. The eight product tables are fact tables
keyed by the same trade, holding only their own fields; the catalogue routes
the family's ten codes to the header; and the import writes the header row
before the fact row where the export reads both. A new case deletes a header
and proves the family closes with it.
The task's acceptance has one line that does not hold as written: "each product fact table checks its own" trade type. The catalogue states one routing per trade type, so a fact table cannot carry a check while the header carries the family's, and the bond pilot this family follows does not do it either. The story's acceptance is reconciled here rather than at story close, because the reason is structural, not a deferral.
Two clauses of the staging unit that this task's Plan listed as already done were already done: the cross-currency swap's own type (its own task) and the seven unwritten columns.