1c75db4a19
SIM shows perp/option fees and slip separately; LIVE keeps real exchange fees only with slip forced to zero. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { apiFetch } from "../api/client";
|
||
import { closeReasonZh } from "../labels";
|
||
|
||
type Summary = {
|
||
mode?: "SIM" | "LIVE" | string;
|
||
show_slip?: boolean;
|
||
groups: number;
|
||
wins: number;
|
||
win_rate: number;
|
||
total_pnl: number;
|
||
fees_perp?: number;
|
||
fees_option?: number;
|
||
total_fees: number;
|
||
total_slip: number;
|
||
close_reasons: Record<string, number>;
|
||
equity_curve: { group_id: string; realized_pnl: number }[];
|
||
};
|
||
|
||
export default function StatsPage() {
|
||
const [s, setS] = useState<Summary | null>(null);
|
||
const [err, setErr] = useState("");
|
||
|
||
useEffect(() => {
|
||
apiFetch<Summary>("/api/stats/summary")
|
||
.then(setS)
|
||
.catch((e) => setErr(e instanceof Error ? e.message : String(e)));
|
||
}, []);
|
||
|
||
const reasonEntries = s
|
||
? Object.entries(s.close_reasons).sort((a, b) => b[1] - a[1])
|
||
: [];
|
||
const showSlip = s?.show_slip ?? s?.mode !== "LIVE";
|
||
|
||
return (
|
||
<div className="card">
|
||
<h2 style={{ marginTop: 0 }}>统计</h2>
|
||
{err ? <div className="err">{err}</div> : null}
|
||
{!s ? (
|
||
<p style={{ color: "var(--muted)" }}>加载中…</p>
|
||
) : (
|
||
<>
|
||
<div className="kv">
|
||
<span>组数 / 胜率</span>
|
||
<span className="mono">
|
||
{s.groups} / {(s.win_rate * 100).toFixed(1)}%
|
||
</span>
|
||
</div>
|
||
<div className="kv">
|
||
<span>总盈亏</span>
|
||
<span className="mono">{s.total_pnl.toFixed(2)}</span>
|
||
</div>
|
||
<div className="kv">
|
||
<span>永续手续费</span>
|
||
<span className="mono">
|
||
{(s.fees_perp ?? 0).toFixed(4)}
|
||
</span>
|
||
</div>
|
||
<div className="kv">
|
||
<span>期权手续费</span>
|
||
<span className="mono">
|
||
{(s.fees_option ?? 0).toFixed(4)}
|
||
</span>
|
||
</div>
|
||
<div className="kv">
|
||
<span>手续费合计</span>
|
||
<span className="mono">{s.total_fees.toFixed(4)}</span>
|
||
</div>
|
||
{showSlip ? (
|
||
<div className="kv">
|
||
<span>滑点合计</span>
|
||
<span className="mono">{s.total_slip.toFixed(4)}</span>
|
||
</div>
|
||
) : null}
|
||
<div className="kv" style={{ alignItems: "flex-start" }}>
|
||
<span>平仓原因</span>
|
||
<span className="mono" style={{ textAlign: "right" }}>
|
||
{reasonEntries.length === 0
|
||
? "—"
|
||
: reasonEntries.map(([k, n]) => (
|
||
<div key={k}>
|
||
{closeReasonZh(k)} × {n}
|
||
</div>
|
||
))}
|
||
</span>
|
||
</div>
|
||
<h3>按组盈亏</h3>
|
||
{s.equity_curve.map((x) => (
|
||
<div key={x.group_id} className="kv">
|
||
<span className="mono">{x.group_id}</span>
|
||
<span className="mono">{x.realized_pnl.toFixed(2)}</span>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|