Task: Model the unmodelled trading families and bind the unbound
Table of Contents
- 1. Goal
- 2. Status
- 3. Acceptance
- 4. Plan
- 4.1. Bind the unbound entities
- 4.2. Author the nine missing models
- 4.3. 2a. The profiles the nine new models bind
- 4.4. 2b. The commodity port, and the environment blocker
- 4.5. 2c. How the model was authored
- 4.6. 2d. The environment defects, and how they cleared
- 4.7. 2e. The ports, one at a time
- 4.8. Classify the unmodelled hand-written domain types
- 4.9. Clear the legacy fragments
- 4.10. Close the type defects
- 4.11. 5a. The defect states
- 4.12. 5b. The capability units
- 4.13. 5c. What the date unit found on the way
- 4.14. 5d. What the decimal unit found
- 4.15. 5e. The money columns, and the calls worth challenging
- 4.16. 5f. The collection columns, closed
- 4.17. 5g. Defect 6, one concept under two names
- 4.18. 5h. M04, every feature in the namespace the loader reads
- 4.19. 5i. The average type, the commodity producer, and the call type
- 4.20. 5j. Defects 14, 15 and 16 are carried to PR 4
- 4.21. Regenerate and prove
- 4.22. Record
- 5. Notes
- 6. Defect 1: collections held as JSON
- 6.1. The census (measured 2026-09-28 at
8ee64b5002) - 6.2. The child that landed:
callable_swap_call_date - 6.3. The child that landed:
commodity_basket_constituent - 6.4. The column that was deleted:
knock_out_dates_json - 6.5. The child that landed:
equity_position_option_underlying - 6.6. What the other collections need
- 6.7. Files changed
- 6.8. Judgement calls
- 6.1. The census (measured 2026-09-28 at
- 7. Defects 4, 5 and 8: the enumeration record
- 8. Test Scenarios
- 9. PRs
- 10. Review
- 11. Result
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_instrumentsandswap_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 andproduct_type; the PR 3 survey foundactivity_typesandfpml_event_typesin 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_namevalues 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-runreports nothing after the work, andcheck_model_drift.py --summaryreports 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 underapi/.../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_typeandtrade_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 readskey_fieldfrom 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_includesblock namedores.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_idfrom the dq catalogue, soores.trading.core.testsnow linksores.dq.api.libandores.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:
- The in-place regeneration and the dry run disagree about one generated
test.
trade_eventing_integration_tests.cppgains soft-FK parent seeding forstatus_id(anores.dq.fsm_staterow) 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 capturedrift_gate_misses_cross_component_outputs.orgrecords, seen from the trading side. The change is kept, and the dry run then agrees. - 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-lookupfixeshas_change_reason_cacheandhas_paginationtrue, which this entity resolves false.trading-instrumentfixeshas_workspace_idandparty_id_from_session, neither of which it sets.fk-scoped-childfixeshas_workspace_id,has_explorer_apiand 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-lookupfixeshas_workspace_idfalse and a presentation tier, which this entity resolves false, andworkspace-scoped-lookupfixes 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-lookupfixeshas_change_reason_cacheandhas_paginationtrue, which every one of these entities resolves false, andhas_workspace_idfalse, which two of them contradict.trading-instrumentfixeshas_workspace_idandparty_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-childfixeshas_workspace_id,has_explorer_apiand a parent, and only two of the twenty-nine are workspace-scoped.tenant-scoped-junctionfixes onlyhas_tenant_id, but it is anchored to the junction metatype, and M07 already records that this component has no junction metatype.trade_party_roleis the entity that would open when one is introduced.readonly-timeseriesdescribes 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_instrumentsandscripted_instrumentsbindtrading-instrument. Each is an instrument sub-type with the same scope as the nine already modelled: tenant, workspace andparty_id, no presentation tier.composite_legsandswap_legsbindtrading-instrumentas well. They carry the same tenant, workspace andparty_idscope; 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_typesandfpml_event_typesbindsimple-lookup. Both key on a textcode, are tenant-scoped, carry no workspace, and have a shell surface, which is exactly the profilecountryanchors and whattrade_type,party_role_type,lifecycle_eventandtrade_id_typealready bind in this component.product_typeis 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.cppprojects/ores.ore/core/src/domain/commodity_instrument_mapper.cppprojects/ores.ore/core/tests/xml_commodity_mapper_roundtrip_tests.cppprojects/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_lifecycleandtrade_parties. Each has an org underprojects/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_legandproduct_type. Eight carry a table and a hand-written stack and this PR authors them;product_typeis 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_dataandtrade_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,instrumentwith its identity and payload is the polymorphic container the wire and the ORE mapper carry,trade_instrumentis the trade's joined instrument view, and the three*_datatypes 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_daterejected 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_dayreflector wrote the default-constructed sentinel as0000-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 withnumeric field overflow; the repository test pins that refusal rather than hiding it. Every money column isnumeric(28, 10)ornumeric(18, 10), so the ceiling is the schema's, not the type's.bond_leg_amount.valuecarries all six amount roles, so itsraterows move to the decimal with itsnotionalrows. "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_instrumentthe model's** Validationsrow also had to move, because it is a second place the column is named. The first regeneration left the generated insert function writingNEW.payment_frequency, and the live insert caught it asrecord "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_typeis authored in the simple-lookup shape: a textcodekey, the bitemporal pair,ArithmeticandGeometricseeded, a row in the module, an RLS policy beside the other lookups, and no validation function.commodity_instrument.average_typeandequity_asian_option_instrument.average_typepoint at it. ORE states no averaging set:commodityAveragePriceOptionDataandsingleUnderlyingAsianOptionDatacarry no average member, noaverageTypesimple type exists underexternal/ore/xsd/, and noAverageTypeelement appears underexternal/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.cppline 365 readresult.average_type = to_string(d.PriceType);– ORE'spriceType, whose values areSpotandFutureSettlement, written into a column whose set isArithmeticandGeometric. 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 existingcommodity_mapper_roundtrip_apocase asCHECK(r.average_type.empty());, which fails on the oldSpotvalue. call_typeis deleted. ORE's onlyCallTypeelement is onnettingSetDetails(external/ore/xsd/instruments.xsdline 225), a netting-agreement field, and no<CallType>element appears underexternal/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 isknock_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.orgnames four trade subjects in prose, andtrade_status_fsm_implementation.orgdescribes an FSM. Neither is a codegen model and neither belongs in the B02 census. ores.trading.module.orglistsleg_typeandfloating_index_typeas 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.
- The schema states no date list.
knockOutSwapData(external/ore/xsd/instruments.xsd) states exactly three members:BarrierData, oneBarrierStartDate, andLegDatawithminOccurs"2" maxOccurs="2". The only list anywhere in the block is =BarrierData/Levels, anxsd::vector<float>of barrier levels. No member holds observation dates. - The corpus document states no date list either.
Exotic_KnockOutSwap.xml(assets/test_data/golden_datasetandexternal/ore/examples) holds oneBarrierDatawith oneLevel, oneBarrierStartDate, and twoLegDatablocks. The file names no observation date. - Nothing produces the instrument.
trade_mapper.cppsendsKnockOutSwaptoscripted_instrument_mapper::forward_knock_out_swap, which writes the scripted instrument'sparameters_jsonand never aknock_out_swap_instrumentrow. 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_jsonhas 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 anequity_swap_basket_underlyingrow keyed like this one, and it lands with the product split.knock_out_swap_instrument.knock_out_dates_jsonwas 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_jsonwas migrated. The child isequity_position_option_underlying, keyed like the two children above, and the producer is wired for the nestedEquityOptionPositionsub-trade. See "The child that landed" above.scripted_instrument's three columns are the defect's stated exception. The repair the defect names isjsonbrather thantext, 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_jsononequity_swap_instrumentwaits for defect 9 so that its removal cannot weaken the XOR assertion.knock_out_dates_jsongets 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_datais a new carrier struct rather than a reuse ofwith_legs<>, because a constituent's parent reference is a directinstrument_idcolumn (the callable child's shape) and not a groupedidentity.instrument_id, sowith_legs'sstamp_idsdoes not apply.- The equity position carrier wraps the whole
equity_instrument_variantrather 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/SubTradeshape a total return swap uses, not only the top-levelEquityOptionPositionDataelement.ContractForDifferencekeeps the composite dispatch it had: its reverse rebuilds a different data member, and intercepting it madeHybrid_CFD.xmlunreadable on export. - The equity entries verb is hand-written in
ores.shell/applicationrather 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
EquityPositionsub-trade is dispatched, so its parent row is produced, but it gets no child rows:equityPositionData.Underlyingis a bareunderlyinglist 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_tblandores_dq_lei_relationships_tbldo not exist; a change reason names a category the seed does not hold.ores.shell.api.tests:to_iso8601_utcstates a space where the test wantsT.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/applicationand the import handler both still namedknock_out_dates_jsonafter the model lost it. The shell verb's argument was removed and the handler's assignment deleted; the generated shell inores.shell/tradinghad 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
ContractForDifferenceas well asTotalReturnSwap. The reverse then wroteTotalReturnSwapDataunder aContractForDifferencetrade, andHybrid_CFD.xmlstopped reading back, which the corpus walk's mapped/lost check caught as181 =182=. TheContractForDifferenceinterception was removed; those documents stay with the composite mapper, which is where they were before this unit. projects/ores.sql/modeling/ores_schema.pumlstill namesknock_out_dates_json. The file is generated byplantuml_er_generate.shand 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:
- The generated
ores_trading_equity_swap_instruments_insert_fnstill wroteNEW.payment_frequencyafter the first regeneration, because the model's** Validationsrow names the column a second time. The live insert inores.trading.core.testsreportedrecord "new" has no field "payment_frequency". The row is corrected inores.trading.equity_swap_instrument.organd the component regenerates clean. - The shell application's callable swap command, not the generated shell, was
the second
call_typeconsumer. 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-formatfrom apt on ubuntu-24.04, which is 18.1.3, while the codegen venv held 18.1.8. The one-line difference intrade_service.cppis what the drift gate caught. The venv now carries 18.1.3, and regeneration runs with it first onPATH.
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.