// 投入口径 reporting(任务 A):纯前端推导,不触后端与 worker 容器。 // 口径(在结果页注明"不含初始账户闲置现金"): // 累计投入 = 首日 cash − 末日 cash(保留 2 位) // 最终持仓估值 = Σ 末日 positions[k] × closes[k] // 投入回报率 = 估值 / 投入 − 1(带正负号与百分号) // 未建仓/无数据返回 null,由 UI 以 "—" 占位,不编造数字。 import type { BacktestResult } from './types'; export interface CapitalReporting { invested: number | null; holdingValue: number | null; returnPct: string | null; } /** round(v, 2) with fixed exponent (avoids FP drift vs Number(v.toFixed(2))) */ function r2(v: number): number { return Number(v.toFixed(2)); } export function fmtInvestmentPct(x: number): string { return `${x >= 0 ? '+' : ''}${(x * 100).toFixed(2)}%`; } export function capitalReporting( result: Pick | { equity?: BacktestResult['equity'] | undefined } ): CapitalReporting { const eq = result?.equity ?? []; const first = eq[0]; const last = eq[eq.length - 1]; if (!first || !last || eq.length < 2) return { invested: null, holdingValue: null, returnPct: null }; const invested = r2(first.cash - last.cash); let holdingValue = 0; const pos = (last as { positions?: Record }).positions; const cls = (last as { closes?: Record }).closes; if (pos && cls) { for (const sym of Object.keys(pos)) { holdingValue += (pos[sym] ?? 0) * (cls[sym] ?? 0); } } const investedOut = eq.length >= 2 ? invested : null; const holdingValueOut = r2(holdingValue); const returnPct = investedOut !== null && investedOut > 0 ? fmtInvestmentPct(holdingValueOut / investedOut - 1) : null; return { invested: investedOut, holdingValue: holdingValueOut, returnPct }; }