Equity Per-Type Instrument Migration
Table of Contents
- Overview
- Goals
- Non-goals
- Constraints and decisions
- Current state
- Phase plan
- Phase 1 — Add
get_<type>_instrumentto per-type services [DONE] - Phase 2 — Rewrite
equity_instrument_mapperto produce per-type variant - Phase 3 — Flip importer dispatch to per-type save requests
- Phase 4 — Bundle-aware read path + client form
- Phase 5 — Delete legacy generic wire messages and handlers
- Phase 6 — Delete legacy generic C++ infrastructure
- Phase 7 — Delete legacy generic SQL
- Phase 1 — Add
- Verification
- Cross-PR invariants retained
- Follow-up (PR #3)
Overview
PR #2 of the four-PR migration series outlined in
doc/plans/2026-04-21-fx-forward-import-e2e.org. The FX pattern —
delete the legacy generic table, wire reads through per-type services,
introduce a discriminated export variant, update the client form —
repeats for equity, which is the largest remaining family: nine
per-type tables and fifteen ORE trade types mapping into them.
Scope divergence from the FX PR: the FX branch inherited an ORE
importer that already produced fx_instrument_variant (per-type),
so the migration only had to flip the read side. Equity is one
step behind — equity_instrument_mapper still produces the legacy
generic equity_instrument and the importer writes only to the
legacy generic table. The nine save_equity_<type>_instrument_request
messages and the typed_equity_instrument_handler exist but have
zero callers across the codebase; the per-type tables are empty in
practice.
Flipping only the read side would therefore break every imported equity trade (blank economics, mirroring the FX defect we just fixed). The mapper rewrite is included here so read and write flip in the same PR.
Goals
- Rewrite
equity_instrument_mappersoequity_mapping_resultcarriesequity_instrument_variant(nine alternatives), mirroringfx_mapping_result/fx_instrument_variant. Each of the fifteen ORE trade types maps to exactly one per-type domain object. - Update both importer dispatch sites
(
ore_import_execute_handler.cppandOreImporter.cpp) tostd::visitthe variant and send the correctsave_equity_<type>_instrument_requestper alternative, mirroring the FX dispatch blocks already in place. - Introduce
equity_export_resultas a discriminated union over the nine per-type equity instruments, symmetric withfx_export_result, and slot it intoinstrument_export_resultin place of the bareequity_instrument. - Route
product_type::equityintrade_handler::populate_instrument_for_tradebytrade_type_codeto the correct per-type service, mirroring the FX block. - Rewire
EquityInstrumentFormto pattern-match onequity_export_result, scoped to a single variant for this PR (matching how PR #1 scopedFxInstrumentFormtofx_forward_instrument). - Delete the legacy single-table equity infrastructure in its
entirety:
ores_trading_equity_instruments_tbl,equity_instrumentdomain,equity_instrument_service/ repository / mapper / handler, and the four legacysave_/delete_/list_/history_wire messages.
Non-goals
- Full multi-variant
EquityInstrumentForm: scoping to one variant is deliberate. Extending the form to cover all nine variants needs a UI story (per-variant forms or a dispatching shell) that is best decided alongside the analogous follow-up for FX. - Data migration of existing rows from the legacy table to per-type tables. This PR changes the importer going forward; any existing legacy rows become stranded. Acceptable because equity trades in local environments are disposable; production has its own migration story (out of scope for the source tree).
- Touching Bond / Credit / Commodity / Composite / Scripted: PR #3.
Constraints and decisions
- Single branch, ordered commits: work lands on
feature/equity-per-type-migrationin FX-style phases. Each commit builds cleanly. No backwards-compatibility shims. get_naming: all new service methods useget_<type>_instrument, matching the codegen template.- One variant shape:
equity_export_resultis symmetric withfx_export_result— a struct with aninstrumentvariant field. No legs (equity does not have swap-style legs; if a future equity variant needs sub-rows, it can evolve independently). - Trade-type → per-type routing is string-matched in the handler,
mirroring FX. Driving the routing from
ores_trading_trade_types_tblis a cross-cutting improvement deferred to a dedicated PR. - Mapper rewrite is type-driven: the existing 1191-line
equity_instrument_mapper.cppwrites every field into the flatequity_instrumentstruct, which has a superset of fields. Per-type targets only hold the relevant subset, so eachforward_equity_<type>becomes shorter after the split (each one assigns to a narrower domain type). The reverse direction changes signature from(const equity_instrument&)to(const <per-type>_instrument&).
Current state
Per-type assets already in place
Nine per-type tables, domain structs, repositories, and services. As
of Phase 1 of this branch (commit 1ddbbe766), all nine services
now have both save_<type>_instrument and get_<type>_instrument.
equity_optionequity_digital_optionequity_barrier_optionequity_asian_optionequity_forwardequity_variance_swapequity_swapequity_accumulatorequity_position
Save-side wire protocol exists for all nine
(save_equity_<type>_instrument_request) and is wired through
typed_equity_instrument_handler + registrar_other_instruments.cpp.
No callers yet.
Legacy generic assets to delete
ores_trading_equity_instruments_tbl(SQL table + notify trigger + policy).trading::domain::equity_instrument+ JSON / table I/O helpers.equity_instrument_entity/ mapper / repository.equity_instrument_service(both save andget_equity_instrument).equity_instrument_handler(wire handler for the four generic save / delete / list / history messages).save_equity_instrument_request/ response, plus the three sibling legacy messages, frominstrument_protocol.hpp.- Registrar entries for those four wire subjects.
- ORE mapper output wrapping (
equity_mapping_resultbecomes variant-holding).
Trade-type → per-type table mapping
From ores.ore/src/domain/trade_mapper.cpp::map_equity_instrument
and the per-forward_equity_<type> semantics in
equity_instrument_mapper.cpp:
| ORE trade type | Per-type table / domain |
|---|---|
| EquityOption | equity_option_instrument |
| EquityCliquetOption | equity_option_instrument |
| EquityOutperformanceOption | equity_option_instrument |
| EquityForward | equity_forward_instrument |
| EquitySwap | equity_swap_instrument |
| EquityWorstOfBasketSwap | equity_swap_instrument |
| EquityVarianceSwap | equity_variance_swap_instrument |
| EquityBarrierOption | equity_barrier_option_instrument |
| EquityDoubleBarrierOption | equity_barrier_option_instrument |
| EquityEuropeanBarrierOption | equity_barrier_option_instrument |
| EquityAsianOption | equity_asian_option_instrument |
| EquityDigitalOption | equity_digital_option_instrument |
| EquityTouchOption | equity_digital_option_instrument |
| EquityAccumulator | equity_accumulator_instrument |
| EquityTaRF | equity_accumulator_instrument |
Fifteen ORE trade-type names collapsing onto eight per-type tables.
equity_position has no ORE forward mapping (it's a book-level
position, not a trade); it remains reachable via the save request
for non-ORE code paths.
Client form
EquityInstrumentForm holds trading::domain::equity_instrument
directly — the legacy generic type. Same shape as FxInstrumentForm
pre-PR-1. Needs migration to the new variant, scoped to one concrete
per-type (recommend equity_option_instrument as it has the largest
trade-type fan-in).
Phase plan
Seven phases. Each commit builds clean. No backwards-compatibility shims.
Phase 1 — Add get_<type>_instrument to per-type services [DONE]
Commit 1ddbbe766. Nine services gained get_<type>_instrument
methods delegating to the repository's read_latest(ctx, id).
Phase 2 — Rewrite equity_instrument_mapper to produce per-type variant
The bulk of the PR's new code.
Header (equity_instrument_mapper.hpp)
- Introduce
equity_instrument_variantover the nine per-type domain types, mirroringfx_instrument_variant. equity_mapping_result.instrumentbecomesequity_instrument_variant(wasequity_instrument).- Each
forward_equity_<type>keeps its signature but the return variant now carries the matching per-type alternative. - Each
reverse_equity_<type>changes its parameter type fromconst equity_instrument&toconst <per-type>_instrument&.
Implementation (equity_instrument_mapper.cpp)
- Per-type
forward_*functions write into the narrower per-type struct instead of the flatequity_instrument. Fields that exist on the per-type struct are copied over; fields that don't are dropped (the legacy generic held a superset). - A shared
make_base<T>()helper replaces the existingmake_base(tradeTypeCode)— templated on the per-type struct soinstrument_id,trade_id, audit stamps, etc. can be set uniformly. - Reverse functions change input type; the field-read direction is straightforward because per-type holds exactly the fields the ORE payload carries for that type.
Tests
xml_equity_mapper_roundtrip_tests.cppandxml_equity_golden_roundtrip_tests.cppchange argument types on the reverse path and destructure the variant on the forward path. Assertions target per-type fields instead of flat fields; any flat-field assertion that has no per-type equivalent is dropped.
Phase 3 — Flip importer dispatch to per-type save requests
Two sites, mirroring the FX visit blocks.
ores.ore.service/src/messaging/ore_import_execute_handler.cpp
The equity_mapping_result branch of the outer std::visit becomes
a nested std::visit over equity_instrument_variant, dispatching
to the correct save_equity_<type>_instrument_request per
alternative — exactly the same shape as the existing FX block
(lines 413–447 at branch cut).
ores.qt.trading/src/OreImporter.cpp
Parallel client-side change.
Registrar
No change: save_equity_<type>_instrument_request subscriptions are
already in registrar_other_instruments.cpp.
After this phase the ORE importer writes to per-type tables only.
The legacy generic table continues to exist but is no longer
populated by this path. save_equity_instrument_request has no
callers (importer was the last one) but is still registered;
Phase 5 deletes the subscription.
Phase 4 — Bundle-aware read path + client form
instrument_protocol.hpp
- Add
equity_export_resultas a struct withinstrumentvariant over the nine per-type equity types, symmetric withfx_export_result. - Replace
equity_instrumentwithequity_export_resultininstrument_export_result.
Handler
Update trade_handler::populate_instrument_for_trade to route
product_type::equity by trade_type_code into the right per-type
service (exactly parallel to the FX block). Uses the get_ methods
added in Phase 1.
Client form
Update EquityInstrumentForm::setInstrument to pattern-match
equity_export_result → chosen per-type variant, mirroring
FxInstrumentForm::setInstrument.
At this point reads and writes both flow through per-type infrastructure end-to-end.
Phase 5 — Delete legacy generic wire messages and handlers
- Drop
save_equity_instrument_request/delete_equity_instrument_request/get_equity_instruments_request/get_equity_instrument_history_requestand their response pairs frominstrument_protocol.hpp. - Delete
equity_instrument_handler.hpp. - Remove the four
nats.queue_subscribeentries fromregistrar_other_instruments.cpp. - Sweep any remaining call sites (shell? cli? HTTP? CSV import?).
Phase 6 — Delete legacy generic C++ infrastructure
equity_instrument_service(header + cpp).equity_instrument_repository,equity_instrument_mapper,equity_instrument_entity.trading::domain::equity_instrument+equity_instrument_json_ioequity_instrument_table+equity_instrument_table_io.
CMakeLists.txtentries for any file-level sources removed.
Phase 7 — Delete legacy generic SQL
projects/ores.sql/create/trading/trading_equity_instruments_create.sqland its_notify_trigger_create.sql.- The matching drop scripts.
- The
\irincludes intrading_create.sql. - Any IAM policy or seed script touching the table.
Verification
- Trading core + Qt trading + ORE targets build clean.
ores.trading.core.tests,ores.ore.tests(including the two equity roundtrip suites rewritten in Phase 2) pass.- End-to-end smoke: import an equity trade (e.g.
EquityOption) via ORE XML, confirm the per-type table receives the row, open the trade inTradeDetailDialog, confirm the equity tab renders the correct per-type economics.
Cross-PR invariants retained
get_naming across all new service methods.- No backwards-compatibility shims: each phase deletes the legacy path it replaces in the same commit.
- Variant shape symmetric with FX and Swap; single
instrument_export_resultdiscriminated union is the sole response carrier.
Follow-up (PR #3)
Bond, Credit, Commodity, Composite, Scripted — the five single-type
families with no per-type infrastructure. Either stand up per-type
tables (unnecessary until a second sub-type appears) or rename
save_<family>_instrument_request → save_<family>_<type>_request
for shape uniformity. The default per the parent plan is the latter.