diff --git a/CHANGELOG.md b/CHANGELOG.md index e71e17bdd..25d80a7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Optimized the annualized performance calculation + ## 1.52.0 - 11.09.2021 ### Added 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..d1f967fb2 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -1,3 +1,4 @@ +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { OrderType } from '@ghostfolio/api/models/order-type'; import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { Currency } from '@prisma/client'; @@ -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..1f2f57a7d 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'; @@ -103,6 +104,23 @@ export class PortfolioCalculator { } } + 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); + } + public getTransactionPoints(): TransactionPoint[] { return this.transactionPoints; } @@ -118,6 +136,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 +429,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 +461,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 +486,8 @@ export class PortfolioCalculator { grossPerformancePercentage.div(completeInitialValue); netPerformancePercentage = netPerformancePercentage.div(completeInitialValue); + netAnnualizedPerformance = + netAnnualizedPerformance.div(completeInitialValue); } return { @@ -460,6 +495,7 @@ export class PortfolioCalculator { grossPerformance, grossPerformancePercentage, hasErrors, + netAnnualizedPerformance, netPerformance, netPerformancePercentage, totalInvestment 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;