diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-18 10:36:09 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-18 10:36:09 +0800 |
| commit | f3831e6cc62938c94b398e93e9f25e6f22164266 (patch) | |
| tree | 2f8a2cfb65bfde324af14c9a24709e0843b3401d /frontend/src/lib/capitalMetrics.ts | |
| parent | f401925aa27c641ba821b64374e1399c829249ad (diff) | |
| download | strategy-lab-f3831e6cc62938c94b398e93e9f25e6f22164266.tar.gz | |
feat(frontend): 投入口径 reporting 推导函数(资本三件套:投入/持仓估值/回报率)
[变更性质] 新增纯函数推导层,任务 A3。
[新增功能] capitalReporting:累计投入=首日cash−末日cash(2位);持仓估值=Σ末日positions×closes;投入回报率=估值/投入−1(带正负号百分号);未投入/无数据返回 null 不除零。
[实现方案] 纯前端推导,零后端改动;测试锚点取现网 run 26e9c9ab(投入 12029.09 / 估值 10846.00 / -9.84%)真实账目,另有多 symbol/未建仓/空数据等 9 用例。
[影响范围] frontend/src/lib/capitalMetrics.ts + 新增测试,vitest 84/84。
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 }; +} |
