optimize annual performance calculation (#367)

* Optimize annual performance calculation

* Update changelog

Co-authored-by: Valentin Zickner <github@zickner.ch>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
pull/368/head
Valentin Zickner 3 years ago committed by GitHub
parent ba926ffcf2
commit d2aeeb3e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -6,6 +6,7 @@ export interface CurrentPositions {
positions: TimelinePosition[];
grossPerformance: Big;
grossPerformancePercentage: Big;
netAnnualizedPerformance: Big;
netPerformance: Big;
netPerformancePercentage: Big;
currentValue: Big;

@ -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[] = [

@ -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

@ -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);
});
});

@ -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<InvestmentItem[]> {
@ -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,

@ -1,4 +1,5 @@
export interface PortfolioPerformance {
annualizedPerformancePercent: number;
currentGrossPerformance: number;
currentGrossPerformancePercent: number;
currentNetPerformance: number;

Loading…
Cancel
Save