From aabfb39e8fb6251453e1e783539afe8473428f1e Mon Sep 17 00:00:00 2001 From: Valentin Zickner Date: Tue, 20 Jul 2021 22:17:37 +0200 Subject: [PATCH] optimize portfolio calculator to fetch all symbols for one day --- .../src/app/core/portfolio-calculator.spec.ts | 73 +++++++++++----- apps/api/src/app/core/portfolio-calculator.ts | 83 +++++++++++++------ 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/apps/api/src/app/core/portfolio-calculator.spec.ts b/apps/api/src/app/core/portfolio-calculator.spec.ts index cd7ae8bbb..f9f40aa40 100644 --- a/apps/api/src/app/core/portfolio-calculator.spec.ts +++ b/apps/api/src/app/core/portfolio-calculator.spec.ts @@ -1,6 +1,7 @@ import { CurrentRateService, - GetValueParams + GetValueParams, + GetValuesParams } from '@ghostfolio/api/app/core/current-rate.service'; import { PortfolioCalculator, @@ -11,7 +12,14 @@ import { import { OrderType } from '@ghostfolio/api/models/order-type'; import { Currency } from '@prisma/client'; import Big from 'big.js'; -import { differenceInCalendarDays, parse } from 'date-fns'; +import { + addDays, + differenceInCalendarDays, + endOfDay, + isBefore, + parse +} from 'date-fns'; +import { resetHours } from '@ghostfolio/common/helper'; function toYearMonthDay(date: Date) { const year = date.getFullYear(); @@ -32,6 +40,27 @@ function dateEqual(date1: Date, date2: Date) { ); } +function mockGetValue(symbol: string, date: Date) { + const today = new Date(); + if (symbol === 'VTI') { + if (dateEqual(today, date)) { + return { marketPrice: 213.32 }; + } else { + const startDate = parse('2019-02-01', 'yyyy-MM-dd', new Date()); + const daysInBetween = differenceInCalendarDays(date, startDate); + + const marketPrice = new Big('144.38').plus( + new Big('0.08').mul(daysInBetween) + ); + return { marketPrice: marketPrice.toNumber() }; + } + } else if (symbol === 'AMZN') { + return { marketPrice: 2021.99 }; + } else { + return { marketPrice: 0 }; + } +} + jest.mock('@ghostfolio/api/app/core/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -43,24 +72,30 @@ jest.mock('@ghostfolio/api/app/core/current-rate.service', () => { currency, userCurrency }: GetValueParams) => { - const today = new Date(); - if (symbol === 'VTI') { - if (dateEqual(today, date)) { - return Promise.resolve({ marketPrice: new Big('213.32') }); - } else { - const startDate = parse('2019-02-01', 'yyyy-MM-dd', new Date()); - const daysInBetween = differenceInCalendarDays(date, startDate); - - const marketPrice = new Big('144.38').plus( - new Big('0.08').mul(daysInBetween) - ); - return Promise.resolve({ marketPrice }); + return Promise.resolve(mockGetValue(symbol, date)); + }, + getValues: ({ + currencies, + dateRangeEnd, + dateRangeStart, + symbols, + userCurrency + }: GetValuesParams) => { + const result = []; + for ( + let date = resetHours(dateRangeStart); + isBefore(date, endOfDay(dateRangeEnd)); + date = addDays(date, 1) + ) { + for (const symbol of symbols) { + result.push({ + date, + symbol, + marketPrice: mockGetValue(symbol, date).marketPrice + }); } - } else if (symbol === 'AMZN') { - return Promise.resolve({ marketPrice: new Big('2021.99') }); } - - return Promise.resolve({ marketPrice: new Big('0') }); + return Promise.resolve(result); } }; }) @@ -545,7 +580,7 @@ describe('PortfolioCalculator', () => { quantity: new Big('25'), symbol: 'VTI', investment: new Big('4460.95'), - marketPrice: new Big('213.32'), + marketPrice: 213.32, transactionCount: 5, grossPerformance: new Big('872.05'), // 213.32*25-4460.95 grossPerformancePercentage: new Big('0.19548526659119694236') // 872.05/4460.95 diff --git a/apps/api/src/app/core/portfolio-calculator.ts b/apps/api/src/app/core/portfolio-calculator.ts index f0ceece4b..fb935ddc1 100644 --- a/apps/api/src/app/core/portfolio-calculator.ts +++ b/apps/api/src/app/core/portfolio-calculator.ts @@ -1,4 +1,7 @@ -import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; +import { + CurrentRateService, + GetValueObject +} from '@ghostfolio/api/app/core/current-rate.service'; import { OrderType } from '@ghostfolio/api/models/order-type'; import { Currency } from '@prisma/client'; import Big from 'big.js'; @@ -11,6 +14,7 @@ import { isBefore, parse } from 'date-fns'; +import { resetHours } from '@ghostfolio/common/helper'; const DATE_FORMAT = 'yyyy-MM-dd'; @@ -198,38 +202,65 @@ export class PortfolioCalculator { currentDate: Date ): Promise { let investment: Big = new Big(0); - const promises = []; + + let value = new Big(0); + const currentDateAsString = format(currentDate, DATE_FORMAT); if (j >= 0) { + const currencies: { [name: string]: Currency } = {}; + const symbols: string[] = []; + for (const item of this.transactionPoints[j].items) { + currencies[item.symbol] = item.currency; + symbols.push(item.symbol); investment = investment.add(item.investment); - promises.push( - this.currentRateService - .getValue({ - date: currentDate, - symbol: item.symbol, - currency: item.currency, - userCurrency: this.currency - }) - .then(({ marketPrice }) => new Big(marketPrice).mul(item.quantity)) - ); } - } - const result = await Promise.all(promises).catch((e) => { - console.error( - `failed to fetch info for date ${currentDate} with exception`, - e - ); - return null; - }); + let marketSymbols: GetValueObject[] = []; + if (symbols.length > 0) { + try { + marketSymbols = await this.currentRateService.getValues({ + dateRangeStart: resetHours(currentDate), + dateRangeEnd: resetHours(currentDate), + symbols, + currencies, + userCurrency: this.currency + }); + } catch (e) { + console.error( + `failed to fetch info for date ${currentDate} with exception`, + e + ); + return null; + } + } - if (result == null) { - return null; + const marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + } = {}; + for (const marketSymbol of marketSymbols) { + const date = format(marketSymbol.date, DATE_FORMAT); + if (!marketSymbolMap[date]) { + marketSymbolMap[date] = {}; + } + marketSymbolMap[date][marketSymbol.symbol] = new Big( + marketSymbol.marketPrice + ); + } + + for (const item of this.transactionPoints[j].items) { + if ( + !marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol) + ) { + return null; + } + value = value.add( + item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol]) + ); + } } - const value = result.reduce((a, b) => a.add(b), new Big(0)); return { - date: format(currentDate, DATE_FORMAT), + date: currentDateAsString, grossPerformance: value.minus(investment), investment, value @@ -310,9 +341,9 @@ export interface TimelineSpecification { export interface TimelinePeriod { date: string; - grossPerformance: number; + grossPerformance: Big; investment: Big; - value: number; + value: Big; } export interface PortfolioOrder {