diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e261220..7155cc66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts deleted file mode 100644 index 92970f54..00000000 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b5443c9c..368c0459 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/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 { 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 diff --git a/libs/common/src/lib/calculation-helper.spec.ts b/libs/common/src/lib/calculation-helper.spec.ts new file mode 100644 index 00000000..69621ec0 --- /dev/null +++ b/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); + }); + }); +}); diff --git a/libs/common/src/lib/calculation-helper.ts b/libs/common/src/lib/calculation-helper.ts new file mode 100644 index 00000000..7d2ec909 --- /dev/null +++ b/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); +} diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 9ee6a7ae..3c1b3d54 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/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 { 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; + 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];