From 7a9f2ebcc59d79339ed24e8200102d9f4de7a5d2 Mon Sep 17 00:00:00 2001 From: jaredrainsha <63041352+jaredrainsha@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:22:27 +0800 Subject: [PATCH] fix: prefer market data for quote fallback --- .../portfolio/current-rate.service.spec.ts | 48 +++++++++++++++++++ .../src/app/portfolio/current-rate.service.ts | 36 +++++++------- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index ea93e8b4f..e4a8f4dbf 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -1,3 +1,4 @@ +import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; @@ -152,4 +153,51 @@ describe('CurrentRateService', () => { ] }); }); + + it('uses the latest market price when the current quote is unavailable', async () => { + const historicalDate = new Date(Date.now() - 24 * 60 * 60 * 1000); + const activitiesService = { + getLatestActivity: jest.fn().mockResolvedValue({ unitPrice: 100 }) + }; + const dataProviderService = { + getQuotes: jest.fn().mockResolvedValue({}) + }; + const marketDataService = { + getRange: jest.fn().mockResolvedValue([ + { + createdAt: historicalDate, + dataSource: DataSource.YAHOO, + date: historicalDate, + id: '40520fdf-4e31-47ab-8bd0-ca61c70d4684', + isCarriedForward: false, + marketPrice: 200, + state: 'CLOSE', + symbol: 'AMZN' + } + ]), + getRangeCount: jest.fn().mockResolvedValue(1) + }; + const service = new CurrentRateService( + activitiesService as unknown as ActivitiesService, + dataProviderService as unknown as DataProviderService, + marketDataService as unknown as MarketDataService, + null + ); + + const response = await service.getValues({ + dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }], + dateQuery: { + gte: historicalDate, + lt: new Date(Date.now() + 24 * 60 * 60 * 1000) + } + }); + const latestValue = response.values + .filter(({ dataSource, symbol }) => { + return dataSource === DataSource.YAHOO && symbol === 'AMZN'; + }) + .sort((a, b) => b.date.getTime() - a.date.getTime())[0]; + + expect(latestValue.marketPrice).toBe(200); + expect(activitiesService.getLatestActivity).not.toHaveBeenCalled(); + }); }); diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 9cfeda3bd..2a5391c13 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -126,7 +126,7 @@ export class CurrentRateService { for (const { dataSource, symbol } of quoteErrors) { try { // If missing quote, fallback to the latest available historical market price - let value: GetValueObject = response.values.find((currentValue) => { + const hasValueForToday = response.values.some((currentValue) => { return ( currentValue.dataSource === dataSource && currentValue.symbol === symbol && @@ -134,22 +134,8 @@ export class CurrentRateService { ); }); - if (!value) { - // Fallback to unit price of latest activity - const latestActivity = - await this.activitiesService.getLatestActivity({ - dataSource, - symbol - }); - - value = { - dataSource, - symbol, - date: today, - marketPrice: latestActivity?.unitPrice ?? 0 - }; - - response.values.push(value); + if (hasValueForToday) { + continue; } const [latestValue] = response.values @@ -172,7 +158,21 @@ export class CurrentRateService { return 0; }); - value.marketPrice = latestValue.marketPrice; + // Fallback to unit price of latest activity + const latestActivity = latestValue + ? undefined + : await this.activitiesService.getLatestActivity({ + dataSource, + symbol + }); + + response.values.push({ + dataSource, + symbol, + date: today, + marketPrice: + latestValue?.marketPrice ?? latestActivity?.unitPrice ?? 0 + }); } catch {} } }