import { add, decimalFromString, formatDecimal, } from '../../../../common/decimal'; import type { Money } from '../../../../common/money.types'; const categoryTypes = ['STOCK', 'BOND', 'OTHER', 'CASH'] as const; // Build from positions and cash, never from totals (which already include cash). export function portfolioStructure( positions: { type: string; currentValue: Money | null }[], cash: Money[], ) { const currencies = new Map< string, { values: ReturnType[]; hasNegativeValues: boolean; } >(); 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); group.values[index] = add(group.values[index], value); group.hasNegativeValues ||= value.value < 0n; currencies.set(money.currency, group); }; for (const position of positions) { const type = ['STOCK', 'BOND'].includes(position.type) ? position.type : 'OTHER'; if (position.currentValue) include(type, position.currentValue); else unvalued.set(type, (unvalued.get(type) ?? 0) + 1); } 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 > 0n; const categories = group.values.map((value, index) => ({ 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 * 10n ** BigInt(total.scale - value.scale) * 100_000_000n + total.value / 2n) / total.value, scale: 6, }) : null, })); return { currency, totalValue: formatDecimal(total), hasNegativeValues: group.hasNegativeValues, categories, }; }), }; }