From 6c0acf07e173ef8784419e03ee8c294e979a30a7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:35:12 +0200 Subject: [PATCH] Fix missing quotes --- .../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 | 75 +++++++++++++------ .../exchange-rate-data.service.ts | 9 ++- .../data-gathering/data-gathering.service.ts | 4 +- 7 files changed, 78 insertions(+), 34 deletions(-) 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..d4dc69bd4 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,14 @@ export class DataProviderService implements OnModuleInit { useCache?: boolean; user?: UserWithSettings; }): Promise<{ - [symbol: string]: DataProviderResponse; + [assetProfileIdentifier: string]: DataProviderResponse; }> { + // Keyed by asset profile identifier; each entry carries its symbol so the + // market data can be persisted without parsing the key const response: { - [symbol: string]: DataProviderResponse; + [assetProfileIdentifier: string]: DataProviderResponse & { + symbol: string; + }; } = {}; const startTimeTotal = performance.now(); @@ -538,11 +543,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 +568,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 +662,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 +684,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 +695,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 +728,21 @@ export class DataProviderService implements OnModuleInit { try { await this.marketDataService.updateMany({ - data: Object.keys(response) - .filter((symbol) => { + data: Object.values(response) + .filter((dataProviderResponse) => { return ( - isNumber(response[symbol].marketPrice) && - response[symbol].marketPrice > 0 && - response[symbol].marketState === 'open' + isNumber(dataProviderResponse.marketPrice) && + dataProviderResponse.marketPrice > 0 && + dataProviderResponse.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; }