Browse Source

Support benchmarks whose market data starts later than portfolio

pull/8106/head
Thomas Kaul 5 days ago
parent
commit
64161b821a
  1. 121
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.spec.ts
  2. 38
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

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

@ -0,0 +1,121 @@
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', () => {
let benchmarksService: BenchmarksService;
let getExchangeRatesByCurrency: jest.Mock;
let getPerformance: jest.Mock;
let get: jest.Mock;
let marketDataItems: jest.Mock;
beforeEach(() => {
get = jest.fn().mockResolvedValue({ currency: 'USD', marketPrice: 200 });
getExchangeRatesByCurrency = jest.fn().mockResolvedValue({});
getPerformance = jest.fn().mockResolvedValue({
chart: [
{ date: '2009-01-01' },
{ date: '2009-06-01' },
{ date: '2010-07-18' },
{ date: '2010-07-19' }
]
});
marketDataItems = jest.fn();
benchmarksService = new BenchmarksService(
new BenchmarkService(null, null, null, null, null, null),
{ getExchangeRatesByCurrency } as unknown as ExchangeRateDataService,
{ marketDataItems } as unknown as MarketDataService,
{ getPerformance } as unknown as PortfolioService,
{ get } as unknown as SymbolService
);
});
describe('getMarketDataForUser', () => {
const getMarketDataForUser = () => {
return benchmarksService.getMarketDataForUser({
dataSource: DataSource.COINGECKO,
dateRange: 'max',
endDate: parseDate('2010-07-19'),
startDate: parseDate('2009-01-02'),
symbol: 'bitcoin',
userId: 'user-id',
userSettings: { baseCurrency: 'CHF' }
});
};
it('calculates the change since the baseline date', async () => {
marketDataItems.mockResolvedValue([
{ date: new Date('2009-01-01'), marketPrice: 50 },
{ date: new Date('2009-06-01'), marketPrice: 25 },
{ date: new Date('2010-07-18'), marketPrice: 75 },
{ date: new Date('2010-07-19'), marketPrice: 100 }
]);
await expect(getMarketDataForUser()).resolves.toEqual({
marketData: [
{ date: '2009-01-01', value: 0 },
{ date: '2009-06-01', value: -50 },
{ date: '2010-07-18', value: 50 },
{ date: '2010-07-19', value: 100 }
]
});
});
it('returns 0% 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 }
]);
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 }
]
});
});
it('uses the exchange rate at the start of the market data', async () => {
getExchangeRatesByCurrency.mockResolvedValue({
USDCHF: {
'2009-01-01': 2,
'2009-06-01': 2,
'2010-07-18': 1,
'2010-07-19': 1.5
}
});
marketDataItems.mockResolvedValue([
{ date: new Date('2010-07-18'), marketPrice: 50 },
{ date: new Date('2010-07-19'), marketPrice: 100 }
]);
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('returns no market data if the benchmark has none', async () => {
marketDataItems.mockResolvedValue([]);
await expect(getMarketDataForUser()).resolves.toEqual({
marketData: []
});
});
});
});

38
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, isSameDay, min } from 'date-fns'; import { format, isBefore, isSameDay, min } from 'date-fns';
import { isNumber } from 'lodash-es'; import { isNumber } from 'lodash-es';
@Injectable() @Injectable()
@ -94,6 +94,23 @@ 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.
// In this case, the first market data item is the start and the value
// before it is 0%.
const [firstMarketDataItem] = marketDataItems;
const marketPriceAtStartDate = firstMarketDataItem?.marketPrice;
if (!marketPriceAtStartDate) {
this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) since ${format(
baselineDate,
DATE_FORMAT
)}`
);
return { marketData };
}
const exchangeRates = const exchangeRates =
await this.exchangeRateDataService.getExchangeRatesByCurrency({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: [currentSymbolItem.currency], currencies: [currentSymbolItem.currency],
@ -103,22 +120,15 @@ export class BenchmarksService {
const exchangeRateAtStartDate = const exchangeRateAtStartDate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(baselineDate, DATE_FORMAT) format(firstMarketDataItem.date, DATE_FORMAT)
]; ];
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => { for (const { date } of chart) {
return isSameDay(date, baselineDate); if (!isBefore(resetHours(parseDate(date)), firstMarketDataItem.date)) {
})?.marketPrice; break;
}
if (!marketPriceAtStartDate) {
this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
baselineDate,
DATE_FORMAT
)}`
);
return { marketData }; marketData.push({ date, value: 0 });
} }
for (const marketDataItem of marketDataItems) { for (const marketDataItem of marketDataItems) {

Loading…
Cancel
Save