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>
This commit is contained in:
parent
ba926ffcf2
commit
d2aeeb3e88
@ -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/),
|
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).
|
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
|
## 1.52.0 - 11.09.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -6,6 +6,7 @@ export interface CurrentPositions {
|
|||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
netAnnualizedPerformance: Big;
|
||||||
netPerformance: Big;
|
netPerformance: Big;
|
||||||
netPerformancePercentage: Big;
|
netPerformancePercentage: Big;
|
||||||
currentValue: 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 { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
@ -1147,6 +1148,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('3897.2'),
|
currentValue: new Big('3897.2'),
|
||||||
grossPerformance: new Big('303.2'),
|
grossPerformance: new Big('303.2'),
|
||||||
grossPerformancePercentage: new Big('0.27537838148272398344'),
|
grossPerformancePercentage: new Big('0.27537838148272398344'),
|
||||||
|
netAnnualizedPerformance: new Big('0.1412977563032074'),
|
||||||
netPerformance: new Big('253.2'),
|
netPerformance: new Big('253.2'),
|
||||||
netPerformancePercentage: new Big('0.2566937088951485493'),
|
netPerformancePercentage: new Big('0.2566937088951485493'),
|
||||||
totalInvestment: new Big('2923.7'),
|
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[] = [
|
const ordersMixedSymbols: PortfolioOrder[] = [
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
addDays,
|
addDays,
|
||||||
addMonths,
|
addMonths,
|
||||||
addYears,
|
addYears,
|
||||||
|
differenceInDays,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -14,7 +15,7 @@ import {
|
|||||||
max,
|
max,
|
||||||
min
|
min
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten, isNumber } from 'lodash';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { CurrentPositions } from './interfaces/current-positions.interface';
|
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[] {
|
public getTransactionPoints(): TransactionPoint[] {
|
||||||
return this.transactionPoints;
|
return this.transactionPoints;
|
||||||
}
|
}
|
||||||
@ -118,6 +136,7 @@ export class PortfolioCalculator {
|
|||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
|
netAnnualizedPerformance: new Big(0),
|
||||||
netPerformance: new Big(0),
|
netPerformance: new Big(0),
|
||||||
netPerformancePercentage: new Big(0),
|
netPerformancePercentage: new Big(0),
|
||||||
positions: [],
|
positions: [],
|
||||||
@ -410,6 +429,11 @@ export class PortfolioCalculator {
|
|||||||
let netPerformance = new Big(0);
|
let netPerformance = new Big(0);
|
||||||
let netPerformancePercentage = new Big(0);
|
let netPerformancePercentage = new Big(0);
|
||||||
let completeInitialValue = 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) {
|
for (const currentPosition of positions) {
|
||||||
if (currentPosition.marketPrice) {
|
if (currentPosition.marketPrice) {
|
||||||
currentValue = currentValue.add(
|
currentValue = currentValue.add(
|
||||||
@ -437,6 +461,15 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage = grossPerformancePercentage.plus(
|
grossPerformancePercentage = grossPerformancePercentage.plus(
|
||||||
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
|
netAnnualizedPerformance = netAnnualizedPerformance.plus(
|
||||||
|
this.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: differenceInDays(
|
||||||
|
today,
|
||||||
|
parseDate(currentPosition.firstBuyDate)
|
||||||
|
),
|
||||||
|
netPerformancePercent: currentPosition.netPerformancePercentage
|
||||||
|
}).mul(currentInitialValue)
|
||||||
|
);
|
||||||
netPerformancePercentage = netPerformancePercentage.plus(
|
netPerformancePercentage = netPerformancePercentage.plus(
|
||||||
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
@ -453,6 +486,8 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage.div(completeInitialValue);
|
grossPerformancePercentage.div(completeInitialValue);
|
||||||
netPerformancePercentage =
|
netPerformancePercentage =
|
||||||
netPerformancePercentage.div(completeInitialValue);
|
netPerformancePercentage.div(completeInitialValue);
|
||||||
|
netAnnualizedPerformance =
|
||||||
|
netAnnualizedPerformance.div(completeInitialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -460,6 +495,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
|
netAnnualizedPerformance,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformancePercentage,
|
netPerformancePercentage,
|
||||||
totalInvestment
|
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
|
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(
|
public async getInvestments(
|
||||||
aImpersonationId: string
|
aImpersonationId: string
|
||||||
): Promise<InvestmentItem[]> {
|
): Promise<InvestmentItem[]> {
|
||||||
@ -573,6 +558,7 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
performance: {
|
performance: {
|
||||||
|
annualizedPerformancePercent: 0,
|
||||||
currentGrossPerformance: 0,
|
currentGrossPerformance: 0,
|
||||||
currentGrossPerformancePercent: 0,
|
currentGrossPerformancePercent: 0,
|
||||||
currentNetPerformance: 0,
|
currentNetPerformance: 0,
|
||||||
@ -591,6 +577,8 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasErrors = currentPositions.hasErrors;
|
const hasErrors = currentPositions.hasErrors;
|
||||||
|
const annualizedPerformancePercent =
|
||||||
|
currentPositions.netAnnualizedPerformance.toNumber();
|
||||||
const currentValue = currentPositions.currentValue.toNumber();
|
const currentValue = currentPositions.currentValue.toNumber();
|
||||||
const currentGrossPerformance =
|
const currentGrossPerformance =
|
||||||
currentPositions.grossPerformance.toNumber();
|
currentPositions.grossPerformance.toNumber();
|
||||||
@ -603,6 +591,7 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
|
annualizedPerformancePercent,
|
||||||
currentGrossPerformance,
|
currentGrossPerformance,
|
||||||
currentGrossPerformancePercent,
|
currentGrossPerformancePercent,
|
||||||
currentNetPerformance,
|
currentNetPerformance,
|
||||||
@ -731,12 +720,6 @@ export class PortfolioService {
|
|||||||
const fees = this.getFees(orders);
|
const fees = this.getFees(orders);
|
||||||
const firstOrderDate = orders[0]?.date;
|
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 totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
|
||||||
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
|
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
|
||||||
|
|
||||||
@ -748,7 +731,6 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
...performanceInformation.performance,
|
||||||
annualizedPerformancePercent,
|
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
netWorth,
|
netWorth,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export interface PortfolioPerformance {
|
export interface PortfolioPerformance {
|
||||||
|
annualizedPerformancePercent: number;
|
||||||
currentGrossPerformance: number;
|
currentGrossPerformance: number;
|
||||||
currentGrossPerformancePercent: number;
|
currentGrossPerformancePercent: number;
|
||||||
currentNetPerformance: number;
|
currentNetPerformance: number;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user