Task: Model the basket constituents
Table of Contents
This page documents a task in the Eight columns in the trading models hold JSON blobs where th… story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
A basket is a row per constituent, each stating its kind, its position and the code it names, so a worst-of basket of FX underlyings and a basket of equities are the same shape and each constituent resolves against the vocabulary its kind belongs to.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Eight columns in the trading models hold JSON blobs where th… |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-26 |
3. Acceptance
- A basket constituent table holds one row per constituent, with the kind, the position and the code it names.
- The equity swap and the equity accumulator draw their constituents from it, and no JSON blob holds a list the domain can state as rows.
- A constituent of a kind whose catalogue does not hold the code is refused when it is written, not when a report reads it.
- An export writes the basket back as the constituents the document states, and a roundtrip test proves it.
4. Plan
4.1. What already exists, and what is missing
The table this task was going to create already exists. equity_basket_constituent
is modelled, generated and documented: trade_id, trade_activity_id,
sequence_number, underlying_name and weight, with a docstring that says the
basket_json column "is retired with this table".
It is not retired, and the table is connected to nothing:
instrument_batchcarriescommodity_basket_constituentsandcredit_basket_constituentsand no equity array.write_instrument_batchwrites those two and no equity one.equity_instrument_projectionstill writesr.basket_json = names_json(names)on import and readsnames_of(v.basket_json)on export.equity_swap_instrumentstill declaresbasket_json.
So the columns were modelled and the wiring was not done, which is why the blob survived. The task is to finish it rather than to design it.
4.2. Steps
- The batch. An
equity_basket_constituentsarray oninstrument_batch, and the write for it inwrite_instrument_batch, so a booking can carry the rows. - The variant. Not needed, and the plan was wrong to say so.
equity_instrumentis a flat header, not a variant: the equity family keeps the header in one array and each product's facts in another, and the batch is what carries the children. So the constituents are carried byequity_basket_constituentson the batch, which step 1 added, exactly as the commodity family carries its own. - The projection. Import writes one constituent per
Underlyingelement with its ordinal; export reads the rows back in ordinal order. The commodity family is the template and already does both: its read builds astd::vector<commodity_basket_constituent>, and its write takes the same vector as a parameter rather than reading a column off the instrument. The equity side has to thread that parameter through the same way, which is what makes this more than a column swap. - The column.
basket_jsonleavesequity_swap_instrumentonce nothing reads it, which is what the existing docstring promises. - The tests.
xml_portfolio_projection_testsassertsbasket_jsontoday and has to assert the rows instead, with a roundtrip case for a two constituent worst-of basket. - The kind. A constituent states its kind and the vocabulary its code belongs
to, which is the modelling the corpus demanded: an
EquityWorstOfBasketSwapin the examples carriesType=FXconstituents. The existing table has no kind column, so it cannot hold that trade as it stands.
5. 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 |
|---|---|---|---|
| Columns replaced by rows | 5 | grep -c "_json" projects/ores.trading/modeling/ores.trading.equity_*_instrument.org |
124692ba7d |
| Corpus documents that round trip | 132 of 132, failed=0 | ores.ore.core.tests |
124692ba7d |
| ORE suite | 475,515 assertions, 394 cases | ores.ore.core.tests |
124692ba7d |
| Trading suite | 1,416 assertions, 221 cases | ores.trading.core.tests |
124692ba7d |
| Synthetic suite | 1,640 assertions, 74 cases | ores.synthetic.core.tests |
124692ba7d |
| Build targets | 8,010 of 8,010 | compass build |
124692ba7d |
6. Notes
7. 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 |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #3917 | [trading] Model the basket, the prices and the levels as rows |
9. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
10. Result
Five columns that held JSON became rows, and each one is now something the store can answer a question about.
| Column | Became | Why there |
|---|---|---|
basket_json |
equity_basket_constituent rows |
one row per underlying, in the document's order |
initial_prices_json |
equity_basket_constituent.initial_price |
the document states one price per underlying |
knock_out_levels_json |
equity_swap_period.knock_out_level |
the document states one level per determination date |
fixed_trigger_levels_json |
equity_swap_period.fixed_trigger_level |
the same list, observed on the same dates |
range_bounds_json |
range_bound rows |
the shared table the FX accumulator already writes |
The corpus decided the shape. InitialPrices carries one entry per underlying
and the two level lists carry one per date, so a basket of two names over four
dates states two prices and four levels. Folding all four into the constituent
would have been wrong and would have failed on the first real basket.
Three things the work cost that a reader should know:
- The instrument batch is generated from
ores.trading.trade_type_catalogue.org. Editing the header directly is reverted by the next regeneration, and the array only exists once the catalogue names the type. as_equitybuilt the wrapper without its children twice: first without the basket, then without the period rows, and each time a round trip kept the first row and lost the rest.- A leaf must be read with the accessor its schema type deserves. Reading
Leverageas text threw, the whole mapping was caught, and the accumulator imported with no instrument at all rather than with a wrong one.