From 3ec2460bfea3fa10c01174f535ad2348ace67de1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 17:38:21 +0200 Subject: [PATCH 1/2] Feature/update locales (#4664) * Update locales * Clean up --------- Co-authored-by: github-actions[bot] Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- apps/client/src/locales/messages.tr.xlf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 020b0268a..459db2d02 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -3588,7 +3588,7 @@ How does Ghostfolio work? - NasılGhostfolio çalışır? + NasılGhostfolio çalışır? apps/client/src/app/pages/landing/landing-page.html 383 @@ -5384,7 +5384,7 @@ Switch to Ghostfolio Premium or Ghostfolio Open Source easily - Ghostfolio Premium veya Ghostfolio Open Source'a kolayca geçin + Ghostfolio Premium veya Ghostfolio Open Source’a kolayca geçin libs/ui/src/lib/i18n.ts 12 @@ -5392,7 +5392,7 @@ Switch to Ghostfolio Premium easily - Ghostfolio Premium'a kolayca geçin + Ghostfolio Premium’a kolayca geçin libs/ui/src/lib/i18n.ts 13 @@ -5400,7 +5400,7 @@ Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Ghostfolio Açık Kaynak veya Ghostfolio Temel'e kolayca geçin. + Ghostfolio Açık Kaynak veya Ghostfolio Temel’e kolayca geçin. libs/ui/src/lib/i18n.ts 14 @@ -6372,7 +6372,7 @@ If you retire today, you would be able to withdraw per year or per month, based on your total assets of and a withdrawal rate of 4%. - Eğer bugün emekli olursanız, toplam tutarındaki varlıklarınız ve %4'lük bir çekilme oranına dayanarak yıllık veya aylık çekebilirsiniz. + If you retire today, you would be able to withdraw per year or per month, based on your total assets of and a withdrawal rate of 4%. apps/client/src/app/pages/portfolio/fire/fire-page.html 67 @@ -6620,7 +6620,7 @@ Approximation based on the top holdings of each ETF - Her ETF'nin en üst tutarlarına dayalı yaklaşım + Her ETF’nin en üst tutarlarına dayalı yaklaşım apps/client/src/app/pages/portfolio/allocations/allocations-page.html 340 From 40d3eaa023cebc932526366c79f36d115f041fe0 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Tue, 6 May 2025 22:41:04 +0700 Subject: [PATCH 2/2] Bugfix/fix performance calculation on date of activity when unit price differs from market price (#4650) * Fix performance calculation on date of activity when unit price differs from market price * Update changelog --- CHANGELOG.md | 4 + .../portfolio-calculator-baln-buy.spec.ts | 78 +++++++++++++++++++ .../roai/portfolio-calculator-btceur.spec.ts | 17 ++-- .../roai/portfolio-calculator-btcusd.spec.ts | 17 ++-- ...folio-calculator-novn-buy-and-sell.spec.ts | 18 +++-- .../calculator/roai/portfolio-calculator.ts | 29 +++++-- 6 files changed, 134 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3f035cd..89e9fa525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the language localization for Turkish (`tr`) +### Fixed + +- Fixed an issue in the performance calculation on the date of an activity when the unit price differs from the market price + ## 2.160.0 - 2025-05-04 ### Added diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index b21192db1..e7f95aea8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -193,5 +193,83 @@ describe('PortfolioCalculator', () => { { date: '2021-12-01', investment: 0 } ]); }); + + it.only('with BALN.SW buy (with unit price lower than closing price)', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 135.0 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + const snapshotOnBuyDate = portfolioSnapshot.historicalData.find( + ({ date }) => { + return date === '2021-11-30'; + } + ); + + // Closing price on 2021-11-30: 136.6 + expect(snapshotOnBuyDate?.netPerformanceWithCurrencyEffect).toEqual(1.65); // 2 * (136.6 - 135.0) - 1.55 = 1.65 + }); + + it.only('with BALN.SW buy (with unit price lower than closing price), calculated on buy date', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2021-11-30').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 135.0 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + const snapshotOnBuyDate = portfolioSnapshot.historicalData.find( + ({ date }) => { + return date === '2021-11-30'; + } + ); + + // Closing price on 2021-11-30: 136.6 + expect(snapshotOnBuyDate?.netPerformanceWithCurrencyEffect).toEqual(1.65); // 2 * (136.6 - 135.0) - 1.55 = 1.65 + }); }); }); 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 2c5b90050..ba818eb40 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 @@ -142,19 +142,22 @@ describe('PortfolioCalculator', () => { valueWithCurrencyEffect: 0 }); + /** + * Closing price on 2021-12-12: 50098.3 + */ expect(portfolioSnapshot.historicalData[1]).toEqual({ date: '2021-12-12', investmentValueWithCurrencyEffect: 44558.42, - netPerformance: -4.46, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: -4.46, - netWorth: 44558.42, + netPerformance: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42 + netPerformanceInPercentage: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 + netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 + netPerformanceWithCurrencyEffect: 5535.42, + netWorth: 50098.3, // 1 * 50098.3 = 50098.3 totalAccountBalance: 0, totalInvestment: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42, - value: 44558.42, - valueWithCurrencyEffect: 44558.42 + value: 50098.3, // 1 * 50098.3 = 50098.3 + valueWithCurrencyEffect: 50098.3 }); expect( 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 96205fd77..cf07eff97 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 @@ -142,19 +142,22 @@ describe('PortfolioCalculator', () => { valueWithCurrencyEffect: 0 }); + /** + * Closing price on 2021-12-12: 50098.3 + */ expect(portfolioSnapshot.historicalData[1]).toEqual({ date: '2021-12-12', investmentValueWithCurrencyEffect: 44558.42, - netPerformance: -4.46, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: -4.46, - netWorth: 44558.42, + netPerformance: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42 + netPerformanceInPercentage: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 + 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, totalInvestment: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42, - value: 44558.42, - valueWithCurrencyEffect: 44558.42 + value: 50098.3, // 1 * 50098.3 = 50098.3 + valueWithCurrencyEffect: 50098.3 }); expect( 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 b5d7d59f8..3d4760be7 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 @@ -145,19 +145,23 @@ describe('PortfolioCalculator', () => { valueWithCurrencyEffect: 0 }); + /** + * Closing price on 2022-03-07 is unknown, + * hence it uses the last unit price (2022-04-11): 87.8 + */ expect(portfolioSnapshot.historicalData[1]).toEqual({ date: '2022-03-07', investmentValueWithCurrencyEffect: 151.6, - netPerformance: 0, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: 0, - netWorth: 151.6, + netPerformance: 24, // 2 * (87.8 - 75.8) = 24 + netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 + netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 + netPerformanceWithCurrencyEffect: 24, + netWorth: 175.6, // 2 * 87.8 = 175.6 totalAccountBalance: 0, totalInvestment: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6, - value: 151.6, - valueWithCurrencyEffect: 151.6 + value: 175.6, // 2 * 87.8 = 175.6 + valueWithCurrencyEffect: 175.6 }); expect( 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 c22a101bd..d9da465f9 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -456,12 +456,19 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { ); } + const marketPriceInBaseCurrency = + order.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ?? + new Big(0); + const marketPriceInBaseCurrencyWithCurrencyEffect = + order.unitPriceFromMarketData?.mul(exchangeRateAtOrderDate ?? 1) ?? + new Big(0); + const valueOfInvestmentBeforeTransaction = totalUnits.mul( - order.unitPriceInBaseCurrency + marketPriceInBaseCurrency ); const valueOfInvestmentBeforeTransactionWithCurrencyEffect = - totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); + totalUnits.mul(marketPriceInBaseCurrencyWithCurrencyEffect); if (!investmentAtStartDate && i >= indexOfStartOrder) { investmentAtStartDate = totalInvestment ?? new Big(0); @@ -558,10 +565,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); - const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); + const valueOfInvestment = totalUnits.mul(marketPriceInBaseCurrency); const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( - order.unitPriceInBaseCurrencyWithCurrencyEffect + marketPriceInBaseCurrencyWithCurrencyEffect ); const grossPerformanceFromSell = @@ -701,17 +708,23 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) ).add(transactionInvestmentWithCurrencyEffect); + // If duration is effectively zero (first day), use the actual investment as the base. + // Otherwise, use the calculated time-weighted average. timeWeightedInvestmentValues[order.date] = - totalInvestmentDays > 0 + totalInvestmentDays > Number.EPSILON ? sumOfTimeWeightedInvestments.div(totalInvestmentDays) - : new Big(0); + : totalInvestment.gt(0) + ? totalInvestment + : new Big(0); timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = - totalInvestmentDays > 0 + totalInvestmentDays > Number.EPSILON ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( totalInvestmentDays ) - : new Big(0); + : totalInvestmentWithCurrencyEffect.gt(0) + ? totalInvestmentWithCurrencyEffect + : new Big(0); } if (PortfolioCalculator.ENABLE_LOGGING) {