ores.refdata.currency_pair

Table of Contents

Currency pair identity: base/quote legs, deliverability, classification, and fixing source. Conventions (pip factor, tick size, calendars, business day convention) live 1:1 in ores.refdata.currency_pair_convention, matching the codebase's existing *_convention entity family. spot_days, calendars, and G11 membership are derived at read time from the two legs, not stored here — see Currency pair support in reference data for the full design rationale.

Flags

Columns

pair_code

Canonical base/quote pair code, e.g. "EUR/USD", always stored in canonical base-currency-precedence order.

std::string(faker::finance::currencyCode()) + "/" + std::string(faker::finance::currencyCode())

base_currency

Base (first) leg of the pair. Soft FK to currency.iso_code.

std::string("EUR")

quote_currency

Quote (second) leg of the pair. Soft FK to currency.iso_code.

std::string("USD")

classification

Liquidity classification; soft FK to ores_refdata_currency_pair_classifications_tbl (e.g. "major", "minor", "exotic"). Free-text pending a working dynamic-combo mechanism for aux-type reference data (tracked separately; out of scope for this entity — see Currency pair support in reference data).

std::string("major")

SQL

Flags

Extra drops

drop function if exists ores_refdata_validate_currency_pair_fn;

Foreign keys

base_currency

quote_currency

classification

Validation function

Insert trigger

Validations

column validation_function
base_currency ores_refdata_validate_currency_fn
quote_currency ores_refdata_validate_currency_fn
classification ores_refdata_validate_currency_pair_classification_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
base_currency Base
quote_currency Quote
classification Classification
modified_by Modified By
version Version

Qt

Columns (Qt model)

enum_name field header type width is_badge badge_key formatter formatter_include
PairCode pair_code Pair string 100        
BaseCurrency base_currency Base string 80        
QuoteCurrency quote_currency Quote string 80        
Classification classification Classification string 100 true currency_pair_classification    
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 (same cell as the "EUR/USD" text), matching the FX Spot grid's existing hand-written pattern (see currency_flag_icon() in FlagIconHelper.hpp).

column accessor field1 field2
PairCode currency_flag_icon base_currency quote_currency
BaseCurrency currency_flag_icon base_currency  
QuoteCurrency currency_flag_icon quote_currency  

Detail fields

field label widget type is_key is_required placeholder flag_source combo_fetch combo_fetch_include combo_allow_blank combo_values badge_key immutable
pair_code Pair Code pairCodeEdit line_edit true true e.g. EUR/USD              
base_currency Base Currency baseCurrencyCombo flagged_combo   true   currency fetch_currency_codes ores.qt/LookupFetcher.hpp false     true
quote_currency Quote Currency quoteCurrencyCombo flagged_combo   true   currency fetch_currency_codes ores.qt/LookupFetcher.hpp false     true
classification Classification classificationCombo static_combo               major,minor,exotic,commodity currency_pair_classification  

Custom repository methods

Qt: derived pair_code

pair_code is base_currency + "/" + quote_currency, not independent user input — kept live-synced while both are still editable (create mode only; once created they're locked and pair_code never changes again, per Pair code doesn't recompute when base/quote currency changes). Base/Quote lock via WidgetUtils::set_combo_locked() rather than the generated setEnabled(false), so the flag icon stays visible while locked rather than fading out — see same task.

Deliverability (physical vs. cash/NDF settlement, and the settlement currency for the latter) was modelled here as a deliverable=/ =non_deliverable boolean, then dropped entirely — real FX conventions allow multiple settlement types per pair (sometimes business-centre dependent), which a single boolean/optional-currency pair on currency_pair can't express; needs a proper settlement_type junction model, tracked as a future story (see the capture in the inbox).

void updatePairCodeFromCurrencies();
connect(ui_->baseCurrencyCombo, &QComboBox::currentTextChanged, this,
        &CurrencyPairDetailDialog::updatePairCodeFromCurrencies);
connect(ui_->quoteCurrencyCombo, &QComboBox::currentTextChanged, this,
        &CurrencyPairDetailDialog::updatePairCodeFromCurrencies);
