Task: Model the unmodelled trading families and bind the unbound

Table of Contents

This page documents a task in the Clean ores.trading to the component clean standard story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

Close the gap between what the component persists and what its models describe. This is item group M. Three things are missing: entities with a table and no model, entities with a model and no profile binding, and types the model declares in a shape the database should not hold.

The last of these is the sixteen-defect investigation doc/agile/versions/v0/sprint_25/ore_trade_generator_trade_populations/trade_model_defects.org, whose model-shape defects belong here. Defect 11 and defect 12 belong to the reference-data work and are out of scope, per decision D5.

2. Status

Field Value
State DONE
Parent story Clean ores.trading to the component clean standard
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-09-28

3. Acceptance

  • Every entity model binds a profile, or records a no-match with its feature values and the reason. Twenty-six of fifty-seven bind none today: the fourteen bond family entities, the seven instrument child entities and the five trade envelope and party entities.
  • The artefacts that have no model have one. Eight are tables and this PR authors them: activity_types, fpml_event_types, commodity_instruments, composite_instruments, composite_legs, credit_instruments, scripted_instruments and swap_legs. Each regenerates, and the drift dry run stays empty with it included. The ninth, product_type, is not a table: it is a database enum on the trades table, which is defect 10, and the defect table assigns that shape to PR 4. This PR records the decision and PR 4 authors it, so two PRs do not claim the same schema change. The task first said seven, counting the six families and product_type; the PR 3 survey found activity_types and fpml_event_types in the same state, and they are the two remaining legacy notify triggers PR 2 measured in the database.
  • Every hand-written domain type with no model is classified as a model to author, a message that is not an entity, or infrastructure, and the classification is recorded. Twenty-four are in that state today.
  • M04 is checked per model: each feature sits in the namespace the loader reads, and regeneration shows the change it should.
  • M09 passes: no presentation drawer, no property naming a retired message type, no dead history property, and the twenty-five singular generator_facet_name values are corrected. The scheduler pass recorded that all twenty-five remaining instances are in ores.trading.
  • The type defects close at the model level: defect 1 JSON-text collections, defect 2 dates held as text, defect 3 amounts held as double, defect 4 free-text enumerations, defect 5 code fields with no reference table, defect 6 one concept under two names, defect 8 enumerations enforced in PL/pgSQL, and defects 14, 15 and 16, which are shapes the model cannot express today.
  • Every generator defect found is fixed at its source with a regression test in the codegen suite.
  • check_component_drift.py --component trading-cpp --dry-run reports nothing after the work, and check_model_drift.py --summary reports no drift.

3.1. What is done

M09's model half is done and this branch carries it.

  • The twenty-five models that named the generator facet in the singular now name it in the plural, which is the loader's default and the only value the facet has. The correction collapses the second generator tree: the fifty files under api/.../generator/ (the singular facet's output) are deleted, and their replacements are emitted under api/.../generators/. The api source list is regenerated.
  • The five models that carried the Qt-era presentation properties – trade, lifecycle_event, party_role_type, trade_id_type and trade_type – no longer name a retired message type. The eleven properties the refdata clean-up removed are the set; here nine of them were present, forty-five lines in all. The properties the generator still reads (key_field, collection_name, domain_include, domain_class, protocol_include, has_pagination, has_uuid_primary_key, save_request_item_field) stay, because core.py reads key_field from the presentation namespace and the canonical trade key depends on it.

What the collapse dragged in:

  • A hand-written consumer, api/tests/generators_tests.cpp, included the singular path for one generator. It is migrated.
  • The trade model's handler_includes block named ores.dq.api/messaging/fsm_protocol.hpp, a header dq deleted when it modelled the FSM pair (dd131c388b). No trading source reads a dq FSM symbol, so the include is removed rather than repointed, and the prose above it no longer claims the save path reads transitions from it.
  • The regenerated trade eventing test seeds status_id from the dq catalogue, so ores.trading.core.tests now links ores.dq.api.lib and ores.dq.core.lib. The build graph could not make that change itself, which is why the generated test and the hand-written tests CMakeLists move together.

One thing this branch cannot finish. ores.workflow is red on main: core/src/service/fsm_state_map.cpp still consumes the deleted dq protocol (get_fsm_states_request, get_fsm_states_response with .success and .states), which dq's modelling replaced. That consumer migration is the workflow component's, not trading's, so the whole-tree build stays blocked until it lands. Trading itself is verified below.

Two findings from doing it:

  1. The in-place regeneration and the dry run disagree about one generated test. trade_eventing_integration_tests.cpp gains soft-FK parent seeding for status_id (an ores.dq.fsm_state row) when the component is regenerated in place, and the dry run does not render it. The seeding depends on another component's regenerated state, which the dry run, rendering trading alone into a temporary root, does not produce. This is the cross-component blind spot the backlog capture drift_gate_misses_cross_component_outputs.org records, seen from the trading side. The change is kept, and the dry run then agrees.
  2. The model's presentation block is not itself the defect: other components keep theirs. What M09 forbids is a property that names a retired message type, which is what the refdata precedent (6bb3577a55) removed.

3.2. Bound or recorded: the M02 survey

Thirty-one of the component's sixty-five org files bound a profile when the survey was measured on 2026-09-26 (twenty-six trading-instrument, three simple-lookup, one workspace-scoped-lookup and one trading-record). The ports and the reference entities added since bind twenty more, so at 2026-09-28 fifty-one of the eighty entity models bind a profile (thirty-two trading-instrument, seventeen simple-lookup, one workspace-scoped-lookup and one trading-record). The other twenty-nine bind none, and every one of the twenty-nine is recorded as a no-match, with the features that decide it and the reason.

Measured 2026-09-26, extended 2026-09-28. Every one of the twenty-nine carries :has_tenant_id: true and no ** Presentation drawer, so the presentation features (has_uuid_primary_key, has_change_reason_cache, has_pagination, has_explorer_api, has_csv_xml_io, has_version_navigation) resolve false. None sets read_only, current_state, system_scope, nullable_tenant_id, has_parent_id or party_id_from_session, and only bond_issue and trade_identifier set :has_workspace_id: true.

Entity Key Reason
bond_future_delivery_basket composite A
bond_issue_call_date composite A
bond_issue_conversion_target composite A
bond_leg composite A
bond_leg_amortization composite A
bond_leg_amount composite A
bond_leg_rate composite A
callable_swap_call_date composite A
commodity_basket_constituent composite A
equity_position_option_underlying composite A
instrument_option_exercise_fee composite A
instrument_option_payment_date composite A
instrument_option_premium composite A
instrument_schedule composite A
instrument_schedule_date composite A
trade_envelope_additional_field composite A
trade_envelope_portfolio_id composite A
ascot uuid B
bond_forward uuid B
bond_future uuid B
bond_option uuid B
bond_repo uuid B
bond_trs uuid B
instrument_option uuid B
instrument_strike uuid B
trade_envelope uuid B
bond_issue uuid C
trade_identifier uuid C
trade_party_role junction D
  • A: the key is composite. No profile declares a composite key; the lookups fix either a text natural key or a uuid surrogate, and a presentation tier this table has no drawer for.
  • B: the key is a uuid, the scope is the tenant alone, and there is no presentation drawer. uuid-identified-lookup fixes has_change_reason_cache and has_pagination true, which this entity resolves false. trading-instrument fixes has_workspace_id and party_id_from_session, neither of which it sets. fk-scoped-child fixes has_workspace_id, has_explorer_api and a parent, none of which it has.
  • C: the key is a uuid and the scope is the tenant and the workspace, with no presentation drawer. uuid-identified-lookup fixes has_workspace_id false and a presentation tier, which this entity resolves false, and workspace-scoped-lookup fixes a text natural key and a presentation tier. No profile describes a workspace-scoped record with no UI surface.
  • D: the shape is a junction. The one junction profile is anchored to the junction metatype, and M07 records that this component has no junction metatype.

The field-group models, the module and the component overview are not domain_entity models, so the survey excludes them. The later ports add groups of their own, such as composite_leg_identity.

The bindings were tested against every profile's own assignment table rather than against a name:

  • No profile declares a composite natural key, and seventeen of the twenty-nine key on two or more columns (for example instrument_id + leg_role + leg_number).
  • uuid-identified-lookup fixes has_change_reason_cache and has_pagination true, which every one of these entities resolves false, and has_workspace_id false, which two of them contradict.
  • trading-instrument fixes has_workspace_id and party_id_from_session, neither of which the bond family sets. It is the closest profile and still the wrong one, which is why the task forbids a forced bind.
  • fk-scoped-child fixes has_workspace_id, has_explorer_api and a parent, and only two of the twenty-nine are workspace-scoped.
  • tenant-scoped-junction fixes only has_tenant_id, but it is anchored to the junction metatype, and M07 already records that this component has no junction metatype. trade_party_role is the entity that would open when one is introduced.
  • readonly-timeseries describes a TimescaleDB hypertable, and every one of the twenty-nine is an ordinary versioned table.

None of this is recorded as a model property, because the loader does not know the names the spec's RecordNoMatch rule uses and an unknown property is silently inert. The record is the survey, which is what the standard's M02 method names and what the components that have already closed M02 did: clean-compute records its workflow batch link as a no-match with no profile, and clean-iam records two entities as genuine no-matches with their feature values.

4. Plan

4.1. Bind the unbound entities

Choose a profile or record a no-match for each of the 26. The bond family is the large group: bond_issue and its children are shared static data rather than per-trade records, so a profile built for a per-trade instrument is the wrong bind, and a forced nearest bind is not allowed. Where no profile fits, record the entity's actual feature values and the reason, as item M02 requires.

4.2. Author the nine missing models

Eight tables have a table, a repository, a service, a handler and SQL, and no model at all: activity_types, fpml_event_types, commodity_instruments, composite_instruments with composite_legs, credit_instruments, scripted_instruments, and the shared swap_legs table. product_type is the ninth, and its current shape is a database enum on the trades table rather than an entity, which is defect 10.

Use the established entity org format. The bond family from the sprint 25 pilot is the worked example. swap_legs is decided: D10 on the story fixes one model over the shared table, not one per family.

4.3. 2a. The profiles the nine new models bind

The nine are new entities, so M02 applies to them too. The decision per group, taken from the table's own columns and the profile's assignment table:

  • commodity_instruments, composite_instruments, credit_instruments and scripted_instruments bind trading-instrument. Each is an instrument sub-type with the same scope as the nine already modelled: tenant, workspace and party_id, no presentation tier.
  • composite_legs and swap_legs bind trading-instrument as well. They carry the same tenant, workspace and party_id scope; a profile fixes variability, never structure, so a leg child over the same scope takes the same bind. D10 fixes their shape as one model each.
  • activity_types and fpml_event_types bind simple-lookup. Both key on a text code, are tenant-scoped, carry no workspace, and have a shell surface, which is exactly the profile country anchors and what trade_type, party_role_type, lifecycle_event and trade_id_type already bind in this component.
  • product_type is deferred to the defect 10 analysis. It is not a table today: it is an enumeration on the trades table, so authoring it as an entity is a schema change with a data migration, not a transcription.

