import type { Money } from '../../../../common/money.types'; import { add, DECIMAL_RADIX, decimalFromString, formatDecimal, } from '../../../../common/decimal'; const ZERO_AMOUNT = 0n; const INITIAL_UNVALUED_COUNT = 0; const POSITION_INCREMENT = 1; const ROUNDING_DIVISOR = 2n; const SHARE_PERCENT_DECIMAL_PLACES = 6; const PERCENT_SCALED_FACTOR = 100_000_000n; const categoryTypes = ['STOCK', 'BOND', 'OTHER', 'CASH'] as const; // Build from positions and cash, never from totals (which already include cash). export function portfolioStructure( positions: { currentValue: Money | null; type: string }[], cash: Money[], ) { const currencies = new Map< string, { hasNegativeValues: boolean; values: ReturnType[]; } >(); const unvalued = new Map(); const include = (type: string, money: Money) => { const group = currencies.get(money.currency) ?? { values: categoryTypes.map(() => decimalFromString('0')), hasNegativeValues: false, }; const index = categoryTypes.findIndex((category) => category === type); const value = decimalFromString(money.value); // eslint-disable-next-line security/detect-object-injection -- The numeric index belongs to the fixed portfolio category array. group.values[index] = add(group.values[index], value); group.hasNegativeValues ||= value.value < ZERO_AMOUNT; currencies.set(money.currency, group); }; for (const position of positions) { const type = ['BOND', 'STOCK'].includes(position.type) ? position.type : 'OTHER'; if (position.currentValue) include(type, position.currentValue); else unvalued.set(type, (unvalued.get(type) ?? INITIAL_UNVALUED_COUNT) + POSITION_INCREMENT); } for (const amount of cash) include('CASH', amount); return { unvaluedCategories: [...unvalued].map(([type, count]) => ({ type, count })), currencies: [...currencies] .sort(([left], [right]) => left.localeCompare(right)) .map(([currency, group]) => { const total = group.values.reduce(add, decimalFromString('0')); const canCalculateShares = !unvalued.size && !group.hasNegativeValues && total.value > ZERO_AMOUNT; const categories = group.values.map((value, index) => ({ // eslint-disable-next-line security/detect-object-injection -- The numeric index belongs to the fixed portfolio category array. type: categoryTypes[index], value: formatDecimal(value), // Rounded to six decimal places using integer arithmetic. Only the // resulting dimensionless percentage is passed to chart geometry. sharePercent: canCalculateShares ? formatDecimal({ value: (value.value * DECIMAL_RADIX ** BigInt(total.scale - value.scale) * PERCENT_SCALED_FACTOR + total.value / ROUNDING_DIVISOR) / total.value, scale: SHARE_PERCENT_DECIMAL_PLACES, }) : null, })); return { currency, totalValue: formatDecimal(total), hasNegativeValues: group.hasNegativeValues, categories, }; }), }; }