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:
parent
3ec2460bfe
commit
40d3eaa023
@ -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
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user