4.4. 2b. The commodity port, and the environment blocker

The commodity family is ported end to end in the tree: the model (450 lines, trading-instrument), its regeneration (twenty-one files changed, twelve created), the four consumers that read the nested shape, and the two supporting files the generated handler displaced. The model found and fixed a real defect on the way: it named its identity primary key id while the bound instrument_identity field group names it instrument_id, which the generated table, mapper and test all emit. The eight table-backed ports after it will hit the same choice, so their models must name the key instrument_id as the nine modelled siblings do.

The port was held until the environment could be rebuilt, because a regenerated model may not land without a proven regeneration. The environment is sound now, and the port is proven with it: the build is green, the trading and ORE suites pass, and the drift dry run is empty.

4.5. 2c. How the model was authored

One focused pass. The model comes from four sources: the create script projects/ores.sql/create/trading/trading_commodity_instruments_create.sql for its columns, indexes and checks; equity_position_instrument.org for the format, since it is an instrument sub-type of the same kind; the hand-written domain type for the field groups and the C++ type behind each column; and the loader for any property whose spelling was in doubt. It binds :profile: trading-instrument, like its nine modelled siblings.

The regeneration changes twenty-one existing files and creates thirteen. The twenty-one are the hand-written stack, which becomes generated: the domain type, JSON I/O, table and table I/O, the entity, mapper, repository and service, the handler and the event registrar, the create and notify-trigger scripts and the drop script.

The domain type is the significant change. It was decomposed into commodity_terms, commodity_option, commodity_pricing and commodity_exotic sub-structs, reached as v.terms. and so on. The model converges it to the canonical shape its nine modelled siblings have: instrument_identity plus flat optional blocks. Four files read the nested shape and move with the model:

  • projects/ores.ore/service/src/messaging/ore_import_execute_handler.cpp
  • projects/ores.ore/core/src/domain/commodity_instrument_mapper.cpp
  • projects/ores.ore/core/tests/xml_commodity_mapper_roundtrip_tests.cpp
  • projects/ores.shell/application/src/app/commands/trading/commodity_instrument_commands.cpp

Nothing else in the tree names the nested types, and no other file reads .terms, .pricing or .exotic in a commodity context.

What this means for the seven that remain: composite_instruments, credit_instruments and scripted_instruments are instrument sub-types and take the same model, regeneration and consumer migration. composite_legs and swap_legs are children over the same scope, and activity_types and fpml_event_types bind simple-lookup instead. Authoring the model is the cheap part, one pass from sources already in the tree; migrating the consumers and proving the regeneration is the work, and each family carries its own two to four consumers.

4.6. 2d. The environment defects, and how they cleared

The rebuild and the build were blocked by eight defects, none of them trading's. Two of the eight are fixed by this branch's own PR #2232 and the rest by main's PR #2240.

# Where Symptom State
1 dq, 5 population functions the file opens a block comment and never closes it; psql reports "unterminated /* comment" in setup_schema.sql fixed on main
2 service_vars.sh the populate scripts use :'storage_service_user', and SERVICE_NAMES has no storage, so psql reports a syntax error at ":" fixed on main
3 iam seed the storage service account row is never seeded; the role assignment raises "Account not found: ores_merry_newton_storage_service" fixed on main
4 dq artefact SQL ores_dq_tags_artefact_tbl carries no unique constraint for on conflict (tenant_id, dataset_id, name) fixed on main
5 seed upsert ores_dq_catalogs_upsert_fn relied on an ON CONFLICT clause that the versioning trigger pre-empts, so two seeds of one catalogue aborted the populate fixed by PR #2232
6 dq publish function ores_dq_coding_schemes_publish_fn publishes a dataset's rows with version 0, so a published key aborts the populate fixed on main
7 workflow, reporting dq's FSM remodelling (PR #2216) deleted fsm_protocol.hpp and the machine filter with it; the consumers stayed, so the build failed and both services died on dq.v1.fsm-states.list fixed by PR #2240
8 iam dq's dataset remodelling (PR #2239) replaced get_datasets_request; the iam consumer stayed, so the build failed fixed by PR #2240

Defects 1 to 6 stopped the database rebuild, and each repair revealed the next. Defects 7 and 8 stopped the default build, which had been red since PR #2216, and with it the fleet: services start reached twenty-two of twenty-four units and took four hundred and eighty-two seconds, because the readiness check waits on units that can never become ready.

On the fixed main the rebuild needs no workaround, the default build exits zero, and the fleet reaches twenty-four units in two seconds.

4.7. 2e. The ports, one at a time

Each port is one commit: the model, its regeneration, its consumers, and the proof.

Family Profile Model State
commodity_instrument trading-instrument ores.trading.commodity_instrument.org Done: build green, eight suites pass, drift empty
activity_type simple-lookup ores.trading.activity_type.org Done: build green, eight suites pass, drift empty
fpml_event_type simple-lookup ores.trading.fpml_event_type.org Done: build green, eight suites pass, drift empty
composite_instrument trading-instrument ores.trading.composite_instrument.org Done: build green, eight suites pass, drift empty
composite_leg trading-instrument ores.trading.composite_leg.org, with a new identity field group Done: build green, eight suites pass, drift empty
credit_instrument trading-instrument ores.trading.credit_instrument.org Done: build green, eight suites pass, drift empty
scripted_instrument trading-instrument ores.trading.scripted_instrument.org Done: build green, eight suites pass, drift empty
swap_leg trading-instrument ores.trading.swap_leg.org, with a new identity field group Done: build green, eight suites pass, drift empty

The composite port found a generator defect and fixed it at its source: the eventing integration test template read a mandatory FK parent's key as a flat member, which is wrong when the parent carries its key in an identity group. The template's org and its tangled mustache both state identity. for such a parent. The all-component dry run stays empty and the codegen suite passes 790 cases.

The composite read needed a correction after the port. The trade read passed composite instrument ids to the generated read_latest(ctx, ids), which filters the leg's own id, so a composite instrument came back with no legs. The read now takes the workspace's current legs and keeps the wanted instruments, and the service registers the composite leg event mapping the port had left unwired. A literate batch read, as the swap_leg model carries, is the next step for this table: the same section added to the composite_leg model produced no generated method.

A port rewrites its family's generated SQL, and the notify trigger's payload changes with it, so the development database takes the regenerated create and trigger scripts before the eventing suites pass. A compass db recreate -y -k does that. The port itself needs no database change to build.

4.8. Classify the unmodelled hand-written domain types

Done. Measured 2026-09-27. The twenty-four types fall into four states, not three:

  • Already modelled, as a field group (5): trade_audit, trade_classification, trade_identity, trade_lifecycle and trade_parties. Each has an org under projects/ores.trading/modeling/, and the entity models compose them. The B02 census counts them, which is why they appeared on both lists.
  • A model to author (9): activity_type, fpml_event_type, commodity_instrument, composite_instrument, composite_leg, credit_instrument, scripted_instrument, swap_leg and product_type. Eight carry a table and a hand-written stack and this PR authors them; product_type is an enumeration on the trades table and waits for PR 4, which owns that shape (defect 10).
  • A message, not an entity (10): rates_instrument_variant, equity_instrument_variant, fx_instrument_variant, instrument, instrument_identity, instrument_payload, trade_instrument, bond_instrument_data, bond_schedule_data and trade_envelope_data. M05 says a joined or denormalised shape is a message rather than an entity, and each of these is one: the three variants are discriminated unions over the instrument sub-types, instrument with its identity and payload is the polymorphic container the wire and the ORE mapper carry, trade_instrument is the trade's joined instrument view, and the three *_data types are the denormalised shapes the mappers produce. None owns a table of its own, which is the test that separates them from the nine.
  • Infrastructure (0): none. Every type is modelled, to be modelled, or a message, so no type needed a recorded reason for staying hand-written, and none is deleted from this PR.

The census of the twenty-four, as the task stated it:

Twenty-four hand-written domain types have no model: activity_type, fpml_event_type, commodity_instrument, composite_instrument, composite_leg, credit_instrument, scripted_instrument, swap_leg, product_type, rates_instrument_variant, equity_instrument_variant, fx_instrument_variant, instrument, instrument_identity, instrument_payload, trade_instrument, trade_audit, trade_classification, trade_envelope_data, trade_identity, trade_lifecycle, trade_parties, bond_instrument_data and bond_schedule_data.

Some become models in step 2. Some are messages, and item M05 says a joined or denormalised shape is a message rather than an entity: the instrument variants, the payload and the container are the candidates. The rest are infrastructure with a recorded reason. No type is left unclassified, and none is deleted from this PR.

4.9. Clear the legacy fragments

grep -rn "Presentation" projects/ores.trading/modeling/
grep -rn "generator_facet_name" projects/ores.trading/modeling/

Five models carry a presentation drawer: trade, lifecycle_event, party_role_type, trade_id_type and trade_type. Twenty-five models name the generator facet in the singular. Fix both in the models and regenerate; do not edit the generated output.

4.10. Close the type defects

Work them in the order the investigation gives, because each makes the next smaller. Defects 2, 3, 4 and 6 are item M08: SQL type spellings must be canonical, flags must be booleans, and one concept must have one name. Defect 5 is a _code suffix with no reference table behind it, so either the table is created or the suffix is dropped. Defect 1 is a collection held as JSON text; where the collection is a keyed child row under the relational rule, model it as one. Defect 8 moves twenty-seven enumeration checks out of trigger functions and into the schema. Defects 14, 15 and 16 need a shape the grammar may not express today: declared coverage, a qualified underlying rather than a free-text name, and an observation schedule rather than two scalar dates. If the grammar cannot express one, record it as a generator defect with a failing regression test rather than forcing the tree.

4.11. 5a. The defect states

The story's defect table assigns eleven defects to this PR. Work them in the order below, because each makes the next smaller.

# Defect State
17 The trade's declared key is nullable Closed
2 Dates held as text, 58 fields Closed
3 Monetary amounts held as double, 18 fields Closed: 57 money columns are decimals, 30 non-money columns stay double
6 One concept under two names Closed: day_count_code renamed to day_count_fraction_code in three entities, payment_frequency renamed to payment_frequency_code in one. Five sibling spellings were measured and judged not renamed, each with its reason in §5g
5 Code fields whose reference table does not exist Closed (Set 1)
4 Enumerations held as free text with no reference table Closed: twelve reference entities. The last, average_type, is trading-local by decision because ORE states no averaging set
8 Enumerations enforced in PL/pgSQL, 27 of them Closed (Set 3)
1 Schedules and collections held as JSON text Closed except equity_swap.basket_json, which waits for defect 9's product split, and the scripted columns, which defect 1 itself excepts
14 Declared coverage is incomplete, and elsewhere undeclared Carried to PR 4, by the owner's ruling
15 An underlying is an unqualified name Carried to PR 4, by the owner's ruling
16 No observation schedule Carried to PR 4, by the owner's ruling

