summaryrefslogtreecommitdiff
path: root/frontend/src/lib/capitalMetrics.ts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 10:36:09 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 10:36:09 +0800
commitf3831e6cc62938c94b398e93e9f25e6f22164266 (patch)
tree2f8a2cfb65bfde324af14c9a24709e0843b3401d /frontend/src/lib/capitalMetrics.ts
parentf401925aa27c641ba821b64374e1399c829249ad (diff)
downloadstrategy-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.ts46
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 };
+}