fix: count brokerage cash once and skip short-position return percent (WealthLedger/backend#140)
This commit is contained in:
@@ -128,6 +128,27 @@ describe('saved brokerage account projection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('omits return percent for a currency with short positions', async () => {
|
||||
portfolio.saved.mockResolvedValueOnce({
|
||||
membership: meta,
|
||||
accounts: [{
|
||||
id: 'a', name: 'A', type: 'BROKERAGE', openedOn: null, meta,
|
||||
snapshot: {
|
||||
cashBalances: [], structure: {}, totalsByCurrency: [],
|
||||
positions: [
|
||||
{ type: 'STOCK', quantity: '-2', averagePurchasePrice: { currency: 'RUB', value: '100' }, absoluteReturn: { currency: 'RUB', value: '-20' } },
|
||||
{ type: 'STOCK', quantity: '1', averagePurchasePrice: { currency: 'USD', value: '10' }, absoluteReturn: { currency: 'USD', value: '1' } },
|
||||
],
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect((await service.list('owner')).data?.[0]?.portfolioSummary?.unrealizedReturnByCurrency).toEqual([
|
||||
{ currency: 'RUB', absolute: '-20', costBasis: '-200', percent: null, partial: false },
|
||||
{ currency: 'USD', absolute: '1', costBasis: '10', percent: '10', partial: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('observes the latest hourly walk without starting another one', async () => {
|
||||
portfolio.saved.mockResolvedValue({ membership: meta, accounts: [] });
|
||||
sync.hourly.mockResolvedValue({
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from './brokerage-account.dto';
|
||||
|
||||
const PERCENT_DECIMAL_PLACES = 2;
|
||||
const ZERO_QUANTITY = 0n;
|
||||
|
||||
function unrealizedReturns(positions: PortfolioPositionDto[]): UnrealizedReturnDto[] {
|
||||
// Cash-like CURRENCY positions must not duplicate the separately returned cash balances.
|
||||
@@ -47,6 +48,13 @@ function unrealizedReturns(positions: PortfolioPositionDto[]): UnrealizedReturnD
|
||||
return money ? [money.currency] : [];
|
||||
}),
|
||||
);
|
||||
// A short position has negative cost basis, so a percent would flip sign or net out.
|
||||
const withShorts = new Set(
|
||||
investments.flatMap((position) => {
|
||||
const money = position.absoluteReturn ?? position.averagePurchasePrice ?? position.currentValue;
|
||||
return money && decimalFromString(position.quantity).value < ZERO_QUANTITY ? [money.currency] : [];
|
||||
}),
|
||||
);
|
||||
const currencies = new Set([...absolute, ...costs].map((money) => money.currency));
|
||||
return [...new Set([...currencies, ...partial])]
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
@@ -58,7 +66,7 @@ function unrealizedReturns(positions: PortfolioPositionDto[]): UnrealizedReturnD
|
||||
currency,
|
||||
absolute: absoluteReturn,
|
||||
costBasis,
|
||||
percent: isZero(cost)
|
||||
percent: isZero(cost) || withShorts.has(currency)
|
||||
? null
|
||||
: formatDecimal(divide(
|
||||
multiply(decimalFromString(absoluteReturn), decimalFromString('100')),
|
||||
|
||||
Reference in New Issue
Block a user