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
|
### Changed
|
||||||
|
|
||||||
|
- Improved the color assignment in the chart of the holdings tab on the home page (experimental)
|
||||||
- Improved the language localization for Catalan (`ca`)
|
- Improved the language localization for Catalan (`ca`)
|
||||||
|
|
||||||
## 2.99.0 - 2024-07-29
|
## 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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
|
import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CURRENCY,
|
DEFAULT_CURRENCY,
|
||||||
EMERGENCY_FUND_TAG_ID,
|
EMERGENCY_FUND_TAG_ID,
|
||||||
@ -70,7 +71,7 @@ import {
|
|||||||
parseISO,
|
parseISO,
|
||||||
set
|
set
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
import { isEmpty, uniq, uniqBy } from 'lodash';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||||
import {
|
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({
|
public async getDividends({
|
||||||
activities,
|
activities,
|
||||||
groupBy
|
groupBy
|
||||||
@ -713,7 +696,7 @@ export class PortfolioService {
|
|||||||
return Account;
|
return Account;
|
||||||
});
|
});
|
||||||
|
|
||||||
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
const dividendYieldPercent = getAnnualizedPerformancePercent({
|
||||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||||
netPerformancePercentage: timeWeightedInvestment.eq(0)
|
netPerformancePercentage: timeWeightedInvestment.eq(0)
|
||||||
? new Big(0)
|
? new Big(0)
|
||||||
@ -721,7 +704,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const dividendYieldPercentWithCurrencyEffect =
|
const dividendYieldPercentWithCurrencyEffect =
|
||||||
this.getAnnualizedPerformancePercent({
|
getAnnualizedPerformancePercent({
|
||||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||||
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
|
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
|
||||||
0
|
0
|
||||||
@ -1724,13 +1707,13 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||||
|
|
||||||
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
const annualizedPerformancePercent = getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercentage: new Big(netPerformancePercentage)
|
netPerformancePercentage: new Big(netPerformancePercentage)
|
||||||
})?.toNumber();
|
})?.toNumber();
|
||||||
|
|
||||||
const annualizedPerformancePercentWithCurrencyEffect =
|
const annualizedPerformancePercentWithCurrencyEffect =
|
||||||
this.getAnnualizedPerformancePercent({
|
getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercentage: new Big(
|
netPerformancePercentage: new Big(
|
||||||
netPerformancePercentageWithCurrencyEffect
|
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 { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@ -14,10 +15,12 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
import { Big } from 'big.js';
|
||||||
import { ChartConfiguration } from 'chart.js';
|
import { ChartConfiguration } from 'chart.js';
|
||||||
import { LinearScale } from 'chart.js';
|
import { LinearScale } from 'chart.js';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
|
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
|
||||||
|
import { differenceInDays } from 'date-fns';
|
||||||
import { orderBy } from 'lodash';
|
import { orderBy } from 'lodash';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
@ -41,6 +44,8 @@ export class GfTreemapChartComponent
|
|||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
|
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
public static readonly HEAT_MULTIPLIER = 5;
|
||||||
|
|
||||||
public chart: Chart<'treemap'>;
|
public chart: Chart<'treemap'>;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
|
||||||
@ -71,24 +76,52 @@ export class GfTreemapChartComponent
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor(ctx) {
|
backgroundColor(ctx) {
|
||||||
const netPerformancePercentWithCurrencyEffect =
|
const annualizedNetPerformancePercentWithCurrencyEffect =
|
||||||
ctx.raw._data.netPerformancePercentWithCurrencyEffect;
|
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];
|
return green[9];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect > 0.02) {
|
} else if (
|
||||||
|
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||||
|
0.02 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||||
|
) {
|
||||||
return green[7];
|
return green[7];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect > 0.01) {
|
} else if (
|
||||||
|
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||||
|
0.01 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||||
|
) {
|
||||||
return green[5];
|
return green[5];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect > 0) {
|
} else if (annualizedNetPerformancePercentWithCurrencyEffect > 0) {
|
||||||
return green[3];
|
return green[3];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect === 0) {
|
} else if (
|
||||||
|
annualizedNetPerformancePercentWithCurrencyEffect === 0
|
||||||
|
) {
|
||||||
return gray[3];
|
return gray[3];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect > -0.01) {
|
} else if (
|
||||||
|
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||||
|
-0.01 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||||
|
) {
|
||||||
return red[3];
|
return red[3];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect > -0.02) {
|
} else if (
|
||||||
|
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||||
|
-0.02 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||||
|
) {
|
||||||
return red[5];
|
return red[5];
|
||||||
} else if (netPerformancePercentWithCurrencyEffect > -0.03) {
|
} else if (
|
||||||
|
annualizedNetPerformancePercentWithCurrencyEffect >
|
||||||
|
-0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER
|
||||||
|
) {
|
||||||
return red[7];
|
return red[7];
|
||||||
} else {
|
} else {
|
||||||
return red[9];
|
return red[9];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user