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
This commit is contained in:
Kenrick Tandrian 2025-05-06 22:41:04 +07:00 committed by GitHub
parent 3ec2460bfe
commit 40d3eaa023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 29 deletions

View File

@ -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

View File

@ -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
});
});
});

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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) {