Browse Source

Fix rounding of value per account and platform (#7594)

Aggregate the value of accounts and platforms with Big.js, so that activities cancelling each other out result in exactly zero instead of a floating-point residue. Such a residue distorted the allocation per account in the holding detail dialog, e.g. -8.5277e-16 for a closed position and 1.0000000000000009 for the remaining one.
pull/7596/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
7b74cf5cea
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 38
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  3. 56
      apps/api/src/app/portfolio/portfolio.service.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the allocation in the accounts tab of the holding detail dialog caused by floating-point rounding
- Fixed the account aggregations in impersonation mode to be based on the impersonated user
- Fixed the base currency of the activities in impersonation mode to be based on the impersonated user
- Fixed the base currency of the dividends in impersonation mode to be based on the impersonated user

38
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -510,5 +510,43 @@ describe('PortfolioService', () => {
expect(accounts[UNKNOWN_KEY]).toBeUndefined();
expect(platforms[UNKNOWN_KEY]).toBeUndefined();
});
it('should not accumulate rounding errors of activities cancelling each other out', async () => {
const { accounts, platforms } = await getValueOfAccountsAndPlatforms({
activities: [
{
account,
accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 0.1,
type: 'BUY'
},
{
account,
accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 0.2,
type: 'BUY'
},
{
account,
accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 0.3,
type: 'SELL'
}
],
filters: [],
portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 1234.5678 }
},
userCurrency: 'USD',
userId: userDummyData.id
});
// 100 (balance) + 0 (activities)
expect(accounts[account.id].valueInBaseCurrency).toBe(100);
expect(platforms[account.platformId].valueInBaseCurrency).toBe(100);
});
});
});

56
apps/api/src/app/portfolio/portfolio.service.ts

@ -2270,40 +2270,56 @@ export class PortfolioService {
}
}
for (const { account, assetProfile, quantity, type } of ordersByAccount) {
const currentValueOfSymbolInBaseCurrency =
getFactor(type) *
quantity *
(portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ??
0);
if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
accounts[account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
currentValueOfSymbolInBaseCurrency;
if (ordersByAccount.length === 0) {
continue;
}
let valueOfAccountInBaseCurrency = new Big(0);
for (const { assetProfile, quantity, type } of ordersByAccount) {
valueOfAccountInBaseCurrency = valueOfAccountInBaseCurrency.plus(
new Big(quantity)
.mul(getFactor(type))
.mul(
portfolioItemsNow[assetProfile.symbol]
?.marketPriceInBaseCurrency ?? 0
)
);
}
const currentAccountId = account?.id || UNKNOWN_KEY;
const currentPlatformId = account?.platformId || UNKNOWN_KEY;
if (accounts[currentAccountId]) {
accounts[currentAccountId].valueInBaseCurrency = new Big(
accounts[currentAccountId].valueInBaseCurrency
)
.plus(valueOfAccountInBaseCurrency)
.toNumber();
} else {
accounts[account?.id || UNKNOWN_KEY] = {
accounts[currentAccountId] = {
balance: 0,
currency: account?.currency,
name: account?.name,
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
valueInBaseCurrency: valueOfAccountInBaseCurrency.toNumber()
};
}
if (
platforms[account?.platformId || UNKNOWN_KEY]?.valueInBaseCurrency
) {
platforms[account?.platformId || UNKNOWN_KEY].valueInBaseCurrency +=
currentValueOfSymbolInBaseCurrency;
if (platforms[currentPlatformId]) {
platforms[currentPlatformId].valueInBaseCurrency = new Big(
platforms[currentPlatformId].valueInBaseCurrency
)
.plus(valueOfAccountInBaseCurrency)
.toNumber();
} else {
platforms[account?.platformId || UNKNOWN_KEY] = {
platforms[currentPlatformId] = {
balance: 0,
currency: account?.currency,
name: account?.platform?.name,
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
valueInBaseCurrency: valueOfAccountInBaseCurrency.toNumber()
};
}
}
}
return { accounts, platforms };
}

Loading…
Cancel
Save