FX Forward ORE Import End-to-End Wiring
Table of Contents
- Overview
- Goals
- Non-goals
- Constraints and decisions
- What is already done
- What is left — phase plan
- Phase 1 — Evolve
get_trades_requestto return bundles [task #17] - Phase 2 —
ClientManager::listTradeshelper [task #19] - Phase 3 —
IInstrumentForm::setInstrumentreplacesloadInstrument[task #20] - Phase 4 —
PortfolioExplorerTradeModelcaches bundles;TradeDetailDialogtakes(trade, instrument)[task #21] - Phase 5 — Delete
instrument_handler+get_instrument_for_trade_request[task #18] - Phase 6 — Build and smoke-test [task #22]
- Phase 1 — Evolve
- Broader context: this PR is #1 of a product-family migration series
Overview
Importing an FX trade via the ORE import flow produced a trade that
appeared in the Portfolio Explorer but had blank instrument economics
in TradeDetailDialog. Two distinct defects combined to hide the
problem from every existing test:
- Read path bug: the importer saved FX forwards into the per-type
table
ores_trading_fx_forward_instruments_tbl(and six sibling variants), while the UI was reading from the legacy genericores_trading_fx_instruments_tbl, empty since the per-type migration. The generic table and its service/repository/handler still existed as a shadow path. - Soft FK ordering bug:
trade_mapper::map()emitstrade.idas the nil UUID at parse time and copies that nil into theinstrument.trade_idback-reference. The planner later mints a real UUID fortrade.id(so re-imports don't collide) but forgot to propagate it into the instrument, leaving the instrument orphaned. Thetrade_importtest was vacuously green because both sides were nil at comparison time.
This plan captures the branch feature/fx-forward-import-e2e-tests:
what has already landed, and the refactor sequence still outstanding
to complete the end-to-end wiring and converge on the target client
API for trade lookup.
Goals
- Make FX forward imports render correctly in
TradeDetailDialogend-to-end, from XML through domain into the per-type tables and back out through a single client RPC. - Replace the
loadInstrument(id)callback-style form API with asetInstrument(variant)push-style API where the dialog already holds the instrument and hands it to the form. - Evolve
get_trades_requestinto a bundle-returning RPC: one round trip yields(trade, instrument)pairs scoped to a single node (portfolio or book), making a separate instrument fetch unnecessary. - Delete the intermediate
instrument_handler+get_instrument_for_trade_requestplumbing once bundles carry the instrument inline. - Establish the target API shape so the remaining six FX variants and the non-FX product families can adopt it uniformly in follow-up PRs.
Non-goals
- This PR only wires
FxForwardon the client form side. The other six FX variants (fx_accumulator,fx_asian_forward,fx_barrier_option,fx_digital_option,fx_vanilla_option,fx_variance_swap) and the non-FX families ride the new API shape but keep their existing per-family forms. Their migration is follow-up work. - No legacy / generic FX infrastructure is preserved for backwards compatibility. When the new path replaces the old, the old is deleted in the same commit.
- No forwarding headers, deprecation shims, or compatibility flags at any phase boundary.
- The subtree-semantics of portfolio filtering are handled by the
database: the wire request carries a single node identifier, not an
include_subtreeflag.
Constraints and decisions
- Single branch, ordered commits: work lands on
feature/fx-forward-import-e2e-tests, one commit per phase, in sequence. Each commit builds clean. get_, notfind_: every service method and wire message uses theget_prefix. The prior mix offind_*_instrument/get_*_requesthas been unified as part of Phase 0.- One node field on
get_trades_request: no separateportfolio_id/book_idpair, noinclude_subtreeflag. The database resolves the node to its subtree closure; the client doesn't care whether the node is a portfolio or a book. - Bundles over round trips:
get_trades_responsecarries both the trade and its instrument variant. The dialog never issues a second RPC to hydrate the instrument. - Variant symmetry:
fx_export_resultmirrorsswap_export_resultas a discriminated union over the seven per-type FX instruments. Clients pattern-match on it the same way for every family.
What is already done
Two commits on feature/fx-forward-import-e2e-tests:
5a186e0f4 [ore] Rewire instrument.trade_id after planner mints trade UUID
- Adds
importer::rewire_instrument_trade_id()which walks the mapping-result variant and reassigns the soft FK to the realitem.trade.id. - Calls it from both planner mint sites (existing-book path and create-portfolio path).
- Adds a planner test asserting every planned trade has a non-nil
UUID and every instrument's
trade_idmatches. - Replaces the vacuous
trade_importassertion withREQUIRE(instr.trade_id.has_value())plus equality on the dereferenced value, and documents the provisional-nil parse stage in a comment.
5f64df94d [trading,ore,qt,sql,codegen] Delete legacy FX generic table; route reads by trade_type_code
- Deletes the legacy single-table FX infrastructure in full: domain
struct, SQL table + trigger + policy, entity, mapper, repository,
service, handler, JSON I/O, table I/O, the four legacy messaging
types (
save/delete/list/history), and their registrar entries. - Wires the read path through the per-type services, mirroring the
swap pattern.
instrument_handlerandtrade_handlerrouteproduct_type::fxbytrade_type_codeto the right per-type service. - Adds
get_<type>_instrumentmethods to the seven per-type FX services (repositories already exposedread_latest). - Swaps
fx_instrument(legacy) forfx_export_resultin theinstrument_export_resultvariant — a discriminated union over the seven per-type FX instruments, symmetric withswap_export_result. - Requires
trade_type_codeonget_instrument_for_trade_requestwhenproduct_typeisfx(previously required only forswap). - Renames
find_*_instrumenttoget_*_instrumentacross the seventeen instrument services, and updates the codegen templatecpp_service.cpp/.hpp.mustachein lockstep so generated services follow the convention. FxInstrumentFormnow holdsfx_forward_instrumentdirectly, sends the new request withtrade_type_code, extractsfx_forward_instrumentfromfx_export_result, and saves viasave_fx_forward_instrument_request. Scoped toFxForwardfor now.
What is left — phase plan
Five phases remain. Tasks refer to the in-session task list.
Phase 1 — Evolve get_trades_request to return bundles [task #17]
Replace the trade-only response with a bundle response that carries the instrument inline.
Wire protocol changes (trading_protocol.hpp)
get_trades_requestkeepsoffset/limitbut replacesbook_idwith a singlenode_id(std::string). The database is responsible for resolving the node to its subtree. Noinclude_subtreeflag.- New
trade_bundlestruct containingtrade+instrument_export_result. get_trades_responsecarriesstd::vector<trade_bundle>andtotal_available_count.
Server routing (trade_handler)
- After fetching the page of trades, call the per-type instrument
services (keyed on
(product_type, trade_type_code)) to populate the instrument side of each bundle. - Reuse the routing added in 5f64df94d; no new decision tables.
- Missing / not-yet-imported instruments surface as an empty variant alternative, not as an error.
Database
ores_trading_trade_repositorygains alist_by_nodepath (or equivalent) that accepts a single portfolio-or-book node id and expands it to the subtree closure server-side.
Phase 2 — ClientManager::listTrades helper [task #19]
Add a helper to ClientManager that wraps get_trades_request and
returns the bundles. Callers stop hand-rolling the request / response
dance.
- Signature:
std::vector<trade_bundle> listTrades(node_id, offset, limit). - Returns already-extracted bundles; surfaces total count via a
second out-param or a small result struct. Match the existing
helper idioms in
ClientManager. - Logs request / response timing at debug under the ClientManager logger.
Phase 3 — IInstrumentForm::setInstrument replaces loadInstrument [task #20]
The form becomes a pure renderer. It no longer triggers an RPC on its own.
Interface change
- Remove
IInstrumentForm::loadInstrument(const std::string& id). - Add
IInstrumentForm::setInstrument(const instrument_export_result&)(or the family-specific variant; settle on one push signature). clear()stays for the create-mode empty path.
FxInstrumentForm
- Remove the
get_instrument_for_trade_requestcall. setInstrumentpattern-matches thefx_export_resultalternative, storesfx_forward_instrument, and populates the widgets.
Seven sibling form implementations
- Mechanically update each
XxxInstrumentForm::loadInstrumenttosetInstrument, stubbing the unimplemented ones uniformly so the build stays clean. (Full content-parity for non-FX families is follow-up.)
Phase 4 — PortfolioExplorerTradeModel caches bundles; TradeDetailDialog takes (trade, instrument) [task #21]
PortfolioExplorerTradeModel
- Call
ClientManager::listTradesinstead of the rawget_trades_requesttoday. - Cache the full
trade_bundleper row (not just the trade). Model rows still expose trade fields for table display; the instrument side is retained for downstream dialog use.
TradeDetailDialog
- Constructor / entry point takes
(trade, instrument_export_result)rather thantradealone. - The code path at
TradeDetailDialog.cpp:374-376that currently callsactiveForm_->loadInstrument(uuid)becomesactiveForm_->setInstrument(instrument)— no UUID, no RPC. - The create-mode branch still calls
activeForm_->clear().
OrgExplorerTradeModel / ClientTradeModel
- Both also use
get_trades_requesttoday. Either migrate them to the bundle shape in the same commit, or gate them on a later phase if scope creeps. The default is migrate-now: the old shape is gone.
Phase 5 — Delete instrument_handler + get_instrument_for_trade_request [task #18]
With bundles carrying the instrument, the entire separate instrument-for-trade round trip is dead code.
- Delete
instrument_handler.hpp/.cppand its registrar entry. - Delete
get_instrument_for_trade_request/get_instrument_for_trade_responsefrominstrument_protocol.hpp. - Sweep call sites: the seven=XxxInstrumentForm= files under
projects/ores.qt.trading/src/all reference the request today (seegrepforget_instrument_for_trade_request). trade_handlerretains the product-type → trade-type-code routing introduced in 5f64df94d — that now lives insideget_trades_requestfulfilment rather than in a standalone handler.
Phase 6 — Build and smoke-test [task #22]
make -C build/output/linux-clang-debug-make -j$(nproc) allcmake --build --preset linux-clang-release-ninja --target rat- End-to-end smoke: import the
XvaRisk/Inputsample XML, open the imported FX Forward inTradeDetailDialog, confirm economics render. - Verify the
trade_importand planner tests added in 5a186e0f4 still pass.
Broader context: this PR is #1 of a product-family migration series
The FX Forward defect that motivated this branch is a specific instance of a systemic divergence between legacy single-generic- table infrastructure and per-type-table infrastructure, present across every product family except rates/swap. The pre-branch analysis catalogued the entire surface:
Inventory across product families
| Family | Legacy generic table | Per-type tables | Per-type services | Per-type handlers |
|---|---|---|---|---|
| FX | was fx_instruments_tbl (deleted in 5f64df94d) |
7 (forward, vanilla_opt, barrier_opt, digital_opt, asian_fwd, accumulator, var_swap) | 7 save + 7 get (get added in 5f64df94d) | typed_fx_instrument_handler |
| Equity | equity_instruments_tbl |
9 (option, digital_opt, barrier_opt, asian_opt, forward, var_swap, swap, accumulator, position) | 9 save only | typed_equity_instrument_handler |
| Bond | bond_instruments_tbl |
— | — | — |
| Credit | credit_instruments_tbl |
— | — | — |
| Commodity | commodity_instruments_tbl |
— | — | — |
| Composite | composite_instruments_tbl |
— | — | — |
| Scripted | scripted_instruments_tbl |
— | — | — |
| Swap (rates) | none (per-type only, by design) | 9 (FRA, vanilla, cap_floor, swaption, balance_guaranteed, callable, knock_out, inflation, rpa) | 9 save + 9 get | routed in rates_instrument_handler via trade_type_code |
Every family with a legacy generic table shares the same underlying bug pattern: writes increasingly flow to per-type tables (via the importer or per-type save requests), while reads still go through the legacy generic service, which points at an empty-or-stale table. The UI shows blanks or stale data silently.
Target API shape (applies to every family)
The shape settled on for FX is the target for every family:
- Per-type save:
save_<family>_<type>_instrument_request→ typed-family handler → per-type service → per-type table. (Already implemented for FX and Equity; missing for Bond/Credit/Commodity/ Composite/Scripted where only generic save exists.) - Per-type get:
get_<family>_<type>_instrumentmethods on the per-type services. (Added for FX in 5f64df94d; to be added for the others as each family migrates.) - Discriminated export variant:
<family>_export_resultis a union over that family's per-type instruments, symmetric withswap_export_result. The top-levelinstrument_export_resultvariant carries<family>_export_result, not the legacy generic type. - Single wire request fanning out server-side: the client sends
get_instrument_for_trade_requestwithproduct_type+trade_type_code; the server routes to the right per-type service. (After Phase 5 of this PR, bundles carry the instrument inline and the standalone request is deleted entirely.)
PR sequencing for the migration series
- PR #1 (this branch,
feature/fx-forward-import-e2e-tests): Delete legacy FX generic infrastructure; add per-type FX get methods; establish target client API (bundle-returningget_trades_request,setInstrument, no standalone instrument fetch).FxInstrumentFormscoped toFxForwardonly. - PR #2 — Equity: same shape as FX. Nine per-type get methods,
equity_export_resultvariant, deleteequity_instruments_tbland the generic domain/service/repository/mapper/handler. Update the three-to-four equity forms. Largest follow-up, same pattern. - PR #3 — Bond / Credit / Commodity / Composite / Scripted: these
five families have no per-type infrastructure today, so the
decision is whether to (a) stand up per-type infrastructure to
match FX/Equity/Swap, or (b) keep them single-type but rename
save_<family>_instrument_request→save_<family>_<type>_requestso the shape is uniform. Default is (b) for the single-type families — deferring (a) until a real second sub-type appears. Deletes each legacy generic table and domain. - PR #4 — Retire the standalone instrument fetch path: remove
get_instrument_for_trade_requestentirely (already deleted inside this branch in Phase 5), clean upinstrument_handlerand registrar entries across all families, and converge onget_trades_requestas the sole read path. - Remaining client forms: the other six FX variants
(
fx_accumulator,fx_asian_forward,fx_barrier_option,fx_digital_option,fx_vanilla_option,fx_variance_swap) need a form story — per-variant forms or a variant-dispatching shell. Decide in PR #2 or later, not here.
Cross-PR invariants
get_naming, neverfind_: enforced by the codegen template update in 5f64df94d. Any new service generated picks up the convention automatically.- No backwards-compatibility shims between PRs: each PR deletes the legacy path it replaces in the same commit. Intermediate states are never shipped.
- Subtree resolution lives in the database: the single-node
filter on
get_trades_request(Phase 1 above) is family-agnostic; later PRs inherit it for free. If a second family needs the subtree-expansion SQL, factor it into a reusable helper at that point — not now.