ores.refdata.currency_pair_convention

Table of Contents

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 of onSaveClicked so 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_success paste 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

Emacs 29.3 (Org mode 9.6.15)