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 };
}
|