ores.trading.trade
Table of Contents
- 1. Flags
- 2. Columns
- 2.1. id
- 2.2. party_id
- 2.3. external_id
- 2.4. book_id
- 2.5. portfolio_id
- 2.6. successor_trade_id
- 2.7. trade_type
- 2.8. counterparty_id
- 2.9. product_type
- 2.10. instrument_id
- 2.11. asset_class
- 2.12. netting_set_id
- 2.13. activity_type_code
- 2.14. status_id
- 2.15. trade_date
- 2.16. execution_timestamp
- 2.17. effective_date
- 2.18. termination_date
- 3. Foreign keys
- 4. Insert trigger
- 5. SQL
- 6. C++
- 7. See also
Temporal trade record. Each lifecycle event (New, Amendment, Novation, etc.) creates a new temporal row for the same trade id. The internal party is derived from book_id via books.party_id.
Composed of five sub-structs to keep each reflected struct small and avoid MSVC C1202 (recursive template dependency context too complex) in rfl's O(n²) field-uniqueness check. The JSON wire format is nested: {"identity":{…}, "parties":{…}, …}.
1. Flags
2. Columns
2.1. id
UUID uniquely identifying this trade.
Surrogate key for the trade record.
2.2. party_id
Internal party owning this trade (denormalized from book_id).
ctx.generate_uuid()
2.3. external_id
Optional external trade identifier.
e.g., UTI prefix or legacy system ID.
std::string()
2.4. book_id
Book that owns this trade.
Soft FK to ores_refdata_books_tbl.
ctx.generate_uuid()
2.5. portfolio_id
Portfolio this trade belongs to.
Soft FK to ores_refdata_portfolios_tbl.
ctx.generate_uuid()
2.6. successor_trade_id
UUID of the trade that replaced this one (e.g., after novation).
Self-referencing soft FK. Absent for active trades.
std::nullopt
2.7. trade_type
ORE instrument type code (e.g. Swap, FxForward, CapFloor).
Soft FK to ores_trading_trade_types_tbl.
std::string("Swap")
2.8. counterparty_id
Optional counterparty (soft FK to ores_refdata_counterparties_tbl).
std::nullopt
2.9. product_type
ORE product type discriminator.
The column is text in the database and an enumeration in the domain struct, so the generated sample states the enumerator: the generator builds the struct, not the row.
domain::product_type::swap
2.10. instrument_id
Optional instrument record for this trade.
std::nullopt
2.11. asset_class
Asset class code for the trade.
std::nullopt
2.12. netting_set_id
Netting set identifier for ORE aggregation.
Groups trades under the same netting agreement.
std::string("NS-001")
2.13. activity_type_code
Activity type code (soft FK to ores_trading_activity_types_tbl).
std::string("new_booking")
2.14. status_id
Current FSM state (soft FK to ores_dq_fsm_states_tbl).
The generated sample leaves it nil. A trade's status is the target of a guarded transition, so the insert trigger resolves it from the activity; a random uuid names no seeded state and is rejected before the trigger ever reaches the transition that would have set it.
boost::uuids::uuid{}
2.15. trade_date
Date the trade was agreed.
ISO 8601 date: YYYY-MM-DD.
std::string("2025-01-15")
2.16. execution_timestamp
Timestamp when the trade was executed (with timezone).
ISO 8601 timestamp: YYYY-MM-DD HH:MM:SS+TZ.
std::string("2025-01-15 10:00:00")
2.17. effective_date
Date from which the trade is effective.
ISO 8601 date: YYYY-MM-DD.
std::string("2025-01-16")
2.18. termination_date
Date on which the trade matures or terminates.
ISO 8601 date: YYYY-MM-DD.
std::string("2026-01-15")
3. Foreign keys
The soft references a trade carries.
3.1. book_id
The book a trade is booked into. skip_check suppresses the inline
check, because the SQL section's "Party id from book id" already
validates the book on its way to deriving the party from it. The entry
is still needed: it is what tells the eventing test to seed a book
before it writes a trade.
3.2. portfolio_id
Declared for the same reason, and resolved in the same place: the derivation above sets it from the book's parent portfolio, so whatever the caller sent is replaced before any check could run.
3.3. successor_trade_id
The trade this one was replaced by, on a cancel and rebook.
3.4. counterparty_id
Absent on an internal trade, which faces no counterparty.
3.5. status_id
The state the trade occupies in its lifecycle machine. skip_check
because the status is not the caller's to supply: the transition
resolution below assigns it from the transition's target state, which
is a seeded state by construction. Checking the caller's value first
would reject every write before the trigger could correct it.
4. Insert trigger
4.1. Validations
Each code column is resolved against its catalogue, so a trade cannot
be booked against a type or class that does not exist. asset_class is
optional, and the generated guard skips the call when it is null.
| column | validation_function |
|---|---|
| asset_class | ores_refdata_validate_asset_class_fn |
| trade_type | ores_trading_validate_trade_type_fn |
| activity_type_code | ores_trading_validate_activity_type_fn |
5. SQL
5.1. Flags
5.2. Party id from book id
A trade states its book, and the book already knows which party owns it
and which portfolio contains it. Neither is therefore the caller's to
supply: the insert trigger derives party_id and portfolio_id from
the book and ignores whatever was sent for them.
Both are derived the same way because both are determined the same way. A value the book fully decides cannot disagree with the book without the caller being wrong, so asking a caller to send it can only introduce an error that the derivation removes.
5.3. Status transition
amend_activity_code names the activity a rewrite of an existing trade
carries. A trade cannot be rewritten under new_booking: that activity
names a transition which starts the machine, and the row already has a
state. The generated round-trip test uses this to amend the row it has
just booked.
A trade's status is not a value the caller supplies: it is the target of a
guarded transition. The activity names the event, reference data maps the
event to a transition in the trade_status machine, and the transition
states which status it may be taken from. An activity that maps to no
transition versions the trade and leaves it where it was, which is why a
fixing on a live trade stays live.
The guard lives in the insert trigger rather than in a service because every path that writes a version passes through it, and because the version select above locks the superseding row: reading the prior status under that lock is what stops two concurrent amendments from both believing their transition is legal.
v_transition record; v_prior_status_id uuid;
Both branches resolve the transition through
ores_trading_resolve_trade_transition_fn, which does the lookup and
refuses an activity naming a transition that no longer exists. What
differs between them is the guard, and that is what stays here.
The booking case. The machine has not started, so the transition must be
one that starts it: a transition stating a from_state_id is an event
that presumes a history this trade does not have.
v_transition := ores_trading_resolve_trade_transition_fn(NEW.activity_type_code); if v_transition.has_transition then if v_transition.from_state_id is not null then raise exception 'Activity % cannot book a trade: transition % leaves state %, but a new trade has no state to leave.', NEW.activity_type_code, v_transition.transition_name, v_transition.from_state_id using errcode = '23514'; end if; NEW.status_id = v_transition.to_state_id; end if;
The amendment case. The prior row is locked by the version select above, so the status read here cannot race another writer.
select status_id into v_prior_status_id from "ores_trading_trades_tbl" where tenant_id = NEW.tenant_id and id = NEW.id and valid_to = ores_utility_infinity_timestamp_fn(); v_transition := ores_trading_resolve_trade_transition_fn(NEW.activity_type_code); if not v_transition.has_transition then NEW.status_id = v_prior_status_id; else if v_transition.from_state_id is null then raise exception 'Activity % can only book a trade: transition % starts the machine, but this trade is already at %.', NEW.activity_type_code, v_transition.transition_name, v_prior_status_id using errcode = '23514'; end if; if v_prior_status_id is distinct from v_transition.from_state_id then raise exception 'Activity % is not legal here: transition % must be taken from %, but the trade is at %.', NEW.activity_type_code, v_transition.transition_name, v_transition.from_state_id, v_prior_status_id using errcode = '23514'; end if; NEW.status_id = v_transition.to_state_id; end if;
5.4. Indexes
The six lookups the trade blotter and the risk feeds issue against the
current slice. Each is partial on valid_to, so the index covers the
live row only; trades_instrument_idx and trades_asset_class_idx add
a not-null predicate because both columns are optional and a null entry
would never be sought.
The name column carries the stem: the template renders it as
<index_name_prefix>_<name>_idx, so book becomes trades_book_idx.
| name | columns | unique | current_only | where_extra |
|---|---|---|---|---|
| book | tenant_id, book_id | false | true | |
| portfolio | tenant_id, portfolio_id | false | true | |
| netting_set | tenant_id, netting_set_id | false | true | |
| trade_type | tenant_id, trade_type | false | true | |
| instrument | tenant_id, product_type, instrument_id | false | true | instrument_id is not null |
| asset_class | tenant_id, asset_class | false | true | asset_class is not null |
6. C++
6.1. Flags
6.2. Repository methods
A trade's node-scoped listing is not a column filter: the node may be a book, a portfolio or a business unit, so the database resolves it to a book-id set first and the read runs against that set. An empty node covers the whole tenant, which is why the unfiltered listing needs no separate call.
std::vector<domain::trade> read_latest_for_node_id(context ctx, std::uint32_t offset, std::uint32_t limit, const std::string& node_id); std::uint32_t count_latest_for_node_id(context ctx, const std::string& node_id);
namespace { using context = ores::database::context; std::vector<std::string> fetch_book_ids(context ctx, const std::string& fn, const std::string& tid, const std::string& id, logging::logger_t& lg, const std::string& desc) { const std::string sql = "SELECT id::text FROM " + fn + "($1::uuid, $2::uuid) AS t(id)"; return execute_parameterized_string_query(ctx, sql, {tid, id}, lg, desc); } std::vector<domain::trade> read_trades_for_books(context ctx, const std::vector<std::string>& book_ids, const std::string& tid, const std::string& wid, std::uint32_t offset, std::uint32_t limit, logging::logger_t& lg) { const auto max = make_timestamp(MAX_TIMESTAMP, lg).value(); std::vector<domain::trade> result; for (const auto& bid : book_ids) { const auto query = sqlgen::read<std::vector<trade_entity>> | where("tenant_id"_c == tid && "workspace_id"_c == wid && "valid_to"_c == max && "book_id"_c == bid) | order_by("id"_c); auto batch = execute_read_query<trade_entity, domain::trade>( ctx, query, [](const auto& entities) { return trade_mapper::map(entities); }, lg, "Reading trades for book"); result.insert(result.end(), std::make_move_iterator(batch.begin()), std::make_move_iterator(batch.end())); } std::ranges::sort(result, {}, [](const auto& t) { return t.identity.id; }); if (offset >= result.size()) return {}; const auto end = std::min(static_cast<std::size_t>(offset + limit), result.size()); return std::vector<domain::trade>(result.begin() + offset, result.begin() + end); } std::uint32_t count_trades_for_books(context ctx, const std::vector<std::string>& book_ids, const std::string& tid, const std::string& wid, logging::logger_t& lg) { const auto max = make_timestamp(MAX_TIMESTAMP, lg).value(); struct count_result { long long count; }; std::uint32_t total = 0; for (const auto& bid : book_ids) { const auto query = sqlgen::select_from<trade_entity>(sqlgen::count().as<"count">()) | where("tenant_id"_c == tid && "workspace_id"_c == wid && "valid_to"_c == max && "book_id"_c == bid) | sqlgen::to<count_result>; const auto r = sqlgen::session(ctx.connection_pool()).and_then(query); ensure_success(r, lg); total += static_cast<std::uint32_t>(r->count); } return total; } } std::vector<domain::trade> trade_repository::read_latest_for_node_id(context ctx, std::uint32_t offset, std::uint32_t limit, const std::string& node_id) { if (node_id.empty()) return read_latest(ctx, offset, limit); const auto tid = ctx.tenant_id().to_string(); const auto wid = ctx.workspace_id(); const auto& nid = node_id; BOOST_LOG_SEV(lg(), debug) << "Reading trades for node: " << nid; const auto book_ids = fetch_book_ids(ctx, "ores_trading_get_book_ids_for_node_fn", tid, nid, lg(), "Fetching book IDs for node subtree"); return read_trades_for_books(ctx, book_ids, tid, wid, offset, limit, lg()); } std::uint32_t trade_repository::count_latest_for_node_id(context ctx, const std::string& node_id) { if (node_id.empty()) return get_total_trade_count(ctx); const auto tid = ctx.tenant_id().to_string(); const auto wid = ctx.workspace_id(); const auto& nid = node_id; BOOST_LOG_SEV(lg(), debug) << "Counting trades for node: " << nid; const auto book_ids = fetch_book_ids(ctx, "ores_trading_get_book_ids_for_node_fn", tid, nid, lg(), "Fetching book IDs for node subtree"); return count_trades_for_books(ctx, book_ids, tid, wid, lg()); }
6.3. Handler includes
The instrument services the export path resolves through, the storage and msgpack headers the storage export needs, and the FSM protocol the save path reads its transitions from.
#include "ores.dq.api/messaging/fsm_protocol.hpp" #include "ores.nats/domain/headers.hpp" #include "ores.nats/domain/wire_codec.hpp" #include "ores.storage/net/storage_transfer.hpp" #include "ores.trading.api/domain/instrument.hpp" #include "ores.trading.core/export.hpp" #include "ores.trading.core/service/activity_type_service.hpp" #include "ores.trading.core/service/balance_guaranteed_swap_instrument_service.hpp" #include "ores.trading.core/service/bond_instrument_reader.hpp" #include "ores.trading.core/service/callable_swap_instrument_service.hpp" #include "ores.trading.core/service/cap_floor_instrument_service.hpp" #include "ores.trading.core/service/commodity_instrument_service.hpp" #include "ores.trading.core/service/composite_instrument_service.hpp" #include "ores.trading.core/service/credit_instrument_service.hpp" #include "ores.trading.core/service/equity_accumulator_instrument_service.hpp" #include "ores.trading.core/service/equity_asian_option_instrument_service.hpp" #include "ores.trading.core/service/equity_barrier_option_instrument_service.hpp" #include "ores.trading.core/service/equity_digital_option_instrument_service.hpp" #include "ores.trading.core/service/equity_forward_instrument_service.hpp" #include "ores.trading.core/service/equity_option_instrument_service.hpp" #include "ores.trading.core/service/equity_position_instrument_service.hpp" #include "ores.trading.core/service/equity_swap_instrument_service.hpp" #include "ores.trading.core/service/equity_variance_swap_instrument_service.hpp" #include "ores.trading.core/service/fra_instrument_service.hpp" #include "ores.trading.core/service/fx_accumulator_instrument_service.hpp" #include "ores.trading.core/service/fx_asian_forward_instrument_service.hpp" #include "ores.trading.core/service/fx_barrier_option_instrument_service.hpp" #include "ores.trading.core/service/fx_digital_option_instrument_service.hpp" #include "ores.trading.core/service/fx_forward_instrument_service.hpp" #include "ores.trading.core/service/fx_vanilla_option_instrument_service.hpp" #include "ores.trading.core/service/fx_variance_swap_instrument_service.hpp" #include "ores.trading.core/service/inflation_swap_instrument_service.hpp" #include "ores.trading.core/service/knock_out_swap_instrument_service.hpp" #include "ores.trading.core/service/rpa_instrument_service.hpp" #include "ores.trading.core/service/scripted_instrument_service.hpp" #include "ores.trading.core/service/swaption_instrument_service.hpp" #include "ores.trading.core/service/trade_envelope_reader.hpp" #include "ores.trading.core/service/vanilla_swap_instrument_service.hpp" #include "ores.utility/uuid/tenant_id.hpp" #include <boost/uuid/string_generator.hpp> #include <chrono> #include <rfl/msgpack.hpp> #include <unordered_map>
6.4. Handler private members
populate_instruments_for_trades resolves a page of trades to their
instrument payloads in two phases: bucket the instrument ids by product
type, then read each product's table in one batch.
template <typename Ctx> static void populate_instruments_for_trades(const Ctx& ctx, std::vector<trade_export_item>& items) { using ores::trading::domain::product_type; using ores::trading::domain::trade_instrument; using ores::trading::domain::swap_instrument_data; using ores::trading::domain::composite_instrument_data; // Phase 1: bucket instrument IDs by (product_type, trade_type) std::vector<std::string> bond_ids, credit_ids, commodity_ids, scripted_ids, composite_ids, fra_ids, vswap_ids, capfloor_ids, swaption_ids, bgs_ids, callable_ids, koswap_ids, infl_ids, rpa_ids, fxfwd_ids, fxopt_ids, fxbar_ids, fxdig_ids, fxasn_ids, fxacc_ids, fxvar_ids, eq_opt_ids, eq_fwd_ids, eq_swp_ids, eq_var_ids, eq_bar_ids, eq_asn_ids, eq_dig_ids, eq_acc_ids, eq_pos_ids; for (const auto& item : items) { const auto& t = item.trade; if (!t.classification.instrument_id || t.classification.product_type == product_type::unknown) continue; const auto id = boost::uuids::to_string(*t.classification.instrument_id); const auto& ttc = t.classification.trade_type; switch (t.classification.product_type) { case product_type::bond: bond_ids.push_back(id); break; case product_type::credit: credit_ids.push_back(id); break; case product_type::commodity: commodity_ids.push_back(id); break; case product_type::scripted: scripted_ids.push_back(id); break; case product_type::composite: composite_ids.push_back(id); break; case product_type::swap: if (ttc == "ForwardRateAgreement") fra_ids.push_back(id); else if (ttc == "Swap" || ttc == "CrossCurrencySwap" || ttc == "FlexiSwap") vswap_ids.push_back(id); else if (ttc == "CapFloor") capfloor_ids.push_back(id); else if (ttc == "Swaption") swaption_ids.push_back(id); else if (ttc == "BalanceGuaranteedSwap") bgs_ids.push_back(id); else if (ttc == "CallableSwap") callable_ids.push_back(id); else if (ttc == "KnockOutSwap") koswap_ids.push_back(id); else if (ttc == "InflationSwap") infl_ids.push_back(id); else if (ttc == "RiskParticipationAgreement") rpa_ids.push_back(id); break; case product_type::fx: if (ttc == "FxForward" || ttc == "FxSwap") fxfwd_ids.push_back(id); else if (ttc == "FxOption") fxopt_ids.push_back(id); else if (ttc == "FxBarrierOption" || ttc == "FxGenericBarrierOption" || ttc == "FxDoubleBarrierOption" || ttc == "FxEuropeanBarrierOption" || ttc == "FxKIKOBarrierOption") fxbar_ids.push_back(id); else if (ttc == "FxDigitalOption" || ttc == "FxDigitalBarrierOption" || ttc == "FxTouchOption" || ttc == "FxDoubleTouchOption") fxdig_ids.push_back(id); else if (ttc == "FxAverageForward" || ttc == "FxTaRF") fxasn_ids.push_back(id); else if (ttc == "FxAccumulator") fxacc_ids.push_back(id); else if (ttc == "FxVarianceSwap") fxvar_ids.push_back(id); break; case product_type::equity: if (ttc == "EquityOption" || ttc == "EquityCliquetOption" || ttc == "EquityOutperformanceOption") eq_opt_ids.push_back(id); else if (ttc == "EquityForward") eq_fwd_ids.push_back(id); else if (ttc == "EquitySwap" || ttc == "EquityWorstOfBasketSwap") eq_swp_ids.push_back(id); else if (ttc == "EquityVarianceSwap") eq_var_ids.push_back(id); else if (ttc == "EquityBarrierOption" || ttc == "EquityDoubleBarrierOption" || ttc == "EquityEuropeanBarrierOption") eq_bar_ids.push_back(id); else if (ttc == "EquityAsianOption") eq_asn_ids.push_back(id); else if (ttc == "EquityDigitalOption" || ttc == "EquityTouchOption") eq_dig_ids.push_back(id); else if (ttc == "EquityAccumulator" || ttc == "EquityTaRF") eq_acc_ids.push_back(id); else if (ttc == "EquityPosition") eq_pos_ids.push_back(id); break; case product_type::unknown: break; } } // Phase 2: batch-fetch legs (one call covers all swap types) std::unordered_map<std::string, std::vector<ores::trading::domain::swap_leg>> legs_map; { std::vector<std::string> all_swap; for (auto* v : {&fra_ids, &vswap_ids, &capfloor_ids, &swaption_ids, &bgs_ids, &callable_ids, &koswap_ids, &infl_ids, &rpa_ids}) all_swap.insert(all_swap.end(), v->begin(), v->end()); if (!all_swap.empty()) { service::fra_instrument_service fra_svc(ctx); for (auto& leg : fra_svc.get_swap_legs_batch(all_swap)) legs_map[boost::uuids::to_string(leg.identity.instrument_id)].push_back( std::move(leg)); } } std::unordered_map<std::string, std::vector<ores::trading::domain::composite_leg>> comp_legs_map; if (!composite_ids.empty()) { service::composite_instrument_service comp_svc(ctx); for (auto& leg : comp_svc.get_legs_batch(composite_ids)) comp_legs_map[boost::uuids::to_string(leg.identity.instrument_id)].push_back( std::move(leg)); } // Phase 3: batch-fetch instruments, build lookup map std::unordered_map<std::string, trade_instrument> imap; auto take_legs = [&](const std::string& id) { auto it = legs_map.find(id); return it != legs_map.end() ? std::move(it->second) : std::vector<ores::trading::domain::swap_leg>{}; }; // Single-table types (credit, commodity, scripted). auto add_flat = [&](auto&& results) { for (auto& v : results) imap[boost::uuids::to_string(v.identity.instrument_id)] = std::move(v); }; if (!bond_ids.empty()) { service::bond_instrument_reader reader(ctx); for (auto& [id, data] : reader.read_instruments(bond_ids)) imap[id] = std::move(data); } if (!credit_ids.empty()) { service::credit_instrument_service svc(ctx); add_flat(svc.get_credit_instruments(credit_ids)); } if (!commodity_ids.empty()) { service::commodity_instrument_service svc(ctx); add_flat(svc.get_commodity_instruments(commodity_ids)); } if (!scripted_ids.empty()) { service::scripted_instrument_service svc(ctx); add_flat(svc.get_scripted_instruments(scripted_ids)); } if (!composite_ids.empty()) { service::composite_instrument_service svc(ctx); for (auto& v : svc.get_composite_instruments(composite_ids)) { const auto id = boost::uuids::to_string(v.identity.instrument_id); composite_instrument_data data; data.instrument = std::move(v); auto it = comp_legs_map.find(id); if (it != comp_legs_map.end()) data.legs = std::move(it->second); imap[id] = std::move(data); } } // Rates / swap types (9 sub-types, all share swap_legs table) auto add_swap = [&](auto&& results) { for (auto& v : results) { const auto id = boost::uuids::to_string(v.identity.instrument_id); swap_instrument_data data; data.instrument = std::move(v); data.legs = take_legs(id); imap[id] = std::move(data); } }; if (!fra_ids.empty()) { service::fra_instrument_service svc(ctx); add_swap(svc.get_fra_instruments(fra_ids)); } if (!vswap_ids.empty()) { service::vanilla_swap_instrument_service svc(ctx); add_swap(svc.get_vanilla_swap_instruments(vswap_ids)); } if (!capfloor_ids.empty()) { service::cap_floor_instrument_service svc(ctx); add_swap(svc.get_cap_floor_instruments(capfloor_ids)); } if (!swaption_ids.empty()) { service::swaption_instrument_service svc(ctx); add_swap(svc.get_swaption_instruments(swaption_ids)); } if (!bgs_ids.empty()) { service::balance_guaranteed_swap_instrument_service svc(ctx); add_swap(svc.get_balance_guaranteed_swap_instruments(bgs_ids)); } if (!callable_ids.empty()) { service::callable_swap_instrument_service svc(ctx); add_swap(svc.get_callable_swap_instruments(callable_ids)); } if (!koswap_ids.empty()) { service::knock_out_swap_instrument_service svc(ctx); add_swap(svc.get_knock_out_swap_instruments(koswap_ids)); } if (!infl_ids.empty()) { service::inflation_swap_instrument_service svc(ctx); add_swap(svc.get_inflation_swap_instruments(infl_ids)); } if (!rpa_ids.empty()) { service::rpa_instrument_service svc(ctx); add_swap(svc.get_rpa_instruments(rpa_ids)); } // FX types auto add_fx = [&](auto&& results) { for (auto& v : results) imap[boost::uuids::to_string(v.identity.instrument_id)] = ores::trading::domain::fx_instrument_variant{std::move(v)}; }; if (!fxfwd_ids.empty()) { service::fx_forward_instrument_service svc(ctx); add_fx(svc.get_fx_forward_instruments(fxfwd_ids)); } if (!fxopt_ids.empty()) { service::fx_vanilla_option_instrument_service svc(ctx); add_fx(svc.get_fx_vanilla_option_instruments(fxopt_ids)); } if (!fxbar_ids.empty()) { service::fx_barrier_option_instrument_service svc(ctx); add_fx(svc.get_fx_barrier_option_instruments(fxbar_ids)); } if (!fxdig_ids.empty()) { service::fx_digital_option_instrument_service svc(ctx); add_fx(svc.get_fx_digital_option_instruments(fxdig_ids)); } if (!fxasn_ids.empty()) { service::fx_asian_forward_instrument_service svc(ctx); add_fx(svc.get_fx_asian_forward_instruments(fxasn_ids)); } if (!fxacc_ids.empty()) { service::fx_accumulator_instrument_service svc(ctx); add_fx(svc.get_fx_accumulator_instruments(fxacc_ids)); } if (!fxvar_ids.empty()) { service::fx_variance_swap_instrument_service svc(ctx); add_fx(svc.get_fx_variance_swap_instruments(fxvar_ids)); } // Equity types auto add_eq = [&](auto&& results) { for (auto& v : results) imap[boost::uuids::to_string(v.identity.instrument_id)] = ores::trading::domain::equity_instrument_variant{std::move(v)}; }; if (!eq_opt_ids.empty()) { service::equity_option_instrument_service svc(ctx); add_eq(svc.get_equity_option_instruments(eq_opt_ids)); } if (!eq_fwd_ids.empty()) { service::equity_forward_instrument_service svc(ctx); add_eq(svc.get_equity_forward_instruments(eq_fwd_ids)); } if (!eq_swp_ids.empty()) { service::equity_swap_instrument_service svc(ctx); add_eq(svc.get_equity_swap_instruments(eq_swp_ids)); } if (!eq_var_ids.empty()) { service::equity_variance_swap_instrument_service svc(ctx); add_eq(svc.get_equity_variance_swap_instruments(eq_var_ids)); } if (!eq_bar_ids.empty()) { service::equity_barrier_option_instrument_service svc(ctx); add_eq(svc.get_equity_barrier_option_instruments(eq_bar_ids)); } if (!eq_asn_ids.empty()) { service::equity_asian_option_instrument_service svc(ctx); add_eq(svc.get_equity_asian_option_instruments(eq_asn_ids)); } if (!eq_dig_ids.empty()) { service::equity_digital_option_instrument_service svc(ctx); add_eq(svc.get_equity_digital_option_instruments(eq_dig_ids)); } if (!eq_acc_ids.empty()) { service::equity_accumulator_instrument_service svc(ctx); add_eq(svc.get_equity_accumulator_instruments(eq_acc_ids)); } if (!eq_pos_ids.empty()) { service::equity_position_instrument_service svc(ctx); add_eq(svc.get_equity_position_instruments(eq_pos_ids)); } // Phase 4: fill items from lookup map (copy — multiple items may share an instrument) for (auto& item : items) { const auto& t = item.trade; if (!t.classification.instrument_id || t.classification.product_type == product_type::unknown) continue; const auto id = boost::uuids::to_string(*t.classification.instrument_id); if (auto it = imap.find(id); it != imap.end()) item.instrument = encode_instrument(it->second); } // Phase 5: fill the trade-level envelope, which is keyed by the // trade rather than the instrument and so crosses product types. std::vector<std::string> trade_ids; trade_ids.reserve(items.size()); for (const auto& item : items) trade_ids.push_back(boost::uuids::to_string(item.trade.identity.id)); service::trade_envelope_reader envelope_reader(ctx); auto envelopes = envelope_reader.read_envelopes(trade_ids); for (auto& item : items) { const auto id = boost::uuids::to_string(item.trade.identity.id); if (auto it = envelopes.find(id); it != envelopes.end()) item.envelope = std::move(it->second); } } /** * @brief Fetches all FSM transitions from the DQ service via NATS. * * Results are cached process-wide for 5 minutes; FSM transitions are * system-level reference data that change only on schema migrations. * The cache avoids an extra NATS round-trip per trade when the ore import * handler sends one save_trade message per trade. * Throws std::runtime_error on failure; callers must handle or propagate. */ // Extract the raw JWT from an incoming message, preferring the delegated // header so the original end-user context propagates to downstream calls. static std::string extract_bearer(const ores::nats::message& msg) { using namespace ores::nats::headers; for (auto hdr : {delegated_authorization, authorization}) { const auto it = msg.headers.find(std::string(hdr)); if (it != msg.headers.end() && it->second.starts_with(bearer_prefix)) return std::string(it->second.substr(bearer_prefix.size())); } return {}; } std::string http_base_url_;
6.5. Handler methods
The subjects the generated set does not cover: the activity type catalogue, a trade's history, its instrument, and the two portfolio export paths.
void list_activity_types(ores::nats::message msg) { BOOST_LOG_SEV(trade_handler_lg(), debug) << "Handling " << msg.subject; auto req_ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_); if (!req_ctx_expected) { error_reply(nats_, msg, req_ctx_expected.error()); return; } const auto& req_ctx = *req_ctx_expected; // Activity types are system-level configuration; look them up under // the system tenant so all tenants see the same standard set. const auto sys_ctx = req_ctx.with_tenant(ores::utility::uuid::tenant_id::system(), req_ctx.actor()); service::activity_type_service svc(sys_ctx); get_activity_types_response resp; try { resp.activity_types = svc.list_types(); } catch (...) { } BOOST_LOG_SEV(trade_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, resp); } void instrument(ores::nats::message msg) { BOOST_LOG_SEV(trade_handler_lg(), debug) << "Handling " << msg.subject; auto ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_); if (!ctx_expected) { error_reply(nats_, msg, ctx_expected.error()); return; } const auto& ctx = *ctx_expected; service::trade_service svc(ctx); get_trade_instrument_response resp; try { if (auto req = decode<get_trade_instrument_request>(msg)) { auto trade_opt = svc.get_trade(req->trade_id); if (!trade_opt) { resp.success = false; resp.message = "Trade not found: " + req->trade_id; } else { std::vector<trade_export_item> items{{.trade = std::move(*trade_opt)}}; populate_instruments_for_trades(ctx, items); resp.trade = std::move(items[0].trade); resp.instrument = decode_instrument(items[0].instrument); resp.success = true; } } } catch (const std::exception& e) { BOOST_LOG_SEV(trade_handler_lg(), error) << msg.subject << " failed: " << e.what(); resp.success = false; resp.message = e.what(); } BOOST_LOG_SEV(trade_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, resp); } void export_portfolio(ores::nats::message msg) { BOOST_LOG_SEV(trade_handler_lg(), debug) << "Handling " << msg.subject; auto ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_); if (!ctx_expected) { error_reply(nats_, msg, ctx_expected.error()); return; } const auto& ctx = *ctx_expected; export_portfolio_response resp; try { if (auto req = decode<export_portfolio_request>(msg)) { service::trade_service svc(ctx); const auto offset = static_cast<std::uint32_t>(req->offset); const auto limit = static_cast<std::uint32_t>(req->limit); auto trades = svc.list_trades(offset, limit, req->node_id); resp.items.reserve(trades.size()); for (auto& t : trades) resp.items.push_back({.trade = std::move(t)}); populate_instruments_for_trades(ctx, resp.items); resp.success = true; } } catch (const std::exception& e) { BOOST_LOG_SEV(trade_handler_lg(), error) << msg.subject << " failed: " << e.what(); resp.success = false; resp.message = e.what(); } BOOST_LOG_SEV(trade_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, resp); } void export_trades_to_storage(ores::nats::message msg) { BOOST_LOG_SEV(trade_handler_lg(), debug) << "Handling " << msg.subject; auto ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_); if (!ctx_expected) { error_reply(nats_, msg, ctx_expected.error()); return; } const auto& ctx = *ctx_expected; export_trades_to_storage_response resp; try { auto req = decode<export_trades_to_storage_request>(msg); if (!req || req->book_ids.empty()) { resp.message = "Invalid request or empty book_ids."; reply(nats_, msg, resp); return; } // Fetch trades for all requested books in pages and batch-populate instruments. service::trade_service svc(ctx); std::vector<trade_export_item> all_items; constexpr std::uint32_t page_size = 1000; for (const auto& bid : req->book_ids) { try { std::uint32_t offset = 0; while (true) { auto trades = svc.list_trades(offset, page_size, bid); const auto n = static_cast<std::uint32_t>(trades.size()); if (n == 0) break; std::vector<trade_export_item> page; page.reserve(n); for (auto& t : trades) page.push_back({.trade = std::move(t)}); populate_instruments_for_trades(ctx, page); all_items.insert(all_items.end(), std::make_move_iterator(page.begin()), std::make_move_iterator(page.end())); offset += n; if (n < page_size) break; } } catch (const std::exception& e) { BOOST_LOG_SEV(trade_handler_lg(), warn) << "export_trades_to_storage: book " << bid << " failed: " << e.what(); } } // Serialise to MsgPack and upload to storage. const auto blob = rfl::msgpack::write(all_items); ores::storage::net::storage_transfer transfer(http_base_url_); transfer.upload_blob(req->storage_bucket, req->storage_key, blob); resp.success = true; resp.trade_count = static_cast<int>(all_items.size()); resp.storage_key = req->storage_key; resp.message = "Exported " + std::to_string(all_items.size()) + " trades to storage."; BOOST_LOG_SEV(trade_handler_lg(), info) << "export_trades_to_storage: exported " << all_items.size() << " trades, " << blob.size() << " bytes (pre-compression) to " << req->storage_bucket << "/" << req->storage_key; } catch (const std::exception& e) { BOOST_LOG_SEV(trade_handler_lg(), error) << msg.subject << " failed: " << e.what(); resp.message = e.what(); } reply(nats_, msg, resp); }
6.6. Protocol includes
The headers the messages below need beyond the entity's own. The payload and envelope types belong to the export item; the activity type and the instrument variant to their own messages.
#include "ores.trading.api/domain/activity_type.hpp" #include "ores.trading.api/domain/instrument_payload.hpp" #include "ores.trading.api/domain/trade_envelope_data.hpp" #include "ores.trading.api/domain/trade_instrument.hpp" #include "ores.trading.api/messaging/instrument_protocol.hpp" #include <optional>
6.7. Protocol messages
The messages the generated set does not cover: the activity type catalogue, the instrument lookup for one trade, and the two portfolio export paths. A trade's export item carries the instrument as a payload rather than a variant, for the reflect-cpp reason its comment states.
struct get_activity_types_request { using response_type = struct get_activity_types_response; static constexpr std::string_view nats_subject = "trading.v1.activity_types.list"; }; struct get_activity_types_response { std::vector<ores::trading::domain::activity_type> activity_types; }; /** * @brief One trade plus its resolved instrument data. * * The instrument is carried as a payload rather than as a trade_instrument * variant. reflect-cpp cannot name the active alternative of that variant: * untagged, the first alternative std::monostate parses from any payload and * wins; tagged, rfl::AddTagsToVariants exceeds the fold limits on macOS and * MSVC. See instrument_payload. The payload's type is empty when the trade has * no linked instrument or the product_type is unrecognised. * * The envelope holds the trade-level data the product tables do not: the * counterparty name, the netting set id, the portfolio id labels and the * document's additional fields. It is absent when the trade has none. */ struct trade_export_item { ores::trading::domain::trade trade; ores::trading::domain::instrument_payload instrument; std::optional<ores::trading::domain::trade_envelope_data> envelope; }; struct get_trade_instrument_request { using response_type = struct get_trade_instrument_response; static constexpr std::string_view nats_subject = "trading.v1.trades.instrument"; std::string trade_id; }; struct get_trade_instrument_response { bool success = false; std::string message; ores::trading::domain::trade trade; ores::trading::domain::trade_instrument instrument; }; /** * @brief Request to export all trades (and instruments) under a taxonomy node. * * @p node_id is resolved by the server to the book-id set just like * get_trades_request; typical callers supply a portfolio id (to export the * whole portfolio subtree) or a book id (to export a single book). */ struct export_portfolio_request { using response_type = struct export_portfolio_response; static constexpr std::string_view nats_subject = "trading.v1.trades.portfolio.export"; std::string node_id; int offset = 0; int limit = 10000; }; struct export_portfolio_response { bool success = false; std::string message; std::vector<trade_export_item> items; }; /** * @brief Exports trades for the given book IDs to object storage. * * The handler resolves trade IDs via ores_trading_get_trade_ids_by_books_fn, * loads full trade_export_items, serialises to MsgPack, compresses with gzip, * and uploads to storage. Returns the storage key and trade count. * * Used by the report execution workflow to offload large trade data sets * to storage instead of passing them through NATS. */ struct export_trades_to_storage_request { using response_type = struct export_trades_to_storage_response; static constexpr std::string_view nats_subject = "trading.v1.trades.export-to-storage"; std::vector<std::string> book_ids; // Target bucket, such as "report-data". std::string storage_bucket; // Target key, such as "{instance_id}/trades.msgpack". std::string storage_key; }; struct export_trades_to_storage_response { bool success = false; std::string message; int trade_count = 0; // Echoed back from the request so the caller can confirm the target. std::string storage_key; };
6.8. Domain groups
The reflected struct is split across five field groups to keep each one
under the MSVC C1202 threshold, which rfl's field-uniqueness check trips
on a struct this wide. The split is also the wire format: the JSON is
nested as {"identity":{...},"parties":{...},...}, so the member order
below is the order the wire carries.
| member | field_group |
|---|---|
| identity | ores.trading.trade_identity |
| parties | ores.trading.trade_parties |
| classification | ores.trading.trade_classification |
| lifecycle | ores.trading.trade_lifecycle |
| audit | ores.trading.trade_audit |
6.9. Repository
6.10. Conventions
6.11. Table display
| column | header |
|---|---|
| identity.id | ID |
| lifecycle.trade_date | Trade Date |
| classification.trade_type | Type |
| audit.modified_by | Modified By |
| identity.version | Version |
6.12. Presentation
6.12.1. Detail fields
| field | label | widget | type | is_key | is_required | placeholder |
|---|---|---|---|---|---|---|
| external_id | External ID | externalIdEdit | line_edit | true | Enter external trade ID (e.g., UTI) | |
| trade_type | Trade Type | tradeTypeEdit | line_edit | true | e.g., Swap, FxForward | |
| netting_set_id | Netting Set | nettingSetIdEdit | line_edit | e.g., NS-001 | ||
| trade_date | Trade Date | tradeDateEdit | line_edit | true | YYYY-MM-DD | |
| effective_date | Effective Date | effectiveDateEdit | line_edit | true | YYYY-MM-DD | |
| termination_date | Termination Date | terminationDateEdit | line_edit | true | YYYY-MM-DD | |
| execution_timestamp | Execution Timestamp | executionTimestampEdit | line_edit | YYYY-MM-DD HH:MM:SS |
6.12.2. Columns
| enum_name | field | header | type | width |
|---|---|---|---|---|
| ExternalId | external_id | External ID | string | 180 |
| TradeType | trade_type | Type | string | 120 |
| TradeDate | trade_date | Trade Date | string | 110 |
| EffectiveDate | effective_date | Effective | string | 110 |
| TerminationDate | termination_date | Termination | string | 110 |
| NettingSetId | netting_set_id | Netting Set | string | 120 |
| Version | version | Version | int | 80 |
| ModifiedBy | modified_by | Modified By | string | 120 |
| RecordedAt | recorded_at | Recorded At | timestamp | 150 |
6.13. Custom repository methods
7. See also
- ores.trading — component group overview.