From 8c52e855bac4c54e1586d0070ab721ce5dc47a4f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jun 2026 16:31:29 +0200 Subject: [PATCH] Fix issue where certain symbols were incorrectly identified as currencies --- .../yahoo-finance/yahoo-finance.service.ts | 9 +-- .../eod-historical-data.service.ts | 19 ++---- .../financial-modeling-prep.service.ts | 16 +++-- libs/common/src/lib/helper.spec.ts | 61 ++++++++++++++++++- libs/common/src/lib/helper.ts | 14 +++++ 5 files changed, 88 insertions(+), 31 deletions(-) diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 4fb0e96ed..84e11d608 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -6,7 +6,7 @@ import { DEFAULT_CURRENCY, REPLACE_NAME_PARTS } from '@ghostfolio/common/config'; -import { isCurrency } from '@ghostfolio/common/helper'; +import { isCurrencySymbol } from '@ghostfolio/common/helper'; import { SectorName } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; @@ -77,12 +77,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { aSymbol.includes(DEFAULT_CURRENCY) && aSymbol.length > DEFAULT_CURRENCY.length ) { - if ( - isCurrency( - aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length) - ) && - isCurrency(aSymbol.substring(aSymbol.length - DEFAULT_CURRENCY.length)) - ) { + if (isCurrencySymbol(aSymbol)) { return `${aSymbol}=X`; } else if ( this.cryptocurrencyService.isCryptocurrency( diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index 06173c25b..ebb6cd743 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -13,7 +13,7 @@ import { DEFAULT_CURRENCY, REPLACE_NAME_PARTS } from '@ghostfolio/common/config'; -import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; +import { DATE_FORMAT, isCurrencySymbol } from '@ghostfolio/common/helper'; import { DataProviderHistoricalResponse, DataProviderInfo, @@ -382,20 +382,11 @@ export class EodHistoricalDataService * Currency: USDCHF -> USDCHF.FOREX */ private convertToEodSymbol(aSymbol: string) { - if ( - aSymbol.startsWith(DEFAULT_CURRENCY) && - aSymbol.length > DEFAULT_CURRENCY.length - ) { - if ( - isCurrency( - aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length) - ) - ) { - let symbol = aSymbol; - symbol = symbol.replace('GBp', 'GBX'); + if (isCurrencySymbol(aSymbol)) { + let symbol = aSymbol; + symbol = symbol.replace('GBp', 'GBX'); - return `${symbol}.FOREX`; - } + return `${symbol}.FOREX`; } return aSymbol; diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 157285278..ca48bb247 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -16,7 +16,11 @@ import { DEFAULT_CURRENCY, REPLACE_NAME_PARTS } from '@ghostfolio/common/config'; -import { DATE_FORMAT, isCurrency, parseDate } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + isCurrencySymbol, + parseDate +} from '@ghostfolio/common/helper'; import { DataProviderHistoricalResponse, DataProviderInfo, @@ -86,9 +90,7 @@ export class FinancialModelingPrepService }; try { - if ( - isCurrency(symbol.substring(0, symbol.length - DEFAULT_CURRENCY.length)) - ) { + if (isCurrencySymbol(symbol)) { response.assetClass = AssetClass.LIQUIDITY; response.assetSubClass = AssetSubClass.CASH; response.currency = symbol.substring( @@ -482,11 +484,7 @@ export class FinancialModelingPrepService for (const { price, symbol } of quotes) { let marketState: MarketState = 'delayed'; - if ( - isCurrency( - symbol.substring(0, symbol.length - DEFAULT_CURRENCY.length) - ) - ) { + if (isCurrencySymbol(symbol)) { marketState = 'open'; } diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index a339c6dab..6a6fe4773 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -1,6 +1,8 @@ import { extractNumberFromString, - getNumberFormatGroup + getNumberFormatGroup, + isCurrency, + isCurrencySymbol } from '@ghostfolio/common/helper'; describe('Helper', () => { @@ -116,4 +118,61 @@ describe('Helper', () => { expect(getNumberFormatGroup()).toEqual(','); }); }); + + describe('Is currency', () => { + it('ISO 4217 currency code', () => { + expect(isCurrency('USD')).toEqual(true); + }); + + it('Derived currency', () => { + expect(isCurrency('GBp')).toEqual(true); + }); + + it('Non-currency', () => { + expect(isCurrency('AAPL')).toEqual(false); + }); + + it('Empty currency', () => { + expect(isCurrency('')).toEqual(false); + }); + }); + + describe('Is currency symbol', () => { + it('Currency symbol (default currency as base)', () => { + expect(isCurrencySymbol('USDCHF')).toEqual(true); + expect(isCurrencySymbol('USDZAR')).toEqual(true); + }); + + it('Currency symbol (default currency as quote)', () => { + expect(isCurrencySymbol('EURUSD')).toEqual(true); + }); + + it('Currency symbol (derived currency)', () => { + expect(isCurrencySymbol('USDGBp')).toEqual(true); + }); + + it('Stock symbol with currency-like prefix', () => { + expect(isCurrencySymbol('ERNA.L')).toEqual(false); + }); + + it('Cryptocurrency symbol', () => { + expect(isCurrencySymbol('BTCUSD')).toEqual(false); + }); + + it('Stock symbol', () => { + expect(isCurrencySymbol('AAPL')).toEqual(false); + }); + + it('Symbol with non-currency suffix', () => { + expect(isCurrencySymbol('USD.AX')).toEqual(false); + }); + + it('Plain currency code', () => { + expect(isCurrencySymbol('USD')).toEqual(false); + }); + + it('Empty symbol', () => { + expect(isCurrencySymbol('')).toEqual(false); + }); + }); }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index ce7fca518..68b8c51bd 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -445,6 +445,20 @@ export function isCurrency(aCurrency: string) { return isISO4217CurrencyCode(aCurrency) || isDerivedCurrency(aCurrency); } +export function isCurrencySymbol(aSymbol: string) { + if (!aSymbol) { + return false; + } + + return ( + aSymbol.length >= 2 * DEFAULT_CURRENCY.length && + isCurrency( + aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length) + ) && + isCurrency(aSymbol.substring(aSymbol.length - DEFAULT_CURRENCY.length)) + ); +} + export function isDerivedCurrency(aCurrency: string) { if (aCurrency === 'USX') { return true;