From 6457039654ecae87b23cacdbd87f770e337cf3f1 Mon Sep 17 00:00:00 2001 From: Valentin Zickner Date: Sat, 11 Sep 2021 21:28:54 +0200 Subject: [PATCH] optimize annual performance calculation --- .../interfaces/current-positions.interface.ts | 1 + .../portfolio/portfolio-calculator.spec.ts | 62 +++++++++++++++++++ .../src/app/portfolio/portfolio-calculator.ts | 38 +++++++++++- .../app/portfolio/portfolio.service.spec.ts | 62 ------------------- .../src/app/portfolio/portfolio.service.ts | 26 ++------ .../portfolio-performance.interface.ts | 1 + 6 files changed, 105 insertions(+), 85 deletions(-) delete mode 100644 apps/api/src/app/portfolio/portfolio.service.spec.ts diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts index e855f7e76..a02d2a6a6 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -6,6 +6,7 @@ export interface CurrentPositions { positions: TimelinePosition[]; grossPerformance: Big; grossPerformancePercentage: Big; + netAnnualizedPerformance: Big; netPerformance: Big; netPerformancePercentage: Big; currentValue: Big; diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 440139c8f..0545646c7 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -18,6 +18,7 @@ import { TimelinePeriod } from './interfaces/timeline-period.interface'; import { TimelineSpecification } from './interfaces/timeline-specification.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface'; import { PortfolioCalculator } from './portfolio-calculator'; +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; function mockGetValue(symbol: string, date: Date) { switch (symbol) { @@ -1147,6 +1148,7 @@ describe('PortfolioCalculator', () => { currentValue: new Big('3897.2'), grossPerformance: new Big('303.2'), grossPerformancePercentage: new Big('0.27537838148272398344'), + netAnnualizedPerformance: new Big('0.1412977563032074'), netPerformance: new Big('253.2'), netPerformancePercentage: new Big('0.2566937088951485493'), totalInvestment: new Big('2923.7'), @@ -2261,6 +2263,66 @@ describe('PortfolioCalculator', () => { ]); }); }); + + describe('annualized performance percentage', () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + + it('Get annualized performance', async () => { + expect( + portfolioCalculator + .getAnnualizedPerformancePercent({ + daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day + netPerformancePercent: new Big(0) + }) + .toNumber() + ).toEqual(0); + + expect( + portfolioCalculator + .getAnnualizedPerformancePercent({ + daysInMarket: 0, + netPerformancePercent: new Big(0) + }) + .toNumber() + ).toEqual(0); + + /** + * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html + */ + expect( + portfolioCalculator + .getAnnualizedPerformancePercent({ + daysInMarket: 65, // < 1 year + netPerformancePercent: new Big(0.1025) + }) + .toNumber() + ).toBeCloseTo(0.729705); + + expect( + portfolioCalculator + .getAnnualizedPerformancePercent({ + daysInMarket: 365, // 1 year + netPerformancePercent: new Big(0.05) + }) + .toNumber() + ).toBeCloseTo(0.05); + + /** + * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation + */ + expect( + portfolioCalculator + .getAnnualizedPerformancePercent({ + daysInMarket: 575, // > 1 year + netPerformancePercent: new Big(0.2374) + }) + .toNumber() + ).toBeCloseTo(0.145); + }); + }); }); const ordersMixedSymbols: PortfolioOrder[] = [ diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 94829e7b5..259c3e128 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -7,6 +7,7 @@ import { addDays, addMonths, addYears, + differenceInDays, endOfDay, format, isAfter, @@ -14,7 +15,7 @@ import { max, min } from 'date-fns'; -import { flatten } from 'lodash'; +import { flatten, isNumber } from 'lodash'; import { CurrentRateService } from './current-rate.service'; import { CurrentPositions } from './interfaces/current-positions.interface'; @@ -118,6 +119,7 @@ export class PortfolioCalculator { hasErrors: false, grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), + netAnnualizedPerformance: new Big(0), netPerformance: new Big(0), netPerformancePercentage: new Big(0), positions: [], @@ -410,6 +412,11 @@ export class PortfolioCalculator { let netPerformance = new Big(0); let netPerformancePercentage = new Big(0); let completeInitialValue = new Big(0); + let netAnnualizedPerformance = new Big(0); + + // use Date.now() to use the mock for today + const today = new Date(Date.now()); + for (const currentPosition of positions) { if (currentPosition.marketPrice) { currentValue = currentValue.add( @@ -437,6 +444,15 @@ export class PortfolioCalculator { grossPerformancePercentage = grossPerformancePercentage.plus( currentPosition.grossPerformancePercentage.mul(currentInitialValue) ); + netAnnualizedPerformance = netAnnualizedPerformance.plus( + this.getAnnualizedPerformancePercent({ + daysInMarket: differenceInDays( + today, + parseDate(currentPosition.firstBuyDate) + ), + netPerformancePercent: currentPosition.netPerformancePercentage + }).mul(currentInitialValue) + ); netPerformancePercentage = netPerformancePercentage.plus( currentPosition.netPerformancePercentage.mul(currentInitialValue) ); @@ -453,6 +469,8 @@ export class PortfolioCalculator { grossPerformancePercentage.div(completeInitialValue); netPerformancePercentage = netPerformancePercentage.div(completeInitialValue); + netAnnualizedPerformance = + netAnnualizedPerformance.div(completeInitialValue); } return { @@ -460,6 +478,7 @@ export class PortfolioCalculator { grossPerformance, grossPerformancePercentage, hasErrors, + netAnnualizedPerformance, netPerformance, netPerformancePercentage, totalInvestment @@ -599,4 +618,21 @@ export class PortfolioCalculator { !isBefore(currentDate, parseDate(timelineSpecification[i + 1].start)) ); } + + public getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent + }: { + daysInMarket: number; + netPerformancePercent: Big; + }): Big { + if (isNumber(daysInMarket) && daysInMarket > 0) { + const exponent = new Big(365).div(daysInMarket).toNumber(); + return new Big( + Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) + ).minus(1); + } + + return new Big(0); + } } diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts deleted file mode 100644 index 6128fa7a2..000000000 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { PortfolioService } from './portfolio.service'; - -describe('PortfolioService', () => { - let portfolioService: PortfolioService; - - beforeAll(async () => { - portfolioService = new PortfolioService( - null, - null, - null, - null, - null, - null, - null, - null, - null - ); - }); - - it('Get annualized performance', async () => { - expect( - portfolioService.getAnnualizedPerformancePercent({ - daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day - netPerformancePercent: 0 - }) - ).toEqual(0); - - expect( - portfolioService.getAnnualizedPerformancePercent({ - daysInMarket: 0, - netPerformancePercent: 0 - }) - ).toEqual(0); - - /** - * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html - */ - expect( - portfolioService.getAnnualizedPerformancePercent({ - daysInMarket: 65, // < 1 year - netPerformancePercent: 0.1025 - }) - ).toBeCloseTo(0.729705); - - expect( - portfolioService.getAnnualizedPerformancePercent({ - daysInMarket: 365, // 1 year - netPerformancePercent: 0.05 - }) - ).toBeCloseTo(0.05); - - /** - * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation - */ - expect( - portfolioService.getAnnualizedPerformancePercent({ - daysInMarket: 575, // > 1 year - netPerformancePercent: 0.2374 - }) - ).toBeCloseTo(0.145); - }); -}); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 55ee5ee00..0375c6b31 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -81,21 +81,6 @@ export class PortfolioService { private readonly symbolProfileService: SymbolProfileService ) {} - public getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent - }: { - daysInMarket: number; - netPerformancePercent: number; - }) { - if (isNumber(daysInMarket) && daysInMarket > 0) { - const exponent = new Big(365).div(daysInMarket).toNumber(); - return Math.pow(1 + netPerformancePercent, exponent) - 1; - } - - return 0; - } - public async getInvestments( aImpersonationId: string ): Promise { @@ -573,6 +558,7 @@ export class PortfolioService { return { hasErrors: false, performance: { + annualizedPerformancePercent: 0, currentGrossPerformance: 0, currentGrossPerformancePercent: 0, currentNetPerformance: 0, @@ -591,6 +577,8 @@ export class PortfolioService { ); const hasErrors = currentPositions.hasErrors; + const annualizedPerformancePercent = + currentPositions.netAnnualizedPerformance.toNumber(); const currentValue = currentPositions.currentValue.toNumber(); const currentGrossPerformance = currentPositions.grossPerformance.toNumber(); @@ -603,6 +591,7 @@ export class PortfolioService { return { hasErrors: currentPositions.hasErrors || hasErrors, performance: { + annualizedPerformancePercent, currentGrossPerformance, currentGrossPerformancePercent, currentNetPerformance, @@ -731,12 +720,6 @@ export class PortfolioService { const fees = this.getFees(orders); const firstOrderDate = orders[0]?.date; - const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays(new Date(), firstOrderDate), - netPerformancePercent: - performanceInformation.performance.currentNetPerformancePercent - }); - const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY); const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL); @@ -748,7 +731,6 @@ export class PortfolioService { return { ...performanceInformation.performance, - annualizedPerformancePercent, fees, firstOrderDate, netWorth, diff --git a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts index 2051be7fd..3a2770786 100644 --- a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts @@ -1,4 +1,5 @@ export interface PortfolioPerformance { + annualizedPerformancePercent: number; currentGrossPerformance: number; currentGrossPerformancePercent: number; currentNetPerformance: number;