117 lines
3.0 KiB
TypeScript
117 lines
3.0 KiB
TypeScript
import {
|
|
add,
|
|
decimalFromString,
|
|
divide,
|
|
formatDecimal,
|
|
multiply,
|
|
} from '../../../../common/decimal';
|
|
|
|
import { MoneyDto } from '../../../../common/money.dto';
|
|
import {
|
|
PortfolioDto,
|
|
PortfolioPositionDto,
|
|
} from '../../portfolio/portfolio.api';
|
|
import { StockPositionDto } from '../stocks/presentation/stock.dto';
|
|
|
|
const PRICE_DECIMAL_PLACES = 9;
|
|
const RETURN_PERCENT_DECIMAL_PLACES = 6;
|
|
|
|
export function instrumentPosition(
|
|
portfolio: PortfolioDto,
|
|
uid: string,
|
|
type: 'STOCK' | 'BOND',
|
|
): StockPositionDto {
|
|
const positions = portfolio.positions.filter(
|
|
(p) => p.type === type && p.instrumentUid === uid,
|
|
);
|
|
const zero = decimalFromString('0');
|
|
const quantity = positions
|
|
.map((p) => decimalFromString(p.quantity))
|
|
.reduce(add, zero);
|
|
const costs = positions.map(cost);
|
|
const totalCost = sumComplete(costs);
|
|
const currentValue = sumComplete(positions.map((p) => p.currentValue));
|
|
const absoluteReturn = sumComplete(positions.map((p) => p.absoluteReturn));
|
|
const positive =
|
|
positions.length > 0 &&
|
|
positions.every((p) => decimalFromString(p.quantity).value > 0n);
|
|
return {
|
|
quantity: formatDecimal(quantity),
|
|
currentValue,
|
|
absoluteReturn,
|
|
averagePurchasePrice:
|
|
positive && totalCost
|
|
? {
|
|
currency: totalCost.currency,
|
|
value: formatDecimal(
|
|
divide(
|
|
decimalFromString(totalCost.value),
|
|
quantity,
|
|
PRICE_DECIMAL_PLACES,
|
|
),
|
|
),
|
|
}
|
|
: null,
|
|
returnPercent: positive ? percent(absoluteReturn, totalCost) : null,
|
|
accounts: positions.map((p) => ({
|
|
...p,
|
|
returnPercent:
|
|
decimalFromString(p.quantity).value > 0n
|
|
? percent(p.absoluteReturn, cost(p))
|
|
: null,
|
|
})),
|
|
isStale: portfolio.isStale,
|
|
observedAt: portfolio.lastSuccessfulAt,
|
|
};
|
|
}
|
|
function cost(position: PortfolioPositionDto): MoneyDto | null {
|
|
const price = position.averagePurchasePrice;
|
|
return price
|
|
? {
|
|
currency: price.currency,
|
|
value: formatDecimal(
|
|
multiply(
|
|
decimalFromString(price.value),
|
|
decimalFromString(position.quantity),
|
|
),
|
|
),
|
|
}
|
|
: null;
|
|
}
|
|
function sumComplete(values: (MoneyDto | null)[]): MoneyDto | null {
|
|
const currency = values[0]?.currency;
|
|
if (
|
|
!currency ||
|
|
!values.every((v): v is MoneyDto => v !== null && v.currency === currency)
|
|
)
|
|
return null;
|
|
return {
|
|
currency,
|
|
value: formatDecimal(
|
|
values.reduce(
|
|
(sum, value) => add(sum, decimalFromString(value.value)),
|
|
decimalFromString('0'),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
function percent(
|
|
result: MoneyDto | null,
|
|
base: MoneyDto | null,
|
|
): string | null {
|
|
if (
|
|
!base ||
|
|
!result ||
|
|
base.currency !== result.currency ||
|
|
decimalFromString(base.value).value <= 0n
|
|
)
|
|
return null;
|
|
return formatDecimal(
|
|
divide(
|
|
multiply(decimalFromString(result.value), decimalFromString('100')),
|
|
decimalFromString(base.value),
|
|
RETURN_PERCENT_DECIMAL_PLACES,
|
|
),
|
|
);
|
|
}
|