Defect 17 is closed: the model declares external_id not null, its generator supplies a TRD- key, the shell verb takes the key first, the ORE importer refuses a keyless item, the SQL fixtures supply one, and a one-shot migration backfills the column and sets it not null.

One gate lesson: check_component_drift.py --all --dry-run does not cover trading, because trading is not in the drift registry's gated set. A hand edit to the generated trade_handler.hpp therefore passed the all-component run, and the next regeneration reverted it. The per-component dry run is the gate that catches a hand-edited generated file, and the edit now lives in the model where it belongs.

4.12. 5b. The capability units

Four units carry the defects that need a generator capability before a model change can express them. The story fixes their shape as D11, D12 and D13.

Unit Defect What it delivers
U1, dates 2 Closed. The generator expresses a plain date column, the 64 columns across 36 models carry typed members, and the 49 hand-written consumers convert at each boundary.
U2, decimal 3 Closed. The type, the reflector and the generator projection, proven on bond_leg_amount.value, then the bulk migration: 57 money columns became decimals and 30 non-money columns stayed double.
U3, collections 1 The four JSON columns become child tables with foreign keys: callable_swap_instrument.call_dates_json, commodity_instrument.basket_json, equity_swap_instrument.basket_json and equity_position_instrument.option_data_json, plus anything else the census finds.
U4, enumerations 4, 5, 8 A reference table and a foreign key per closed set, the checks move from PL/pgSQL to the schema, and the missing reference tables are populated from FpML or ORE.

U1 is in flight. U2 follows it, because both need the same generator seam, and two writers must not hold the templates at once. U3 and U4 each need the enumeration and collection census before they start.

4.13. 5c. What the date unit found on the way

Two generator defects and two shared root causes, each fixed at its source with a test rather than worked around:

  • The generator knew dates only as keys and timestamps. A plain date column put the domain type into the sqlgen entity, which its parser rejects with a static assert, and the history field mapper dropped the field. The projection and its regression test are committed.
  • A grouped column's write record is text while its domain member is typed, so the generated service emitted an assignment between them and did not compile. The conversion and its regression test are committed.
  • from_iso8601_date rejected the ISO 8601 basic form. The ORE corpus states 32 dates that way, and the six ORE mappers and the planner threw on them. The parser now accepts both forms.
  • The year_month_day reflector wrote the default-constructed sentinel as 0000-00-00, which its own parser rejected, so a default date could not survive a JSON round trip. That is what failed the instrument payload variants and the bond future export, and it is why the earlier attempt at this defect looked like a type regression. The sentinel is now the empty string.

4.14. 5d. What the decimal unit found

  • numeric(28, 10) holds at most eighteen integer digits. The type holds a wider value exactly, and the store refuses it with numeric field overflow; the repository test pins that refusal rather than hiding it. Every money column is numeric(28, 10) or numeric(18, 10), so the ceiling is the schema's, not the type's.
  • bond_leg_amount.value carries all six amount roles, so its rate rows move to the decimal with its notional rows. "Rates stay double" is a statement about a column's role, not about a column, and this table cannot honour it without normalisation. The model records the finding, and D12 and D13 own the shape that would fix it.

4.15. 5e. The money columns, and the calls worth challenging

Of the 87 numeric columns declared double, 57 are money and became decimals: 14 amounts, 13 notionals, 8 strikes, 7 prices, 7 barrier levels, 3 payouts, a premium, a fee, a face value, a price-difference amount and a sensitivity. The 30 that stay double are not money: rates, rate-shaped strikes and barriers, FX rates, volatilities and variances, a conversion ratio, quantities, an index level and a yield.

Four judgement calls are recorded so a reviewer can challenge them: the bond forward's dv01 moved as a money sensitivity; the equity and commodity barrier levels moved as prices of the underlying, while the FX barriers stayed rates; commodity_instrument.spread_amount moved as a price difference, while swap_leg.spread stayed a rate; and the three quantity columns stayed double as counts. Two of these are the most arguable: credit_instrument.option_strike stayed double although its name says strike, because the model describes it as a spread in basis points, and a quantity that is not a whole number would lose precision as a double.

4.16. 5f. The collection columns, closed

The callable swap's exercise dates, the commodity basket's constituents and the equity position's option underlyings are child tables now, each with its producer wired and the round trip asserting the values the mappers used to drop or never carried. The knock-out date column had no payload to model – the schema states barrier levels, one start date and two legs – and its family is never produced, so the column is deleted and the coverage gap recorded against defect 14. Two columns remain, both by decision: equity_swap.basket_json waits for defect 9's product split, because removing it would weaken the guard that tells a worst-of-basket swap from a plain swap, and scripted_instrument's three columns hold a user script whose shape the domain does not fix, which defect 1 itself excepts.

4.17. 5g. Defect 6, one concept under two names

Two renames. Both survivors are the name the component and ores.refdata agree on.

Concept Outlier name Survivor Entities
Day count convention day_count_code day_count_fraction_code bond_issue, commodity_instrument, credit_instrument
Payment frequency payment_frequency payment_frequency_code equity_swap_instrument

swap_leg already spelled the day count day_count_fraction_code, and commodity_instrument, credit_instrument and swap_leg already spelled the payment frequency payment_frequency_code, so each rename moves three or one outlier onto the majority name. The survivors are also refdata's: the tables are ores_refdata_day_count_fraction_types_tbl and ores_refdata_payment_frequencies_tbl, and the validators are ores_refdata_validate_day_count_fraction_type_fn and ores_refdata_validate_payment_frequency_fn.

What each rename moved:

  • The generated half regenerates: domain, JSON, table I/O, entity, mapper, repository, service, history field mapper, protocol, generator, insert trigger and checks. For equity_swap_instrument the model's ** Validations row also had to move, because it is a second place the column is named. The first regeneration left the generated insert function writing NEW.payment_frequency, and the live insert caught it as record "new" has no field "payment_frequency".
  • The hand-written half, by the compiler and the tests: the ORE bond, credit and equity mappers; the ORE import handler; the shell application's bond and commodity commands; and the trading core equity swap repository test.

Five measured cases were judged NOT renamed, each with the reason:

Spelling Why it stays
coupon_frequency_code on bond_issue The issue's coupon cadence. ores.refdata's own payment_frequency model names exactly four trading columns as storing a payment-frequency code (swap_leg, credit_instrument, commodity_instrument, equity_swap_instrument), and bond_issue is not one of them
strip_frequency_code, cliquet_frequency, fixing_frequency, frequency on bond_leg_amortization Role-qualified schedule and observation frequencies, not the payment frequency; each names a different schedule from a different ORE member
leg_type on bond_leg, repo_type on bond_repo, funding_leg_type on bond_trs The leg type in three different parent tables, already recorded in Set 2 as soft dependencies on ores_refdata_leg_types_tbl. bond_leg also carries the name in the hand-written bond_leg_data message, so renaming the column alone would leave that message and its round-trip tests on the old name
day_counter and last_period_day_counter on bond_leg, lock_rate_day_counter on bond_forward The ORE DayCounter, LastPeriodDayCounter and LockRateDayCounter member names, kept as the document spells them; day_counter is in bond_leg_data too, which is the leg_type case again
trade_type on trade against trade_type_code on the 29 instrument models One concept under two names, but the reference table is trading's own ores_trading_trade_types_tbl rather than a refdata lookup, and PR 4's identity collapse reshapes the trade and instrument identities together

No rate column is a duplicate under this rule. coupon_rate, fixed_rate, funding_rate, repo_rate, recovery_rate, participation_rate and lock_rate each name a different leg's or product's rate, and none points at a shared reference table.

4.18. 5h. M04, every feature in the namespace the loader reads

Eighty entity models were checked: every ores.codegen.entity model under projects/ores.trading/modeling/. The other ten org files are eight field groups, the module and the component overview, and none is a domain_entity. The check reads _FEATURE_NAMESPACE in org_loader.py and compares it with the drawer each model writes: the root * Flags drawer for the root namespace, * SQL → ** Flags for sql, and the C++ ** Presentation drawer for presentation.

Features stated by trading models, and the generated output that proves each bind:

Feature Namespace Where it is written Stated by Generated evidence
has_tenant_id root the entity root * Flags 29 explicit, 51 by profile "tenant_id" uuid not null in every create script
has_workspace_id root the entity root * Flags, or trading-instrument 2 explicit, the instrument profile "workspace_id" uuid not null default ores_utility_live_workspace_id_fn() in trading_bond_issues_create.sql line 59, and the member at bond_issue.hpp line 71
party_id_from_session sql the profiles, merged into the SQL namespace every trading-instrument entity NEW.party_id : current_setting('app.current_party_id')::uuid;= in trading_callable_swap_instruments_create.sql lines 110 to 111
has_change_reason_cache, has_pagination, has_uuid_primary_key presentation the profiles, or trade's ** Presentation the lookups and trade the paged list_trades_request at trade_protocol.hpp line 115
system_scope, nullable_tenant_id sql the profiles the simple lookups the tenant columns of the lookup create scripts
extra_checks sql the * SQL ** Checks table 58 models the in-list checks in the create scripts

No assignment is inert. No model writes a mapped feature in the wrong drawer, and every mapped feature reaches its namespace. The column-level keys are read too: supplied_by at core.py line 2948, default and default_value at lines 886 and 899, and index_prefix under the SQL namespace at line 3531.

Five features have no bind in trading, so none can be misplaced: has_parent_id, read_only, client_read_only, party_id_is_target and current_state appear in zero of the eighty models. The child tables link with an explicit instrument_id column and a * Foreign keys section rather than the generic parent feature, and no trading table is a current-state table. The loader's read sites are org_loader.py line 3044 (has_parent_id), line 2253 (read_only and client_read_only) and line 3408 (current_state); the namespace rule itself is covered by test_profile_binding.py, test_current_state_shape.py and test_readonly_current_state_profile.py.

4.19. 5i. The average type, the commodity producer, and the call type

Three units, ruled by the owner.

  • The average type is trading-local, and deliberate. ores.trading.average_type is authored in the simple-lookup shape: a text code key, the bitemporal pair, Arithmetic and Geometric seeded, a row in the module, an RLS policy beside the other lookups, and no validation function. commodity_instrument.average_type and equity_asian_option_instrument.average_type point at it. ORE states no averaging set: commodityAveragePriceOptionData and singleUnderlyingAsianOptionData carry no average member, no averageType simple type exists under external/ore/xsd/, and no AverageType element appears under external/ore/examples/. It is the one local reference set in the component and the divergence is stated in the model prose.
  • The commodity producer contradiction is removed, not guessed at. commodity_instrument_mapper.cpp line 365 read result.average_type = to_string(d.PriceType); – ORE's priceType, whose values are Spot and FutureSettlement, written into a column whose set is Arithmetic and Geometric. ORE states no averaging member for that family, so the honest fix is to write nothing and leave the column null. The assertion that would have caught it was added to the existing commodity_mapper_roundtrip_apo case as CHECK(r.average_type.empty());, which fails on the old Spot value.
  • call_type is deleted. ORE's only CallType element is on nettingSetDetails (external/ore/xsd/instruments.xsd line 225), a netting-agreement field, and no <CallType> element appears under external/ore/examples/. Nothing produced the column: the only writer was the import handler copying a domain member no producer ever set. The column and its check leave the model, the shell command loses its argument, and the import handler loses its assignment. The shape is knock_out_dates_json's: an XSD location, a corpus search that finds nothing, and no producer.

