diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fe51ead1..506bd6fe85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the _Copy AI prompt to clipboard_ actions on the analysis page in impersonation mode to be based on the impersonated user (experimental) - Fixed the start date of the date ranges for instances in time zones with a negative UTC offset +- Fixed the dates of the benchmark in the benchmark comparator on the analysis page for instances in time zones with a negative UTC offset - Fixed the value of the accounts by excluding the cash balance when filtering by an asset class other than liquidity on the accounts page and the allocations page - Fixed the value of the accounts by excluding the cash balance when filtering by an asset class other than liquidity in the server of the Model Context Protocol (MCP) (experimental) diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.time-zone.spec.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.time-zone.spec.ts new file mode 100644 index 0000000000..e0bf24120c --- /dev/null +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.time-zone.spec.ts @@ -0,0 +1,80 @@ +/** + * @jest-environment /jest-environment-tz.js + * @jest-environment-options {"timeZone": "America/New_York"} + */ +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 { parseDate } from '@ghostfolio/common/helper'; + +import { DataSource } from '@prisma/client'; + +import { BenchmarksService } from './benchmarks.service'; + +describe('BenchmarksService in a time zone behind UTC', () => { + let benchmarksService: BenchmarksService; + + beforeEach(() => { + benchmarksService = new BenchmarksService( + new BenchmarkService(null, null, null, null, null, null), + { + getExchangeRatesByCurrency: jest.fn().mockResolvedValue({}) + } as unknown as ExchangeRateDataService, + { + getRange: jest.fn().mockResolvedValue([]), + // The market data is stored at midnight (UTC) + marketDataItems: jest.fn().mockResolvedValue([ + { date: new Date('2010-07-18T00:00:00.000Z'), marketPrice: 50 }, + { date: new Date('2010-07-19T00:00:00.000Z'), marketPrice: 100 } + ]) + } as unknown as MarketDataService, + { + getPerformance: jest.fn().mockResolvedValue({ + chart: [{ date: '2010-07-18' }, { date: '2010-07-19' }] + }) + } as unknown as PortfolioService, + { + get: jest.fn().mockResolvedValue({ currency: 'USD', marketPrice: 200 }) + } as unknown as SymbolService + ); + }); + + describe('getMarketDataForUser', () => { + const getMarketDataForUser = ({ + endDate = parseDate('2010-07-19') + }: { endDate?: Date } = {}) => { + return benchmarksService.getMarketDataForUser({ + endDate, + dataSource: DataSource.COINGECKO, + dateRange: 'max', + startDate: parseDate('2010-07-18'), + symbol: 'bitcoin', + userId: 'user-id', + userSettings: { baseCurrency: 'USD' } + }); + }; + + it('returns the dates of the market data without a shift', async () => { + await expect(getMarketDataForUser()).resolves.toEqual({ + marketData: [ + { date: '2010-07-18', value: 0 }, + { date: '2010-07-19', value: 100 } + ] + }); + }); + + it('returns the end date after the market data without a shift', async () => { + await expect( + getMarketDataForUser({ endDate: parseDate('2010-07-20') }) + ).resolves.toEqual({ + marketData: [ + { date: '2010-07-18', value: 0 }, + { date: '2010-07-19', value: 100 }, + { date: '2010-07-20', value: 300 } + ] + }); + }); + }); +}); diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts index 5104f01f35..066516236a 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts @@ -12,6 +12,7 @@ import { } from '@ghostfolio/common/interfaces'; import { DateRange } from '@ghostfolio/common/types'; +import { utc } from '@date-fns/utc'; import { Injectable, Logger } from '@nestjs/common'; import { format, isSameDay, min } from 'date-fns'; import { isNumber } from 'lodash-es'; @@ -104,7 +105,8 @@ export class BenchmarksService { this.logger.error( `No historical market data has been found for ${symbol} (${dataSource}) since ${format( baselineDate, - DATE_FORMAT + DATE_FORMAT, + { in: utc } )}` ); @@ -125,7 +127,8 @@ export class BenchmarksService { this.logger.error( `No historical market data has been found for ${symbol} (${dataSource}) at ${format( baselineDate, - DATE_FORMAT + DATE_FORMAT, + { in: utc } )}` ); @@ -135,8 +138,9 @@ export class BenchmarksService { this.logger.warn( `The market data of ${symbol} (${dataSource}) starts at ${format( firstMarketDataItem.date, - DATE_FORMAT - )}, after the baseline date ${format(baselineDate, DATE_FORMAT)}` + DATE_FORMAT, + { in: utc } + )}, after the baseline date ${format(baselineDate, DATE_FORMAT, { in: utc })}` ); } @@ -147,6 +151,8 @@ export class BenchmarksService { targetCurrency: userCurrency }); + // TODO: Format the keys of the exchange rates in UTC, together with the + // market data in the portfolio calculator const exchangeRateAtStartDate = exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ format(firstMarketDataItem.date, DATE_FORMAT) @@ -164,7 +170,7 @@ export class BenchmarksService { : 1; marketData.push({ - date: format(marketDataItem.date, DATE_FORMAT), + date: format(marketDataItem.date, DATE_FORMAT, { in: utc }), value: marketPriceAtStartDate === 0 ? 0