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

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

4
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);

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

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

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

9
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
}
};
}

4
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;
}

Loading…
Cancel
Save