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_instruments exists 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 cascade that names the family's legs, leg children and fact tables.
  • The eight product tables are fact tables: trade_id as a flat primary key, trade_activity_id flat, their own fields, no instrument_identity, no party_id, no trade_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.tests passes 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.

  1. The header model. ores.trading.rate_instruments mirrors ores.trading.bond_instrument: :profile: trading-instrument, the identity group ores.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 cascade section naming swap_legs, the two leg children, callable_swap_call_dates and the eight fact tables, each with :table: and :column: trade_id.
  2. A fact table's shape. ores.trading.bond_option is the template: flat trade_id as the primary key, flat trade_activity_id, the product's own fields, a * Foreign keys section for the two the facts keep, and :rls_tenant_isolation: true. Each of the eight models drops its identity group, its party_id, its trade_type_code and the shared dates, and keeps its own fields.
  3. 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_for starts answering rate_instruments for every rates code, so the export reads the header for the instrument and the fact table for the fields.
  4. The carrier and the mapper. swap_instrument_data gains 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's rate_instruments array and the facts into theirs, and rebuild_instrument joins them by trade id, exactly as it joins the legs.
  5. 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_instruments carries 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.
  • description and trade_activity_id are written by the handler and stamp_ids rather than by the mapper, so the field move touches them in the handler; start_date and maturity_date are 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 carries party_id, so it needs the restrictive party policy beside the tenant policy, and the generated form states the tenant policy alone; bond_instruments is 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 from trading_rls_policies_create.sql in the same change to avoid a second policy on every fact table.
  • The header's start_date and maturity_date are std::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_date does not survive as a fact column: it is the header's maturity_date. Finding R9 turns on one name for the date, and end_date is one of the names it lists.
  • netting_set_id was dropped from the vanilla swap's facts (design decision D5). trade_booking holds 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.
  • InflationSwap routes to rate_instruments with the rest of the family. A table cannot be both a routed instrument and a fact child, so converting inflation_swap_instruments to 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_variant became rates_fact_variant, because its alternatives are fact rows now, and swap_instrument_data holds the header beside them, with the variant member named facts.

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

PR Title
#2973 [trading,sql] Validate the rate header's trade key
#2962 [trading,ore,sql] Move the rates family onto its header

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.

Emacs 29.3 (Org mode 9.6.15)