#6869·ghostfolio

[BUG] Derived currencies (e.g. GBp) fail indirect conversion via base currency

Author: arogan178Created May 15, 2026Updated Sep 2, 2026

Bug Description

In ExchangeRateDataService, the methods toCurrency(), toCurrencyAtDate(), and getExchangeRates() do not normalize derived currencies (GBp, ILA, ZAc) before constructing lookup symbols or database queries. When the system attempts indirect conversion via the base currency (USD), it constructs symbols like USDGBp instead of USDGBP. These symbols do not exist in the database, causing lookups to fail, exchange rates to be undefined, and ultimately triggering cascading big.js crashes in PortfolioCalculator.

Impact

  • Every portfolio calculation involving a derived currency activity fails
  • The error log is flooded with messages like:
    No exchange rate has been found for GBpEUR at 2026-05-01.
  • toCurrencyAtDate() returns undefined for derived currency pairs, causing new Big(undefined) to crash
  • Affects any user with GBP-denominated stocks (GBp is the standard for UK equities) or ZAR/ILS assets
  • loadCurrencies() stores exchange rates under the wrong key (e.g. USDGBp instead of USDGBP), polluting the exchangeRates map and corrupting the MarketData table with thousands of invalid rows

Root Cause

Ghostfolio defines derived currencies in libs/common/src/lib/config.ts:

typescript
export const DERIVED_CURRENCIES = [
  { currency: 'GBp', factor: 100, rootCurrency: 'GBP' },
  { currency: 'ILA', factor: 100, rootCurrency: 'ILS' },
  { currency: 'ZAc', factor: 100, rootCurrency: 'ZAR' }
];

The derivedCurrencyFactors map correctly handles direct pairs (e.g. GBp -> GBP = 1/100), but the fallback indirect conversion path in toCurrencyAtDate() uses the raw aFromCurrency and aToCurrency values to construct database query symbols like USD{currency} and {currency}USD, bypassing the derived currency logic entirely. The same issue exists in toCurrency() for the exchangeRates dictionary lookup, and in getExchangeRates() for range-based market data queries.

Steps to Reproduce

  1. Have a base currency of EUR and an activity with unit price in GBp
  2. The portfolio calculator attempts to convert GBp to EUR
  3. toCurrencyAtDate() finds no direct GBpEUR pair
  4. It attempts indirect conversion by querying USDGBp and USDEUR
  5. The USDGBp query fails, the factor becomes undefined, crashing the calculator

Expected Behavior

ExchangeRateDataService should normalize derived currencies to their root currency (e.g., GBp to GBP) before performing indirect conversions or querying the database, and then apply the factor (e.g., / 100) to the final result.

Additional Context

Fix applied in PR #6866:

  • Updated toCurrency(), toCurrencyAtDate(), and getExchangeRates() in apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts to normalize derived currencies to rootCurrency before any dictionary lookups or DB queries, applying the scaling factor to the final value

Database cleanup required if affected:

sql
DELETE FROM "MarketData" WHERE symbol IN ('USDGBp', 'USDILA', 'USDZAc');

Verified on a production instance with ~46k activities: no more GBpEUR errors after fix. Deleted 2049 pre-existing corrupt rows from MarketData.