4.20. 5j. Defects 14, 15 and 16 are carried to PR 4

The owner ruled that the three defects needing a shape the grammar does not express are PR 4's. PR 3 claims none of them.

# Defect Evidence already banked
14 Declared coverage is incomplete, and elsewhere undeclared knock_out_swap_instrument is never produced: KnockOutSwap routes to the scripted mapper (scripted_instrument_mapper::forward_knock_out_swap writes script_name = "KnockOutSwap"), its knock_out_dates_json had no payload and is deleted, and that gap is the evidence that declared coverage, underlyings and the schedule need the reshape
15 An underlying is an unqualified name Nine equity tables carry underlying_name text, and the two basket products escape through basket_json; the oresmd URI and the instrument_underlyings table the defect page describes are PR 4's shape
16 No observation schedule The scalar averaging, fixing, cliquet and strip columns remain; the shared schedule table is the same shape as defect 1's child and is authored with the identity reshape

4.21. Regenerate and prove

Regenerate the component and re-run the dry run twice. Every new entity must leave the tree byte-identical. Then run the codegen suite.

projects/ores.codegen/venv/bin/python -m pytest projects/ores.codegen/tests -q
projects/ores.codegen/venv/bin/python projects/ores.codegen/scripts/check_component_drift.py --component trading-cpp --dry-run

4.22. Record

Update the story's checklist rows for M02, M04, M05, M08 and M09.

5. Notes

  • The sprint 25 design analysis is a September 8 snapshot of 38 orgs. This component has 65. Read the design for the shape of each family, not for its counts.
  • The trade model investigation measured its defects by loading every table, so its figures are reproducible. Its per-family detail for equity shows how to do the same for bond.
  • Two models under core/modeling/ are not entity models: ores.trading.protocol.org names four trade subjects in prose, and trade_status_fsm_implementation.org describes an FSM. Neither is a codegen model and neither belongs in the B02 census.
  • ores.trading.module.org lists leg_type and floating_index_type as trading entities after the move to ores.refdata. Correct it here.
  • Missing reference tables (defect 5) may need reference data from another component. If so, record the dependency rather than inventing rows here.

6. Defect 1: collections held as JSON

D12 governs: the trading domain model carries no JSON collection, and a collection is a child table keyed to its parent. This unit closes the collection that has a real producer and leaves the others recorded.

6.1. The census (measured 2026-09-28 at 8ee64b5002)

Eight columns across six instruments hold a collection in a text column. No column is typed json or jsonb; every one is text, and the deployed database agrees with the models on all eight.

Column Instrument Producer Judgement
call_dates_json callable_swap_instrument swap_instrument_mapper (forward only) Migrated: child callable_swap_call_date
knock_out_dates_json knock_out_swap_instrument none Deleted: no ORE source. knockOutSwapData states BarrierData (a Levels float list), one BarrierStartDate and two legs, and no date list anywhere in the schema. The family is never produced, so the column had nothing to hold. The coverage gap belongs to defect 14
basket_json commodity_instrument none Migrated: child commodity_basket_constituent, producer added for CommodityBasketOption
basket_json equity_swap_instrument equity_instrument_mapper (both ways) Collection with a real payload. Remaining, see below
option_data_json equity_position_instrument equity_instrument_mapper (added) Migrated: child equity_position_option_underlying, producer added for the nested EquityOptionPosition sub-trade
events_json, underlyings_json, parameters_json scripted_instrument scripted_instrument_mapper Not a collection: the payload is a user script whose shape the domain does not fix. This is defect 1's stated exception. Left as text

Two further checks found no other collection: no trading model declares a std::vector or std::map column, and no trading create script declares a json or jsonb type.

6.2. The child that landed: callable_swap_call_date

The ORE document states the callable swap's exercise dates as an unbounded list of ISO 8601 dates inside CallableSwapData.OptionData. The mapper built a JSON array by hand and the reverse mapper discarded it (a comment called the parse "a gap"). The child row is therefore the payload's own shape, not a guess:

Decision Value
Table ores_trading_callable_swap_call_dates_tbl
Key (tenant_id, instrument_id, sequence_number, valid_from, valid_to) — the parent id plus the date's ordinal, the shape bond_leg and trade_envelope_additional_field use
Parent instrument_id references the callable swap instrument. The component declares no SQL foreign key yet (defect 7), so the reference is the column and its prose, as in bond_leg
Scope has_tenant_id: true only. The child rides the instrument's tenant, workspace and party, so no workspace column rides it
Natural key none; the ordinal is the identity
Audit/validity the template's audit columns and the bitemporal valid_from=/=valid_to pair with the exclusion constraint
Columns sequence_number integer not null, call_date date not null
Checks "sequence_number" > 0, the generated nil-uuid check, and the column types themselves (a malformed date, a null entry and an out-of-range ordinal are each refused)
Payload losses none. A repeated date stays two rows because the date is not part of the key; the document's order is the ordinal

The shared instrument_schedule + instrument_schedule_date pair is the shape the defect page proposes, and it already exists. It was not used here because the rates family carries no schedule in its container and no read path returns one, so wiring it is the same work as the child plus a change to a table nine families share. That generalisation is defect 16, which this task does not own. The child entity is the minimal migration that satisfies D12; folding it into the shared schedule is recorded as remaining work.

6.3. The child that landed: commodity_basket_constituent

The ORE schema states a commodity basket's members in CommodityBasketOptionData.Underlyings.Underlying, an unbounded list of the shared underlying type whose Name is the commodity code and whose Weight is an optional float (external/ore/xsd/instruments.xsd, complexType basketOptionData and complexType underlying). The column claimed the same shape as {code, weight} but nothing wrote it: no commodity basket product was dispatched, so basket_json was always null. The producer is now added, so the payload's shape comes from the document rather than from the column's own description.

Decision Value
Table ores_trading_commodity_basket_constituents_tbl
Key (tenant_id, instrument_id, sequence_number, valid_from, valid_to) — the parent id plus the constituent's ordinal, the shape callable_swap_call_date uses
Parent instrument_id references the commodity basket instrument. The component declares no SQL foreign key yet (defect 7), so the reference is the column and its prose, as in bond_leg
Scope has_tenant_id: true only. The child rides the instrument's tenant, workspace and party, so no workspace column rides it
Natural key none; the ordinal is the identity
Audit/validity the template's audit columns and the bitemporal valid_from=/=valid_to pair with the exclusion constraint
Columns sequence_number integer not null, underlying_code text not null, weight numeric(28, 10) null
Weight type std::optional<ores::utility::decimal::decimal> — a weight is money-role (D11), and the ORE element is optional, so an absent weight stays null rather than becoming a zero the document never wrote
Checks "sequence_number" > 0, "underlying_code" <> '', and the generated nil-uuid check
Payload losses none. A repeated code stays two rows because the code is not part of the key; the document's order is the ordinal, and an absent weight is preserved as null

The producer is commodity_instrument_mapper::forward_commodity_basket_option, dispatched from trade_mapper::map_commodity_instrument for CommodityBasketOption. It reads every Underlying in document order and emits one constituent per entry. The reverse, reverse_commodity_basket_option, rebuilds Underlyings from the rows. reverse_commodity_basket_option is the only reverse that takes the child rows; the other seven commodity reverse mappers keep their existing signature.

CommodityWorstOfBasketSwap (worstOfBasketSwapData2) and CommodityBasketVarianceSwap (basketVarianceSwapData2) state basket underlyings too, and are not yet dispatched. They are recorded as remaining work below; the child shape they need is the same one.

The corpus states no commodity basket document (the Commodity_* files are forward, option, swap, swaption, variance, average-price and strip products), so no golden file exercises the producer and no value was lost in the corpus. The producer is proven instead by xml_commodity_mapper_roundtrip_tests.cpp, which maps a document built from the schema's basketOptionData shape and asserts the two constituents' codes, ordinals and weights, then rebuilds the document and asserts them again.

6.4. The column that was deleted: knock_out_dates_json

The owner ruled on 2026-09-28 that knock_out_swap_instrument.knock_out_dates_json carries no ORE source and that its family is never produced. The column was deleted from the model and from the development database. There is nothing to model, so no child table replaces it.

The evidence is threefold.

  1. The schema states no date list. knockOutSwapData (external/ore/xsd/instruments.xsd) states exactly three members: BarrierData, one BarrierStartDate, and LegData with minOccurs"2" maxOccurs="2". The only list anywhere in the block is =BarrierData/Levels, an xsd::vector<float> of barrier levels. No member holds observation dates.
  2. The corpus document states no date list either. Exotic_KnockOutSwap.xml (assets/test_data/golden_dataset and external/ore/examples) holds one BarrierData with one Level, one BarrierStartDate, and two LegData blocks. The file names no observation date.
  3. Nothing produces the instrument. trade_mapper.cpp sends KnockOutSwap to scripted_instrument_mapper::forward_knock_out_swap, which writes the scripted instrument's parameters_json and never a knock_out_swap_instrument row. The table is therefore always empty, so the column was always null.

The gap is a coverage gap, not a collection gap: the model declares a column for a family that the mapper never fills. The gap belongs to coverage defect 14, "declared coverage is incomplete, and elsewhere undeclared" (Sprint 26 defect table). The deletion is recorded here; the coverage defect itself stays with defect 14.

6.5. The child that landed: equity_position_option_underlying

The ORE schema states an equity option position's members in equityOptionPositionData: a required Quantity and an unbounded Underlying list of equityOptionUnderlyingData, each of which is a shared underlying, an optionData and a Strike. The column claimed to hold that list as serialised text and nothing ever wrote it: the mapper had no dispatch for EquityPosition or EquityOptionPosition at all, and the corpus path is a TotalReturnSwap whose UnderlyingData/SubTrade is an EquityOptionPosition. The producer is now wired, so the payload's shape comes from the document rather than from the column's own description.