WidgetUtils::set_combo_locked(ui_->baseCurrencyCombo, !createMode);
WidgetUtils::set_combo_locked(ui_->quoteCurrencyCombo, !createMode);
// pair_code is always derived (see updatePairCodeFromCurrencies()),
// never independently typed -- the generated locked_fields loop above
// left it as setReadOnly(!createMode) (is_key's normal, editable-at-
// create behaviour), which is wrong for a field that's never directly
// entered even at create time.
ui_->pairCodeEdit->setReadOnly(true);
WidgetUtils::set_combo_locked(ui_->baseCurrencyCombo, true);
WidgetUtils::set_combo_locked(ui_->quoteCurrencyCombo, true);
&& ui_->baseCurrencyCombo->currentText() != ui_->quoteCurrencyCombo->currentText()
void CurrencyPairDetailDialog::updatePairCodeFromCurrencies() {
    if (!createMode_)
        return;
    ui_->pairCodeEdit->setText(
        ui_->baseCurrencyCombo->currentText() + "/" + ui_->quoteCurrencyCombo->currentText());
}

Qt: reject duplicate pair_code on create

The SQL insert trigger treats a colliding pair_code as the next version of the existing row (upsert-by-natural-key, the same pattern every bitemporal entity uses) rather than rejecting it — the trigger can't distinguish "genuine create" from "republish/upsert" without breaking the DQ→refdata publish flow, which relies on that exact semantics for its own re-versioning. So the check has to live client-side, scoped to just the interactive Add flow — see Creating a Currency Pair with an already-used pair_code silently versions the existing row.

Synchronous (not QtConcurrent-wrapped) — a brief UI-thread block for a quick pre-save check, same as other synchronous process_authenticated_request call sites elsewhere in the app (e.g. SystemSettingController::onRevertClicked). Requests a generously high limit explicitly: the default (100) would silently miss existing pairs beyond the first page and defeat the check.

if (createMode_) {
    // pair_.pair_code isn't synced from the UI until updatePairFromUi()
    // runs, later in this function -- read the live combo-derived value
    // directly rather than the (still-stale/empty) domain object.
    const QString baseCode = ui_->baseCurrencyCombo->currentText();
    const QString quoteCode = ui_->quoteCurrencyCombo->currentText();
    const std::string derivedPairCode = ui_->pairCodeEdit->text().trimmed().toStdString();
    const std::string invertedPairCode = (quoteCode + "/" + baseCode).toStdString();

    refdata::messaging::get_currency_pairs_request checkRequest;
    checkRequest.limit = lookup_fetch_limit;
    auto checkResult = clientManager_->process_authenticated_request(std::move(checkRequest));
    if (!checkResult) {
        // Fail closed: the whole point of this check is to prevent a
        // silent overwrite, so if we can't even ask the server whether
        // the pair already exists, don't let the save through blind.
        MessageBoxHelper::warning(this, "Cannot Verify",
            "Could not check for an existing pair before saving. Please try again.");
        return;
    }

    const bool exists = std::ranges::any_of(checkResult->pairs, [&](const auto& p) {
        return p.pair_code == derivedPairCode;
    });
    if (exists) {
        MessageBoxHelper::warning(this, "Duplicate Pair",
            QString("A currency pair '%1' already exists.")
                .arg(QString::fromStdString(derivedPairCode)));
        return;
    }

    // FX market convention quotes each pair in one canonical
    // direction only -- EUR/USD and USD/EUR are not independent
    // pairs, they're the same market inverted. Reject the inverse
    // rather than silently allowing both to coexist.
    const bool inverseExists = std::ranges::any_of(checkResult->pairs, [&](const auto& p) {
        return p.pair_code == invertedPairCode;
    });
    if (inverseExists) {
        MessageBoxHelper::warning(this, "Inverted Pair",
            QString("'%1' is the inverse of the existing pair '%2'. FX market "
                   "convention allows only one direction per currency pair.\n\n"
                   "To use '%1' instead, delete '%2' first, then create '%1'.")
                .arg(QString::fromStdString(derivedPairCode))
                .arg(QString::fromStdString(invertedPairCode)));
        return;
    }
}

Qt: Conventions toolbar button

Cross-navigation from the Currency Pairs list window to the Currency Pair Conventions list window (ores.refdata.currency_pair_convention), via a "Conventions" toolbar button. Signal declared on the MdiWindow and relayed through the Controller, mirroring the generated show{{...}}History plumbing.

void showConventionsRequested();
{
    toolbar_->addSeparator();
    auto* conventionsAction = toolbar_->addAction(
        IconUtils::createRecoloredIcon(Icon::Tag, IconUtils::DefaultIconColor), tr("Conventions"));
    conventionsAction->setToolTip(tr("Open Currency Pair Conventions"));
    connect(conventionsAction, &QAction::triggered, this,
            &CurrencyPairMdiWindow::showConventionsRequested);
}
void showConventionsRequested();
connect(listWindow_, &CurrencyPairMdiWindow::showConventionsRequested,
        this, &CurrencyPairController::showConventionsRequested);

See also

Emacs 29.3 (Org mode 9.6.15)