import { add, type Decimal, decimalFromString, divide, formatDecimal, multiply, } from '#src/common/decimal'; import type { PerformanceFlow } from './performance-flows'; const UNIT_SCALE = 12; const CENT_SCALE = 2; const ZERO: Decimal = { scale: 0, value: 0n }; const FIRST = 0; const NEXT = 1; const LAST = -1; const CENT = decimalFromString('1'); export type IndexClose = { close: string; tradeDate: string }; export type IndexPoint = { date: string; netInvested: string; value: string }; function valueAt(units: Decimal, close: string): string { return formatDecimal(divide(multiply(units, decimalFromString(close)), CENT, CENT_SCALE)); } /** A closed price on or before every cash flow is mandatory; no invented prices. */ export function shadowIndex( flows: PerformanceFlow[], closes: IndexClose[], from: string, to: string, ): null | { asOf: string; points: IndexPoint[]; value: string } { const prices = closes.filter((close) => close.tradeDate <= to) .sort((a, b) => a.tradeDate.localeCompare(b.tradeDate)); if (!prices.length) return null; const events = [...flows].sort((a, b) => a.at.valueOf() - b.at.valueOf()); let closeIndex = FIRST; let eventIndex = FIRST; let units = ZERO; let invested = ZERO; const points: IndexPoint[] = []; // Advance prices only up to the date of the event, never to the next trading day. const applyEvent = (event: PerformanceFlow): boolean => { while (prices[closeIndex + NEXT]?.tradeDate <= event.date) closeIndex++; // eslint-disable-next-line security/detect-object-injection -- Numeric index advances only through this validated sorted local array. const price = prices[closeIndex]; if (!price || price.tradeDate > event.date) return false; units = add(units, divide(event.amount, decimalFromString(price.close), UNIT_SCALE)); // Withdrawals larger than the index position cannot be replayed honestly. if (units.value < ZERO.value) return false; invested = add(invested, event.amount); return true; }; for (const close of prices) { // eslint-disable-next-line security/detect-object-injection -- Sequential local iteration bounded by array length. while (events[eventIndex]?.date <= close.tradeDate) { // eslint-disable-next-line security/detect-object-injection -- While condition ensures an event exists at this index. const event = events[eventIndex]; if (!event || !applyEvent(event)) return null; eventIndex++; } if (close.tradeDate >= from) { points.push({ date: close.tradeDate, netInvested: formatDecimal(invested), value: valueAt(units, close.close), }); } } // A flow on a market holiday after the last close still buys at that close. // eslint-disable-next-line security/detect-object-injection -- Sequential local iteration bounded by array length. while (events[eventIndex]?.date <= to) { // eslint-disable-next-line security/detect-object-injection -- While condition ensures an event exists at this index. const event = events[eventIndex]; if (!event || !applyEvent(event)) return null; eventIndex++; } if (eventIndex !== events.length || !points.length) return null; const last = points.at(LAST); const finalPrice = prices.at(LAST); if (!last || !finalPrice) return null; return { asOf: last.date, points, value: valueAt(units, finalPrice.close) }; }