Browse Source

Support benchmarks whose market data starts later than portfolio

pull/8106/head
Thomas Kaul 5 days ago
parent
commit
73542572c9
  1. 66
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.spec.ts
  2. 35
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

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

@ -11,9 +11,10 @@ import { BenchmarksService } from './benchmarks.service';
describe('BenchmarksService', () => {
let benchmarksService: BenchmarksService;
let get: jest.Mock;
let getExchangeRatesByCurrency: jest.Mock;
let getPerformance: jest.Mock;
let get: jest.Mock;
let getRangeCount: jest.Mock;
let marketDataItems: jest.Mock;
beforeEach(() => {
@ -27,23 +28,26 @@ describe('BenchmarksService', () => {
{ date: '2010-07-19' }
]
});
getRangeCount = jest.fn().mockResolvedValue(0);
marketDataItems = jest.fn();
benchmarksService = new BenchmarksService(
new BenchmarkService(null, null, null, null, null, null),
{ getExchangeRatesByCurrency } as unknown as ExchangeRateDataService,
{ marketDataItems } as unknown as MarketDataService,
{ getRangeCount, marketDataItems } as unknown as MarketDataService,
{ getPerformance } as unknown as PortfolioService,
{ get } as unknown as SymbolService
);
});
describe('getMarketDataForUser', () => {
const getMarketDataForUser = () => {
const getMarketDataForUser = ({
endDate = parseDate('2010-07-19')
}: { endDate?: Date } = {}) => {
return benchmarksService.getMarketDataForUser({
endDate,
dataSource: DataSource.COINGECKO,
dateRange: 'max',
endDate: parseDate('2010-07-19'),
startDate: parseDate('2009-01-02'),
symbol: 'bitcoin',
userId: 'user-id',
@ -67,9 +71,11 @@ describe('BenchmarksService', () => {
{ date: '2010-07-19', value: 100 }
]
});
expect(getRangeCount).not.toHaveBeenCalled();
});
it('returns 0% before the market data of the benchmark starts', async () => {
it('returns no values before the market data of the benchmark starts', async () => {
marketDataItems.mockResolvedValue([
{ date: new Date('2010-07-18'), marketPrice: 50 },
{ date: new Date('2010-07-19'), marketPrice: 100 }
@ -77,12 +83,17 @@ describe('BenchmarksService', () => {
await expect(getMarketDataForUser()).resolves.toEqual({
marketData: [
{ date: '2009-01-01', value: 0 },
{ date: '2009-06-01', value: 0 },
{ date: '2010-07-18', value: 0 },
{ date: '2010-07-19', value: 100 }
]
});
expect(getRangeCount).toHaveBeenCalledWith({
assetProfileIdentifiers: [
{ dataSource: DataSource.COINGECKO, symbol: 'bitcoin' }
],
dateQuery: { lt: new Date('2009-01-01') }
});
});
it('uses the exchange rate at the start of the market data', async () => {
@ -102,14 +113,51 @@ describe('BenchmarksService', () => {
await expect(getMarketDataForUser()).resolves.toEqual({
marketData: [
{ date: '2009-01-01', value: 0 },
{ date: '2009-06-01', value: 0 },
{ date: '2010-07-18', value: 0 },
{ date: '2010-07-19', value: 200 }
]
});
});
it('calculates the value at the end date since the start of the market data', async () => {
getExchangeRatesByCurrency.mockResolvedValue({
USDCHF: {
'2009-01-01': 2,
'2010-07-18': 1,
'2010-07-19': 1,
'2010-07-20': 1.5
}
});
marketDataItems.mockResolvedValue([
{ date: new Date('2010-07-18'), marketPrice: 50 },
{ date: new Date('2010-07-19'), marketPrice: 100 }
]);
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: 500 }
]
});
});
it('returns no market data if the market data at the baseline date is missing', async () => {
getRangeCount.mockResolvedValue(1);
marketDataItems.mockResolvedValue([
{ date: new Date('2010-07-18'), marketPrice: 50 },
{ date: new Date('2010-07-19'), marketPrice: 100 }
]);
await expect(getMarketDataForUser()).resolves.toEqual({
marketData: []
});
});
it('returns no market data if the benchmark has none', async () => {
marketDataItems.mockResolvedValue([]);

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

@ -13,7 +13,7 @@ import {
import { DateRange } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { format, isBefore, isSameDay, min } from 'date-fns';
import { format, isSameDay, min } from 'date-fns';
import { isNumber } from 'lodash-es';
@Injectable()
@ -95,8 +95,8 @@ export class BenchmarksService {
const baselineDate = resetHours(parseDate(chart[0].date));
// The market data of the benchmark can start after the baseline date.
// In this case, the first market data item is the start and the value
// before it is 0%.
// In this case, the first market data item is the start and there is no
// value before it.
const [firstMarketDataItem] = marketDataItems;
const marketPriceAtStartDate = firstMarketDataItem?.marketPrice;
@ -111,6 +111,27 @@ export class BenchmarksService {
return { marketData };
}
if (!isSameDay(firstMarketDataItem.date, baselineDate)) {
// If market data exists before the baseline date, the market data at
// the baseline date is missing and the benchmark does not start later
const marketDataCountBeforeBaselineDate =
await this.marketDataService.getRangeCount({
assetProfileIdentifiers: [{ dataSource, symbol }],
dateQuery: { lt: baselineDate }
});
if (marketDataCountBeforeBaselineDate > 0) {
this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
baselineDate,
DATE_FORMAT
)}`
);
return { marketData };
}
}
const exchangeRates =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: [currentSymbolItem.currency],
@ -123,14 +144,6 @@ export class BenchmarksService {
format(firstMarketDataItem.date, DATE_FORMAT)
];
for (const { date } of chart) {
if (!isBefore(resetHours(parseDate(date)), firstMarketDataItem.date)) {
break;
}
marketData.push({ date, value: 0 });
}
for (const marketDataItem of marketDataItems) {
const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[

Loading…
Cancel
Save