Decision Value
Table ores_trading_equity_position_option_underlyings_tbl
Key (tenant_id, instrument_id, sequence_number, valid_from, valid_to) — the parent id plus the entry's ordinal, the shape the two children above use
Parent instrument_id references the equity position instrument. The component declares no SQL foreign key yet (defect 7), so the reference is the column and its prose, as in bond_leg
Scope has_tenant_id: true only. The child rides the instrument's tenant, workspace and party, so no workspace column rides it
Natural key none; the ordinal is the identity
Audit/validity the template's audit columns and the bitemporal valid_from=/=valid_to pair with the exclusion constraint
Quantity stays on the parent: it is a property of the position, not of one entry
Columns sequence_number integer not null, underlying_name text not null, strike numeric(28, 10) not null, weight numeric(28, 10) null, long_short text not null, option_type text null, exercise_type text null, settlement_type text null
Strike type ores::utility::decimal::decimal — a strike is money-role (D11), and the ORE element is a float
Weight type std::optional<ores::utility::decimal::decimal> — a weight is money-role (D11) and the shared underlying type makes Weight optional, so an absent weight stays null
Option terms the corpus entry carries LongShort, OptionType, Style and Settlement, so each has a column; the names follow equity_option_instrument
Checks "sequence_number" > 0, "underlying_name" <> '', and the generated nil-uuid check

The one value this child does not carry is the entry's exercise date list. The corpus states OptionData/ExerciseDates/ExerciseDate as a list, so a single date column would silently drop the rest of it. The list is a collection of its own, and defect 16 (no observation schedule) owns the shape that models it. The omission is recorded rather than papered over with a truncated scalar.

The producer is equity_instrument_mapper::forward_equity_option_position, dispatched from trade_mapper::map_equity_instrument. A TotalReturnSwap (or ContractForDifference) whose underlying sub-trade type is EquityPosition or EquityOptionPosition is dispatched to the same family, so the nested corpus path reaches the producer. The forward mapper reads every Underlying entry in document order and emits one row per entry. The reverse, equity_instrument_mapper::reverse_equity_option_position, rebuilds EquityOptionPositionData from the rows and wraps it back inside the TotalReturnSwap it came from. EquityPosition (the plain position, no option terms) is dispatched too, and produces the parent row with no children: its entries carry no strike or option data, so they cannot be rows of this table. That gap is defect 15's multi-underlying shape.

The corpus does exercise the producer. xml_equity_mapper_roundtrip_tests.cpp loads Hybrid_GenericTRS_with_EquityOptionPosition.xml, maps it, asserts both entries' names, strikes, ordinal, weight and option terms, then reverses and asserts the same values in the rebuilt document.

6.6. What the other collections need

  • equity_swap_instrument.basket_json has a real payload (the underlyings' names, forward and reverse). It is not migrated because the defect-9 XOR check reads the column to tell a basket swap from a single-name swap. Removing the column without a parent-side basket-presence record would weaken that assertion, and the product split is defect 9, which this task does not own. The child is an equity_swap_basket_underlying row keyed like this one, and it lands with the product split.
  • knock_out_swap_instrument.knock_out_dates_json was deleted. The ORE schema states no date list, the corpus states none, and nothing produces the instrument, so the column had nothing to hold. See "The column that was deleted" above; the coverage gap itself belongs to defect 14.
  • equity_position_instrument.option_data_json was migrated. The child is equity_position_option_underlying, keyed like the two children above, and the producer is wired for the nested EquityOptionPosition sub-trade. See "The child that landed" above.
  • scripted_instrument's three columns are the defect's stated exception. The repair the defect names is jsonb rather than text, which is a type change with no collection and no child table; it is not this unit's shape change and stays recorded.

6.7. Files changed

The callable swap unit. Codegen source: the new ores.trading.callable_swap_call_date model; the parent model loses the column and names the child; trade.org reads the child beside the legs. Generated: 33 files for the child, the parent's regenerated set, and the trade handler. Hand-written: instrument.hpp (the rates carrier gains the schedule), instrument_payload.cpp (the wire shape), swap_instrument_mapper.{hpp,cpp}, exporter.cpp, ore_import_execute_handler.cpp, the callable swap shell verb, and the swaption mapper test. SQL: the child's create, notify trigger, drop and RLS policy, and the three aggregate scripts. Database: the table, its notify trigger and its RLS policy were created, and call_dates_json was dropped; both tables held no rows, so no value was lost.

The commodity basket unit. Codegen source: the new ores.trading.commodity_basket_constituent model; the parent model loses basket_json and names the child; trade.org reads the constituents beside the instrument. Generated: the child's 33-file set, the parent's regenerated set, the trade handler, and the cmake lists. Hand-written: instrument.hpp (a commodity_instrument_data carrier with stamp_ids), trade_instrument.hpp (the variant element is the carrier), instrument_payload.cpp (the wire shape, with a fallback that reads a pre-child body as an empty basket), commodity_instrument_mapper.{hpp,cpp} (the CommodityBasketOption forward and reverse), trade_mapper.{hpp,cpp}, exporter.cpp, importer.cpp, ore_import_execute_handler.cpp, the commodity shell verb in ores.shell/application, the generated commodity shell in ores.shell/trading, and the commodity and planner tests. SQL: the child's create, notify trigger, drop and RLS policy, and the three aggregate scripts. Database: the table, its notify trigger and its RLS policy were created, and basket_json was dropped; 28 commodity_instruments rows held 0 non-null baskets, so no value was lost.

The equity position unit. Codegen source: the new ores.trading.equity_position_option_underlying model; the parent model loses option_data_json, loses the cross-column check that read it, and names the child; trade.org reads the entries beside the instrument. Generated: the child's 33-file set, the parent's regenerated set, the trade handler, the shell unit, the cmake lists, and the child's four SQL scripts. Hand-written: instrument.hpp (an equity_instrument_data carrier with stamp_ids), trade_instrument.hpp (the variant element is the carrier), instrument_payload.cpp (the wire shape, with a fallback that reads a pre-child body as an empty entry list), equity_instrument_mapper.{hpp,cpp} (the EquityPosition and EquityOptionPosition forward and reverse, and the nested sub-trade lookup), trade_mapper.{hpp,cpp} (the equity carrier and the sub-trade dispatch), exporter.cpp, importer.cpp, ore_import_execute_handler.cpp (the child saves), a new hand-written verb equity_position_option_underlying_commands.{hpp,cpp} in ores.shell/application, the aggregates (the two SQL masters, the RLS policy file, the three cmake lists, the history registrar, the equity handler registrar and the service application), and the equity, planner and repository tests. SQL: the child's create, notify trigger, drop and RLS policy, and the three aggregate scripts. Database: the table, its insert and notify triggers and its RLS policy were created, and option_data_json was dropped; the table held 64 rows and 0 non-null option_data_json values at the drop, so no value was lost.

The knock-out column deletion. Codegen source: the parent model loses knock_out_dates_json and the entity is regenerated. Hand-written: the knock-out swap shell verb in ores.shell/application loses the argument it used to take, and the import handler loses the field it used to write. SQL: the column leaves the create script. Database: the column was dropped; the table held no rows, so no value was lost.

6.8. Judgement calls

  • D12 is followed literally: a child entity per collection. The defect page's shared schedule table is recorded as the later generalisation.
  • A column with no producer gets no invented child. The task requires the payload's shape to come from the real data.
  • basket_json on equity_swap_instrument waits for defect 9 so that its removal cannot weaken the XOR assertion.
  • knock_out_dates_json gets no child because the ORE schema states no knock-out date list; the finding is recorded instead of invented.
  • The commodity producer is written for CommodityBasketOption, the first basket product dispatched. The other two basket products are recorded as remaining rather than half-wired.
  • commodity_instrument_data is a new carrier struct rather than a reuse of with_legs<>, because a constituent's parent reference is a direct instrument_id column (the callable child's shape) and not a grouped identity.instrument_id, so with_legs's stamp_ids does not apply.
  • The equity position carrier wraps the whole equity_instrument_variant rather than one leaf, because nine equity leaves share the family and only the position leaf states entries. The wire shape is then per-leaf, as the rates family does for its legs and call dates.
  • The equity position producer reads the nested UnderlyingData/SubTrade shape a total return swap uses, not only the top-level EquityOptionPositionData element. ContractForDifference keeps the composite dispatch it had: its reverse rebuilds a different data member, and intercepting it made Hybrid_CFD.xml unreadable on export.
  • The equity entries verb is hand-written in ores.shell/application rather than a template hook. A hook would change a template every component's shell unit is rendered from, for one family's child; the hand-written verb mirrors the commodity basket precedent and is one file pair.
  • A plain EquityPosition sub-trade is dispatched, so its parent row is produced, but it gets no child rows: equityPositionData.Underlying is a bare underlying list with no strike and no option data, which this table's not-null columns cannot hold. That multi-underlying gap stays defect 15's.
  • The entry's exercise date list is not modelled, and no single-date column is invented. The round trip asserts the entry values the mapper used to drop; the dates are the one value it still does not carry, and defect 16 owns the observation schedule that would.

7. Defects 4, 5 and 8: the enumeration record

Measured at 092ab3b017, with projects/ores.codegen/venv. The models were loaded through the generator's own loader (codegen.org_loader.load_org_model), and projects/ores.sql/create/trading/*.sql was scanned for check ( and raise exception. The document's counts in trade_model_defects.org are the September 8 snapshot; the ports replaced the 41 hand-written tables with 68 modelled entities, so the schema check count fell while the model count rose.

7.1. Size

Measure Value
Trading entity models 68
Trading create scripts 142
Trading check (...) constraints in the generated schema 379
Trading raise exception sites in the generated schema 156
Columns matching the closed-set filter 156 instances of 62 distinct names
Distinct names that are a genuine closed set 47

7.2. The closed sets

Only sets with a fixed, enumerable value domain are listed. underlying_name, commodity_code, spread_commodity_code, linked_asset_code, reference_entity, script_name, tenor, rate_index, fx_index, constituent_trade_id, underlying_code and the currency columns beyond currency are open identifiers or code lists, not closed sets; defect 15 owns the underlying name. product_type is the generated product_type_t enum and defect 10 owns it in PR 4.

A modelled lookup means a trading entity that binds simple-lookup; an external lookup means the table exists in another component.

