Browse Source

fix(benchmarks): include start date in market data query for calendar year ranges

For calendar year date ranges (e.g. '2025'), getIntervalFromDateRange()
sets startDate to the last millisecond of the previous year
(2024-12-31T23:59:59.999) as an exclusive boundary. The portfolio
chart excludes this date via isBefore(), so the market data query
(built from chart dates) never fetches the baseline price for that
date. This causes the benchmark line to be missing from the chart.

Add resetHours(startDate) to the date.in array in the market data
query so the start date's market data is always fetched, regardless
of whether it appears in the portfolio chart. For named ranges where
the start date is already in the chart, this is a harmless duplicate.

Add unit tests covering calendar year and named date ranges.
pull/7631/head
aaryamantriescode 2 weeks ago
committed by Thomas Kaul
parent
commit
7d5ba43314
  1. 1
      CHANGELOG.md
  2. 141
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.spec.ts
  3. 11
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

1
CHANGELOG.md

@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the missing benchmark in the performance chart for calendar year date ranges on the Analysis page
- Fixed the country mapping of Macau in the _Financial Modeling Prep_ service
- Fixed the asset profile and historical market data gathering of a symbol getting blocked permanently by a failed job by discarding the failed jobs
- Fixed the asset profile data gathering of a symbol in the admin control panel by removing an existing job before enqueueing a new one

141
apps/api/src/app/endpoints/benchmarks/benchmarks.service.spec.ts

@ -0,0 +1,141 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { BenchmarksService } from './benchmarks.service';
describe('BenchmarksService', () => {
let benchmarksService: BenchmarksService;
const mockBenchmarkService = {
calculateChangeInPercentage: jest.fn(
(startPrice: number, endPrice: number) => {
return (endPrice - startPrice) / startPrice;
}
)
};
const mockExchangeRateDataService = {
getExchangeRatesByCurrency: jest.fn().mockResolvedValue({})
};
const mockMarketDataService = {
marketDataItems: jest.fn()
};
const mockPortfolioService = {
getPerformance: jest.fn()
};
const mockSymbolService = {
get: jest.fn()
};
beforeEach(() => {
jest.clearAllMocks();
benchmarksService = new BenchmarksService(
mockBenchmarkService as unknown as BenchmarkService,
mockExchangeRateDataService as unknown as ExchangeRateDataService,
mockMarketDataService as unknown as MarketDataService,
mockPortfolioService as unknown as PortfolioService,
mockSymbolService as unknown as SymbolService
);
});
describe('getMarketDataForUser', () => {
it('returns benchmark data for a calendar year with the first chart date as baseline', async () => {
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999);
const endDate = new Date(2025, 11, 31, 23, 59, 59, 999);
mockPortfolioService.getPerformance.mockResolvedValue({
chart: [
{ date: '2025-01-01', value: 0 },
{ date: '2025-06-30', value: 10 },
{ date: '2025-12-31', value: 20 }
]
});
mockSymbolService.get.mockResolvedValue({
currency: 'USD',
marketPrice: 150
});
mockMarketDataService.marketDataItems.mockResolvedValue([
{ date: new Date(Date.UTC(2025, 0, 1)), marketPrice: 100 },
{ date: new Date(Date.UTC(2025, 5, 30)), marketPrice: 120 },
{ date: new Date(Date.UTC(2025, 11, 31)), marketPrice: 150 }
]);
const result = await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange: '2025',
endDate,
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
expect(result.marketData.length).toBeGreaterThan(0);
const baselineItem = result.marketData.find(
({ date }) => date === '2025-01-01'
);
expect(baselineItem).toBeDefined();
expect(baselineItem.value).toBe(0);
const junItem = result.marketData.find(
({ date }) => date === '2025-06-30'
);
expect(junItem).toEqual({
date: '2025-06-30',
value: expect.closeTo(((120 - 100) / 100) * 100, 10)
});
const decItem = result.marketData.find(
({ date }) => date === '2025-12-31'
);
expect(decItem).toEqual({
date: '2025-12-31',
value: expect.closeTo(((150 - 100) / 100) * 100, 10)
});
});
it('uses resetHours(startDate) as the baseline when the chart is empty', async () => {
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999);
mockPortfolioService.getPerformance.mockResolvedValue({
chart: []
});
mockSymbolService.get.mockResolvedValue({
currency: 'USD',
marketPrice: 150
});
mockMarketDataService.marketDataItems.mockResolvedValue([
{ date: new Date(Date.UTC(2024, 11, 31)), marketPrice: 100 }
]);
const result = await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange: '2025',
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
const baselineItem = result.marketData.find(
({ date }) => date === '2024-12-31'
);
expect(baselineItem).toBeDefined();
expect(baselineItem.value).toBe(0);
});
});
});

11
apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

@ -88,6 +88,11 @@ export class BenchmarksService {
return { marketData };
}
const baselineDate =
chart.length > 0
? resetHours(parseDate(chart[0].date))
: resetHours(startDate);
const exchangeRates =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
startDate,
@ -97,17 +102,17 @@ export class BenchmarksService {
const exchangeRateAtStartDate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(startDate, DATE_FORMAT)
format(baselineDate, DATE_FORMAT)
];
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
return isSameDay(date, startDate);
return isSameDay(date, baselineDate);
})?.marketPrice;
if (!marketPriceAtStartDate) {
this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
startDate,
baselineDate,
DATE_FORMAT
)}`
);

Loading…
Cancel
Save