Feature/change color assignment by annualized performance in treemap chart (#3617)
* Change color assignment to annualized performance * Update changelog
This commit is contained in:
parent
7efda2f890
commit
fcc2ab1a48
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the color assignment in the chart of the holdings tab on the home page (experimental)
|
||||
- Improved the language localization for Catalan (`ca`)
|
||||
|
||||
## 2.99.0 - 2024-07-29
|
||||
|
@ -1,78 +0,0 @@
|
||||
import { Big } from 'big.js';
|
||||
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
|
||||
describe('PortfolioService', () => {
|
||||
let portfolioService: PortfolioService;
|
||||
|
||||
beforeAll(async () => {
|
||||
portfolioService = new PortfolioService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
describe('annualized performance percentage', () => {
|
||||
it('Get annualized performance', async () => {
|
||||
expect(
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||
netPerformancePercentage: new Big(0)
|
||||
})
|
||||
.toNumber()
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 0,
|
||||
netPerformancePercentage: new Big(0)
|
||||
})
|
||||
.toNumber()
|
||||
).toEqual(0);
|
||||
|
||||
/**
|
||||
* Source: https://www.readyratios.com/reference/analysis/annualized_rate.html
|
||||
*/
|
||||
expect(
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 65, // < 1 year
|
||||
netPerformancePercentage: new Big(0.1025)
|
||||
})
|
||||
.toNumber()
|
||||
).toBeCloseTo(0.729705);
|
||||
|
||||
expect(
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 365, // 1 year
|
||||
netPerformancePercentage: 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(
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 575, // > 1 year
|
||||
netPerformancePercentage: new Big(0.2374)
|
||||
})
|
||||
.toNumber()
|
||||
).toBeCloseTo(0.145);
|
||||
});
|
||||
});
|
||||
});
|
@ -18,6 +18,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
EMERGENCY_FUND_TAG_ID,
|
||||
@ -70,7 +71,7 @@ import {
|
||||
parseISO,
|
||||
set
|
||||
} from 'date-fns';
|
||||
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
||||
import { isEmpty, uniq, uniqBy } from 'lodash';
|
||||
|
||||
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||
import {
|
||||
@ -206,24 +207,6 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercentage
|
||||
}: {
|
||||
daysInMarket: number;
|
||||
netPerformancePercentage: Big;
|
||||
}): Big {
|
||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||
|
||||
return new Big(
|
||||
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
|
||||
).minus(1);
|
||||
}
|
||||
|
||||
return new Big(0);
|
||||
}
|
||||
|
||||
public async getDividends({
|
||||
activities,
|
||||
groupBy
|
||||
@ -713,7 +696,7 @@ export class PortfolioService {
|
||||
return Account;
|
||||
});
|
||||
|
||||
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
||||
const dividendYieldPercent = getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||
netPerformancePercentage: timeWeightedInvestment.eq(0)
|
||||
? new Big(0)
|
||||
@ -721,7 +704,7 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
const dividendYieldPercentWithCurrencyEffect =
|
||||
this.getAnnualizedPerformancePercent({
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
|
||||
0
|
||||
@ -1724,13 +1707,13 @@ export class PortfolioService {
|
||||
|
||||
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||
|
||||
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
||||
const annualizedPerformancePercent = getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercentage: new Big(netPerformancePercentage)
|
||||
})?.toNumber();
|
||||
|
||||
const annualizedPerformancePercentWithCurrencyEffect =
|
||||
this.getAnnualizedPerformancePercent({
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercentage: new Big(
|
||||
netPerformancePercentageWithCurrencyEffect
|
||||
|
50
libs/common/src/lib/calculation-helper.spec.ts
Normal file
50
libs/common/src/lib/calculation-helper.spec.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Big } from 'big.js';
|
||||
|
||||
import { getAnnualizedPerformancePercent } from './calculation-helper';
|
||||
|
||||
describe('CalculationHelper', () => {
|
||||
describe('annualized performance percentage', () => {
|
||||
it('Get annualized performance', async () => {
|
||||
expect(
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||
netPerformancePercentage: new Big(0)
|
||||
}).toNumber()
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: 0,
|
||||
netPerformancePercentage: new Big(0)
|
||||
}).toNumber()
|
||||
).toEqual(0);
|
||||
|
||||
/**
|
||||
* Source: https://www.readyratios.com/reference/analysis/annualized_rate.html
|
||||
*/
|
||||
expect(
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: 65, // < 1 year
|
||||
netPerformancePercentage: new Big(0.1025)
|
||||
}).toNumber()
|
||||
).toBeCloseTo(0.729705);
|
||||
|
||||
expect(
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: 365, // 1 year
|
||||
netPerformancePercentage: 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(
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: 575, // > 1 year
|
||||
netPerformancePercentage: new Big(0.2374)
|
||||
}).toNumber()
|
||||
).toBeCloseTo(0.145);
|
||||
});
|
||||
});
|
||||
});
|
20
libs/common/src/lib/calculation-helper.ts
Normal file
20
libs/common/src/lib/calculation-helper.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Big } from 'big.js';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
export function getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercentage
|
||||
}: {
|
||||
daysInMarket: number;
|
||||
netPerformancePercentage: Big;
|
||||
}): Big {
|
||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||
|
||||
return new Big(
|
||||
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
|
||||
).minus(1);
|
||||
}
|
||||
|
||||
return new Big(0);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper';
|
||||
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
@ -14,10 +15,12 @@ import {
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { Big } from 'big.js';
|
||||
import { ChartConfiguration } from 'chart.js';
|
||||
import { LinearScale } from 'chart.js';
|
||||
import { Chart } from 'chart.js';
|
||||
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { orderBy } from 'lodash';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -41,6 +44,8 @@ export class GfTreemapChartComponent
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
public static readonly HEAT_MULTIPLIER = 5;
|
||||
|
||||
public chart: Chart<'treemap'>;
|
||||
public isLoading = true;
|
||||
|
||||
@ -71,24 +76,52 @@ export class GfTreemapChartComponent
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor(ctx) {
|
||||
const netPerformancePercentWithCurrencyEffect =
|
||||
ctx.raw._data.netPerformancePercentWithCurrencyEffect;
|
||||
const annualizedNetPerformancePercentWithCurrencyEffect =
|
||||
getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(
|
||||
new Date(),
|
||||
ctx.raw._data.dateOfFirstActivity
|
||||
),
|
||||
netPerformancePercentage: new Big(
|
||||
ctx.raw._data.netPerformancePercentWithCurrencyEffect
|
||||
)
|
||||
}).toNumber();
|
||||
|
||||
if (netPerformancePercentWithCurrencyEffect > 0.03) {
|
||||
if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||
0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||
) {
|
||||
return green[9];
|
||||
} else if (netPerformancePercentWithCurrencyEffect > 0.02) {
|
||||
} else if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||
0.02 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||
) {
|
||||
return green[7];
|
||||
} else if (netPerformancePercentWithCurrencyEffect > 0.01) {
|
||||
} else if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||
0.01 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||
) {
|
||||
return green[5];
|
||||
} else if (netPerformancePercentWithCurrencyEffect > 0) {
|
||||
} else if (annualizedNetPerformancePercentWithCurrencyEffect > 0) {
|
||||
return green[3];
|
||||
} else if (netPerformancePercentWithCurrencyEffect === 0) {
|
||||
} else if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect === 0
|
||||
) {
|
||||
return gray[3];
|
||||
} else if (netPerformancePercentWithCurrencyEffect > -0.01) {
|
||||
} else if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||
-0.01 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||
) {
|
||||
return red[3];
|
||||
} else if (netPerformancePercentWithCurrencyEffect > -0.02) {
|
||||
} else if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||
-0.02 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||
) {
|
||||
return red[5];
|
||||
} else if (netPerformancePercentWithCurrencyEffect > -0.03) {
|
||||
} else if (
|
||||
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||
-0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||
) {
|
||||
return red[7];
|
||||
} else {
|
||||
return red[9];
|
||||
|
Loading…
x
Reference in New Issue
Block a user