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 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

40
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;

19
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;

16
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';
}

61
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);
});
});
});

14
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;

Loading…
Cancel
Save