Column Entities Modelled lookup Enforcement today Reference table Row source
trade_type / trade_type_code 1 + 29 trading trade_type trigger lookup; 21 of 29 also carry a family IN-list check ores_trading_trade_types_tbl ORE oreTradeType, 121 rows seeded
currency 21 (14 also carry a <> '' check) external refdata.currency <> '' only ores_refdata_currencies_tbl ISO 4217
option_type 12 none 3 carry an IN-list check; 9 nothing — ORE optionType (Call, Put)
exercise_type / exercise_style 6 + 1 none 2 IN-list; 6 nothing — ORE exerciseStyle (European, Bermudan, American)
long_short 7 none all 7 carry in ('Long','Short') — ORE longShort
barrier_type / lower_barrier_type / upper_barrier_type 5 + 2 none 3 IN-list or <> ''; 4 nothing — ORE barrierType (10 values)
settlement_type 4 none 1 IN-list; 3 nothing — ORE settlementType (Physical, Cash)
day_count_code / day_count_fraction_code 3 + 1 external refdata.day_count_fraction_type 1 refdata trigger; 3 nothing ores_refdata_day_count_fraction_types_tbl ISDA; refdata seeds 12 canonical codes
payment_frequency_code / payment_frequency 3 + 1 external refdata.payment_frequency 3 refdata triggers; 1 nothing ores_refdata_payment_frequencies_tbl ISDA; refdata seeds 9 codes
average_type 2 none 1 IN-list; 1 nothing — ORE averageType (Arithmetic, Geometric)
return_type 2 none 1 IN-list; 1 nothing — ORE returnType (TotalReturn, PriceReturn)
payoff_type 2 none nothing — ORE accumulator/TaRF terms
price_type 2 none nothing — ORE bondPriceType (Clean, Dirty)
moment_type 1 none IN-list — ORE momentType (Variance, Volatility)
target_type 1 none product gate only — ORE TaRF targetType
amortization_type 1 none nothing — ORE amortizationType (5 values)
ascot_option_type 1 none nothing — ORE optionType
call_type 1 none IN-list — ORE Bermudan / One-Time
funding_leg_type / repo_type / leg_type / leg_type_code 1 each external refdata.leg_type 1 refdata trigger; 3 nothing ores_refdata_leg_types_tbl ISDA/Ore leg types, 10 rows
back_stub_rounding_type / front_stub_rounding_type 2 none nothing — ORE roundingType (5 values)
coupon_frequency_code / strip_frequency_code / cliquet_frequency 3 external refdata.payment_frequency nothing ores_refdata_payment_frequencies_tbl ISDA; refdata seeds 9 codes
business_day_convention_code 1 external refdata.business_day_convention_type refdata trigger ores_refdata_business_day_convention_types_tbl refdata seed
floating_index_code / inflation_index_code 2 external refdata.floating_index_type 1 refdata trigger; 1 nothing ores_refdata_floating_index_types_tbl refdata seed, 28 rows
asset_class 1 external refdata.asset_class_code refdata trigger ores_refdata_asset_classes_tbl refdata seed
change_reason_code all audited external dq.change_reason ores_dq_validate_change_reason_fn on every insert ores_dq_change_reasons_tbl dq seed, BCBS 239 taxonomy
activity_type_code 1 trading activity_type trigger lookup ores_trading_activity_types_tbl trading seed
role 1 trading party_role_type trigger lookup ores_trading_party_role_types_tbl trading seed, 4 rows
id_type 1 trading trade_id_type nothing: the validate function is generated but never called ores_trading_trade_id_types_tbl trading seed, 3 rows
fpml_event_type_code 1 trading fpml_event_type trigger lookup ores_trading_fpml_event_types_tbl trading seed, 5 rows
category 1 none IN-list in the schema — trading activity categories, in the activity seed
rate_kind 1 none IN-list in the schema — internal rate-group discriminator
schedule_kind 1 none IN-list in the schema — internal schedule-choice discriminator

Already modelled lookups, the ones the defect page calls done: trade_type, party_role_type, lifecycle_event (workspace-scoped-lookup), trade_id_type, activity_type and fpml_event_type. Each keys on a text code, carries version, the four audit columns and the bitemporal pair, and generates ores_trading_validate_<entity>_fn.

No closed set lacks a source. Every value list is either an ORE XSD simple type under external/ore/xsd/ore_types.xsd or external/ore/xsd/instruments.xsd, or a seed under projects/ores.sql/populate/refdata, .../populate/dq or .../populate/trading. Nothing is invented.

7.3. Two value-domain gaps, as a dependency

The reference data is another component's, and two trading columns store a spelling that component does not key on. Both are recorded rather than patched here, because a local conversion would be a second mapping no other consumer sees.

Trading column Stored spelling Owning table key Evidence
credit_instrument.day_count_code ORE dayCounter names (Actual365Fixed, Thirty360) ISDA short codes (A365F, 30/360) bond_instrument_mapper.cpp:1107 writes to_string(*ld.DayCounter); refdata_day_count_fraction_types_populate.sql seeds the short codes
bond_issue.coupon_frequency_code SemiAnnual Semiannual the model generator is std::string("SemiAnnual"); the refdata seed keys Semiannual

bond_issue.day_count_code stores 30/360, which does match. The precedent for the missing conversion is projects/ores.ore/core/include/ores.ore.core/domain/payment_frequency_conversion.hpp, which converts an ORE schedule tenor to the canonical ores.refdata code. The real foreign key in PR 4 needs that conversion helper to exist first; this unit does not add a local conversion.

7.4. The enumeration checks that still live in PL/pgSQL

The 156 raise exception sites classify as:

Class Sites Stays?
State: a create collides with a live row 68 Yes, legitimate
State: optimistic-concurrency version conflict 68 Yes, legitimate
Reference: soft-FK existence check (book, counterparty, successor trade, trade, instrument) 6 Yes until PR 4 replaces it with a real foreign key (defect 7)
State: FSM transition legality and transition existence 5 Yes, legitimate
Closed set: the enumeration value is not in the reference table 5 Defect 8; leaves PL/pgSQL
Closed set: the enumeration value is null or empty 5 Defect 8; a foreign key cannot state it, so a not-null column or a schema check states it

The 10 closed-set sites live in five generated functions: ores_trading_validate_{trade_type,activity_type,party_role_type,fpml_event_type,trade_id_type}_fn. They are called from 42 trigger sites across 41 columns:

Validation function Owner Trigger call sites
ores_trading_validate_trade_type_fn trading 30
ores_refdata_validate_payment_frequency_fn refdata 3
ores_trading_validate_activity_type_fn trading 1
ores_trading_validate_fpml_event_type_fn trading 1
ores_trading_validate_party_role_type_fn trading 1
ores_refdata_validate_asset_class_fn refdata 1
ores_refdata_validate_business_day_convention_type_fn refdata 1
ores_refdata_validate_day_count_fraction_type_fn refdata 1
ores_refdata_validate_floating_index_type_fn refdata 1
ores_refdata_validate_leg_type_fn refdata 1
ores_trading_validate_trade_id_type_fn trading 0 (generated, never called)

The four trading-owned functions are this component's to retire into the schema's Checks. The six ores_refdata_validate_* functions are another component's, so the dependency is recorded here and refdata is not edited; their real foreign key is PR 4's. Two oddities are recorded rather than left silent: trade_id_type's validate function is generated and never called, and trade_identifier.id_type is unvalidated today even though ores_trading_trade_id_types_tbl holds its values.

7.5. The plan the census gives

Work in the defect page's order. The four defect-5 fields all resolve to an existing table owned by refdata or dq, so they add no trading table and no trading rows; their work is the recorded reference plus the PL/pgSQL removal.

Field Owning component Table Action
currency refdata ores_refdata_currencies_tbl record the dependency; the ISO 4217 table is refdata's
day_count_code refdata ores_refdata_day_count_fraction_types_tbl record the dependency; the ORE-spelling gap above is the PR-4 precondition
payment_frequency_code refdata ores_refdata_payment_frequencies_tbl record the dependency; the conversion helper exists
change_reason_code dq ores_dq_change_reasons_tbl record the dependency; dq's taxonomy, not a trading copy

7.6. Set 1, defect 5: done

Measured and applied on feature/model-missing-trading-families. No local reference table is created, because every field already has an owner in the tree; the defect page's proposed ores_trading_* tables are superseded by D13's "record the dependency instead of a local copy". The real foreign key is defect 7's work in PR 4.

Field Columns edited Model files
currency 21 21
day_count_code 3 3
payment_frequency_code / payment_frequency 4 4
coupon_frequency_code, strip_frequency_code, cliquet_frequency 3 3
asset_class 1 1

Thirty-one column descriptions across 24 models now state the soft FK, the way swap_leg.payment_frequency_code and swap_leg.day_count_fraction_code already do: "Soft FK to <table>: <concept> belongs to <component>, so the dependency is recorded rather than copied. PR 4 tightens the soft reference into a real foreign key." The regeneration changed 21 generated C++ domain headers and nothing else. change_reason_code is an implicit audit column validated by ores_dq_validate_change_reason_fn, so its dependency is recorded here rather than per model.

Gate Result
pytest projects/ores.codegen/tests -q 806 passed
check_component_drift.py --component trading-cpp --dry-run twice No drift, both runs
check_component_drift.py --all --dry-run No drift
cmake --build build/output/linux-clang-debug-make -j 3 Exit 0, zero error:
=ctest -R 'ores\.trading\. ores\.shell\.trading\. ores\.shell\.application\. ores\.ore\. ores\.utility\.'= 9 of 9 passed

No create script gained a table or a check, so no DDL was applied for Set 1.

7.7. Set 2, defect 4: done, eleven reference entities

Each new entity binds simple-lookup, keys on a text code, carries the four audit columns and the bitemporal pair, and states its values in the ORE XSD spelling so a stored value round-trips through the ORE XML unchanged. None declares a * Validation function: the closed set is stated in the schema's Checks instead, and the real foreign key is PR 4's.

Entity Table Rows Row source Referencing columns
long_short_type ores_trading_long_short_types_tbl 2 ORE longShort simple type long_short in 7 models
moment_type ores_trading_moment_types_tbl 2 ORE momentType simple type fx_variance_swap_instrument.moment_type
price_type ores_trading_price_types_tbl 2 ORE bondPriceType simple type bond_option.price_type, bond_trs.price_type
amortization_type ores_trading_amortization_types_tbl 5 ORE amortizationType simple type bond_leg_amortization.amortization_type
option_type ores_trading_option_types_tbl 2 ORE optionType simple type option_type in 12 models plus ascot.ascot_option_type
exercise_type ores_trading_exercise_types_tbl 3 ORE exerciseStyle simple type exercise_type in 6 models plus fx_vanilla_option_instrument.exercise_style
settlement_type ores_trading_settlement_types_tbl 2 ORE settlementType simple type settlement_type in 4 models
barrier_type ores_trading_barrier_types_tbl 10 ORE barrierType simple type barrier_type in 5 models plus the two barrier halves on equity_barrier_option_instrument
activity_category ores_trading_activity_categories_tbl 5 the trading activity seed activity_type.category
return_type ores_trading_return_types_tbl 2 ORE EquityLegData.ReturnType, corpus values bond_trs.return_type, equity_swap_instrument.return_type
payoff_type ores_trading_payoff_types_tbl 7 ORE optionData.PayoffType, corpus values instrument_option.payoff_type, instrument_option.payoff_type_2, equity_accumulator_instrument.payoff_type, equity_accumulator_instrument.target_type

ascot_option_type is the same set as option_type, and equity_accumulator_instrument.target_type holds two of the payoff_type values, so both point at an existing table rather than a second copy. Each entity has a seed under projects/ores.sql/populate/trading/ wired into populate_trading.sql, a row in ores.trading.module.org, and an RLS policy beside the other lookups.

