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

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

@ -13,7 +13,7 @@ import {
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; 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'; import { isNumber } from 'lodash-es';
@Injectable() @Injectable()
@ -95,8 +95,8 @@ export class BenchmarksService {
const baselineDate = resetHours(parseDate(chart[0].date)); const baselineDate = resetHours(parseDate(chart[0].date));
// The market data of the benchmark can start after the baseline 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 // In this case, the first market data item is the start and there is no
// before it is 0%. // value before it.
const [firstMarketDataItem] = marketDataItems; const [firstMarketDataItem] = marketDataItems;
const marketPriceAtStartDate = firstMarketDataItem?.marketPrice; const marketPriceAtStartDate = firstMarketDataItem?.marketPrice;
@ -111,6 +111,27 @@ export class BenchmarksService {
return { marketData }; 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 = const exchangeRates =
await this.exchangeRateDataService.getExchangeRatesByCurrency({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: [currentSymbolItem.currency], currencies: [currentSymbolItem.currency],
@ -123,14 +144,6 @@ export class BenchmarksService {
format(firstMarketDataItem.date, DATE_FORMAT) 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) { for (const marketDataItem of marketDataItems) {
const exchangeRate = const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[

Loading…
Cancel
Save