summaryrefslogtreecommitdiff
path: root/frontend/src/lib/capitalMetrics.ts
diff options
context:
space:
mode:
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 };
+}