Browse Source

Feature/change color assignment by annualized performance in treemap chart (#3617)

* Change color assignment to annualized performance

* Update changelog
pull/3625/head
Thomas Kaul 8 months ago
committed by GitHub
parent
commit
fcc2ab1a48
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 78
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  3. 29
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 50
      libs/common/src/lib/calculation-helper.spec.ts
  5. 20
      libs/common/src/lib/calculation-helper.ts
  6. 55
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

1
CHANGELOG.md

@ -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

78
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -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);
});
});
});

29
apps/api/src/app/portfolio/portfolio.service.ts

@ -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

@ -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

@ -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);
}

55
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -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(
if (netPerformancePercentWithCurrencyEffect > 0.03) { new Date(),
ctx.raw._data.dateOfFirstActivity
),
netPerformancePercentage: new Big(
ctx.raw._data.netPerformancePercentWithCurrencyEffect
)
}).toNumber();
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…
Cancel
Save