Browse Source

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
pull/4668/head^2
Kenrick Tandrian 2 months ago
committed by GitHub
parent
commit
40d3eaa023
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 78
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  3. 17
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  4. 17
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  5. 18
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  6. 29
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

4
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`) - 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 ## 2.160.0 - 2025-05-04
### Added ### Added

78
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 } { 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
});
}); });
}); });

17
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -142,19 +142,22 @@ describe('PortfolioCalculator', () => {
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
/**
* Closing price on 2021-12-12: 50098.3
*/
expect(portfolioSnapshot.historicalData[1]).toEqual({ expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2021-12-12', date: '2021-12-12',
investmentValueWithCurrencyEffect: 44558.42, investmentValueWithCurrencyEffect: 44558.42,
netPerformance: -4.46, netPerformance: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
netPerformanceWithCurrencyEffect: -4.46, netPerformanceWithCurrencyEffect: 5535.42,
netWorth: 44558.42, netWorth: 50098.3, // 1 * 50098.3 = 50098.3
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 44558.42, totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 44558.42, value: 50098.3, // 1 * 50098.3 = 50098.3
valueWithCurrencyEffect: 44558.42 valueWithCurrencyEffect: 50098.3
}); });
expect( expect(

17
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -142,19 +142,22 @@ describe('PortfolioCalculator', () => {
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
/**
* Closing price on 2021-12-12: 50098.3
*/
expect(portfolioSnapshot.historicalData[1]).toEqual({ expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2021-12-12', date: '2021-12-12',
investmentValueWithCurrencyEffect: 44558.42, investmentValueWithCurrencyEffect: 44558.42,
netPerformance: -4.46, netPerformance: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
netPerformanceWithCurrencyEffect: -4.46, netPerformanceWithCurrencyEffect: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
netWorth: 44558.42, netWorth: 50098.3, // 1 * 50098.3 = 50098.3
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 44558.42, totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 44558.42, value: 50098.3, // 1 * 50098.3 = 50098.3
valueWithCurrencyEffect: 44558.42 valueWithCurrencyEffect: 50098.3
}); });
expect( expect(

18
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -145,19 +145,23 @@ describe('PortfolioCalculator', () => {
valueWithCurrencyEffect: 0 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({ expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2022-03-07', date: '2022-03-07',
investmentValueWithCurrencyEffect: 151.6, investmentValueWithCurrencyEffect: 151.6,
netPerformance: 0, netPerformance: 24, // 2 * (87.8 - 75.8) = 24
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 24,
netWorth: 151.6, netWorth: 175.6, // 2 * 87.8 = 175.6
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 151.6, totalInvestment: 151.6,
totalInvestmentValueWithCurrencyEffect: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6,
value: 151.6, value: 175.6, // 2 * 87.8 = 175.6
valueWithCurrencyEffect: 151.6 valueWithCurrencyEffect: 175.6
}); });
expect( expect(

29
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( const valueOfInvestmentBeforeTransaction = totalUnits.mul(
order.unitPriceInBaseCurrency marketPriceInBaseCurrency
); );
const valueOfInvestmentBeforeTransactionWithCurrencyEffect = const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); totalUnits.mul(marketPriceInBaseCurrencyWithCurrencyEffect);
if (!investmentAtStartDate && i >= indexOfStartOrder) { if (!investmentAtStartDate && i >= indexOfStartOrder) {
investmentAtStartDate = totalInvestment ?? new Big(0); investmentAtStartDate = totalInvestment ?? new Big(0);
@ -558,10 +565,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); const valueOfInvestment = totalUnits.mul(marketPriceInBaseCurrency);
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
order.unitPriceInBaseCurrencyWithCurrencyEffect marketPriceInBaseCurrencyWithCurrencyEffect
); );
const grossPerformanceFromSell = const grossPerformanceFromSell =
@ -701,17 +708,23 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect); ).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] = timeWeightedInvestmentValues[order.date] =
totalInvestmentDays > 0 totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: new Big(0); : totalInvestment.gt(0)
? totalInvestment
: new Big(0);
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = timeWeightedInvestmentValuesWithCurrencyEffect[order.date] =
totalInvestmentDays > 0 totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
totalInvestmentDays totalInvestmentDays
) )
: new Big(0); : totalInvestmentWithCurrencyEffect.gt(0)
? totalInvestmentWithCurrencyEffect
: new Big(0);
} }
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {

Loading…
Cancel
Save