summaryrefslogtreecommitdiff
path: root/frontend/src/lib/capitalMetrics.ts
blob: cd062a85e50ca977dae69eafeea2ba35206d8a85 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 };
}