Browse Source

Bugfix/missing quotes for symbols across data sources (#7085)

* Fix missing quotes

* Update changelog
pull/7079/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
1587206e67
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 4
      apps/api/src/app/endpoints/watchlist/watchlist.service.ts
  3. 4
      apps/api/src/app/portfolio/current-rate.service.ts
  4. 8
      apps/api/src/app/symbol/symbol.service.ts
  5. 8
      apps/api/src/services/benchmark/benchmark.service.ts
  6. 73
      apps/api/src/services/data-provider/data-provider.service.ts
  7. 9
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  8. 4
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts

1
CHANGELOG.md

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue with the localization of the country names - 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 ## 3.12.0 - 2026-06-17

4
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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { WatchlistResponse } from '@ghostfolio/common/interfaces'; import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
@ -127,7 +128,8 @@ export class WatchlistService {
const performancePercent = const performancePercent =
this.benchmarkService.calculateChangeInPercentage( this.benchmarkService.calculateChangeInPercentage(
allTimeHigh?.marketPrice, allTimeHigh?.marketPrice,
quotes[symbol]?.marketPrice quotes[getAssetProfileIdentifier({ dataSource, symbol })]
?.marketPrice
); );
return { return {

4
apps/api/src/app/portfolio/current-rate.service.ts

@ -51,13 +51,13 @@ export class CurrentRateService {
const values: GetValueObject[] = []; const values: GetValueObject[] = [];
if (includesToday) { if (includesToday) {
const quotesBySymbol = await this.dataProviderService.getQuotes({ const quotes = await this.dataProviderService.getQuotes({
items: dataGatheringItems, items: dataGatheringItems,
user: this.request?.user user: this.request?.user
}); });
for (const { dataSource, symbol } of dataGatheringItems) { for (const { dataSource, symbol } of dataGatheringItems) {
const quote = quotesBySymbol[symbol]; const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })];
if (quote?.dataProviderInfo) { if (quote?.dataProviderInfo) {
dataProviderInfos.push(quote.dataProviderInfo); dataProviderInfos.push(quote.dataProviderInfo);

8
apps/api/src/app/symbol/symbol.service.ts

@ -1,7 +1,10 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; 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 { import {
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
HistoricalDataItem, HistoricalDataItem,
@ -46,7 +49,8 @@ export class SymbolService {
items: [dataGatheringItem] items: [dataGatheringItem]
}); });
({ currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {}); ({ currency, marketPrice } =
quotes[getAssetProfileIdentifier(dataGatheringItem)] ?? {});
} }
if (dataGatheringItem.dataSource && marketPrice >= 0) { if (dataGatheringItem.dataSource && marketPrice >= 0) {

8
apps/api/src/services/benchmark/benchmark.service.ts

@ -8,7 +8,10 @@ import {
CACHE_TTL_INFINITE, CACHE_TTL_INFINITE,
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { calculateBenchmarkTrend } from '@ghostfolio/common/helper'; import {
calculateBenchmarkTrend,
getAssetProfileIdentifier
} from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
Benchmark, Benchmark,
@ -266,8 +269,9 @@ export class BenchmarkService {
let storeInCache = true; let storeInCache = true;
const benchmarks = allTimeHighs.map((allTimeHigh, index) => { const benchmarks = allTimeHighs.map((allTimeHigh, index) => {
const { dataSource, symbol } = benchmarkAssetProfiles[index];
const { marketPrice } = const { marketPrice } =
quotes[benchmarkAssetProfiles[index].symbol] ?? {}; quotes[getAssetProfileIdentifier({ dataSource, symbol })] ?? {};
let performancePercentFromAllTimeHigh = 0; let performancePercentFromAllTimeHigh = 0;

73
apps/api/src/services/data-provider/data-provider.service.ts

@ -77,7 +77,9 @@ export class DataProviderService implements OnModuleInit {
useCache: false useCache: false
}); });
if (quotes[symbol]?.marketPrice > 0) { if (
quotes[getAssetProfileIdentifier({ dataSource, symbol })]?.marketPrice > 0
) {
return true; return true;
} }
@ -514,7 +516,6 @@ export class DataProviderService implements OnModuleInit {
return result; return result;
} }
// TODO: Change symbol in response to assetProfileIdentifier
public async getQuotes({ public async getQuotes({
items, items,
requestTimeout, requestTimeout,
@ -526,10 +527,12 @@ export class DataProviderService implements OnModuleInit {
useCache?: boolean; useCache?: boolean;
user?: UserWithSettings; user?: UserWithSettings;
}): Promise<{ }): Promise<{
[symbol: string]: DataProviderResponse; [assetProfileIdentifier: string]: DataProviderResponse;
}> { }> {
const response: { const response: {
[symbol: string]: DataProviderResponse; [assetProfileIdentifier: string]: DataProviderResponse & {
symbol: string;
};
} = {}; } = {};
const startTimeTotal = performance.now(); const startTimeTotal = performance.now();
@ -538,11 +541,17 @@ export class DataProviderService implements OnModuleInit {
return symbol === `${DEFAULT_CURRENCY}USX`; return symbol === `${DEFAULT_CURRENCY}USX`;
}) })
) { ) {
response[`${DEFAULT_CURRENCY}USX`] = { response[
getAssetProfileIdentifier({
dataSource: this.getDataSourceForExchangeRates(),
symbol: `${DEFAULT_CURRENCY}USX`
})
] = {
currency: 'USX', currency: 'USX',
dataSource: this.getDataSourceForExchangeRates(), dataSource: this.getDataSourceForExchangeRates(),
marketPrice: 100, marketPrice: 100,
marketState: 'open' marketState: 'open',
symbol: `${DEFAULT_CURRENCY}USX`
}; };
} }
@ -557,8 +566,13 @@ export class DataProviderService implements OnModuleInit {
if (quoteString) { if (quoteString) {
try { try {
const cachedDataProviderResponse = JSON.parse(quoteString); const cachedDataProviderResponse = JSON.parse(
response[symbol] = cachedDataProviderResponse; quoteString
) as DataProviderResponse;
response[getAssetProfileIdentifier({ dataSource, symbol })] = {
...cachedDataProviderResponse,
symbol
};
continue; continue;
} catch {} } catch {}
} }
@ -646,14 +660,19 @@ export class DataProviderService implements OnModuleInit {
continue; continue;
} }
response[symbol] = dataProviderResponse; response[
getAssetProfileIdentifier({
symbol,
dataSource: DataSource[dataSource]
})
] = { ...dataProviderResponse, symbol };
this.redisCacheService.set( this.redisCacheService.set(
this.redisCacheService.getQuoteKey({ this.redisCacheService.getQuoteKey({
symbol, symbol,
dataSource: DataSource[dataSource] dataSource: DataSource[dataSource]
}), }),
JSON.stringify(response[symbol]), JSON.stringify(dataProviderResponse),
this.configurationService.get('CACHE_QUOTES_TTL') this.configurationService.get('CACHE_QUOTES_TTL')
); );
@ -663,7 +682,7 @@ export class DataProviderService implements OnModuleInit {
rootCurrency rootCurrency
} of DERIVED_CURRENCIES) { } of DERIVED_CURRENCIES) {
if (symbol === `${DEFAULT_CURRENCY}${rootCurrency}`) { if (symbol === `${DEFAULT_CURRENCY}${rootCurrency}`) {
response[`${DEFAULT_CURRENCY}${currency}`] = { const derivedDataProviderResponse: DataProviderResponse = {
...dataProviderResponse, ...dataProviderResponse,
currency, currency,
marketPrice: new Big( marketPrice: new Big(
@ -674,12 +693,22 @@ export class DataProviderService implements OnModuleInit {
marketState: 'open' marketState: 'open'
}; };
response[
getAssetProfileIdentifier({
dataSource: DataSource[dataSource],
symbol: `${DEFAULT_CURRENCY}${currency}`
})
] = {
...derivedDataProviderResponse,
symbol: `${DEFAULT_CURRENCY}${currency}`
};
this.redisCacheService.set( this.redisCacheService.set(
this.redisCacheService.getQuoteKey({ this.redisCacheService.getQuoteKey({
dataSource: DataSource[dataSource], dataSource: DataSource[dataSource],
symbol: `${DEFAULT_CURRENCY}${currency}` symbol: `${DEFAULT_CURRENCY}${currency}`
}), }),
JSON.stringify(response[`${DEFAULT_CURRENCY}${currency}`]), JSON.stringify(derivedDataProviderResponse),
this.configurationService.get('CACHE_QUOTES_TTL') this.configurationService.get('CACHE_QUOTES_TTL')
); );
} }
@ -697,21 +726,21 @@ export class DataProviderService implements OnModuleInit {
try { try {
await this.marketDataService.updateMany({ await this.marketDataService.updateMany({
data: Object.keys(response) data: Object.values(response)
.filter((symbol) => { .filter(({ marketPrice, marketState }) => {
return ( return (
isNumber(response[symbol].marketPrice) && isNumber(marketPrice) &&
response[symbol].marketPrice > 0 && marketPrice > 0 &&
response[symbol].marketState === 'open' marketState === 'open'
); );
}) })
.map((symbol) => { .map((dataProviderResponse) => {
return { return {
symbol, dataSource: dataProviderResponse.dataSource,
dataSource: response[symbol].dataSource,
date: getStartOfUtcDate(new Date()), date: getStartOfUtcDate(new Date()),
marketPrice: response[symbol].marketPrice, marketPrice: dataProviderResponse.marketPrice,
state: 'INTRADAY' state: 'INTRADAY',
symbol: dataProviderResponse.symbol
}; };
}) })
}); });

9
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -11,6 +11,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
DATE_FORMAT, DATE_FORMAT,
getAssetProfileIdentifier,
getYesterday, getYesterday,
resetHours resetHours
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
@ -176,11 +177,13 @@ export class ExchangeRateDataService {
requestTimeout: ms('30 seconds') requestTimeout: ms('30 seconds')
}); });
for (const symbol of Object.keys(quotes)) { for (const { dataSource, symbol } of this.currencyPairs) {
if (isNumber(quotes[symbol].marketPrice)) { const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })];
if (isNumber(quote?.marketPrice)) {
result[symbol] = { result[symbol] = {
[format(getYesterday(), DATE_FORMAT)]: { [format(getYesterday(), DATE_FORMAT)]: {
marketPrice: quotes[symbol].marketPrice marketPrice: quote.marketPrice
} }
}; };
} }

4
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -301,9 +301,9 @@ export class DataGatheringService {
const data: Prisma.MarketDataUpdateInput[] = []; const data: Prisma.MarketDataUpdateInput[] = [];
for (const { dataSource, symbol } of assetProfileIdentifiers) { 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; continue;
} }

Loading…
Cancel
Save