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
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the annualized performance to the portfolio summary tab on the home page
|
||||||
- Added the Ghostfolio Slack channel to the about page
|
- Added the Ghostfolio Slack channel to the about page
|
||||||
|
|
||||||
## 1.51.0 - 11.09.2021
|
## 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';
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
|
differenceInDays,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -58,7 +59,7 @@ import {
|
|||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty, isNumber } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
@ -80,6 +81,21 @@ 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[]> {
|
||||||
@ -715,6 +731,12 @@ 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);
|
||||||
|
|
||||||
@ -726,6 +748,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
...performanceInformation.performance,
|
||||||
|
annualizedPerformancePercent,
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
netWorth,
|
netWorth,
|
||||||
|
@ -146,7 +146,7 @@
|
|||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<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">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -156,4 +156,17 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PortfolioPerformance } from './portfolio-performance.interface';
|
import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||||
|
|
||||||
export interface PortfolioSummary extends PortfolioPerformance {
|
export interface PortfolioSummary extends PortfolioPerformance {
|
||||||
|
annualizedPerformancePercent: number;
|
||||||
cash: number;
|
cash: number;
|
||||||
committedFunds: number;
|
committedFunds: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user