Browse Source

Bugfix/false positive in currency symbol detection (#7024)

* Fix issue where certain symbols (e.g. ERNA.L) were incorrectly identified as currencies

* Update changelog
pull/7027/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
a19bd1fc02
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 40
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  3. 19
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  4. 16
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  5. 61
      libs/common/src/lib/helper.spec.ts
  6. 14
      libs/common/src/lib/helper.ts

1
CHANGELOG.md

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue in the import dividends dialog - 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 - Fixed the last request date in the users table of the admin control panel
## 3.9.0 - 2026-06-12 ## 3.9.0 - 2026-06-12

40
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -6,7 +6,7 @@ import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
REPLACE_NAME_PARTS REPLACE_NAME_PARTS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper'; import { isCurrencySymbol } from '@ghostfolio/common/helper';
import { SectorName } from '@ghostfolio/common/types'; import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -73,31 +73,21 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
* DOGEUSD -> DOGE-USD * DOGEUSD -> DOGE-USD
*/ */
public convertToYahooFinanceSymbol(aSymbol: string) { public convertToYahooFinanceSymbol(aSymbol: string) {
if ( if (isCurrencySymbol(aSymbol)) {
aSymbol.includes(DEFAULT_CURRENCY) && return `${aSymbol}=X`;
aSymbol.length > DEFAULT_CURRENCY.length } else if (
this.cryptocurrencyService.isCryptocurrency(
aSymbol.replace(new RegExp(`-${DEFAULT_CURRENCY}$`), DEFAULT_CURRENCY)
)
) { ) {
if ( // Add a dash before the last three characters
isCurrency( // BTCUSD -> BTC-USD
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length) // DOGEUSD -> DOGE-USD
) && // SOL1USD -> SOL1-USD
isCurrency(aSymbol.substring(aSymbol.length - DEFAULT_CURRENCY.length)) return aSymbol.replace(
) { new RegExp(`-?${DEFAULT_CURRENCY}$`),
return `${aSymbol}=X`; `-${DEFAULT_CURRENCY}`
} 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}`
);
}
} }
return aSymbol; return aSymbol;

19
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -13,7 +13,7 @@ import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
REPLACE_NAME_PARTS REPLACE_NAME_PARTS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { DATE_FORMAT, isCurrencySymbol } from '@ghostfolio/common/helper';
import { import {
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
DataProviderInfo, DataProviderInfo,
@ -382,20 +382,11 @@ export class EodHistoricalDataService
* Currency: USDCHF -> USDCHF.FOREX * Currency: USDCHF -> USDCHF.FOREX
*/ */
private convertToEodSymbol(aSymbol: string) { private convertToEodSymbol(aSymbol: string) {
if ( if (isCurrencySymbol(aSymbol)) {
aSymbol.startsWith(DEFAULT_CURRENCY) && let symbol = aSymbol;
aSymbol.length > DEFAULT_CURRENCY.length symbol = symbol.replace('GBp', 'GBX');
) {
if (
isCurrency(
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
)
) {
let symbol = aSymbol;
symbol = symbol.replace('GBp', 'GBX');
return `${symbol}.FOREX`; return `${symbol}.FOREX`;
}
} }
return aSymbol; return aSymbol;

16
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -16,7 +16,11 @@ import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
REPLACE_NAME_PARTS REPLACE_NAME_PARTS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency, parseDate } from '@ghostfolio/common/helper'; import {
DATE_FORMAT,
isCurrencySymbol,
parseDate
} from '@ghostfolio/common/helper';
import { import {
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
DataProviderInfo, DataProviderInfo,
@ -86,9 +90,7 @@ export class FinancialModelingPrepService
}; };
try { try {
if ( if (isCurrencySymbol(symbol)) {
isCurrency(symbol.substring(0, symbol.length - DEFAULT_CURRENCY.length))
) {
response.assetClass = AssetClass.LIQUIDITY; response.assetClass = AssetClass.LIQUIDITY;
response.assetSubClass = AssetSubClass.CASH; response.assetSubClass = AssetSubClass.CASH;
response.currency = symbol.substring( response.currency = symbol.substring(
@ -482,11 +484,7 @@ export class FinancialModelingPrepService
for (const { price, symbol } of quotes) { for (const { price, symbol } of quotes) {
let marketState: MarketState = 'delayed'; let marketState: MarketState = 'delayed';
if ( if (isCurrencySymbol(symbol)) {
isCurrency(
symbol.substring(0, symbol.length - DEFAULT_CURRENCY.length)
)
) {
marketState = 'open'; marketState = 'open';
} }

61
libs/common/src/lib/helper.spec.ts

@ -1,6 +1,8 @@
import { import {
extractNumberFromString, extractNumberFromString,
getNumberFormatGroup getNumberFormatGroup,
isCurrency,
isCurrencySymbol
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
describe('Helper', () => { describe('Helper', () => {
@ -116,4 +118,61 @@ describe('Helper', () => {
expect(getNumberFormatGroup()).toEqual(','); 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);
});
});
}); });

14
libs/common/src/lib/helper.ts

@ -445,6 +445,20 @@ export function isCurrency(aCurrency: string) {
return isISO4217CurrencyCode(aCurrency) || isDerivedCurrency(aCurrency); 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) { export function isDerivedCurrency(aCurrency: string) {
if (aCurrency === 'USX') { if (aCurrency === 'USX') {
return true; return true;

Loading…
Cancel
Save