Task: Keep a floor a floor and hold the whole collar

Table of Contents

This page documents a task in the Carry the rates family's fields across the ORE boundary story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

A cap/floor document round-trips as the lists it stated, cap and floor alike, including a collar that states both.

2. Status

Field Value
State DONE
Parent story Carry the rates family's fields across the ORE boundary
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-18

3. Acceptance

  • The strike child records which list its row came from, and its key admits more than one strike per trade.
  • Every cap and floor the document states is written, not just the first.
  • The corpus collar document proves both lists come back, with the document's own values as the expectation.

4. Plan

  1. Give instrument_strikes a strike_role and a sequence_number, both in the key, mirroring swap_leg_rates beside it: that table already carries a rate_role in its key for exactly this shape, a two-arm closed set that is data rather than a table.
  2. Write every cap and every floor the document states, each with its role and its ordinal, rather than the first of whichever list is filled.
  3. Read the role back on export and write the row into the list it names.
  4. Prove it on the corpus collar, IR_CapFloor_on_FedFunds_amortising.xml, which states a cap of 0.03 and a floor of 0.0121 in one trade.

5. Decisions

  • The role goes on the strike row, not on the product fact. A collar states both lists in one document, so the arm is a property of the strike, not of the product. A cap_floor_type column on cap_floor_instruments would have been cheaper and would have made a collar unrepresentable.
  • The vocabulary is cap, floor and single. single is the one strike a bond, a swaption or an index option states, with no arm to name; the column is not nullable, so every writer must say what it means rather than leave a hole.
  • Both columns join the key. One strike per trade is the defect; an ordinal alone would let a second cap overwrite the first.

6. Measurements

What Value Command Commit
Strike rows a collar states 2 (cap 0.03, floor 0.0121) IR_CapFloor_on_FedFunds_amortising.xml:120,123 63bdfaeef2
Writers of a strike row 2 constructors and 3 transport sites grep -rn 'instrument_strike row;' projects/ --include=*.cpp gives the two constructors; grep -c 'put_instrument_strike_request' gives the three sites that send one 63bdfaeef2
Assertions in the collar case 15 the case body 63bdfaeef2

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
       

7. Notes

  • A key change on a shared table has four writers, and the first pass fixed one of them. The importer built put_instrument_strike_request in three places and copied eight of its ten fields in two of them, so a CapFloor or an index option strike would have been sent with an empty role and an indeterminate ordinal: the service throws and the table refuses it. The three sites now call one builder that takes the row, so a new key column cannot be forgotten by two of three callers.
  • sequence_number is generated as a bare int with no initialiser, and the model has no way to say otherwise: :default_value: changes the database default, not the C++ member. A missed assignment is therefore undefined rather than loud. The single builder is the answer for these three sites, and the same pattern is in swap_leg_rate, so it is a codegen-wide gap worth its own capture if it bites.
  • Strike history is now a no-op for this entity. A compound key has no single-string id on the history_provider interface, so the registrar registers nothing, exactly as swap_leg_rate's registrar has since it became compound; the generated comment states the reason. It is a change for this entity, and it is not covered by a test.
  • The trade_id_one_of filter on the strike protocol is gone, because the generator emits it only for a single-key entity. All the sibling protocols still declare theirs; the batch read that replaces it takes three parallel key vectors.
  • Two readers consume strikes without filtering by role: the credit reverse took front() and now asks for the single row; the bond reader assigns into a map keyed by trade and keeps the last row, which is safe only while a bond states one strike.
  • The ordinal now orders the reverse's output. Nothing else reads it, and read_strikes_by_trade_ids still orders by trade alone.

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
#3553 [ore,trading,sql] Give a strike its arm and its ordinal

10. Review

Comment summary File Decision Notes
       

11. Result

A cap/floor's strikes round-trip as the lists the document stated. Every cap and every floor is its own row, carrying the list it came from and its ordinal, and the export returns each row to the list its role names.

Proved red first: with the mapper reverted to the merge base the case fails on result.strikes.size() = 2= with 1 = 2=, which is the floor being dropped; with the fix it passes fifteen assertions, on the corpus collar's own values (0.03 and 0.0121).

The arm is data on the row rather than a column on the product, because a collar states both lists in one document: a cap_floor_type on the fact table would have made a collar unrepresentable. The shape mirrors swap_leg_rates, which already carries a rate_role in its key for the same reason.

The role is not nullable, so every writer says what it means: the rates mapper writes cap or floor, the credit mapper writes single for the one strike an index option states, and the generator writes single rather than a word outside the set.

Order within a role is preserved on the way out because the reverse sorts by the ordinal, and a role the product does not state is skipped rather than rendered as a cap.

The gate: build, db, ctest, ore-xml-roundtrip, pgtap and the static checks with the fleet down, then step-consumers with it up.

Emacs 29.3 (Org mode 9.6.15)