Feature/add annualized performance (#364)
* Add annualized performance * Update changelog
This commit is contained in:
parent
df2dfc20a1
commit
39f315aba0
@ -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
|
||||
|
62
apps/api/src/app/portfolio/portfolio.service.spec.ts
Normal file
62
apps/api/src/app/portfolio/portfolio.service.spec.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
@ -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<InvestmentItem[]> {
|
||||
@ -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,
|
||||
|
@ -146,7 +146,7 @@
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Net Worth</div>
|
||||
<div class="d-flex flex-grow-1 font-weight-bold" i18n>Net Worth</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
@ -156,4 +156,17 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1 ml-3" i18n>Annualized Performance</div>
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
|
||||
export interface PortfolioSummary extends PortfolioPerformance {
|
||||
annualizedPerformancePercent: number;
|
||||
cash: number;
|
||||
committedFunds: number;
|
||||
fees: number;
|
||||
|
Loading…
x
Reference in New Issue
Block a user