From b0bffe8222f65715c6269ed6bde0224d1f68cf1b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:50:15 +0200 Subject: [PATCH 01/10] Include cash in portfolio performance --- .../calculator/portfolio-calculator.ts | 35 +++--- .../roai/portfolio-calculator-cash.spec.ts | 104 ++++++++++++++++++ 2 files changed, 123 insertions(+), 16 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index ab3f76703..1d3f50580 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -392,19 +392,21 @@ export abstract class PortfolioCalculator { const includeInTotalAssetValue = item.assetSubClass !== AssetSubClass.CASH; - if (includeInTotalAssetValue) { - valuesBySymbol[item.symbol] = { - currentValues, - currentValuesWithCurrencyEffect, - investmentValuesAccumulated, - investmentValuesAccumulatedWithCurrencyEffect, - investmentValuesWithCurrencyEffect, - netPerformanceValues, - netPerformanceValuesWithCurrencyEffect, - timeWeightedInvestmentValues, - timeWeightedInvestmentValuesWithCurrencyEffect - }; - } + // Cash positions are included in the chart (value, net worth and + // performance) so that the home screen reflects the entire portfolio. + // They remain excluded from the overall performance aggregate (see + // includeInTotalAssetValue) which keeps the FIRE wealth asset-only. + valuesBySymbol[item.symbol] = { + currentValues, + currentValuesWithCurrencyEffect, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, + investmentValuesWithCurrencyEffect, + netPerformanceValues, + netPerformanceValuesWithCurrencyEffect, + timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect + }; positions.push({ includeInTotalAssetValue, @@ -610,9 +612,10 @@ export abstract class PortfolioCalculator { netPerformance: totalNetPerformanceValue.toNumber(), netPerformanceWithCurrencyEffect: totalNetPerformanceValueWithCurrencyEffect.toNumber(), - netWorth: totalCurrentValueWithCurrencyEffect - .plus(totalAccountBalanceWithCurrencyEffect) - .toNumber(), + // Cash is now reflected through the cash positions in + // totalCurrentValueWithCurrencyEffect, so the account balance must no + // longer be added here to avoid counting cash twice in the net worth. + netWorth: totalCurrentValueWithCurrencyEffect.toNumber(), totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(), totalInvestment: totalInvestmentValue.toNumber(), totalInvestmentValueWithCurrencyEffect: 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 7774ee629..396124b11 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 @@ -287,5 +287,109 @@ describe('PortfolioCalculator', () => { totalLiabilitiesWithCurrencyEffect: new Big(0) }); }); + + it('should include cash in the net worth and performance without counting it twice', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2025-01-01').getTime()); + + const accountId = randomUUID(); + + jest + .spyOn(accountBalanceService, 'getAccountBalances') + .mockResolvedValue({ + balances: [ + { + accountId, + id: randomUUID(), + date: parseDate('2023-12-31'), + value: 1000, + valueInBaseCurrency: 850 + }, + { + accountId, + id: randomUUID(), + date: parseDate('2024-12-31'), + value: 2000, + valueInBaseCurrency: 1800 + } + ] + }); + + jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({ + accounts: [ + { + balance: 2000, + comment: null, + createdAt: parseDate('2023-12-31'), + currency: 'USD', + id: accountId, + isExcluded: false, + name: 'USD', + platformId: null, + updatedAt: parseDate('2023-12-31'), + userId: userDummyData.id + } + ], + balanceInBaseCurrency: 1820 + }); + + jest + .spyOn(dataProviderService, 'getDataSourceForExchangeRates') + .mockReturnValue(DataSource.YAHOO); + + jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({ + activities: [], + count: 0 + }); + + const { activities } = + await activitiesService.getActivitiesForPortfolioCalculator({ + userCurrency: 'CHF', + userId: userDummyData.id, + withCash: true + }); + + jest.spyOn(currentRateService, 'getValues').mockResolvedValue({ + dataProviderInfos: [], + errors: [], + values: [] + }); + + const accountBalanceItems = + await accountBalanceService.getAccountBalanceItems({ + userCurrency: 'CHF', + userId: userDummyData.id + }); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + accountBalanceItems, + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const lastDataItem = portfolioSnapshot.historicalData.at(-1); + + // The cash position is now reflected in the portfolio value (it used to + // be excluded, which left the value at 0 for a cash-only portfolio). + expect(lastDataItem.valueWithCurrencyEffect).toBeGreaterThan(0); + + // The account balance is still tracked … + expect(lastDataItem.totalAccountBalance).toBeGreaterThan(0); + + // … but it is no longer added on top of the cash position, so the net + // worth equals the portfolio value and cash is not counted twice. + expect(lastDataItem.netWorth).toBeCloseTo( + lastDataItem.valueWithCurrencyEffect + ); + + // Cash now contributes to the performance (here through the currency + // effect), so it is no longer flat at 0 %. + expect( + lastDataItem.netPerformanceInPercentageWithCurrencyEffect + ).not.toBe(0); + }); }); }); From bf34d29a779b26a87fb5d25a6965f18b58cd6ef5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:50:36 +0200 Subject: [PATCH 02/10] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df95d849b..7f70d713f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Included cash in the performance calculation of the portfolio on the home page + ## 3.21.0 - 2026-07-05 ### Added From a451fba43319a64125092b6aec2a07a0d07e6d26 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:47:29 +0200 Subject: [PATCH 03/10] Refactoring --- .../src/app/portfolio/calculator/portfolio-calculator.ts | 7 ------- .../calculator/roai/portfolio-calculator-cash.spec.ts | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 1d3f50580..be450bf4b 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -392,10 +392,6 @@ export abstract class PortfolioCalculator { const includeInTotalAssetValue = item.assetSubClass !== AssetSubClass.CASH; - // Cash positions are included in the chart (value, net worth and - // performance) so that the home screen reflects the entire portfolio. - // They remain excluded from the overall performance aggregate (see - // includeInTotalAssetValue) which keeps the FIRE wealth asset-only. valuesBySymbol[item.symbol] = { currentValues, currentValuesWithCurrencyEffect, @@ -612,9 +608,6 @@ export abstract class PortfolioCalculator { netPerformance: totalNetPerformanceValue.toNumber(), netPerformanceWithCurrencyEffect: totalNetPerformanceValueWithCurrencyEffect.toNumber(), - // Cash is now reflected through the cash positions in - // totalCurrentValueWithCurrencyEffect, so the account balance must no - // longer be added here to avoid counting cash twice in the net worth. netWorth: totalCurrentValueWithCurrencyEffect.toNumber(), totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(), totalInvestment: totalInvestmentValue.toNumber(), 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 396124b11..f8937cd97 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 @@ -148,15 +148,15 @@ describe('PortfolioCalculator', () => { balances: [ { accountId, - id: randomUUID(), date: parseDate('2023-12-31'), + id: randomUUID(), value: 1000, valueInBaseCurrency: 850 }, { accountId, - id: randomUUID(), date: parseDate('2024-12-31'), + id: randomUUID(), value: 2000, valueInBaseCurrency: 1800 } @@ -376,10 +376,10 @@ describe('PortfolioCalculator', () => { // be excluded, which left the value at 0 for a cash-only portfolio). expect(lastDataItem.valueWithCurrencyEffect).toBeGreaterThan(0); - // The account balance is still tracked … + // The account balance is still tracked ... expect(lastDataItem.totalAccountBalance).toBeGreaterThan(0); - // … but it is no longer added on top of the cash position, so the net + // ... but it is no longer added on top of the cash position, so the net // worth equals the portfolio value and cash is not counted twice. expect(lastDataItem.netWorth).toBeCloseTo( lastDataItem.valueWithCurrencyEffect From babbf9574ebed716fa553c272841f98ce83720c0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:06:16 +0200 Subject: [PATCH 04/10] Refactoring --- .../calculator/roai/portfolio-calculator-cash.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f8937cd97..1c2eb69d1 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 @@ -299,15 +299,15 @@ describe('PortfolioCalculator', () => { balances: [ { accountId, - id: randomUUID(), date: parseDate('2023-12-31'), + id: randomUUID(), value: 1000, valueInBaseCurrency: 850 }, { accountId, - id: randomUUID(), date: parseDate('2024-12-31'), + id: randomUUID(), value: 2000, valueInBaseCurrency: 1800 } From 946593a2f04b40fe3741d044aeeeadb8ad813ee3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:39:45 +0200 Subject: [PATCH 05/10] Include cash in portfolio performance --- .../calculator/portfolio-calculator.ts | 45 ++++-- .../roai/portfolio-calculator-cash.spec.ts | 130 ++++-------------- .../portfolio-calculator-liability.spec.ts | 8 ++ .../calculator/roai/portfolio-calculator.ts | 6 +- .../src/app/portfolio/portfolio.service.ts | 9 +- .../src/lib/models/timeline-position.ts | 2 - 6 files changed, 73 insertions(+), 127 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index ccf4005ff..2082c3c2b 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 { @@ -319,6 +318,7 @@ export abstract class PortfolioCalculator { totalCurrentValueWithCurrencyEffect: Big; totalInvestmentValue: Big; totalInvestmentValueWithCurrencyEffect: Big; + totalLiabilitiesWithCurrencyEffect: Big; totalNetPerformanceValue: Big; totalNetPerformanceValueWithCurrencyEffect: Big; totalTimeWeightedInvestmentValue: Big; @@ -389,9 +389,6 @@ export abstract class PortfolioCalculator { hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; - const includeInTotalAssetValue = - item.assetSubClass !== AssetSubClass.CASH; - valuesBySymbol[item.symbol] = { currentValues, currentValuesWithCurrencyEffect, @@ -405,7 +402,6 @@ export abstract class PortfolioCalculator { }; positions.push({ - includeInTotalAssetValue, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, activitiesCount: item.activitiesCount, @@ -479,8 +475,28 @@ export abstract class PortfolioCalculator { {} as { [date: string]: Big } ); + const liabilityItemsMap = this.activities.reduce( + (map, { assetProfile, date, quantity, type, unitPrice }) => { + if (type === 'LIABILITY') { + const exchangeRate = + exchangeRatesByCurrency[ + `${assetProfile.currency}${this.currency}` + ]?.[date] ?? 1; + + map[date] = (map[date] ?? new Big(0)).plus( + quantity.mul(unitPrice).mul(exchangeRate) + ); + } + + return map; + }, + {} as { [date: string]: Big } + ); + const accountBalanceMap: { [date: string]: Big } = {}; + const liabilitiesMap: { [date: string]: Big } = {}; + let accumulatedLiabilities = new Big(0); let lastKnownBalance = new Big(0); for (const dateString of chartDates) { @@ -492,6 +508,14 @@ export abstract class PortfolioCalculator { // Add the most recent balance to the accountBalanceMap accountBalanceMap[dateString] = lastKnownBalance; + if (liabilityItemsMap[dateString] !== undefined) { + accumulatedLiabilities = accumulatedLiabilities.plus( + liabilityItemsMap[dateString] + ); + } + + liabilitiesMap[dateString] = accumulatedLiabilities; + for (const symbol of Object.keys(valuesBySymbol)) { const symbolValues = valuesBySymbol[symbol]; @@ -550,6 +574,7 @@ export abstract class PortfolioCalculator { accumulatedValuesByDate[dateString] ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) ).add(investmentValueAccumulatedWithCurrencyEffect), + totalLiabilitiesWithCurrencyEffect: liabilitiesMap[dateString], totalNetPerformanceValue: ( accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0) @@ -580,6 +605,7 @@ export abstract class PortfolioCalculator { totalCurrentValueWithCurrencyEffect, totalInvestmentValue, totalInvestmentValueWithCurrencyEffect, + totalLiabilitiesWithCurrencyEffect, totalNetPerformanceValue, totalNetPerformanceValueWithCurrencyEffect, totalTimeWeightedInvestmentValue, @@ -608,7 +634,9 @@ export abstract class PortfolioCalculator { netPerformance: totalNetPerformanceValue.toNumber(), netPerformanceWithCurrencyEffect: totalNetPerformanceValueWithCurrencyEffect.toNumber(), - netWorth: totalCurrentValueWithCurrencyEffect.toNumber(), + netWorth: totalCurrentValueWithCurrencyEffect + .minus(totalLiabilitiesWithCurrencyEffect) + .toNumber(), totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(), totalInvestment: totalInvestmentValue.toNumber(), totalInvestmentValueWithCurrencyEffect: @@ -770,11 +798,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-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 1c2eb69d1..559fb0ed1 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 @@ -137,7 +137,7 @@ describe('PortfolioCalculator', () => { }); describe('Cash Performance', () => { - it('should calculate performance for cash assets in CHF default currency', async () => { + const computePortfolioSnapshot = async () => { jest.useFakeTimers().setSystemTime(parseDate('2025-01-01').getTime()); const accountId = randomUUID(); @@ -217,7 +217,11 @@ describe('PortfolioCalculator', () => { userId: userDummyData.id }); - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + return portfolioCalculator.computeSnapshot(); + }; + + it('should calculate performance for cash assets in CHF default currency', async () => { + const portfolioSnapshot = await computePortfolioSnapshot(); const position = portfolioSnapshot.positions.find(({ symbol }) => { return symbol === 'USD'; @@ -246,7 +250,6 @@ describe('PortfolioCalculator', () => { '0.08211603004634809014' ), grossPerformanceWithCurrencyEffect: new Big(70), - includeInTotalAssetValue: false, investment: new Big(1820), investmentWithCurrencyEffect: new Big(1750), marketPrice: 1, @@ -281,115 +284,40 @@ describe('PortfolioCalculator', () => { }); expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big(1820), hasErrors: false, totalFeesWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0), + totalInvestment: new Big(1820), totalLiabilitiesWithCurrencyEffect: new Big(0) }); }); it('should include cash in the net worth and performance without counting it twice', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2025-01-01').getTime()); - - const accountId = randomUUID(); - - jest - .spyOn(accountBalanceService, 'getAccountBalances') - .mockResolvedValue({ - balances: [ - { - accountId, - date: parseDate('2023-12-31'), - id: randomUUID(), - value: 1000, - valueInBaseCurrency: 850 - }, - { - accountId, - date: parseDate('2024-12-31'), - id: randomUUID(), - value: 2000, - valueInBaseCurrency: 1800 - } - ] - }); - - jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({ - accounts: [ - { - balance: 2000, - comment: null, - createdAt: parseDate('2023-12-31'), - currency: 'USD', - id: accountId, - isExcluded: false, - name: 'USD', - platformId: null, - updatedAt: parseDate('2023-12-31'), - userId: userDummyData.id - } - ], - balanceInBaseCurrency: 1820 - }); - - jest - .spyOn(dataProviderService, 'getDataSourceForExchangeRates') - .mockReturnValue(DataSource.YAHOO); - - jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({ - activities: [], - count: 0 - }); - - const { activities } = - await activitiesService.getActivitiesForPortfolioCalculator({ - userCurrency: 'CHF', - userId: userDummyData.id, - withCash: true - }); - - jest.spyOn(currentRateService, 'getValues').mockResolvedValue({ - dataProviderInfos: [], - errors: [], - values: [] - }); - - const accountBalanceItems = - await accountBalanceService.getAccountBalanceItems({ - userCurrency: 'CHF', - userId: userDummyData.id - }); - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - accountBalanceItems, - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + const portfolioSnapshot = await computePortfolioSnapshot(); const lastDataItem = portfolioSnapshot.historicalData.at(-1); - // The cash position is now reflected in the portfolio value (it used to - // be excluded, which left the value at 0 for a cash-only portfolio). - expect(lastDataItem.valueWithCurrencyEffect).toBeGreaterThan(0); - - // The account balance is still tracked ... - expect(lastDataItem.totalAccountBalance).toBeGreaterThan(0); - - // ... but it is no longer added on top of the cash position, so the net - // worth equals the portfolio value and cash is not counted twice. - expect(lastDataItem.netWorth).toBeCloseTo( - lastDataItem.valueWithCurrencyEffect - ); - - // Cash now contributes to the performance (here through the currency - // effect), so it is no longer flat at 0 %. - expect( - lastDataItem.netPerformanceInPercentageWithCurrencyEffect - ).not.toBe(0); + /** + * Value with currency effect: 2000 USD * 0.91 = 1820 CHF + * Net worth: 1820 CHF - 0 CHF (liabilities) = 1820 CHF + * Total account balance: 1800 CHF (balance in base currency on 2024-12-31) + * Net performance with currency effect: 70 CHF / 852.45 CHF ≈ 8.21 % + */ + expect(lastDataItem).toEqual({ + date: '2025-01-01', + investmentValueWithCurrencyEffect: 0, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0.08211603004634808, + netPerformanceWithCurrencyEffect: 70, + netWorth: 1820, + totalAccountBalance: 1800, + totalInvestment: 1820, + totalInvestmentValueWithCurrencyEffect: 1750, + value: 1820, + valueWithCurrencyEffect: 1820 + }); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts index 94654af61..51a4c0368 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts @@ -113,6 +113,14 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot.totalLiabilitiesWithCurrencyEffect).toEqual( new Big(3000) ); + + /** + * Net worth: 0 USD (current value) - 3000 USD (liabilities) = -3000 USD + */ + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({ + netWorth: -3000, + valueWithCurrencyEffect: 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 f79e5bc79..daacdc60d 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 diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6617b8f9b..108e30618 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, @@ -1969,8 +1963,7 @@ export class PortfolioService { .plus(totalOfExcludedActivities) .toNumber(); - const netWorth = new Big(balanceInBaseCurrency) - .plus(currentValueInBaseCurrency) + const netWorth = new Big(currentValueInBaseCurrency) .plus(excludedAccountsAndActivities) .minus(liabilities) .toNumber(); 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; From 38cba506352f17a1613c1dfe91919026ebc1214f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:40:59 +0200 Subject: [PATCH 06/10] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f069097ab..00340c890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Included cash in the performance calculation of the portfolio on the home page +- Included cash in the performance calculation of the portfolio +- Considered liabilities in the net worth calculation of the portfolio ## 3.27.0 - 2026-07-15 From e92c6ca29829709a17aea17be6d7a133632c24f3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:50:40 +0200 Subject: [PATCH 07/10] Include cash in portfolio performance --- .../src/app/portfolio/calculator/portfolio-calculator.ts | 9 +++++++++ .../calculator/roai/portfolio-calculator-cash.spec.ts | 1 + .../portfolio/calculator/roai/portfolio-calculator.ts | 1 + apps/api/src/app/portfolio/portfolio.service.ts | 2 ++ libs/common/src/lib/models/portfolio-snapshot.ts | 4 ++++ 5 files changed, 17 insertions(+) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 2082c3c2b..1bc9c2e02 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -193,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), @@ -205,6 +206,7 @@ export abstract class PortfolioCalculator { 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); @@ -447,6 +449,12 @@ export abstract class PortfolioCalculator { ) }); + if (item.assetSubClass === 'CASH') { + totalCashInBaseCurrency = totalCashInBaseCurrency.plus( + marketPriceInBaseCurrency.mul(item.quantity) + ); + } + totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( totalInterestInBaseCurrency ); @@ -661,6 +669,7 @@ export abstract class PortfolioCalculator { ...overall, errors, historicalData, + totalCashInBaseCurrency, totalInterestWithCurrencyEffect, totalLiabilitiesWithCurrencyEffect, hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, 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 559fb0ed1..d5d5ef439 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 @@ -286,6 +286,7 @@ 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), 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 daacdc60d..4348ca3a4 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -113,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 108e30618..f2bcad8f5 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1887,6 +1887,7 @@ export class PortfolioService { const { currentValueInBaseCurrency, + totalCashInBaseCurrency, totalInvestment, totalInvestmentWithCurrencyEffect } = await portfolioCalculator.getSnapshot(); @@ -2015,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/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; From 89aae550a78c22cad1c500618f4d7bc35726a3e4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:55:49 +0200 Subject: [PATCH 08/10] Include cash in portfolio performance --- .../calculator/portfolio-calculator.ts | 85 +++++-------------- .../roai/portfolio-calculator-btceur.spec.ts | 6 +- .../roai/portfolio-calculator-btcusd.spec.ts | 6 +- .../roai/portfolio-calculator-cash.spec.ts | 8 +- .../portfolio-calculator-liability.spec.ts | 8 -- ...folio-calculator-novn-buy-and-sell.spec.ts | 6 +- .../historical-data-item.interface.ts | 2 +- 7 files changed, 35 insertions(+), 86 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 1bc9c2e02..d2b48daf0 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -202,6 +202,7 @@ export abstract class PortfolioCalculator { }; } + const cashSymbols = new Set(); const currencies: { [symbol: string]: string } = {}; const dataGatheringItems: DataGatheringItem[] = []; let firstIndex = transactionPoints.length; @@ -315,12 +316,11 @@ export abstract class PortfolioCalculator { const accumulatedValuesByDate: { [date: string]: { investmentValueWithCurrencyEffect: Big; - totalAccountBalanceWithCurrencyEffect: Big; + totalCashValueWithCurrencyEffect: Big; totalCurrentValue: Big; totalCurrentValueWithCurrencyEffect: Big; totalInvestmentValue: Big; totalInvestmentValueWithCurrencyEffect: Big; - totalLiabilitiesWithCurrencyEffect: Big; totalNetPerformanceValue: Big; totalNetPerformanceValueWithCurrencyEffect: Big; totalTimeWeightedInvestmentValue: Big; @@ -351,6 +351,8 @@ export abstract class PortfolioCalculator { ] ?? 1 ); + const valueInBaseCurrency = marketPriceInBaseCurrency.mul(item.quantity); + const { currentValues, currentValuesWithCurrencyEffect, @@ -444,15 +446,14 @@ 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') { - totalCashInBaseCurrency = totalCashInBaseCurrency.plus( - marketPriceInBaseCurrency.mul(item.quantity) - ); + cashSymbols.add(item.symbol); + + totalCashInBaseCurrency = + totalCashInBaseCurrency.plus(valueInBaseCurrency); } totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( @@ -474,56 +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 liabilityItemsMap = this.activities.reduce( - (map, { assetProfile, date, quantity, type, unitPrice }) => { - if (type === 'LIABILITY') { - const exchangeRate = - exchangeRatesByCurrency[ - `${assetProfile.currency}${this.currency}` - ]?.[date] ?? 1; - - map[date] = (map[date] ?? new Big(0)).plus( - quantity.mul(unitPrice).mul(exchangeRate) - ); - } - - return map; - }, - {} as { [date: string]: Big } - ); - - const accountBalanceMap: { [date: string]: Big } = {}; - const liabilitiesMap: { [date: string]: Big } = {}; - - let accumulatedLiabilities = new Big(0); - 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; - - if (liabilityItemsMap[dateString] !== undefined) { - accumulatedLiabilities = accumulatedLiabilities.plus( - liabilityItemsMap[dateString] - ); - } - - liabilitiesMap[dateString] = accumulatedLiabilities; - for (const symbol of Object.keys(valuesBySymbol)) { const symbolValues = valuesBySymbol[symbol]; @@ -566,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), @@ -582,7 +541,6 @@ export abstract class PortfolioCalculator { accumulatedValuesByDate[dateString] ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) ).add(investmentValueAccumulatedWithCurrencyEffect), - totalLiabilitiesWithCurrencyEffect: liabilitiesMap[dateString], totalNetPerformanceValue: ( accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0) @@ -608,12 +566,11 @@ export abstract class PortfolioCalculator { ).map(([date, values]) => { const { investmentValueWithCurrencyEffect, - totalAccountBalanceWithCurrencyEffect, + totalCashValueWithCurrencyEffect, totalCurrentValue, totalCurrentValueWithCurrencyEffect, totalInvestmentValue, totalInvestmentValueWithCurrencyEffect, - totalLiabilitiesWithCurrencyEffect, totalNetPerformanceValue, totalNetPerformanceValueWithCurrencyEffect, totalTimeWeightedInvestmentValue, @@ -642,10 +599,8 @@ export abstract class PortfolioCalculator { netPerformance: totalNetPerformanceValue.toNumber(), netPerformanceWithCurrencyEffect: totalNetPerformanceValueWithCurrencyEffect.toNumber(), - netWorth: totalCurrentValueWithCurrencyEffect - .minus(totalLiabilitiesWithCurrencyEffect) - .toNumber(), - totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(), + netWorth: totalCurrentValueWithCurrencyEffect.toNumber(), + totalCashInBaseCurrency: totalCashValueWithCurrencyEffect.toNumber(), totalInvestment: totalInvestmentValue.toNumber(), totalInvestmentValueWithCurrencyEffect: totalInvestmentValueWithCurrencyEffect.toNumber(), 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 d5d5ef439..5ae61590b 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 @@ -301,8 +301,10 @@ describe('PortfolioCalculator', () => { /** * Value with currency effect: 2000 USD * 0.91 = 1820 CHF - * Net worth: 1820 CHF - 0 CHF (liabilities) = 1820 CHF - * Total account balance: 1800 CHF (balance in base currency on 2024-12-31) + * 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(lastDataItem).toEqual({ @@ -313,7 +315,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentageWithCurrencyEffect: 0.08211603004634808, netPerformanceWithCurrencyEffect: 70, netWorth: 1820, - totalAccountBalance: 1800, + totalCashInBaseCurrency: 1820, totalInvestment: 1820, totalInvestmentValueWithCurrencyEffect: 1750, value: 1820, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts index 51a4c0368..94654af61 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts @@ -113,14 +113,6 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot.totalLiabilitiesWithCurrencyEffect).toEqual( new Big(3000) ); - - /** - * Net worth: 0 USD (current value) - 3000 USD (liabilities) = -3000 USD - */ - expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({ - netWorth: -3000, - valueWithCurrencyEffect: 0 - }); }); }); }); 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/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; From 07da67d86975c44089ae05ea36eacaf43fba6f36 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:56:41 +0200 Subject: [PATCH 09/10] Update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00340c890..63922ad72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Included cash in the performance calculation of the portfolio -- Considered liabilities in the net worth calculation of the portfolio ## 3.27.0 - 2026-07-15 From 31b9a360d26c43a711c74c1dbf164c6e0c617fb2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:11:30 +0200 Subject: [PATCH 10/10] Include cash in portfolio performance --- .../roai/portfolio-calculator-cash.spec.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) 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 2dcd7565e..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 @@ -139,7 +139,7 @@ describe('PortfolioCalculator', () => { }); describe('Cash Performance', () => { - const computePortfolioSnapshot = async () => { + it('should calculate performance for cash assets in CHF default currency', async () => { jest.useFakeTimers().setSystemTime(parseDate('2025-01-01').getTime()); const accountId = randomUUID(); @@ -219,11 +219,7 @@ describe('PortfolioCalculator', () => { userId: userDummyData.id }); - return portfolioCalculator.computeSnapshot(); - }; - - it('should calculate performance for cash assets in CHF default currency', async () => { - const portfolioSnapshot = await computePortfolioSnapshot(); + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const position = portfolioSnapshot.positions.find(({ symbol }) => { return symbol === 'USD'; @@ -294,12 +290,6 @@ describe('PortfolioCalculator', () => { totalInvestment: new Big(1820), totalLiabilitiesWithCurrencyEffect: new Big(0) }); - }); - - it('should include cash in the net worth and performance without counting it twice', async () => { - const portfolioSnapshot = await computePortfolioSnapshot(); - - const lastDataItem = portfolioSnapshot.historicalData.at(-1); /** * Value with currency effect: 2000 USD * 0.91 = 1820 CHF @@ -309,7 +299,7 @@ describe('PortfolioCalculator', () => { * consists of cash, hence it matches the value) * Net performance with currency effect: 70 CHF / 852.45 CHF ≈ 8.21 % */ - expect(lastDataItem).toEqual({ + expect(portfolioSnapshot.historicalData.at(-1)).toEqual({ date: '2025-01-01', investmentValueWithCurrencyEffect: 0, netPerformance: 0,