From 1587206e677ad32c7a74b6d629cdeddcce305b7a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:39:27 +0200 Subject: [PATCH] Bugfix/missing quotes for symbols across data sources (#7085) * Fix missing quotes * Update changelog --- CHANGELOG.md | 1 + .../endpoints/watchlist/watchlist.service.ts | 4 +- .../src/app/portfolio/current-rate.service.ts | 4 +- apps/api/src/app/symbol/symbol.service.ts | 8 +- .../services/benchmark/benchmark.service.ts | 8 +- .../data-provider/data-provider.service.ts | 73 +++++++++++++------ .../exchange-rate-data.service.ts | 9 ++- .../data-gathering/data-gathering.service.ts | 4 +- 8 files changed, 77 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 038612a02..a3433eeb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with the localization of the country names +- Fixed an issue in the data provider service where quotes could be missing for symbols that exist in multiple data sources by keying the quotes response by the asset profile identifier ## 3.12.0 - 2026-06-17 diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts index 666023dbf..791cc6c69 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts @@ -4,6 +4,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { WatchlistResponse } from '@ghostfolio/common/interfaces'; import { BadRequestException, Injectable } from '@nestjs/common'; @@ -127,7 +128,8 @@ export class WatchlistService { const performancePercent = this.benchmarkService.calculateChangeInPercentage( allTimeHigh?.marketPrice, - quotes[symbol]?.marketPrice + quotes[getAssetProfileIdentifier({ dataSource, symbol })] + ?.marketPrice ); return { diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index f0a451975..9cfeda3bd 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -51,13 +51,13 @@ export class CurrentRateService { const values: GetValueObject[] = []; if (includesToday) { - const quotesBySymbol = await this.dataProviderService.getQuotes({ + const quotes = await this.dataProviderService.getQuotes({ items: dataGatheringItems, user: this.request?.user }); for (const { dataSource, symbol } of dataGatheringItems) { - const quote = quotesBySymbol[symbol]; + const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })]; if (quote?.dataProviderInfo) { dataProviderInfos.push(quote.dataProviderInfo); diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index f2bf4beb1..732fcb23d 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -1,7 +1,10 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getAssetProfileIdentifier +} from '@ghostfolio/common/helper'; import { DataProviderHistoricalResponse, HistoricalDataItem, @@ -46,7 +49,8 @@ export class SymbolService { items: [dataGatheringItem] }); - ({ currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {}); + ({ currency, marketPrice } = + quotes[getAssetProfileIdentifier(dataGatheringItem)] ?? {}); } if (dataGatheringItem.dataSource && marketPrice >= 0) { diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts index 022a0e928..56a629163 100644 --- a/apps/api/src/services/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -8,7 +8,10 @@ import { CACHE_TTL_INFINITE, PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; -import { calculateBenchmarkTrend } from '@ghostfolio/common/helper'; +import { + calculateBenchmarkTrend, + getAssetProfileIdentifier +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, Benchmark, @@ -266,8 +269,9 @@ export class BenchmarkService { let storeInCache = true; const benchmarks = allTimeHighs.map((allTimeHigh, index) => { + const { dataSource, symbol } = benchmarkAssetProfiles[index]; const { marketPrice } = - quotes[benchmarkAssetProfiles[index].symbol] ?? {}; + quotes[getAssetProfileIdentifier({ dataSource, symbol })] ?? {}; let performancePercentFromAllTimeHigh = 0; diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 5b54afb0b..fb9efff44 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -77,7 +77,9 @@ export class DataProviderService implements OnModuleInit { useCache: false }); - if (quotes[symbol]?.marketPrice > 0) { + if ( + quotes[getAssetProfileIdentifier({ dataSource, symbol })]?.marketPrice > 0 + ) { return true; } @@ -514,7 +516,6 @@ export class DataProviderService implements OnModuleInit { return result; } - // TODO: Change symbol in response to assetProfileIdentifier public async getQuotes({ items, requestTimeout, @@ -526,10 +527,12 @@ export class DataProviderService implements OnModuleInit { useCache?: boolean; user?: UserWithSettings; }): Promise<{ - [symbol: string]: DataProviderResponse; + [assetProfileIdentifier: string]: DataProviderResponse; }> { const response: { - [symbol: string]: DataProviderResponse; + [assetProfileIdentifier: string]: DataProviderResponse & { + symbol: string; + }; } = {}; const startTimeTotal = performance.now(); @@ -538,11 +541,17 @@ export class DataProviderService implements OnModuleInit { return symbol === `${DEFAULT_CURRENCY}USX`; }) ) { - response[`${DEFAULT_CURRENCY}USX`] = { + response[ + getAssetProfileIdentifier({ + dataSource: this.getDataSourceForExchangeRates(), + symbol: `${DEFAULT_CURRENCY}USX` + }) + ] = { currency: 'USX', dataSource: this.getDataSourceForExchangeRates(), marketPrice: 100, - marketState: 'open' + marketState: 'open', + symbol: `${DEFAULT_CURRENCY}USX` }; } @@ -557,8 +566,13 @@ export class DataProviderService implements OnModuleInit { if (quoteString) { try { - const cachedDataProviderResponse = JSON.parse(quoteString); - response[symbol] = cachedDataProviderResponse; + const cachedDataProviderResponse = JSON.parse( + quoteString + ) as DataProviderResponse; + response[getAssetProfileIdentifier({ dataSource, symbol })] = { + ...cachedDataProviderResponse, + symbol + }; continue; } catch {} } @@ -646,14 +660,19 @@ export class DataProviderService implements OnModuleInit { continue; } - response[symbol] = dataProviderResponse; + response[ + getAssetProfileIdentifier({ + symbol, + dataSource: DataSource[dataSource] + }) + ] = { ...dataProviderResponse, symbol }; this.redisCacheService.set( this.redisCacheService.getQuoteKey({ symbol, dataSource: DataSource[dataSource] }), - JSON.stringify(response[symbol]), + JSON.stringify(dataProviderResponse), this.configurationService.get('CACHE_QUOTES_TTL') ); @@ -663,7 +682,7 @@ export class DataProviderService implements OnModuleInit { rootCurrency } of DERIVED_CURRENCIES) { if (symbol === `${DEFAULT_CURRENCY}${rootCurrency}`) { - response[`${DEFAULT_CURRENCY}${currency}`] = { + const derivedDataProviderResponse: DataProviderResponse = { ...dataProviderResponse, currency, marketPrice: new Big( @@ -674,12 +693,22 @@ export class DataProviderService implements OnModuleInit { marketState: 'open' }; + response[ + getAssetProfileIdentifier({ + dataSource: DataSource[dataSource], + symbol: `${DEFAULT_CURRENCY}${currency}` + }) + ] = { + ...derivedDataProviderResponse, + symbol: `${DEFAULT_CURRENCY}${currency}` + }; + this.redisCacheService.set( this.redisCacheService.getQuoteKey({ dataSource: DataSource[dataSource], symbol: `${DEFAULT_CURRENCY}${currency}` }), - JSON.stringify(response[`${DEFAULT_CURRENCY}${currency}`]), + JSON.stringify(derivedDataProviderResponse), this.configurationService.get('CACHE_QUOTES_TTL') ); } @@ -697,21 +726,21 @@ export class DataProviderService implements OnModuleInit { try { await this.marketDataService.updateMany({ - data: Object.keys(response) - .filter((symbol) => { + data: Object.values(response) + .filter(({ marketPrice, marketState }) => { return ( - isNumber(response[symbol].marketPrice) && - response[symbol].marketPrice > 0 && - response[symbol].marketState === 'open' + isNumber(marketPrice) && + marketPrice > 0 && + marketState === 'open' ); }) - .map((symbol) => { + .map((dataProviderResponse) => { return { - symbol, - dataSource: response[symbol].dataSource, + dataSource: dataProviderResponse.dataSource, date: getStartOfUtcDate(new Date()), - marketPrice: response[symbol].marketPrice, - state: 'INTRADAY' + marketPrice: dataProviderResponse.marketPrice, + state: 'INTRADAY', + symbol: dataProviderResponse.symbol }; }) }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 708bfa591..d8a08c1c4 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -11,6 +11,7 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT, + getAssetProfileIdentifier, getYesterday, resetHours } from '@ghostfolio/common/helper'; @@ -176,11 +177,13 @@ export class ExchangeRateDataService { requestTimeout: ms('30 seconds') }); - for (const symbol of Object.keys(quotes)) { - if (isNumber(quotes[symbol].marketPrice)) { + for (const { dataSource, symbol } of this.currencyPairs) { + const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })]; + + if (isNumber(quote?.marketPrice)) { result[symbol] = { [format(getYesterday(), DATE_FORMAT)]: { - marketPrice: quotes[symbol].marketPrice + marketPrice: quote.marketPrice } }; } diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index dfd371a13..789dea49b 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -301,9 +301,9 @@ export class DataGatheringService { const data: Prisma.MarketDataUpdateInput[] = []; for (const { dataSource, symbol } of assetProfileIdentifiers) { - const quote = quotes[symbol]; + const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })]; - if (quote?.dataSource !== dataSource || !quote.marketPrice) { + if (!quote?.marketPrice) { continue; }