Investigation: the trade shape
Table of Contents
- 1. Context
- 2. Methodology
- 3. Paths taken
- 3.1. Anchor and components
- 3.2. Identity and the trade id
- 3.3. Party, counterparty and cancel-and-rebook
- 3.4. Links, sets and groups
- 3.5. Book moves
- 3.6. Dates
- 3.7. The operations queue
- 3.8. One activity per version
- 3.9. Counterparty and netting set
- 3.10. The ORE envelope
- 3.11. Structures
- 3.12. Acts
- 3.13. Workspaces, and what replaced them
- 3.14. The instrument tables
- 3.15. Where each entity lives
- 3.16. The C1202 check
- 4. Conclusions
- 5. Recommendations
- 6. See also
A point-in-time record, written while the story that commissioned it was open, of an investigation into the target shape of the trade on data-oriented principles.
It is a snapshot, not a description of the system. Whatever it concludes that the code must keep doing belongs on the page that owns the subject; cite that page from code, recipes and models, not this one.
1. Context
The question came out of the review of PR 2496, which closed the
Clean ores.trading story. trade.hpp defines trade as a struct of
five sub-structs (identity, parties, classification, lifecycle, audit),
which reads as the aggregate entity that data-oriented design argues
against ("there is no entity",
https://www.dataorienteddesign.com/dodmain/node5.html). The owner asked
why the type exists, what trade_envelope and trade_identifier add,
and whether the trade and its join to the instruments hold together.
1.1. What the current model is
Read on main at 1786aa23cd (2026-10-02).
trademaps one-to-one toores_trading_trades_tbl, 19 columns plus the temporal and audit columns. It holds no instrument, envelope, identifier or party role; those are separate tables keyed by the trade. At the top level the model already follows "there is no entity".- The five sub-structs are not tables, are never loaded separately, and
are used by no other model. The trade model states their reason: to
keep each reflected struct under MSVC's C1202 limit in reflect-cpp's
reflection (a reason section 16's correction shows does not hold for
plain structs). They also fix the wire as nested JSON.
trade_auditis a field-for-field copy ofores.dq.audit_record, which every instrument uses. The split is driven by a serialiser limit, not by any access pattern. - The real aggregates are the export carriers:
trade_instrument(a variant over eight families), the*_datacarriers,trade_export_itemandinstrument_payload, built for the ORE import and export journey.
1.2. What does not hold together
- The family routing lives in three places:
trade_types.product_type, a stringswitchin the hand-written export handler that maps trade-type codes to 30 instrument tables, and a check constraint on each instrument table'strade_type_code. Nothing checks that they agree. trades.product_typeis a nullable copy oftrade_types.product_type, kept in step by nothing, and the dispatch reads the copy.- All 30 instrument tables copy
trade_type_codewith no check against the trade, and their trigger takesparty_idfrom the session (app.current_party_id) while the trade derives its own from the book. The two can disagree, and row-level security filters onparty_id. - Nothing stops one trade having rows in two instrument tables.
- The counterparty is stated three times:
trades.counterparty_id,trade_party_roleandtrade_envelope.counter_party(document text). The netting set twice. - Identifiers have two homes:
external_idis the trade's key, andtrade_identifierholds UTI, USI and the rest. trade_envelope, its two children andtrade_identifiernever check that their trade exists;trade_party_roledoes.- Smaller:
trade_party_rolewrites toores_trading_party_roles_tbl;trade_typeskeeps two columns that only drove Qt forms; the instrument rebuild is a third hand-written operation besides the two exports P04 names.
2. Methodology
- Read the trade models, the generated SQL and the domain headers at
1786aa23cd. From the owner's instruction mid-way, the design was then done from the documents only, as a model from scratch. - Read the knowledge cluster under Trade: Trade Modeling, Trade Identifiers, Trade Structures and Deal Composition, Confirmations, Trade Versioning, Trade Lifecycle, Trade Classification, Operational Authorisation and its queue, Trade Activity, Positions, the Trade Blotter, and the trade-entry prototype. Read the architecture notes Data-Oriented Design in ORE Studio and Temporal Composite Entity Versioning.
- Read the owner's internal analysis notes on structures and deal composition, deal linkage, netting sets, internal trades, funding and sweeps, non-contributing trades, confirmations and versioning, books, P&L attribution and the trade blotter.
- Read the ORE schema for netting sets (
nettingsetdefinitions.xsd, thenettingSetDetailstype). - Researched prior art on book moves: Basel RBC25 and MAR12, IFRS 9 paragraph 4.4.1, the EBA no-action letter, and press coverage. Vendor manuals are not public.
- Ran the design as a brainstorm with the owner, one decision at a time, recording the alternatives each decision rejected.
3. Paths taken
The design moved in sections. Each records the decision and what it set aside.
3.1. Anchor and components
Decided: the trade becomes an immutable anchor row plus component tables keyed by the trade, one per access pattern, each versioning on its own timeline.
- The anchor holds only facts fixed for the trade's life. Because it never changes, other tables reference it with real foreign keys instead of trigger-checked soft keys against temporal rows.
- A copy is allowed only of an immutable fact, and is pinned by a
composite foreign key, so it cannot drift. Instruments may carry
trade_typeandparty_id(row-level security needs the party on every row) only through a key to the anchor. Each instrument table checks thattrade_typeis one of its own types, and the lists are disjoint, so a trade's instrument can sit in exactly one table. That closes findings 1 to 4. - The five field groups go: each table is narrow (see section 16's
correction on what the C1202 limit counts).
trade_auditbecomesores.dq.audit_record;instrument_identityloses its unpinned copies and becomes the one shared key group.
Set aside: keeping trade as one wide temporal row, which versions
booking, lifecycle and classification together on every change.
3.2. Identity and the trade id
external_idleaves the anchor. Every identifier scheme — UTI, USI, a legacy system's id, ORE'sTrade/@id— is a row oftrade_identifier(scheme, value, issuer), with a real foreign key to the anchor. That closes finding 6.- The firm's own id is the anchor's UUID, called the trade id throughout; "deal id" is not used.
- Set aside: a human-readable deal number on the anchor, and the firm's number as one more scheme.
3.3. Party, counterparty and cancel-and-rebook
- The firm's legal entity (party) and the counterparty are fixed for a trade's life and sit on the anchor. A change of either is a cancel and rebook: the old trade is cancelled, a new trade is booked, and a typed link joins them (novation for the counterparty).
successor_trade_idgoes; the link carries the relation. The knowledge notes described the field two contradictory ways, and that is now resolved in Trade Versioning.
3.4. Links, sets and groups
The trade-links documentation merged on main during the design. Its catalogue filed structures, merges, compressions, allocations and process runs as links. The owner's principle: where a collection of trades is named and an operation attaches to it, it is an entity with an identity, not a by-product of links.
- The vocabulary of the analysis notes was adopted: a set is trades sharing an attribute (book, counterparty, netting set), with the identity on the attribute's entity; a group is trades tied to an operation, with an identity of its own (a structure, a compression); a link joins particular trades for a reason and holds nothing.
- With the groups taken out, a link joins two trades, so the pairwise
trade_link(from trade, to trade, type) is enough. Each link type names its two roles and states whether it has an economic effect. - Exercise joined the catalogue: exercised option to resulting trade.
- Restructure is an amendment of the same trade; a roll that books a replacement is a link.
- Set aside: one link record with several ends, and links plus a separate run table.
Recorded in Trade sets and trade groups, Netting sets, Trade compression, Trade merge and Trade allocation, with the link catalogue, Trade Links, Trade Versioning and Trade Activity brought into line.
3.5. Book moves
Researched before deciding. No rule forbids moving a trade between two books that share a legal entity, a branch, a regulatory book and an accounting classification; the restrictions apply when a move crosses one of them (RBC25.14-16 for the trading and banking book, IFRS 9 4.4.1 for classification). The accounting constraint people remember governs the ledger postings, not the trade.
Decided: a move within those four lines amends the trade's booking, cuts an internal version with book move as the reason, and carries its fair value and whether historic cash flows move. Any other change of book is a close-out and rebooking, linked, or internal risk transfer trades. The temporal booking keeps the book history, which removes the P&L break the analysis notes describe.
Recorded in Book moves.
3.6. Dates
Decided: the dates are economic terms and live on the instrument; each family states them in its own words. The blotter maps them to common names (start, end, expiry, settlement) through one generated view.
Set aside: dates copied onto the booking, and a separate dates table.
3.7. The operations queue
Authorisation is stored as decisions, not as a status column.
authorisation_decision: the unit, the internal version decided on, the decision (authorise, deny, hold over, return to unseen, un-authorise), hold reason, a required comment, amendment reason, department, operational-error flag, escalation, who or which rule decided, and when.auto_authorisation_ruleholds Operations' rules.- The status is derived: a unit is authorised when its latest decision is authorise and covers the current version, so an amendment withdraws the release without anything resetting it.
- Denied is final, enforced in the schema. Decisions are append-only metadata and cut no version.
- Decided: the unit Operations authorises is the unit the customer confirms — a structure that confirms as a whole, or each trade.
3.8. One activity per version
Decided: trade_activity records each business event — type, actor,
time, comment, operational-error flag — and every component version
carries a foreign key to the activity that caused it. The activity
type is reference data stating the economic, confirmable and real axes
and the priority order. The external version moves only for a
confirmable activity; a null amend is recorded and versions nothing.
Set aside: a reason code per row grouped by transaction time, and an activity table for confirmable events only.
3.9. Counterparty and netting set
- The anchor gains
counterparty_id, immutable likeparty_id. trade_bookingkeepsnetting_set_id, versioned, because re-papering can move a trade to another set.- A composite key from the booking to
netting_set(set, counterparty, party), pinned to the anchor, stops a trade being filed in another counterparty's or another entity's set. trade_party_rolekeeps only the other roles: broker, sales person, agent, calculation agent.
3.10. The ORE envelope
Decided: import resolves the envelope's counterparty name and
portfolio ids to entities and rejects a document that does not
resolve; export regenerates them. AdditionalFields becomes
trade_additional_field (name, value, order). That closes finding 5.
Set aside: a provenance copy of the document's text, and keeping today's envelope tables.
3.11. Structures
structureis an immutable anchor: tenant, party, counterparty, kind, template.structure_kindstates whether the kind confirms as a whole (strategy, typed, dynamic) or trade by trade (package).structure_templatestates the roles and leg counts a strategy or typed product allows, checked when a leg is linked.structure_member, keyed bytrade_id, is temporal: a trade sits in at most one structure at a time and can be linked and unlinked.- A structure's legs share its party and counterparty.
- Decided: one level of nesting, two levels in total, through a
nullable
parent_structure_idand a check that a structure with a parent has no children.
3.12. Acts
Decided: an act is an activity. trade_activity_participant
(activity, trade, role) replaces the activity's single unit, and each
kind of act adds a detail table keyed by the activity: compression
(provider, cycle reference), allocation (block, source, template),
merge (policy, expiry-netting style), runs (process, run date), bulk
book move (books, cash flows moved). Links keep the activity that
created them.
Set aside: groups as separate entities pointing at an activity, and a table pair per kind.
3.13. Workspaces, and what replaced them
The design first kept a copy of a trade in another workspace under the same trade id, with the workspace in every key. It paused there, because the layered workspace design overrides a parent's rows copy-on-write and was never designed for a trade split into components.
The owner then reframed the question from the use cases rather than the data model. Users need two things without disturbing official data: to import trades and market data, and to experiment with trades and market data and run reports against the changes. Prior art — Calypso's pricing environment, Murex's what-if simulation and hypothetical trades, ORE's run-time stress scenarios, and the analysis notes on personal portfolios, test books and perturbed market data — isolates by explicit inputs and by classification, not by a copy of everything. A layered workspace is an application-level database branch, and it pushes a workspace into every key, query and foreign key.
Decided: workspaces are removed from the trade model. The Sandbox replaces them:
- A sandbox is an entity anchored to a node of the official portfolio tree (a business, a desk), opened only by users holding that right at the node, private or shared through the anchor's permissions so desks keep their Chinese walls. A single tenant-wide sandbox portfolio was considered and rejected for exactly that reason.
- Its books are virtual books: created by the trading system, never mapped to the ledger, holding no position of the firm.
- A trade in a virtual book is a draft recorded before any agreement, a test or a hypothetical, and is never live. A draft becomes real by Execute into a real book; a hypothetical by a new actual trade linked to it. Both go through the ordinary lifecycle and authorisation.
- Real concluded deals are never parked in the sandbox: they are booked into real books at once and held as unauthorised until Operations releases them.
- Imported and perturbed market data sit in the universe under their own source; the official configuration does not resolve to them. A report definition selects its inputs explicitly.
- The regulatory boundary was checked: Basel RBC25.1-2 (every instrument held is in the trading or banking book), MAR12, EMIR Article 9 (reporting of concluded contracts), MiFIR Article 25 and MiFID II Article 16(6) (record keeping). Each is met by the rule that a sandbox trade is never live.
The anchor's key returns to (tenant, id), and the real foreign keys need no workspace.
3.14. The instrument tables
The instrument refactor began with the bonds, and the clean-trading story's decisions record its outcome (D10, D18, D19, D21, D28, D29). It is not one table per trade type, nor one wide table per family, but a factoring of the shape:
| Level | Table | Holds |
|---|---|---|
| Family header | One per family | The fields every product in the family shares; a trade_type check listing exactly the family's codes; keyed by the trade and pinned to the anchor |
| Product facts | One per product with fields of its own, zero or one per trade | Only those fields; keyed by the trade, with a foreign key to the header and a check on its own codes |
| Shared children | Legs, schedules, option block, strikes, premiums | Keyed by the trade, an ordinal and a role code; the role is data, not a table |
| Security | Issue or contract | The terms the issuer fixes, keyed by the reference datum id and shared by every trade on it |
Decided: every instrument family is reworked to this rule, as the bonds were. How far a family can be cleaned is a function of how clean its data model becomes under the data-oriented review; the rule is the target, and the review decides each family's tables. The rates, FX, equity, credit and commodity families are still to do: ten of their tables share one list of all 121 codes, five equity tables hold two products each (defect 9), and the risk participation table has no constraint.
The routing from trade type to instrument table becomes data: each trade type names the table that holds it, and the generator produces the dispatch the export handler hand-codes today.
Set aside: one table per distinct full shape, which repeats the header's columns in every product table, and one table per ORE trade type.
3.15. Where each entity lives
Decided: trading owns what describes trades; reference data owns what exists independently of any trade.
| Entity | Component |
|---|---|
| Trade anchor, components, instruments, links, activities, structures, agreement | ores.trading |
authorisation_decision, auto_authorisation_rule |
ores.trading |
trade_link_type, trade_activity_type, structure_kind, structure_template |
ores.trading |
netting_agreement, netting_set, csa |
ores.refdata |
sandbox, the virtual flag on a book |
ores.refdata |
Every foreign key between the two runs from trading into reference data: a trade names a book, a counterparty, a netting set; reference data never names a trade. A netting set exists before its first trade and outlives its last.
Set aside: a credit component created now for agreements, netting sets and CSAs, before anything in it computes; and placing them in trading.
3.16. The C1202 check
Superseded on 2026-10-03: the nine-field limit below does not hold. 220 of the 287 reflected structs the models generate have more than nine members and compile on MSVC. The failure the CI log shows comes from a tagged variant write over a large carrier; see the decomposition section of Data-Oriented Design and MSVC fails C1202 on the bond export carrier. The counts below still show that each table is narrow.
Every reflected struct keeps nine effective fields or fewer, a nested group counting as one.
| Table | Count | Outcome |
|---|---|---|
trade |
9 | At the limit; the tenant and id nest as a key group, leaving 8 |
trade_booking |
7 | Fits |
trade_state |
4 | Fits |
trade_identifier, trade_party_role, trade_additional_field, trade_link |
7-8 | Fit |
trade_activity, trade_activity_participant |
7-8, 4 | Fit |
authorisation_decision |
about 14 | The decision's context — hold reason, amendment reason, department, operational-error flag, escalation — nests as one group, leaving 9 |
agreement, structure, structure_member |
7-8 | Fit |
netting_set |
9 | At the limit; the ORE details (call type, initial margin type) nest, leaving 8 |
sandbox |
about 11 | Its lifecycle (status, review date) and its visibility (mode, members) nest, leaving 9 |
| Instrument tables | per family | Checked in each family's rework; the header and facts split keeps headers narrow |
4. Conclusions
4.1. The target model so far
| Table | Key | Content | Changes |
|---|---|---|---|
trade (anchor) |
tenant, id | party, counterparty, trade type, counterparty scope, booking nature, origin, audit | Never |
trade_booking |
trade | book, netting set, trade date, execution time | Temporal: moves, re-papering |
trade_state |
trade | trade status | Temporal: lifecycle events |
trade_identifier |
trade, scheme | value, issuer | When a scheme is added |
trade_party_role |
trade, role | the other parties | When a role is added |
trade_additional_field |
trade, order | name, value | With the trade |
| Instrument family tables | trade | the economics, including the dates | Temporal: economic amendments |
trade_link, trade_link_type |
from, to, type | roles, economic effect | When a link is made or removed |
trade_activity, trade_activity_type |
id | type, actor, time, comment; per-type axes and priority | Append only |
trade_activity_participant |
activity, trade | role | With the activity |
| Act detail tables | activity | per kind | With the activity |
authorisation_decision, auto_authorisation_rule |
id | the decision and its context | Append only |
agreement |
structure or trade | external version, confirmation status | On a confirmable activity |
structure, structure_kind, structure_template |
id | kind, template, parent | Never; reference data |
structure_member |
trade | structure, role, sequence | Temporal: link and unlink |
netting_set |
id | agreement, counterparty, legal entity, limit, call type, initial margin type | Reference data |
4.2. The integrity the schema carries
- Real foreign keys to the immutable anchors, from every component, link end, participant, member, identifier, decision and agreement.
- Composite keys pin every copied fact: an instrument's trade type and party, a booking's netting set to the trade's counterparty and party, a structure's legs to its party and counterparty.
- Disjoint trade-type checks put each trade's instrument in exactly one table.
- A book move within the four-attribute boundary is the only change of book; anything else is rejected as a move.
- Denied authorisation is final; the authorised status is derived.
- Structures nest one level at most.
- A trade's booking names a virtual book only while the trade is a pre-agreement draft, a test or a hypothetical; going live requires a real book in the same write.
4.3. Documentation produced on the way
Trade sets, groups and links; Netting sets; Trade compression; Trade
merge; Trade allocation; Book moves; Sandbox; and consistency passes over the
link catalogue, Trade Links, Trade Versioning, Trade Activity, Trade
Structures, the Trade hub, the book cluster and the funding and sweep
notes. On branch feature/document-trade-groups.
5. Recommendations
5.1. Workspaces are settled
The sandbox replaces workspaces in the trade model; the five questions the layered design raised no longer apply. Removing workspaces from the other entities that carry them, and from report definitions, is a separate piece of work this design depends on only in that trades no longer carry one.
5.2. Build it
The implementation is scaffolded as stories, listed on the trade shape story: the code generator support first, then netting agreements and sandboxes in reference data, then the split of the trade, activities and links, authorisation and the agreement, and structures. Each instrument family is a story of its own in the backlog, reworked to the rule of section 14. The knowledge notes still say "deal id" in places, and a task on the story renames it to trade id.
6. See also
- Story: Design the trade shape on data-oriented principles — the story this investigation serves.
- Trade sets and trade groups — the vocabulary the design follows.
- Book moves — the book move rule and its sources.
- Trade Modeling — the four entities a trade is built from.
- Data-Oriented Design in ORE Studio — the review criteria and the C1202 rule.
- Trade — the hub of the trade knowledge cluster.