Show strategy stats in control monitor detail modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 09:49:27 +08:00
parent 0e3b9e53ac
commit 079dc83132
5 changed files with 211 additions and 7 deletions
+170
View File
@@ -65,6 +65,40 @@ function leveragePair(strat: Record<string, unknown>): string {
return `${a}/${b}`;
}
type NodeStats = {
mode?: 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;
close_at_ms?: number | null;
}[];
};
const CLOSE_REASON_ZH: Record<string, string> = {
fixed_usdt: "固定净盈利达标·双腿全平",
premium_multiple: "权利金倍数达标·双腿全平",
target_perp_only: "净盈利达标·只平永续(期权归档)",
expiry: "到期结算",
emergency: "紧急全平",
manual: "手动平仓",
liquidity_retry: "等待流动性后全平",
unknown: "未知",
};
function closeReasonZh(v: string): string {
return CLOSE_REASON_ZH[v] || v;
}
type RiskLines = {
riskBased: boolean;
sizing: string;
@@ -167,12 +201,43 @@ export default function MonitorPage() {
cachedNodes.length ? "live" : "connecting",
);
const [detailId, setDetailId] = useState<number | null>(null);
const [detailStats, setDetailStats] = useState<NodeStats | null>(null);
const [detailStatsErr, setDetailStatsErr] = useState("");
const [detailStatsLoading, setDetailStatsLoading] = useState(false);
const applyNodes = useCallback((list: NodeCard[]) => {
cachedNodes = list;
setNodes(list);
}, []);
useEffect(() => {
if (detailId == null) {
setDetailStats(null);
setDetailStatsErr("");
setDetailStatsLoading(false);
return;
}
let cancelled = false;
setDetailStats(null);
setDetailStatsErr("");
setDetailStatsLoading(true);
apiFetch<NodeStats>(`/api/nodes/${detailId}/stats`)
.then((s) => {
if (!cancelled) setDetailStats(s);
})
.catch((ex) => {
if (!cancelled) {
setDetailStatsErr(ex instanceof Error ? ex.message : String(ex));
}
})
.finally(() => {
if (!cancelled) setDetailStatsLoading(false);
});
return () => {
cancelled = true;
};
}, [detailId]);
const refresh = useCallback(async () => {
try {
const r = await apiFetch<{ nodes: NodeCard[] }>("/api/nodes/status/all");
@@ -611,6 +676,111 @@ export default function MonitorPage() {
</dl>
</section>
<section className="modal-section">
<h4></h4>
{detailStatsLoading ? (
<p className="meta"></p>
) : null}
{detailStatsErr ? (
<p className="err soft">{detailStatsErr}</p>
) : null}
{detailStats ? (
<>
<dl className="kv detail-kv">
<div>
<dt> / </dt>
<dd className="mono">
{detailStats.groups} /{" "}
{(detailStats.win_rate * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt></dt>
<dd className={`mono ${pnlClass(detailStats.total_pnl)}`}>
{fmt(detailStats.total_pnl, 2)} USDT
</dd>
</div>
<div>
<dt></dt>
<dd className="mono">
{fmt(detailStats.fees_perp ?? 0, 4)}
</dd>
</div>
<div>
<dt></dt>
<dd className="mono">
{fmt(detailStats.fees_option ?? 0, 4)}
</dd>
</div>
<div>
<dt></dt>
<dd className="mono">
{fmt(detailStats.total_fees, 4)}
</dd>
</div>
{(detailStats.show_slip ?? detailStats.mode !== "LIVE") ? (
<div>
<dt></dt>
<dd className="mono">
{fmt(detailStats.total_slip ?? 0, 4)}
</dd>
</div>
) : null}
<div className="span-2">
<dt></dt>
<dd className="mono">
{Object.keys(detailStats.close_reasons || {}).length ===
0
? "—"
: Object.entries(detailStats.close_reasons)
.sort((a, b) => b[1] - a[1])
.map(([k, n]) => (
<div key={k}>
{closeReasonZh(k)} × {n}
</div>
))}
</dd>
</div>
</dl>
{detailStats.equity_curve?.length ? (
<>
<h4 style={{ marginTop: "0.75rem" }}></h4>
<div className="legs-table-wrap">
<table className="legs-table">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{[...detailStats.equity_curve]
.reverse()
.slice(0, 30)
.map((x) => (
<tr key={x.group_id}>
<td className="mono">{x.group_id}</td>
<td className={pnlClass(x.realized_pnl)}>
{fmt(x.realized_pnl, 2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{detailStats.equity_curve.length > 30 ? (
<p className="meta">
30 {detailStats.equity_curve.length}
</p>
) : null}
</>
) : (
<p className="meta"></p>
)}
</>
) : null}
</section>
<section className="modal-section">
<h4></h4>
<p className="meta">