diff --git a/CHANGELOG.md b/CHANGELOG.md index b645fb0e7..d1f8b82ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Included cash in the performance calculation of the portfolio + ### Fixed - Fixed the missing validation of the tags when creating or updating an activity diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index cee94f020..d2b48daf0 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -39,7 +39,6 @@ import { GroupBy } from '@ghostfolio/common/types'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Logger } from '@nestjs/common'; -import { AssetSubClass } from '@prisma/client'; import { Big } from 'big.js'; import { plainToClass } from 'class-transformer'; import { @@ -194,6 +193,7 @@ export abstract class PortfolioCalculator { hasErrors: false, historicalData: [], positions: [], + totalCashInBaseCurrency: new Big(0), totalFeesWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0), totalInvestment: new Big(0), @@ -202,10 +202,12 @@ export abstract class PortfolioCalculator { }; } + const cashSymbols = new Set(); const currencies: { [symbol: string]: string } = {}; const dataGatheringItems: DataGatheringItem[] = []; let firstIndex = transactionPoints.length; let firstTransactionPoint: TransactionPoint = null; + let totalCashInBaseCurrency = new Big(0); let totalInterestWithCurrencyEffect = new Big(0); let totalLiabilitiesWithCurrencyEffect = new Big(0); @@ -314,7 +316,7 @@ export abstract class PortfolioCalculator { const accumulatedValuesByDate: { [date: string]: { investmentValueWithCurrencyEffect: Big; - totalAccountBalanceWithCurrencyEffect: Big; + totalCashValueWithCurrencyEffect: Big; totalCurrentValue: Big; totalCurrentValueWithCurrencyEffect: Big; totalInvestmentValue: Big; @@ -349,6 +351,8 @@ export abstract class PortfolioCalculator { ] ?? 1 ); + const valueInBaseCurrency = marketPriceInBaseCurrency.mul(item.quantity); + const { currentValues, currentValuesWithCurrencyEffect, @@ -389,25 +393,19 @@ export abstract class PortfolioCalculator { hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; - const includeInTotalAssetValue = - item.assetSubClass !== AssetSubClass.CASH; - - if (includeInTotalAssetValue) { - valuesBySymbol[item.symbol] = { - currentValues, - currentValuesWithCurrencyEffect, - investmentValuesAccumulated, - investmentValuesAccumulatedWithCurrencyEffect, - investmentValuesWithCurrencyEffect, - netPerformanceValues, - netPerformanceValuesWithCurrencyEffect, - timeWeightedInvestmentValues, - timeWeightedInvestmentValuesWithCurrencyEffect - }; - } + valuesBySymbol[item.symbol] = { + currentValues, + currentValuesWithCurrencyEffect, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, + investmentValuesWithCurrencyEffect, + netPerformanceValues, + netPerformanceValuesWithCurrencyEffect, + timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect + }; positions.push({ - includeInTotalAssetValue, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, activitiesCount: item.activitiesCount, @@ -448,11 +446,16 @@ export abstract class PortfolioCalculator { quantity: item.quantity, symbol: item.symbol, tags: item.tags, - valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( - item.quantity - ) + valueInBaseCurrency }); + if (item.assetSubClass === 'CASH') { + cashSymbols.add(item.symbol); + + totalCashInBaseCurrency = + totalCashInBaseCurrency.plus(valueInBaseCurrency); + } + totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( totalInterestInBaseCurrency ); @@ -472,28 +475,7 @@ export abstract class PortfolioCalculator { } } - const accountBalanceItemsMap = this.accountBalanceItems.reduce( - (map, { date, value }) => { - map[date] = new Big(value); - - return map; - }, - {} as { [date: string]: Big } - ); - - const accountBalanceMap: { [date: string]: Big } = {}; - - let lastKnownBalance = new Big(0); - for (const dateString of chartDates) { - if (accountBalanceItemsMap[dateString] !== undefined) { - // If there's an exact balance for this date, update lastKnownBalance - lastKnownBalance = accountBalanceItemsMap[dateString]; - } - - // Add the most recent balance to the accountBalanceMap - accountBalanceMap[dateString] = lastKnownBalance; - for (const symbol of Object.keys(valuesBySymbol)) { const symbolValues = valuesBySymbol[symbol]; @@ -536,7 +518,14 @@ export abstract class PortfolioCalculator { accumulatedValuesByDate[dateString] ?.investmentValueWithCurrencyEffect ?? new Big(0) ).add(investmentValueWithCurrencyEffect), - totalAccountBalanceWithCurrencyEffect: accountBalanceMap[dateString], + totalCashValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalCashValueWithCurrencyEffect ?? new Big(0) + ).add( + cashSymbols.has(symbol) + ? currentValueWithCurrencyEffect + : new Big(0) + ), totalCurrentValue: ( accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) ).add(currentValue), @@ -577,7 +566,7 @@ export abstract class PortfolioCalculator { ).map(([date, values]) => { const { investmentValueWithCurrencyEffect, - totalAccountBalanceWithCurrencyEffect, + totalCashValueWithCurrencyEffect, totalCurrentValue, totalCurrentValueWithCurrencyEffect, totalInvestmentValue, @@ -610,10 +599,8 @@ export abstract class PortfolioCalculator { netPerformance: totalNetPerformanceValue.toNumber(), netPerformanceWithCurrencyEffect: totalNetPerformanceValueWithCurrencyEffect.toNumber(), - netWorth: totalCurrentValueWithCurrencyEffect - .plus(totalAccountBalanceWithCurrencyEffect) - .toNumber(), - totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(), + netWorth: totalCurrentValueWithCurrencyEffect.toNumber(), + totalCashInBaseCurrency: totalCashValueWithCurrencyEffect.toNumber(), totalInvestment: totalInvestmentValue.toNumber(), totalInvestmentValueWithCurrencyEffect: totalInvestmentValueWithCurrencyEffect.toNumber(), @@ -637,6 +624,7 @@ export abstract class PortfolioCalculator { ...overall, errors, historicalData, + totalCashInBaseCurrency, totalInterestWithCurrencyEffect, totalLiabilitiesWithCurrencyEffect, hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, @@ -774,11 +762,6 @@ export abstract class PortfolioCalculator { ? 0 : netPerformanceWithCurrencyEffectSinceStartDate / timeWeightedInvestmentValue - // TODO: Add net worth - // netWorth: totalCurrentValueWithCurrencyEffect - // .plus(totalAccountBalanceWithCurrencyEffect) - // .toNumber() - // netWorth: 0 }); } } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index bc80e8996..4f5da58b8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -145,7 +145,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0, netWorth: 0, - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 0, totalInvestmentValueWithCurrencyEffect: 0, value: 0, @@ -163,7 +163,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 netPerformanceWithCurrencyEffect: 5535.42, netWorth: 50098.3, // 1 * 50098.3 = 50098.3 - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42, value: 50098.3, // 1 * 50098.3 = 50098.3 @@ -182,7 +182,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712, netPerformanceWithCurrencyEffect: -1463.18, netWorth: 43099.7, - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42, value: 43099.7, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index 18b0e6d64..eb5571feb 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -145,7 +145,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0, netWorth: 0, - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 0, totalInvestmentValueWithCurrencyEffect: 0, value: 0, @@ -163,7 +163,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 netPerformanceWithCurrencyEffect: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42 netWorth: 50098.3, // 1 * 50098.3 = 50098.3 - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42, value: 50098.3, // 1 * 50098.3 = 50098.3 @@ -182,7 +182,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712, netPerformanceWithCurrencyEffect: -1463.18, netWorth: 43099.7, - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42, value: 43099.7, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 31e451051..551189fcc 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -248,7 +248,6 @@ describe('PortfolioCalculator', () => { '0.08211603004634809014' ), grossPerformanceWithCurrencyEffect: new Big(70), - includeInTotalAssetValue: false, investment: new Big(1820), investmentWithCurrencyEffect: new Big(1750), marketPrice: 1, @@ -283,11 +282,37 @@ describe('PortfolioCalculator', () => { }); expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big(1820), hasErrors: false, + totalCashInBaseCurrency: new Big(1820), totalFeesWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0), + totalInvestment: new Big(1820), totalLiabilitiesWithCurrencyEffect: new Big(0) }); + + /** + * Value with currency effect: 2000 USD * 0.91 = 1820 CHF + * Net worth: 1820 CHF (the cash is included in the value and therefore + * not added on top of it again) + * Cash in base currency: 2000 USD * 0.91 = 1820 CHF (the whole portfolio + * consists of cash, hence it matches the value) + * Net performance with currency effect: 70 CHF / 852.45 CHF ≈ 8.21 % + */ + expect(portfolioSnapshot.historicalData.at(-1)).toEqual({ + date: '2025-01-01', + investmentValueWithCurrencyEffect: 0, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0.08211603004634808, + netPerformanceWithCurrencyEffect: 70, + netWorth: 1820, + totalCashInBaseCurrency: 1820, + totalInvestment: 1820, + totalInvestmentValueWithCurrencyEffect: 1750, + value: 1820, + valueWithCurrencyEffect: 1820 + }); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index f86b23d9a..10cc2da01 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -142,7 +142,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0, netWorth: 0, - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 0, totalInvestmentValueWithCurrencyEffect: 0, value: 0, @@ -161,7 +161,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 netPerformanceWithCurrencyEffect: 24, netWorth: 175.6, // 2 * 87.8 = 175.6 - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6, value: 175.6, // 2 * 87.8 = 175.6 @@ -180,7 +180,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, netPerformanceWithCurrencyEffect: 19.86, netWorth: 0, - totalAccountBalance: 0, + totalCashInBaseCurrency: 0, totalInvestment: 0, totalInvestmentValueWithCurrencyEffect: 0, value: 0, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index 747fca7a5..18a8f7cd8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -40,11 +40,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); - for (const currentPosition of positions.filter( - ({ includeInTotalAssetValue }) => { - return includeInTotalAssetValue; - } - )) { + for (const currentPosition of positions) { if (currentPosition.feeInBaseCurrency) { totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( currentPosition.feeInBaseCurrency @@ -117,6 +113,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { createdAt: new Date(), errors: [], historicalData: [], + totalCashInBaseCurrency: new Big(0), totalLiabilitiesWithCurrencyEffect: new Big(0) }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6617b8f9b..f2bcad8f5 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -541,12 +541,6 @@ export class PortfolioService { let filteredValueInBaseCurrency = currentValueInBaseCurrency; - if (!this.activitiesService.areCashActivitiesExcludedByFilters(filters)) { - filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus( - cashDetails.balanceInBaseCurrency - ); - } - const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => { return { dataSource, @@ -1893,6 +1887,7 @@ export class PortfolioService { const { currentValueInBaseCurrency, + totalCashInBaseCurrency, totalInvestment, totalInvestmentWithCurrencyEffect } = await portfolioCalculator.getSnapshot(); @@ -1969,8 +1964,7 @@ export class PortfolioService { .plus(totalOfExcludedActivities) .toNumber(); - const netWorth = new Big(balanceInBaseCurrency) - .plus(currentValueInBaseCurrency) + const netWorth = new Big(currentValueInBaseCurrency) .plus(excludedAccountsAndActivities) .minus(liabilities) .toNumber(); @@ -2022,6 +2016,7 @@ export class PortfolioService { fireWealth: { today: { valueInBaseCurrency: new Big(currentValueInBaseCurrency) + .minus(totalCashInBaseCurrency ?? 0) .minus(emergencyFundHoldingsValueInBaseCurrency) .toNumber() } diff --git a/libs/common/src/lib/interfaces/historical-data-item.interface.ts b/libs/common/src/lib/interfaces/historical-data-item.interface.ts index 0b45cf0b7..adb5b7e65 100644 --- a/libs/common/src/lib/interfaces/historical-data-item.interface.ts +++ b/libs/common/src/lib/interfaces/historical-data-item.interface.ts @@ -11,7 +11,7 @@ export interface HistoricalDataItem { netWorth?: number; netWorthInPercentage?: number; quantity?: number; - totalAccountBalance?: number; + totalCashInBaseCurrency?: number; totalInvestment?: number; totalInvestmentValueWithCurrencyEffect?: number; value?: number; diff --git a/libs/common/src/lib/models/portfolio-snapshot.ts b/libs/common/src/lib/models/portfolio-snapshot.ts index 6b13ca048..17e4bf97d 100644 --- a/libs/common/src/lib/models/portfolio-snapshot.ts +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -26,6 +26,10 @@ export class PortfolioSnapshot { @Type(() => TimelinePosition) positions: TimelinePosition[]; + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalCashInBaseCurrency: Big; + @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) totalFeesWithCurrencyEffect: Big; diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 13f9001d5..b16db4988 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -51,8 +51,6 @@ export class TimelinePosition { @Type(() => Big) grossPerformanceWithCurrencyEffect: Big; - includeInTotalAssetValue?: boolean; - @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) investment: Big;