The checks that entered the schema with this set: the option right on 10 models, the exercise style on 4, the settlement form on 3, the barrier set on 4 columns (and fx_barrier_option_instrument's <> '' check was replaced by the in-list), the price type on 2, the amortization type on 1, the return type on 2, and the payoff type on 4 columns.

7.7.1. The sets that belong to another component

rounding_type, leg_type, day_count_fraction_type, payment_frequency, business_day_convention_type, floating_index_type, asset_class_code and currency are ores.refdata lookups. rounding_type was the one that looked local: bond_leg_rate's two stub rounding columns named it, and a trading rounding_type model was authored and then withdrawn when ores_refdata_rounding_types_tbl turned up already holding the same five codes. A foreign key to it and a local in-list check would be the local copy D13 forbids, so the two columns record the dependency and carry no in-list. The same treatment applies to bond_trs.funding_leg_type, bond_repo.repo_type and bond_leg.leg_type against ores_refdata_leg_types_tbl; the two existing in ('Fixed', 'Floating') checks stay, because they are a product gate and were already there.

7.7.2. Set R, return_type: realigned from the corpus

ORE states EquityLegData.ReturnType as a bare xs:string (external/ore/xsd/instruments.xsd, lines 856 and 1127), so there is no XSD enum. The corpus settles it: every ReturnType element under external/ore/examples/ is Total (15 documents) or Price (2), and nothing else. The model said TotalReturn and PriceReturn, neither of which any ORE document states. Realigned: the check on bond_trs.return_type became in ('Total', 'Price'), the same check was added to equity_swap_instrument.return_type, the equity_swap_instrument fixture generator moved from TotalReturn to Total, and the bond TRS mapper's hardcoded default moved from TotalReturn to Total (bond_instrument_mapper.cpp, the BondTRS branch). The reference entity ores.trading.return_type carries the two rows. Two hand-written round-trip assertions and two repository/reader fixtures that expected TotalReturn moved with it. Live data: 30 bond_trs rows and 120 equity_swap_instruments rows held TotalReturn; all 150 were updated to Total.

7.7.3. Set P, payoff_type and target_type: realigned from the corpus

ORE states optionData.PayoffType and PayoffType2 as bare xs:string (external/ore/xsd/instruments.xsd, lines 337 and 338), so the corpus settles the set: every PayoffType element under external/ore/examples/ is one of Accumulator (6), Asian (3), AverageStrike (1), Decumulator (2), TargetExact (6), TargetFull (2) or Vanilla (3). Realigned: the seven-value check was added to instrument_option.payoff_type, instrument_option.payoff_type_2 and equity_accumulator_instrument.payoff_type, the model's own "Accumulator, Decumulator, or TaRF" wording was replaced, and the reference entity ores.trading.payoff_type carries the seven rows.

equity_accumulator_instrument.target_type is not a separate ORE set. TargetFull and TargetExact are two of the PayoffType values, and ORE's own tarfData.TargetType is an stFreeStyleNumber scripted value that no corpus document states (no <TargetType> element appears anywhere under external/ore/examples/). The column therefore points at ores_trading_payoff_types_tbl and keeps a narrower check over the two target values. Live data needed no correction: instrument_option held null in all 64 rows for both payoff columns, equity_accumulator_instrument.payoff_type held Decumulator in all 128 rows, and target_type was null in all 128.

7.7.4. Two sets ORE genuinely does not state

Column Model's stated set Why no table Exact place
commodity_instrument.average_type, equity_asian_option_instrument.average_type Arithmetic, Geometric ORE states no average-type element or enum for either family. commodityAveragePriceOptionData has no average member, singleUnderlyingAsianOptionData has none either, and no averageType simple type exists; the only Arithmetic in ORE is part of the scripted product names EquityAsianOptionArithmeticPrice and ...ArithmeticStrike, and ORE never states Geometric at all. The set is the model's own, so no reference table is created commodity_instrument_mapper.cpp line 365, result.average_type = to_string(d.PriceType);, writes ORE's priceType (Spot, FutureSettlement) into a column whose schema check is in ('Arithmetic', 'Geometric'). The mapper cannot produce a realigned value without guessing, because ORE states none, so the set is left as it is and the mapper is reported rather than patched
callable_swap_instrument.call_type Bermudan, One-Time ORE has no call type on a callable swap. The one CallType element in the schema is on nettingSetDetails (instruments.xsd line 225), a netting-agreement field, and no <CallType> element appears under external/ore/examples/. Nothing produces the value: the only writers are the trading repository, service and history mappers, which copy the domain member callable_swap_instrument has no ORE mapper at all, so there is no producer to realign; the column keeps its check and its fixture generator

Neither set is invented, and neither is seeded from the model's own list.

rate_kind and schedule_kind are the two remaining closed sets. They are storage-shape discriminators rather than domain enumerations: rate_kind names which arm of the rate group the document engaged and schedule_kind names which arm of the schedule choice. Their valid set is already readable in the schema's check, so defect 4's complaint ("the valid set is not written down anywhere a person or a query can read it") does not apply, and they stay checks.

7.8. Set 3, defect 8: done

The four trading-owned validate functions and the one that was never called are retired. The 33 ** Validations rows that named them are deleted, the empty * Insert trigger sections are removed, and the * Validation function section is removed from trade_type, activity_type, party_role_type, fpml_event_type and trade_id_type, so the generated create scripts no longer emit the functions at all. They were then dropped from the development database.

Retired function Trigger call sites Schema check that replaces it
ores_trading_validate_trade_type_fn 30 the full ores_trading_trade_types_tbl set (121 codes) stated on trade.trade_type and on the 11 instrument tables that carried no family check; the 18 already carrying a family in-list keep it, and every family set was checked to be a subset of the reference set
ores_trading_validate_activity_type_fn 1 "activity_type_code" in (<32 activity codes>) on trade
ores_trading_validate_party_role_type_fn 1 "role" in ('Counterparty', 'CalculationAgent', 'ExecutingBroker', 'NovationTransferee') on trade_party_role
ores_trading_validate_fpml_event_type_fn 1 "fpml_event_type_code" is null or ... in ('New', 'Amendment', 'Novation', 'PartialTermination', 'FullTermination') on activity_type
ores_trading_validate_trade_id_type_fn 0 "id_type" in ('UTI', 'USI', 'Internal') on trade_identifier, so the column is validated for the first time

The 68 create-collides, the 68 version conflicts, the 6 soft-FK existence checks and the 5 FSM checks stay: they are state or reference checks a check constraint cannot express, and the six soft references are PR 4's to tighten.

The six ores_refdata_validate_* functions stay too. They are another component's functions, so refdata is not edited and the dependency is recorded: asset_class, business_day_convention_code, day_count_fraction_code, floating_index_code, leg_type_code and payment_frequency_code, on swap_leg, credit_instrument, equity_swap_instrument and trade. Their real foreign key is PR 4's, behind the two value-domain gaps above.

7.8.1. One test fixture wrote a value outside its own model's set

The new price_type check failed on the development database, and the reason was a fixture, not data drift: service_bond_instrument_reader_tests.cpp wrote price_type = "Price" and asserted it round-tripped, while the model states the ORE bondPriceType set (Clean, Dirty) and no ORE document states Price. The fixture now writes Clean, and the 52 stale rows the fixture had written were moved to Clean with the DDL user.

7.8.2. DDL applied to ores_dev_merry_newton

Change Count
New tables created (11 lookups) with their indexes, insert fn, insert trigger and delete rule 11
Notify trigger functions and triggers created 11
RLS policies created 11
Reference rows seeded 40 across 11 tables
alter table ... add check (...) for the checks the create scripts could not add, because create table if not exists skips an existing table 49
Trading validate functions dropped 5
Generated create scripts re-applied to resync every insert trigger (159 files; 16 must be owner of table errors, all on ores_trading_commodity_basket_constituents_tbl, which the postgres role owns and which no retired function touched) 159
Stale price_type'Price'= rows updated to Clean 52
bond_trs and equity_swap_instruments rows realigned from TotalReturn to Total 150

The check count splits as 43 for Sets 1 to 3 and 6 for Sets R and P, and the seed count as 31 for Sets 1 to 3 plus 2 return types and 7 payoff types.

ores_trading_commodity_basket_constituents_tbl is owned by the postgres role rather than the DDL user, so the DDL user cannot re-apply its create script. It is pre-existing and outside this unit; the next compass db recreate resolves it.

7.9. Gates after each set

Gate Set 1 Set 2 Set 3 Set R Set P
pytest projects/ores.codegen/tests -q 806 806 806 806 806
check_component_drift.py --component trading-cpp --dry-run twice No drift No drift No drift No drift No drift
check_component_drift.py --all --dry-run No drift No drift No drift No drift No drift
cmake --build build/output/linux-clang-debug-make -j 3 0, zero error: 0, zero error: 0, zero error: 0, zero error: 0, zero error:
ctest the nine owned suites 9 of 9 9 of 9 9 of 9 9 of 9 9 of 9
validate_docs.sh 40 components 40 components 40 components 40 components 40 components

8. Test Scenarios

Scenario State Notes
The commodity model regenerates with no drift Met The dry run is empty after the in-place regeneration, on the rebased tree
The commodity port builds Met cmake --build build/output/linux-clang-debug-make -j 3 exits 0 with no errors for the whole tree
The commodity port's suites pass Met ores.trading.api, ores.trading.core, ores.trading.service, ores.ore.api, ores.ore.core, ores.ore.service, ores.shell.trading and ores.shell.application are green
The database rebuilds from the tree Not re-measured compass db recreate -y -k exited 0 on 2026-09-27, before the enumeration unit added the ten lookup tables. Their generated create scripts are reachable from no aggregator, so a rebuild is not re-verified here; the finding is recorded below
The fleet starts Met 24 of 24 units ready in two seconds, over busctl
The eight ports repeat the method Met Every family in §2e is ported, regenerated and proven; the drift dry run is empty with all of them
The knock-out column is deleted, with no source invented Met The model, the create script, the shell verb and the import handler lose the field, the database column is dropped, and the gap is recorded under defect 14
The equity position child lands and its producer is wired Met The table is created with its triggers and its policy, option_data_json is dropped, the corpus round trip passes, and the eight suites plus the codegen suite are green
Defect 6 renames each duplicate onto one name Met day_count_code becomes day_count_fraction_code in three entities and payment_frequency becomes payment_frequency_code in one; §5g records the five spellings judged not renamed
M04 finds no inert assignment Met Eighty entity models checked against _FEATURE_NAMESPACE; zero misplaced features; §5h records the binds and their generated evidence
The average type is a local reference entity Met ores.trading.average_type is seeded with two rows, points at both average_type columns, and the commodity mapper no longer writes ORE's price type into it
call_type is deleted Met The column and its check are dropped from the model and the database; the shell argument and the import assignment are gone; §5i records the ORE evidence

9. PRs

PR Title
#2333 Model the unmodelled trading families and bind the unbound

10. Review

# Comment summary File Decision Notes
1 The scripted instrument's trade-type check was widened to the master list ores.trading.scripted_instrument.org Accepted Restored the model's own four codes. The same master list in ten other models this PR adds has no documented per-table set, so it is recorded rather than guessed
2 Commodity payment_frequency_code carried no check ores.trading.commodity_instrument.org Accepted Checked against refdata's nine values
3 Equity position option long_short carried no check ores.trading.equity_position_instrument.org Accepted The in ('Long','Short') check its siblings carry, plus the soft-FK sentence
4 Credit first_exercise_date emitted an empty date when the schedule was absent ores.ore.core credit mapper Accepted Returns std::optional and nullopt on the early returns; the reverse emits no empty element
5 Swaption start_date=/=maturity_date assigned an invalid-date sentinel ores.ore.core swap mapper Accepted .ok() guards; the audit found only the swaption's targets optional
6 try_one_swap lost its pre-PR bare payload fallback instrument_payload.cpp Accepted The fallback is restored, so payloads written before this PR no longer decode to monostate
7 The synthetic generator template had no decimal branch cpp_domain_type_generator.cpp.mustache Accepted The branch is added and re-tangled; it exposed two conditional-null row checks, fixed at their models
8 The commodity shell verb parsed UUIDs unguarded ores.shell/application commodity verb Accepted Both conversions wrap in try/catch with fail(out), matching the equity child verb
9 New create policy statements were not re-runnable trading_rls_policies_create.sql Accepted All fifteen now drop themselves first; the hygiene gate is 1676 files, 0 problems
10 Three duplications and one CLI gap mappers, shell, child verb Recorded The ISO-8601 conversion pair (seven translation units), the triplicated per-entity field lists, and the equity position option CLI's missing read-back each get their own change

11. Result

Measured 2026-09-27 on feature/model-missing-trading-families, rebased onto dbfb31d45d, the merge of main's PR #2240.

Check Result
Rebase Fifteen commits replay onto main. One conflict in the port's regenerated commodity_instrument.hpp, resolved to the port's flat shape, which its message already states; one word of prose in trade.org
Database compass db recreate -y -k exits 0 with no local workaround
Build cmake --build build/output/linux-clang-debug-make -j 3 exits 0, no errors, the first green default build of this work
Fleet compass services start reports 24 of 24 units ready in two seconds
Suite ctest runs 86 tests in 310 seconds: 81 pass, 5 fail
Owned All eight suites this branch owns pass: ores.trading.api, .core, .service, ores.ore.api, .core, .service, ores.shell.trading and ores.shell.application
Drift check_component_drift.py --component trading-cpp --dry-run reports no drift; main's lookup-template fix reflowed one log statement in trade_service.cpp, which is regenerated
Transport ORES_USE_BUSCTL=1 is in .env and busctl --user list answers

After the seven ports the full suite is unchanged: 86 tests, 81 pass, the same five fail, none of them trading's. Each port was checked on its own commit and again here, with the component and all-component drift dry runs empty and the codegen suite at 790 cases.

The five failures belong to other components:

  • ores.dq.api.tests: the coding-scheme table drops its description and its subject area.
  • ores.dq.core.tests: a written row stays at version 0; ores_dq_lei_entities_tbl and ores_dq_lei_relationships_tbl do not exist; a change reason names a category the seed does not hold.
  • ores.shell.api.tests: to_iso8601_utc states a space where the test wants T.
  • ores.refdata.core.tests: the suites insert a business unit, a book and a portfolio whose natural keys the seed holds.
  • ores.marketdata.core.tests: a series classification rule event never arrives.

11.1. The knock-out deletion and the equity position child (measured 2026-09-28)

Both units were checked on feature/model-missing-trading-families at ef918bbdd8, in one tree, in this order.

Check Result
Build cmake --build build/output/linux-clang-debug-make -j 3 exits 0 with 0 error: lines
Drift check_component_drift.py --component trading-cpp --dry-run reports no drift, twice; --all --dry-run reports no drift
Codegen pytest projects/ores.codegen/tests is 806 passed
Owned The eight ctest suites are 8 of 8: ores.trading.api, .core, .service, ores.ore.api, .core, .service, ores.shell.trading and ores.shell.application. With the codegen suite that is the nine owned suites
Suite ctest runs 86 tests in 334 seconds: 81 pass, 5 fail, the same five the branch already records (dq, shell.api, refdata, marketdata)
Round trip equity_option_position_forward_carries_every_entry and equity_option_position_roundtrip_rebuilds_the_entries pass, 40 assertions, on Hybrid_GenericTRS_with_EquityOptionPosition.xml
Corpus walk roundtrip mirrors every portfolio in the ORE example corpus passes, so every mapped trade still reaches an output
Database The child table, its insert and notify triggers and its tenant policy exist; option_data_json is gone; the parent held 64 rows with 0 non-null option_data_json values at the drop, and the child starts at 0

Two findings from the work:

  • The knock-out shell verb in ores.shell/application and the import handler both still named knock_out_dates_json after the model lost it. The shell verb's argument was removed and the handler's assignment deleted; the generated shell in ores.shell/trading had no such argument. The whole-tree build is what caught both, which is why the build gate runs after the regeneration rather than before.
  • The first equity attempt intercepted ContractForDifference as well as TotalReturnSwap. The reverse then wrote TotalReturnSwapData under a ContractForDifference trade, and Hybrid_CFD.xml stopped reading back, which the corpus walk's mapped/lost check caught as 181 = 182=. The ContractForDifference interception was removed; those documents stay with the composite mapper, which is where they were before this unit.
  • projects/ores.sql/modeling/ores_schema.puml still names knock_out_dates_json. The file is generated by plantuml_er_generate.sh and is already stale for the two children that landed before this unit, so regenerating it would sweep in unrelated schema history. It is left as it was found, and the drift gate does not cover it.

11.2. PR 3 close-out (measured 2026-09-28)

The four remaining units were applied to one still tree and then every gate was run once. The generated half was written by check_component_drift.py --component trading-cpp in its in-place mode: it reported the new average_type files as untracked, which is the expected failure for a new entity, and the dry run then agreed with the tree.

Check Result
pytest projects/ores.codegen/tests -q 806 passed
check_component_drift.py --component trading-cpp --dry-run twice No drift, both runs
check_component_drift.py --all --dry-run No drift
cmake --build build/output/linux-clang-debug-make -j 3 Exit 0, zero error:
ctest, the nine owned suites selected (ores.trading.*, ores.shell.trading.*, ores.shell.application.*, ores.ore.* and ores.utility.*) 9 of 9 passed
validate_docs.sh 40 components pass

Two defects were caught by the gates rather than by the diff, which is the reason the build and the suites run after the regeneration:

  1. The generated ores_trading_equity_swap_instruments_insert_fn still wrote NEW.payment_frequency after the first regeneration, because the model's ** Validations row names the column a second time. The live insert in ores.trading.core.tests reported record "new" has no field "payment_frequency". The row is corrected in ores.trading.equity_swap_instrument.org and the component regenerates clean.
  2. The shell application's callable swap command, not the generated shell, was the second call_type consumer. The whole-tree build named it.

The DDL applied to ores_dev_merry_newton:

Change Count
Columns renamed: day_count_code to day_count_fraction_code on bond_issues, commodity_instruments and credit_instruments 3
Column renamed: payment_frequency to payment_frequency_code on equity_swap_instruments 1
Check and column dropped: ores_trading_callable_swap_instruments_tbl_call_type_check and call_type 2
Table created: ores_trading_average_types_tbl, with its two indexes, insert function, insert trigger, delete rule and notify trigger 1
RLS policy created and enabled: average_types_tenant_isolation_policy 1
Privileges granted on the new table: rw (select, insert, update, delete, truncate) and ro (select) 2
Reference rows seeded: Arithmetic and Geometric 2
Generated create and notify-trigger scripts re-applied to resync every trigger function with the renamed columns (bond_issues, commodity_instruments, credit_instruments, equity_swap_instruments, callable_swap_instruments) 10

One finding is recorded rather than fixed. The ten lookup tables the enumeration unit added – long_short_types, moment_types, price_types, amortization_types, average_types, option_types, exercise_types, settlement_types, barrier_types, activity_categories, return_types and payoff_types – have generated create scripts that no aggregator includes: projects/ores.sql/create/trading/trading_create.sql reaches 140 scripts and none of them is a lookup. A fresh compass db recreate would create the reference tables' consumers and then fail on the seeds. The drop side is the same: drop_trading.sql omits the lookup drops along with 20 pre-existing equity instrument drops. Wiring the aggregators is a hand edit that no gate measures, and a rebuild is not one of this PR's gates, so it is left to the shell-and-verification PR (W04) with the count above so the gap is not rediscovered.

Another finding, not fixed: the one-shot trading_bond_instruments_flat_columns_to_relational_rows_migration.sql still names day_count_code. It is a historical reshape against a pre-reshape database, is referenced by no aggregator, and is neither compiled nor run, so it is expected to keep the legacy name on its source side.

11.3. PR 3 merged (recorded 2026-09-28)

PR #2333 merged into main as 9631df6bff at 2026-09-28T17:45:18Z, with all fifteen checks green, after one review round and a rebase onto main. The review round is in the table above; the fixes rode in as ec0c04117c and the ER diagram regeneration as 1ac467197a.

Check Result
Review Ten findings decided: nine fixed at their source, each with a test that fails without the fix, and one recorded as follow-ups
Rebase The branch rebased onto main's merge of PR #2240, then onto 9631df6bff with no conflict on the final push
Database A fresh compass db recreate -y -k from the tree exits 0
Build cmake --build build/output/linux-clang-debug-make -j 3 exits 0 with zero error:
Fleet The fleet is 24 units ready, 25 running with nats
Suite ctest is 89 of 89 on the fresh database, the nine owned suites included
Codegen pytest projects/ores.codegen/tests -q is 978 passed
Drift check_component_drift.py --component trading-cpp --dry-run twice, --all --dry-run and --sweep --dry-run, all no drift
Hygiene check_sql_hygiene.py projects/ores.sql is 1676 files with 0 problems

Two environment facts this round established, both of which cost a CI cycle:

  • The codegen gates evaluate the merge with main, so the rebase must precede the final local run. A template-driven reflow passed every local gate while CI stayed red, because main's copy of the affected file still held the old wrap.
  • CI installs clang-format from apt on ubuntu-24.04, which is 18.1.3, while the codegen venv held 18.1.8. The one-line difference in trade_service.cpp is what the drift gate caught. The venv now carries 18.1.3, and regeneration runs with it first on PATH.

The lookup aggregator finding above was corrected after it was written: the close-out unit wired trading_create.sql to 165 of 166 and drop_trading.sql to 163 of 164 reachable scripts and removed two dangling \ir lines, so a fresh recreate reaches the lookup tables.

Emacs 29.3 (Org mode 9.6.15)