diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6db98a9..69efdefe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue in the import dividends dialog +- Fixed an issue where certain symbols were incorrectly identified as currencies in various data providers - Fixed the last request date in the users table of the admin control panel ## 3.9.0 - 2026-06-12 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..85ec6c020 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'; @@ -73,31 +73,21 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { * DOGEUSD -> DOGE-USD */ public convertToYahooFinanceSymbol(aSymbol: string) { - if ( - aSymbol.includes(DEFAULT_CURRENCY) && - aSymbol.length > DEFAULT_CURRENCY.length + if (isCurrencySymbol(aSymbol)) { + return `${aSymbol}=X`; + } else if ( + this.cryptocurrencyService.isCryptocurrency( + aSymbol.replace(new RegExp(`-${DEFAULT_CURRENCY}$`), DEFAULT_CURRENCY) + ) ) { - if ( - isCurrency( - aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length) - ) && - isCurrency(aSymbol.substring(aSymbol.length - DEFAULT_CURRENCY.length)) - ) { - return `${aSymbol}=X`; - } else if ( - this.cryptocurrencyService.isCryptocurrency( - aSymbol.replace(new RegExp(`-${DEFAULT_CURRENCY}$`), DEFAULT_CURRENCY) - ) - ) { - // Add a dash before the last three characters - // BTCUSD -> BTC-USD - // DOGEUSD -> DOGE-USD - // SOL1USD -> SOL1-USD - return aSymbol.replace( - new RegExp(`-?${DEFAULT_CURRENCY}$`), - `-${DEFAULT_CURRENCY}` - ); - } + // Add a dash before the last three characters + // BTCUSD -> BTC-USD + // DOGEUSD -> DOGE-USD + // SOL1USD -> SOL1-USD + return aSymbol.replace( + new RegExp(`-?${DEFAULT_CURRENCY}$`), + `-${DEFAULT_CURRENCY}` + ); } return aSymbol; 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;