diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a582a2e3..50f1f7c43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- Fixed an issue in the performance calculation on the date of an activity when the unit price differs from the market price
- Fixed the horizontal overflow in the table of the benchmark component
## 2.160.0 - 2025-05-04
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) {
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