diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3f035c..89e9fa52 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 b21192db..e7f95aea 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 2c5b9005..ba818eb4 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 96205fd7..cf07eff9 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 b5d7d59f..3d4760be 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 c22a101b..d9da465f 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) {