ores.refdata.currency_pair_convention
Table of Contents
- Flags
- Columns
- SQL
- Validation function
- Foreign keys
- Insert trigger
- C++
- Flags
- Repository
- Domain includes
- Entity includes
- Conventions
- Table display
- Qt
- Custom repository methods
- Calendar assignment: custom service constructor init
- Calendar assignment: custom service includes
- Calendar assignment: custom service methods
- Calendar assignment: custom service members
- Calendar assignment: custom service implementations
- Calendar assignment: custom protocol includes
- Calendar assignment: custom protocol messages
- Calendar assignment: custom handler methods
- Calendar assignment: custom registrar subscriptions
- Calendars tab: detail dialog header includes
- Calendars tab: detail dialog private declarations
- Calendars tab: constructor wiring
- Calendars tab: setupConnections wiring
- Calendars tab: implementation
- Qt: pair_code stays visually normal while locked
- Qt: reject duplicate pair_code on create
- Calendars tab: a single change-reason prompt for both writes
- Calendars tab: commit calendar changes after a successful convention save
- Calendars tab: keep read-only state in sync while browsing history
- See also
Quoting and date-convention fields for a currency pair — pip factor,
tick size, calendars, business day convention, spot-relative/end-of-
month flags — folded in from the retired fx_convention entity (see
Currency pair support in reference data). Keyed 1:1 by
pair_code, the same value space as
ores.refdata.currency_pair's own primary key — every pair has at
most one convention record, so a separate identifier scheme would be
pure overhead. Like every other soft-FK relationship in this codebase,
pair_code is validated via trigger, not a hard DB foreign key.
Flags
Columns
pair_code
Same value as the owning currency_pair.pair_code (e.g. "EUR/USD") —
a 1:1 extension key, not an independent identifier.
std::string(faker::finance::currencyCode()) + "/" + std::string(faker::finance::currencyCode())
pip_factor
Converts pips to absolute rate moves (0.0001 for most pairs, 0.01 for JPY crosses).
0.0001
tick_size
Minimum rate increment, in pips.
0.1
decimal_places
Decimal places for rate display.
4
business_day_convention
Soft FK to ores_trading_business_day_convention_types_tbl.
std::string("Following")
spot_relative
Whether forward dates are generated relative to the spot date.
true
end_of_month
Whether end-of-month convention applies.
false
SQL
Flags
Extra drops
drop function if exists ores_refdata_validate_currency_pair_convention_fn;
Validation function
Foreign keys
pair_code
The convention's pair_code doubles as its primary key, so this FK
entry exists for eventing-test seeding only: the test must create the
currency pair its convention references before writing (the shared
test tenant's pair validation turns strict as soon as any pair
exists). skip_check suppresses the redundant inline SQL check – the
trigger's DQ validation (see below) remains the production guard, and
seed_currency makes the seeded pair's own base_currency /
quote_currency insert pass by reusing or seeding a currency first.
Insert trigger
Validations
| column | validation_function |
|---|---|
| pair_code | ores_refdata_validate_currency_pair_fn |
| business_day_convention | ores_refdata_validate_business_day_convention_type_fn |
| change_reason_code | ores_dq_validate_change_reason_fn |
C++
Flags
Repository
Domain includes
#include <chrono> #include <optional> #include <string>
Entity includes
#include <optional> #include <string> #include "sqlgen/Timestamp.hpp"
Conventions
Table display
| column | header |
|---|---|
| pair_code | Pair |
| pip_factor | Pip Factor |
| tick_size | Tick Size |
| decimal_places | Decimal Places |
| business_day_convention | Business Day Convention |
| spot_relative | Spot Relative |
| end_of_month | End Of Month |
| modified_by | Modified By |
| version | Version |
Qt
Columns (Qt model)
| enum_name | field | header | type | width | is_badge | badge_key | formatter | formatter_include | tooltip |
|---|---|---|---|---|---|---|---|---|---|
| PairCode | pair_code | Pair | string | 100 | The currency pair these quoting and date conventions apply to. | ||||
| PipFactor | pip_factor | Pip Factor | double | 90 | Converts a pip count into an absolute rate move. | ||||
| TickSize | tick_size | Tick Size | double | 90 | Minimum rate increment allowed when quoting or trading this pair. | ||||
| DecimalPlaces | decimal_places | Decimal Places | int | 100 | Number of decimal places used when displaying or rounding rates for this pair. | ||||
| BusinessDayConvention | business_day_convention | Business Day Convention | string | 150 | true | currency_pair_convention_business_day_convention | Rule for adjusting a settlement date that falls on a non-business day. | ||
| SpotRelative | spot_relative | Spot Relative | bool | 100 | true | currency_pair_convention_spot_relative | boolYesNoLabel | ores.qt/BoolYesNoLabel.hpp | Whether forward value dates are calculated relative to the spot date. |
| EndOfMonth | end_of_month | End Of Month | bool | 100 | true | currency_pair_convention_end_of_month | boolYesNoLabel | ores.qt/BoolYesNoLabel.hpp | Whether this pair follows the end-of-month forward date rule. |
| Version | version | Version | int | 70 | |||||
| ModifiedBy | modified_by | Modified By | string | auto | |||||
| RecordedAt | recorded_at | Recorded At | timestamp | auto |
Icon columns (Qt model)
Composites base+quote flags into the PairCode column's decoration,
splitting the combined pair_code string (e.g. "EUR/USD") since this
entity has no separate base/quote fields of its own.
| column | accessor | field1 | is_pair |
|---|---|---|---|
| PairCode | currency_flag_icon_from_pair_code | pair_code | true |
Related entity shortcuts
| signal | icon | label | tooltip |
|---|---|---|---|
| Calendars | CalendarClock | Calendars | Open Calendars list |
Detail fields
| field | label | widget | type | is_key | is_required | placeholder | combo_values | badge_key | flag_source | combo_fetch | combo_fetch_include | tooltip |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pair_code | Pair Code | pairCodeCombo | flagged_combo | true | true | e.g. EUR/USD | currency_pair | fetch_currency_pair_codes | ores.qt/LookupFetcher.hpp | The currency pair these quoting and date conventions apply to (e.g. EUR/USD). | ||
| pip_factor | Pip Factor | pipFactorEdit | line_edit | true | e.g. 0.0001 | Converts a pip count into an absolute rate move (0.0001 for most pairs, 0.01 for JPY crosses). | ||||||
| tick_size | Tick Size | tickSizeEdit | line_edit | true | e.g. 0.1 | Minimum rate increment allowed when quoting or trading this pair, expressed in pips. | ||||||
| decimal_places | Decimal Places | decimalPlacesSpinBox | spin_box | true | Number of decimal places used when displaying or rounding rates for this pair. | |||||||
| business_day_convention | Business Day Convention | businessDayConventionCombo | static_combo | Following,ModifiedFollowing,Preceding,ModifiedPreceding,Unadjusted,HalfMonthModifiedFollowing,Nearest | currency_pair_convention_business_day_convention | Rule for adjusting a settlement date that falls on a non-business day (e.g. rolling forward to the next good business day). | ||||||
| spot_relative | Spot Relative | spotRelativeCheckBox | check_box | Whether forward value dates for this pair are calculated relative to the spot date, rather than directly from the trade date. | ||||||||
| end_of_month | End Of Month | endOfMonthCheckBox | check_box | Whether this pair follows the end-of-month rule: if the spot date falls on the last business day of a month, forward dates preserve month-end alignment. |
Custom repository methods
Calendar assignment: custom service constructor init
, calendar_repo_(ctx_)
Calendar assignment: custom service includes
#include "ores.refdata.core/repository/currency_pair_convention_calendar_repository.hpp"
Calendar assignment: custom service methods
/** * @brief Lists the calendars assigned to a currency pair convention for * advancing its spot/maturity dates, via the * currency_pair_convention_calendars junction. * * @param pair_code The currency pair code (e.g. EURGBP). * @return Vector of calendar assignment rows for the convention. */ std::vector<ores::refdata::domain::currency_pair_convention_calendar> list_calendars_for_pair_convention(const std::string& pair_code); /** * @brief Assigns a calendar to a currency pair convention (creates the * junction row). * * @param row The currency_pair_convention_calendar row to write; caller * is responsible for stamping tenant_id/modified_by/performed_by/ * change_reason_code before calling (see * ores::service::messaging::stamp()). */ void assign_calendar_to_pair_convention( const ores::refdata::domain::currency_pair_convention_calendar& row); /** * @brief Revokes a calendar from a currency pair convention (removes the * junction row). * * @param pair_code The currency pair code (e.g. EURGBP). * @param calendar_code The QuantLib/ORE calendar token to revoke. */ void revoke_calendar_from_pair_convention(const std::string& pair_code, const std::string& calendar_code);
Calendar assignment: custom service members
repository::currency_pair_convention_calendar_repository calendar_repo_;
Calendar assignment: custom service implementations
std::vector<ores::refdata::domain::currency_pair_convention_calendar> currency_pair_convention_service::list_calendars_for_pair_convention(const std::string& pair_code) { BOOST_LOG_SEV(lg(), debug) << "Listing calendars for pair convention: " << pair_code; return calendar_repo_.read_latest_by_pair(pair_code); } void currency_pair_convention_service::assign_calendar_to_pair_convention( const ores::refdata::domain::currency_pair_convention_calendar& row) { BOOST_LOG_SEV(lg(), debug) << "Assigning calendar to pair convention: " << row.pair_code << "/" << row.calendar_code; calendar_repo_.write(row); BOOST_LOG_SEV(lg(), info) << "Assigned calendar to pair convention: " << row.pair_code << "/" << row.calendar_code; } void currency_pair_convention_service::revoke_calendar_from_pair_convention( const std::string& pair_code, const std::string& calendar_code) { BOOST_LOG_SEV(lg(), debug) << "Revoking calendar from pair convention: " << pair_code << "/" << calendar_code; calendar_repo_.remove(pair_code, calendar_code); BOOST_LOG_SEV(lg(), info) << "Revoked calendar from pair convention: " << pair_code << "/" << calendar_code; }
Calendar assignment: custom protocol includes
#include "ores.refdata.api/domain/currency_pair_convention_calendar.hpp"
Calendar assignment: custom protocol messages
struct get_currency_pair_convention_calendars_request { using response_type = struct get_currency_pair_convention_calendars_response; static constexpr std::string_view nats_subject = "refdata.v1.currency-pair-conventions.calendars.list"; std::string pair_code; }; struct get_currency_pair_convention_calendars_response { std::vector<ores::refdata::domain::currency_pair_convention_calendar> calendars; bool success = false; std::string message; }; struct assign_currency_pair_convention_calendar_request { using response_type = struct assign_currency_pair_convention_calendar_response; static constexpr std::string_view nats_subject = "refdata.v1.currency-pair-conventions.calendars.assign"; std::string pair_code; std::string calendar_code; std::string change_reason_code; std::string change_commentary; }; struct assign_currency_pair_convention_calendar_response { bool success = false; std::string message; }; struct revoke_currency_pair_convention_calendar_request { using response_type = struct revoke_currency_pair_convention_calendar_response; static constexpr std::string_view nats_subject = "refdata.v1.currency-pair-conventions.calendars.revoke"; std::string pair_code; std::string calendar_code; }; struct revoke_currency_pair_convention_calendar_response { bool success = false; std::string message; };
Calendar assignment: custom handler methods
void list_calendars(ores::nats::message msg) { BOOST_LOG_SEV(currency_pair_convention_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; service::currency_pair_convention_service svc(req_ctx); if (auto req = decode<get_currency_pair_convention_calendars_request>(msg)) { get_currency_pair_convention_calendars_response resp; try { resp.calendars = svc.list_calendars_for_pair_convention(req->pair_code); resp.success = true; } catch (const std::exception& e) { BOOST_LOG_SEV(currency_pair_convention_handler_lg(), error) << msg.subject << " failed: " << e.what(); resp.success = false; resp.message = e.what(); } BOOST_LOG_SEV(currency_pair_convention_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, resp); } else { BOOST_LOG_SEV(currency_pair_convention_handler_lg(), warn) << "Failed to decode: " << msg.subject; error_reply(nats_, msg, ores::service::error_code::bad_request); } } void assign_calendar(ores::nats::message msg) { BOOST_LOG_SEV(currency_pair_convention_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; if (!has_permission(req_ctx, "refdata::currency_pair_conventions:write")) { error_reply(nats_, msg, ores::service::error_code::forbidden); return; } service::currency_pair_convention_service svc(req_ctx); if (auto req = decode<assign_currency_pair_convention_calendar_request>(msg)) { try { domain::currency_pair_convention_calendar row; row.pair_code = req->pair_code; row.calendar_code = req->calendar_code; row.change_reason_code = req->change_reason_code; row.change_commentary = req->change_commentary; ores::service::messaging::stamp(row, req_ctx); svc.assign_calendar_to_pair_convention(row); BOOST_LOG_SEV(currency_pair_convention_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, assign_currency_pair_convention_calendar_response{.success = true}); } catch (const std::exception& e) { BOOST_LOG_SEV(currency_pair_convention_handler_lg(), error) << msg.subject << " failed: " << e.what(); reply(nats_, msg, assign_currency_pair_convention_calendar_response{ .success = false, .message = e.what()}); } } else { BOOST_LOG_SEV(currency_pair_convention_handler_lg(), warn) << "Failed to decode: " << msg.subject; error_reply(nats_, msg, ores::service::error_code::bad_request); } } void revoke_calendar(ores::nats::message msg) { BOOST_LOG_SEV(currency_pair_convention_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; if (!has_permission(req_ctx, "refdata::currency_pair_conventions:write")) { error_reply(nats_, msg, ores::service::error_code::forbidden); return; } service::currency_pair_convention_service svc(req_ctx); if (auto req = decode<revoke_currency_pair_convention_calendar_request>(msg)) { try { svc.revoke_calendar_from_pair_convention(req->pair_code, req->calendar_code); BOOST_LOG_SEV(currency_pair_convention_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, revoke_currency_pair_convention_calendar_response{.success = true}); } catch (const std::exception& e) { BOOST_LOG_SEV(currency_pair_convention_handler_lg(), error) << msg.subject << " failed: " << e.what(); reply(nats_, msg, revoke_currency_pair_convention_calendar_response{ .success = false, .message = e.what()}); } } else { BOOST_LOG_SEV(currency_pair_convention_handler_lg(), warn) << "Failed to decode: " << msg.subject; error_reply(nats_, msg, ores::service::error_code::bad_request); } }
Calendar assignment: custom registrar subscriptions
subs.push_back(nats.queue_subscribe(
get_currency_pair_convention_calendars_request::nats_subject, queue_group,
[h](ores::nats::message msg) { h->list_calendars(std::move(msg)); }));
subs.push_back(nats.queue_subscribe(
assign_currency_pair_convention_calendar_request::nats_subject, queue_group,
[h](ores::nats::message msg) { h->assign_calendar(std::move(msg)); }));
subs.push_back(nats.queue_subscribe(
revoke_currency_pair_convention_calendar_request::nats_subject, queue_group,
[h](ores::nats::message msg) { h->revoke_calendar(std::move(msg)); }));
Calendars tab: detail dialog header includes
#include "ores.qt/CalendarAssignmentWidget.hpp"
Calendars tab: detail dialog private declarations
void setupCalendarsTab(); CalendarAssignmentWidget* calendarWidget_ = nullptr; QWidget* calendarsTab_ = nullptr;
Calendars tab: constructor wiring
setupCalendarsTab();
Calendars tab: setupConnections wiring
setupConnections() runs before setupCalendarsTab() in the generated
constructor (see the base template's ctor body), so any
connect(calendarWidget_, ...) placed in this paste point would
silently no-op – Qt's static connect() returns an invalid
connection with a runtime warning (not an exception or a log we'd
see) when the sender is still nullptr, and calendarWidget_ isn't
constructed until setupCalendarsTab() runs afterwards. All
calendarWidget_-dependent connections live in the
calendars_tab_implementation block below instead, after
calendarWidget_ actually exists. Left empty deliberately.
Calendars tab: implementation
void CurrencyPairConventionDetailDialog::setupCalendarsTab() { calendarWidget_ = new CalendarAssignmentWidget(this); calendarWidget_->setCallbacks( [](ClientManager* cm, const std::string& leftKey) -> CalendarAssignmentWidget::LoadResult { refdata::messaging::get_currency_pair_convention_calendars_request request; request.pair_code = leftKey; auto response = cm->process_authenticated_request(std::move(request)); if (!response) return {.success = false, .message = "Failed to communicate with server"}; CalendarAssignmentWidget::LoadResult result; result.success = response->success; result.message = QString::fromStdString(response->message); for (const auto& cc : response->calendars) result.calendarCodes.push_back(cc.calendar_code); return result; }, [](ClientManager* cm, const std::string& leftKey, const std::string& calendarCode, const std::string& changeReasonCode, const std::string& changeCommentary) -> CalendarAssignmentWidget::MutateResult { refdata::messaging::assign_currency_pair_convention_calendar_request request; request.pair_code = leftKey; request.calendar_code = calendarCode; request.change_reason_code = changeReasonCode; request.change_commentary = changeCommentary; auto response = cm->process_authenticated_request(std::move(request)); if (!response) return {.success = false, .message = "Failed to communicate with server"}; return {.success = response->success, .message = QString::fromStdString(response->message)}; }, [](ClientManager* cm, const std::string& leftKey, const std::string& calendarCode) -> CalendarAssignmentWidget::MutateResult { refdata::messaging::revoke_currency_pair_convention_calendar_request request; request.pair_code = leftKey; request.calendar_code = calendarCode; auto response = cm->process_authenticated_request(std::move(request)); if (!response) return {.success = false, .message = "Failed to communicate with server"}; return {.success = response->success, .message = QString::fromStdString(response->message)}; }); calendarsTab_ = new QWidget(); auto* layout = new QVBoxLayout(calendarsTab_); layout->addWidget(calendarWidget_); // Provenance is the base template's own tab and should stay last for // consistency across every entity's detail dialog -- insert Calendars // before it rather than appending, which would push Provenance out of // its usual final position. int provenanceTabIndex = -1; for (int i = 0; i < tabWidget()->count(); ++i) { if (tabWidget()->tabText(i) == tr("Provenance")) { provenanceTabIndex = i; break; } } if (provenanceTabIndex >= 0) tabWidget()->insertTab(provenanceTabIndex, calendarsTab_, tr("Calendars")); else tabWidget()->addTab(calendarsTab_, tr("Calendars")); // calendarWidget_/calendarsTab_ only exist from this point on -- see // the calendars_tab_connections paste point above for why these // connections live here instead of setupConnections(). connect(calendarWidget_, &CalendarAssignmentWidget::statusMessage, this, &CurrencyPairConventionDetailDialog::statusMessage); connect(calendarWidget_, &CalendarAssignmentWidget::errorMessage, this, [this](const QString&, const QString& message) { emit errorMessage(message); }); connect(calendarWidget_, &CalendarAssignmentWidget::assignmentsChanged, this, [this]() { ui_->saveButton->setEnabled(!readOnly_); }); connect(tabWidget(), &QTabWidget::currentChanged, this, [this](int) { if (!calendarWidget_ || !calendarsTab_ || tabWidget()->currentWidget() != calendarsTab_) return; calendarWidget_->setClientManager(clientManager_); calendarWidget_->setImageCache(imageCache()); calendarWidget_->setLeftKey(convention_.pair_code); calendarWidget_->setReadOnly(readOnly_ || convention_.pair_code.empty()); calendarWidget_->load(); }); // Calendar commits are triggered from onSaveClicked itself (see the // calendars_tab_save_guard and on_save_success paste points) so both // the convention's own fields and calendar assignments share a single // change-reason prompt per Save click -- no separate saveButton // connection here. }
Qt: pair_code stays visually normal while locked
Same reasoning as ores.refdata.currency_pair's own base/quote lock: the
generated is_key lock loop uses setEnabled(false), which grays out
the combo and makes its flag icon hard to see — override with
WidgetUtils::set_combo_locked() so the flag stays visible while
still blocking interaction, once created.
WidgetUtils::set_combo_locked(ui_->pairCodeCombo, !createMode);
WidgetUtils::set_combo_locked(ui_->pairCodeCombo, true);
Qt: reject duplicate pair_code on create
pair_code is a soft FK to currency_pair (see that entity's own
Qt: reject duplicate pair_code on create seam for the analogous
identity check). Since a pair has at most one convention record,
attaching a convention to a pair that already has one is the same
class of accidental-overwrite bug: the trigger's upsert-by-natural-key
semantics silently version the existing convention forward instead of
rejecting. Same client-side mitigation, simpler than currency_pair's
(no inverted-pair concept — conventions aren't directional).
if (createMode_) { const std::string selectedPairCode = ui_->pairCodeCombo->currentText().toStdString(); refdata::messaging::get_currency_pair_conventions_request checkRequest; checkRequest.limit = lookup_fetch_limit; auto checkResult = clientManager_->process_authenticated_request(std::move(checkRequest)); if (!checkResult) { MessageBoxHelper::warning(this, "Cannot Verify", "Could not check for an existing convention before saving. Please try again."); return; } const bool exists = std::ranges::any_of(checkResult->conventions, [&](const auto& c) { return c.pair_code == selectedPairCode; }); if (exists) { MessageBoxHelper::warning(this, "Duplicate Convention", QString("'%1' already has a convention. Edit the existing one instead.") .arg(QString::fromStdString(selectedPairCode))); return; } }
Calendars tab: a single change-reason prompt for both writes
Same reasoning as ores.refdata.currency's own guard/success-hook pair: calendar assignments and the convention's own fields must not pop two independent change-reason dialogs for one Save click.
- Only calendar assignments changed (
!hasChanges_): commit them here, with their own prompt, and return – skip the rest ofonSaveClickedso the convention's own fields aren't re-saved for no real change. - Both changed, or only the convention's own fields changed: fall
through to the normal save flow, which prompts once; the calendar
commit (reusing that same prompt result) happens afterwards in the
on_save_successpaste point below, once the convention save has actually succeeded.
if (!hasChanges_ && calendarWidget_ && calendarWidget_->hasPendingChanges()) { const auto crSel = promptChangeReason(ChangeReasonDialog::OperationType::Amend, true, "common"); if (!crSel) return; calendarWidget_->commitChanges( crSel->reason_code, crSel->commentary, [this](bool success, const QString& message) { if (!success) MessageBoxHelper::critical(this, "Calendar Assignment Failed", message); }); return; }
Calendars tab: commit calendar changes after a successful convention save
Runs inside onSaveClicked's async watcher, after the convention save
has succeeded and reused its change-reason prompt (crReasonCode=/
=crCommentary, captured into the watcher lambda) – not a second,
independent prompt. Only reached when the convention itself had
changes to save; the calendar-only case is handled entirely by the
calendars_tab_save_guard block above.
if (self->calendarWidget_ && self->calendarWidget_->hasPendingChanges()) { self->calendarWidget_->commitChanges( crReasonCode, crCommentary, [self](bool success, const QString& message) { if (!success) MessageBoxHelper::critical(self, "Calendar Assignment Failed", message); }); }
Calendars tab: keep read-only state in sync while browsing history
if (calendarWidget_ && calendarsTab_ && tabWidget()->currentWidget() == calendarsTab_) calendarWidget_->setReadOnly(readOnly_ || convention_.pair_code.empty());
See also
- ores.refdata — component group overview.
- Currency pair support in reference data — parent story.
- ores.refdata.currency_pair — owning identity entity.