From 39f315aba074f848336b8a121d9d2f0d3d0a2770 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 11 Sep 2021 20:16:53 +0200 Subject: [PATCH] Feature/add annualized performance (#364) * Add annualized performance * Update changelog --- CHANGELOG.md | 3 + .../app/portfolio/portfolio.service.spec.ts | 62 +++++++++++++++++++ .../src/app/portfolio/portfolio.service.ts | 25 +++++++- .../portfolio-summary.component.html | 15 ++++- .../interfaces/portfolio-summary.interface.ts | 1 + 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio.service.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f16e2d3..a59a7cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the annualized performance to the portfolio summary tab on the home page - Added the Ghostfolio Slack channel to the about page ## 1.51.0 - 11.09.2021 diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts new file mode 100644 index 00000000..6128fa7a --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -0,0 +1,62 @@ +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 7d963045..55ee5ee0 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -47,6 +47,7 @@ import { } from '@prisma/client'; import Big from 'big.js'; import { + differenceInDays, endOfToday, format, isAfter, @@ -58,7 +59,7 @@ import { subDays, subYears } from 'date-fns'; -import { isEmpty } from 'lodash'; +import { isEmpty, isNumber } from 'lodash'; import { HistoricalDataItem, @@ -80,6 +81,21 @@ 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 { @@ -715,6 +731,12 @@ 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); @@ -726,6 +748,7 @@ export class PortfolioService { return { ...performanceInformation.performance, + annualizedPerformancePercent, fees, firstOrderDate, netWorth, diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index d2551e5f..9bc6d59c 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -146,7 +146,7 @@

-
Net Worth
+
Net Worth
+
+
Annualized Performance
+
+ +
+
diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index e34bd0e6..8a771c55 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -1,6 +1,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface'; export interface PortfolioSummary extends PortfolioPerformance { + annualizedPerformancePercent: number; cash: number; committedFunds: number; fees: number;