Show strategy stats in control monitor detail modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user