From f82fbcfc5885f9e4992d43a0dcb33cf3cac4aaa1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 15 May 2026 11:22:35 +0300 Subject: [PATCH] Bugfix/exception in portfolio details endpoint for unmatched asset profile (#6861) * Resolve exception in portfolio details endpoint when an asset profile is unmatched * Use asset profile identifier for value uniqueness * Update changelog --- CHANGELOG.md | 6 ++++ .../src/app/portfolio/current-rate.service.ts | 21 +++++++++--- .../src/app/portfolio/portfolio.service.ts | 34 ++++++++++++++++--- .../data-provider/data-provider.service.ts | 4 +++ 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f23e1af95..be6630ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Resolved an exception in the portfolio details endpoint when an asset profile is unmatched + ## 3.3.0 - 2026-05-14 ### Added diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index b454b01cd..f0a451975 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -2,7 +2,10 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; -import { resetHours } from '@ghostfolio/common/helper'; +import { + getAssetProfileIdentifier, + resetHours +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, DataProviderInfo, @@ -114,8 +117,8 @@ export class CurrentRateService { errors: quoteErrors.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }), - values: uniqBy(values, ({ date, symbol }) => { - return `${date}-${symbol}`; + values: uniqBy(values, ({ dataSource, date, symbol }) => { + return `${date}-${getAssetProfileIdentifier({ dataSource, symbol })}`; }) }; @@ -124,7 +127,11 @@ export class CurrentRateService { try { // If missing quote, fallback to the latest available historical market price let value: GetValueObject = response.values.find((currentValue) => { - return currentValue.symbol === symbol && isToday(currentValue.date); + return ( + currentValue.dataSource === dataSource && + currentValue.symbol === symbol && + isToday(currentValue.date) + ); }); if (!value) { @@ -147,7 +154,11 @@ export class CurrentRateService { const [latestValue] = response.values .filter((currentValue) => { - return currentValue.symbol === symbol && currentValue.marketPrice; + return ( + currentValue.dataSource === dataSource && + currentValue.marketPrice && + currentValue.symbol === symbol + ); }) .sort((a, b) => { if (a.date < b.date) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 60b413cf9..ade683d41 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -36,7 +36,12 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getAssetProfileIdentifier, + getSum, + parseDate +} from '@ghostfolio/common/helper'; import { AccountsResponse, Activity, @@ -64,7 +69,7 @@ import { } from '@ghostfolio/common/types'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Account, @@ -558,9 +563,17 @@ export class PortfolioService { const cashSymbolProfiles = this.getCashSymbolProfiles(cashDetails); symbolProfiles.push(...cashSymbolProfiles); - const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; + const symbolProfileMap: { + [assetProfileIdentifier: string]: EnhancedSymbolProfile; + } = {}; + for (const symbolProfile of symbolProfiles) { - symbolProfileMap[symbolProfile.symbol] = symbolProfile; + symbolProfileMap[ + getAssetProfileIdentifier({ + dataSource: symbolProfile.dataSource, + symbol: symbolProfile.symbol + }) + ] = symbolProfile; } const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; @@ -571,6 +584,7 @@ export class PortfolioService { for (const { activitiesCount, currency, + dataSource, dateOfFirstActivity, dividend, grossPerformance, @@ -600,7 +614,17 @@ export class PortfolioService { } } - const assetProfile = symbolProfileMap[symbol]; + const assetProfile = + symbolProfileMap[getAssetProfileIdentifier({ dataSource, symbol })]; + + if (!assetProfile) { + Logger.warn( + `Asset profile not found for ${symbol} (${dataSource})`, + 'PortfolioService' + ); + + continue; + } let markets: PortfolioPosition['markets']; let marketsAdvanced: PortfolioPosition['marketsAdvanced']; 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 e7c7e8a2e..5f0a6928a 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -82,6 +82,7 @@ export class DataProviderService implements OnModuleInit { return false; } + // TODO: Change symbol in response to assetProfileIdentifier public async getAssetProfiles(items: AssetProfileIdentifier[]): Promise<{ [symbol: string]: Partial; }> { @@ -330,6 +331,7 @@ export class DataProviderService implements OnModuleInit { }); } + // TODO: Change symbol in response to assetProfileIdentifier public async getHistorical( aItems: AssetProfileIdentifier[], aGranularity: Granularity = 'month', @@ -395,6 +397,7 @@ export class DataProviderService implements OnModuleInit { } } + // TODO: Change symbol in response to assetProfileIdentifier public async getHistoricalRaw({ assetProfileIdentifiers, from, @@ -508,6 +511,7 @@ export class DataProviderService implements OnModuleInit { return result; } + // TODO: Change symbol in response to assetProfileIdentifier public async getQuotes({ items, requestTimeout,