From f3cb92a704811b3c105a519aa87c261914261672 Mon Sep 17 00:00:00 2001 From: Daniel Devaud Date: Sat, 11 May 2024 17:06:57 +0200 Subject: [PATCH] Refactor time weighted graph --- .../portfolio-calculator.ts | 98 +++++++++++-------- .../portfolio-calculator.factory.ts | 2 +- .../calculator/portfolio-calculator.ts | 4 +- .../src/app/portfolio/portfolio.service.ts | 51 +--------- 4 files changed, 63 insertions(+), 92 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts index 49faa0b28..12de53cdc 100644 --- a/apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts @@ -1,14 +1,14 @@ -import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; -import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { - HistoricalDataItem, - SymbolMetrics, - UniqueAsset -} from '@ghostfolio/common/interfaces'; -import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; + getFactor, + getInterval +} from '@ghostfolio/api/helper/portfolio.helper'; +import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; +import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; +import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; +import { DateRange } from '@ghostfolio/common/types'; import { Big } from 'big.js'; -import { addDays, eachDayOfInterval, format } from 'date-fns'; +import { addDays, differenceInDays, eachDayOfInterval, format } from 'date-fns'; import { PortfolioOrder } from '../../interfaces/portfolio-order.interface'; import { TWRPortfolioCalculator } from '../twr/portfolio-calculator'; @@ -17,44 +17,60 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator { private holdings: { [date: string]: { [symbol: string]: Big } } = {}; private holdingCurrencies: { [symbol: string]: string } = {}; - protected calculateOverallPerformance( - positions: TimelinePosition[] - ): PortfolioSnapshot { - return super.calculateOverallPerformance(positions); - } - - protected getSymbolMetrics({ - dataSource, - end, - exchangeRates, - isChartMode = false, - marketSymbolMap, - start, - step = 1, - symbol + public async getChart({ + dateRange = 'max', + withDataDecimation = true, + withTimeWeightedReturn = false }: { - end: Date; - exchangeRates: { [dateString: string]: number }; - isChartMode?: boolean; - marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - }; - start: Date; - step?: number; - } & UniqueAsset): SymbolMetrics { - return super.getSymbolMetrics({ - dataSource, - end, - exchangeRates, - isChartMode, - marketSymbolMap, - start, + dateRange?: DateRange; + withDataDecimation?: boolean; + withTimeWeightedReturn?: boolean; + }): Promise { + const { endDate, startDate } = getInterval(dateRange, this.getStartDate()); + + const daysInMarket = differenceInDays(endDate, startDate) + 1; + const step = withDataDecimation + ? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) + : 1; + + let item = super.getChartData({ step, - symbol + end: endDate, + start: startDate }); + + if (!withTimeWeightedReturn) { + return item; + } + + if (withTimeWeightedReturn) { + let timeWeighted = await this.getTimeWeightedChartData({ + step, + end: endDate, + start: startDate + }); + + return item.then((data) => { + return data.map((item) => { + let timeWeightedItem = timeWeighted.find( + (timeWeightedItem) => timeWeightedItem.date === item.date + ); + if (timeWeightedItem) { + item.timeWeightedPerformance = + timeWeightedItem.timeWeightedPerformance; + item.timeWeightedPerformanceWithCurrencyEffect = + timeWeightedItem.timeWeightedPerformanceWithCurrencyEffect; + } + + return item; + }); + }); + } + + return item; } - public override async getChartData({ + private async getTimeWeightedChartData({ end = new Date(Date.now()), start, step = 1 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts index 89181ea12..38e2b3eaf 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -64,7 +64,7 @@ export class PortfolioCalculatorFactory { redisCacheService: this.redisCacheService }); case PerformanceCalculationType.TWR: - return new TWRPortfolioCalculator({ + return new CPRPortfolioCalculator({ accountBalanceItems, activities, currency, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 274f4cffa..2bee83c41 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -424,10 +424,12 @@ export abstract class PortfolioCalculator { public async getChart({ dateRange = 'max', - withDataDecimation = true + withDataDecimation = true, + withTimeWeightedReturn = false }: { dateRange?: DateRange; withDataDecimation?: boolean; + withTimeWeightedReturn?: boolean; }): Promise { const { endDate, startDate } = getInterval(dateRange, this.getStartDate()); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 81601dbb3..3da510d1f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1282,18 +1282,9 @@ export class PortfolioService { let currentNetWorth = 0; let items = await portfolioCalculator.getChart({ - dateRange - }); - - items = await this.calculatedTimeWeightedPerformance( - calculateTimeWeightedPerformance, - activities, dateRange, - userId, - userCurrency, - filters, - items - ); + withTimeWeightedReturn: calculateTimeWeightedPerformance + }); const itemOfToday = items.find(({ date }) => { return date === format(new Date(), DATE_FORMAT); @@ -1342,44 +1333,6 @@ export class PortfolioService { }; } - private async calculatedTimeWeightedPerformance( - calculateTimeWeightedPerformance: boolean, - activities: Activity[], - dateRange: string, - userId: string, - userCurrency: string, - filters: Filter[], - items: HistoricalDataItem[] - ) { - if (calculateTimeWeightedPerformance) { - const portfolioCalculatorCPR = this.calculatorFactory.createCalculator({ - activities, - dateRange, - userId, - calculationType: PerformanceCalculationType.CPR, - currency: userCurrency, - hasFilters: filters?.length > 0, - isExperimentalFeatures: - this.request.user.Settings.settings.isExperimentalFeatures - }); - let timeWeightedInvestmentItems = await portfolioCalculatorCPR.getChart({ - dateRange - }); - - items = items.map((item) => { - let matchingItem = timeWeightedInvestmentItems.find( - (timeWeightedInvestmentItem) => - timeWeightedInvestmentItem.date === item.date - ); - item.timeWeightedPerformance = matchingItem.netPerformanceInPercentage; - item.timeWeightedPerformanceWithCurrencyEffect = - matchingItem.netPerformanceInPercentageWithCurrencyEffect; - return item; - }); - } - return items; - } - @LogPerformance public async getReport(impersonationId: string): Promise { const userId = await this.getUserId(impersonationId, this.request.user.id);