diff options
Diffstat (limited to 'frontend/src/lib/capitalMetrics.ts')
| -rw-r--r-- | frontend/src/lib/capitalMetrics.ts | 46 |
1 files changed, 46 insertions, 0 deletions
diff --git a/frontend/src/lib/capitalMetrics.ts b/frontend/src/lib/capitalMetrics.ts new file mode 100644 index 0000000..cd062a8 --- /dev/null +++ b/frontend/src/lib/capitalMetrics.ts @@ -0,0 +1,46 @@ +// 投入口径 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<BacktestResult, 'equity'> | { 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<string, number> }).positions; + const cls = (last as { closes?: Record<string, number> }).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 }; +} |
