diff --git a/CHANGELOG.md b/CHANGELOG.md index d98e93424..f42ab8abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the quantity to the accounts tab of the holding detail dialog + ### Fixed - Fixed the allocation in the accounts tab of the holding detail dialog caused by floating-point rounding diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index c5e0a8332..c398d6a28 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -426,7 +426,10 @@ describe('PortfolioService', () => { return ( portfolioService as unknown as { getValueOfAccountsAndPlatforms: (aArgs: object) => Promise<{ - accounts: Record; + accounts: Record< + string, + { quantity?: number; valueInBaseCurrency: number } + >; platforms: Record; }>; } @@ -443,6 +446,10 @@ describe('PortfolioService', () => { }; beforeEach(() => { + jest + .spyOn(accountService, 'accounts') + .mockResolvedValue([account] as unknown as AccountWithBalance[]); + jest .spyOn(accountService, 'getAccounts') .mockResolvedValue([account] as unknown as AccountWithBalance[]); @@ -548,5 +555,56 @@ describe('PortfolioService', () => { expect(accounts[account.id].valueInBaseCurrency).toBe(100); expect(platforms[account.platformId].valueInBaseCurrency).toBe(100); }); + + it('should aggregate the quantity per account if the activities are filtered by a single holding', async () => { + const { accounts } = 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' + } + ], + filters: [{ id: 'AAPL', type: 'SYMBOL' }], + portfolioItemsNow: { + AAPL: { marketPriceInBaseCurrency: 10 } + }, + userCurrency: 'USD', + userId: userDummyData.id + }); + + expect(accounts[account.id].quantity).toBe(0.3); + }); + + it('should not expose a quantity if the activities are not filtered by a single holding', async () => { + const { accounts } = await getValueOfAccountsAndPlatforms({ + activities: [ + { + account, + accountId: account.id, + assetProfile: { symbol: 'AAPL' }, + quantity: 1, + type: 'BUY' + } + ], + filters: [], + portfolioItemsNow: { + AAPL: { marketPriceInBaseCurrency: 10 } + }, + userCurrency: 'USD', + userId: userDummyData.id + }); + + expect(accounts[account.id].quantity).toBeUndefined(); + }); }); }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 2c7701784..1ec3dd234 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -249,6 +249,10 @@ export class PortfolioService { } } + const quantityOfHolding = filterBySymbol + ? (details.accounts[account.id]?.quantity ?? 0) + : undefined; + const valueInBaseCurrency = details.accounts[account.id]?.valueInBaseCurrency ?? 0; @@ -264,6 +268,7 @@ export class PortfolioService { account.currency, userCurrency ), + quantity: quantityOfHolding, value: this.exchangeRateDataService.toCurrency( valueInBaseCurrency, userCurrency, @@ -2194,6 +2199,10 @@ export class PortfolioService { const accounts: PortfolioDetails['accounts'] = {}; const platforms: PortfolioDetails['platforms'] = {}; + const { SYMBOL: [filterBySymbol] = [] } = groupBy(filters, ({ type }) => { + return type; + }); + let currentAccounts: (AccountWithBalance & { Order?: Order[]; platform?: Platform; @@ -2274,23 +2283,33 @@ export class PortfolioService { continue; } + let quantityOfAccount = new Big(0); let valueOfAccountInBaseCurrency = new Big(0); for (const { assetProfile, quantity, type } of ordersByAccount) { + const currentQuantityOfSymbol = new Big(quantity).mul(getFactor(type)); + + quantityOfAccount = quantityOfAccount.plus(currentQuantityOfSymbol); + valueOfAccountInBaseCurrency = valueOfAccountInBaseCurrency.plus( - new Big(quantity) - .mul(getFactor(type)) - .mul( - portfolioItemsNow[assetProfile.symbol] - ?.marketPriceInBaseCurrency ?? 0 - ) + currentQuantityOfSymbol.mul( + portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ?? + 0 + ) ); } const currentAccountId = account?.id || UNKNOWN_KEY; const currentPlatformId = account?.platformId || UNKNOWN_KEY; + // The quantity is only meaningful if the activities are filtered by a + // single holding + const quantityOfHolding = filterBySymbol + ? quantityOfAccount.toNumber() + : undefined; + if (accounts[currentAccountId]) { + accounts[currentAccountId].quantity = quantityOfHolding; accounts[currentAccountId].valueInBaseCurrency = new Big( accounts[currentAccountId].valueInBaseCurrency ) @@ -2301,6 +2320,7 @@ export class PortfolioService { balance: 0, currency: account?.currency, name: account?.name, + quantity: quantityOfHolding, valueInBaseCurrency: valueOfAccountInBaseCurrency.toNumber() }; } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 8292ff598..041a93e6e 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -418,6 +418,7 @@ [showAllocationInPercentage]="user?.settings?.isExperimentalFeatures" [showBalance]="false" [showFooter]="false" + [showQuantity]="true" [showValue]="false" [showValueInBaseCurrency]="false" /> diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 761f7c3ed..265e27690 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -113,6 +113,7 @@ export const DEFAULT_REDACTED_PATHS = [ 'accounts[*].comment', 'accounts[*].dividendInBaseCurrency', 'accounts[*].interestInBaseCurrency', + 'accounts[*].quantity', 'accounts[*].value', 'accounts[*].valueInBaseCurrency', 'activities[*].account.comment', diff --git a/libs/common/src/lib/interfaces/portfolio-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-details.interface.ts index 746736f6b..15a07f671 100644 --- a/libs/common/src/lib/interfaces/portfolio-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-details.interface.ts @@ -10,6 +10,8 @@ export interface PortfolioDetails { balance: number; currency: string; name: string; + /** Only set if the activities are filtered by a single holding */ + quantity?: number; valueInBaseCurrency: number; valueInPercentage?: number; }; diff --git a/libs/common/src/lib/types/account-with-value.type.ts b/libs/common/src/lib/types/account-with-value.type.ts index 27b7541dc..0d43c612f 100644 --- a/libs/common/src/lib/types/account-with-value.type.ts +++ b/libs/common/src/lib/types/account-with-value.type.ts @@ -9,6 +9,8 @@ export type AccountWithValue = AccountWithBalance & { dividendInBaseCurrency: number; interestInBaseCurrency: number; platform?: Platform; + /** Only set if the accounts are filtered by a single holding */ + quantity?: number; tags?: Tag[]; value: number; valueInBaseCurrency: number; diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index b6e0d846c..0b4bb72ce 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -131,6 +131,26 @@ + + + Quantity + + + + + + + (); public readonly showBalance = input(true); public readonly showFooter = input(true); + public readonly showQuantity = input(); public readonly showValue = input(true); public readonly showValueInBaseCurrency = input(true); public readonly totalBalanceInBaseCurrency = input(); @@ -103,6 +104,10 @@ export class GfAccountsTableComponent { columns.push('activitiesCount'); } + if (this.showQuantity()) { + columns.push('quantity'); + } + if (this.showBalance()) { columns.push('balance'); }