Move fleet stats to nav page; backfill funds and loss metrics from curve/status.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -220,12 +220,26 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di
|
|||||||
return None
|
return None
|
||||||
return _sf(key, default)
|
return _sf(key, default)
|
||||||
|
|
||||||
|
latest_funds = 0.0
|
||||||
|
try:
|
||||||
|
from ..sim.funds_wallets import FundsWallets
|
||||||
|
|
||||||
|
latest_funds = float(FundsWallets(db).total_usdt_equiv())
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
from ..sim.ledger import Ledger
|
||||||
|
|
||||||
|
latest_funds = float(Ledger(db).snapshot().get("equity") or 0)
|
||||||
|
except Exception:
|
||||||
|
latest_funds = 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"mode": settings.mode,
|
"mode": settings.mode,
|
||||||
"env_name": settings.env_name,
|
"env_name": settings.env_name,
|
||||||
"exchange": exchange_name,
|
"exchange": exchange_name,
|
||||||
"sim": settings.is_sim,
|
"sim": settings.is_sim,
|
||||||
|
"latest_funds": latest_funds,
|
||||||
"market_connected": bool(snap.connected) if snap else False,
|
"market_connected": bool(snap.connected) if snap else False,
|
||||||
"pair": snap.pair.to_dict() if snap and snap.pair else None,
|
"pair": snap.pair.to_dict() if snap and snap.pair else None,
|
||||||
"index_px": (
|
"index_px": (
|
||||||
|
|||||||
@@ -278,6 +278,26 @@ async def node_status(
|
|||||||
return await _collect_one_status(node)
|
return await _collect_one_status(node)
|
||||||
|
|
||||||
|
|
||||||
|
def _max_single_loss_from_curve(curve: list[Any]) -> float:
|
||||||
|
pnls = [float(x.get("realized_pnl") or 0) for x in curve if isinstance(x, dict)]
|
||||||
|
if not pnls:
|
||||||
|
return 0.0
|
||||||
|
worst = min(pnls)
|
||||||
|
return float(worst) if worst < 0 else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _loss_streak_from_curve(curve: list[Any]) -> int:
|
||||||
|
streak = 0
|
||||||
|
for item in reversed(curve):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if float(item.get("realized_pnl") or 0) < 0:
|
||||||
|
streak += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return streak
|
||||||
|
|
||||||
|
|
||||||
async def _collect_one_stats(node: dict[str, Any]) -> dict[str, Any]:
|
async def _collect_one_stats(node: dict[str, Any]) -> dict[str, Any]:
|
||||||
base = {
|
base = {
|
||||||
"id": node["id"],
|
"id": node["id"],
|
||||||
@@ -304,16 +324,37 @@ async def _collect_one_stats(node: dict[str, Any]) -> dict[str, Any]:
|
|||||||
if code >= 400 or not isinstance(data, dict):
|
if code >= 400 or not isinstance(data, dict):
|
||||||
base["error"] = _http_detail(data) if data else f"HTTP {code}"
|
base["error"] = _http_detail(data) if data else f"HTTP {code}"
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
curve = data.get("equity_curve") if isinstance(data.get("equity_curve"), list) else []
|
||||||
|
max_loss = data.get("max_single_loss")
|
||||||
|
if max_loss is None:
|
||||||
|
max_loss = _max_single_loss_from_curve(curve)
|
||||||
|
loss_streak = data.get("loss_streak")
|
||||||
|
if loss_streak is None:
|
||||||
|
loss_streak = _loss_streak_from_curve(curve)
|
||||||
|
|
||||||
|
latest_funds = data.get("latest_funds")
|
||||||
|
if latest_funds is None:
|
||||||
|
# 旧版策略机 stats 无资金字段:回落 status.latest_funds
|
||||||
|
try:
|
||||||
|
sc, sd = await call_node(node, "GET", "/api/fleet/status", timeout=8.0)
|
||||||
|
if sc < 400 and isinstance(sd, dict) and sd.get("latest_funds") is not None:
|
||||||
|
latest_funds = sd.get("latest_funds")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if latest_funds is None:
|
||||||
|
latest_funds = 0.0
|
||||||
|
|
||||||
base.update(
|
base.update(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"latest_funds": data.get("latest_funds"),
|
"latest_funds": latest_funds,
|
||||||
"groups": data.get("groups"),
|
"groups": data.get("groups"),
|
||||||
"fees_perp": data.get("fees_perp"),
|
"fees_perp": data.get("fees_perp"),
|
||||||
"fees_option": data.get("fees_option"),
|
"fees_option": data.get("fees_option"),
|
||||||
"total_fees": data.get("total_fees"),
|
"total_fees": data.get("total_fees"),
|
||||||
"max_single_loss": data.get("max_single_loss"),
|
"max_single_loss": max_loss,
|
||||||
"loss_streak": data.get("loss_streak"),
|
"loss_streak": loss_streak,
|
||||||
"total_pnl": data.get("total_pnl"),
|
"total_pnl": data.get("total_pnl"),
|
||||||
"mode": data.get("mode"),
|
"mode": data.get("mode"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { apiFetch, clearSession, getToken, getUsername } from "./api";
|
|||||||
import LoginPage from "./pages/Login";
|
import LoginPage from "./pages/Login";
|
||||||
import MonitorPage from "./pages/Monitor";
|
import MonitorPage from "./pages/Monitor";
|
||||||
import SettingsPage from "./pages/Settings";
|
import SettingsPage from "./pages/Settings";
|
||||||
|
import StatsPage from "./pages/Stats";
|
||||||
|
|
||||||
function Shell({ children }: { children: ReactNode }) {
|
function Shell({ children }: { children: ReactNode }) {
|
||||||
const user = getUsername();
|
const user = getUsername();
|
||||||
@@ -29,6 +30,9 @@ function Shell({ children }: { children: ReactNode }) {
|
|||||||
<NavLink to="/monitor" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/monitor" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
监控区
|
监控区
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
|
数据统计
|
||||||
|
</NavLink>
|
||||||
<NavLink to="/settings" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/settings" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
系统设置
|
系统设置
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -54,13 +58,15 @@ function Shell({ children }: { children: ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 监控页保活:切设置不断 SSE、不丢卡片状态 */
|
/** 监控页保活:切设置/统计不断 SSE、不丢卡片状态 */
|
||||||
function AuthedPages() {
|
function AuthedPages() {
|
||||||
const loc = useLocation();
|
const loc = useLocation();
|
||||||
const onMonitor =
|
const onMonitor =
|
||||||
loc.pathname === "/" ||
|
loc.pathname === "/" ||
|
||||||
loc.pathname === "/monitor" ||
|
loc.pathname === "/monitor" ||
|
||||||
loc.pathname.startsWith("/monitor/");
|
loc.pathname.startsWith("/monitor/");
|
||||||
|
const onStats =
|
||||||
|
loc.pathname === "/stats" || loc.pathname.startsWith("/stats/");
|
||||||
const onSettings =
|
const onSettings =
|
||||||
loc.pathname === "/settings" || loc.pathname.startsWith("/settings/");
|
loc.pathname === "/settings" || loc.pathname.startsWith("/settings/");
|
||||||
return (
|
return (
|
||||||
@@ -68,10 +74,15 @@ function AuthedPages() {
|
|||||||
<div className={onMonitor ? "page-pane" : "page-pane hidden"} aria-hidden={!onMonitor}>
|
<div className={onMonitor ? "page-pane" : "page-pane hidden"} aria-hidden={!onMonitor}>
|
||||||
<MonitorPage />
|
<MonitorPage />
|
||||||
</div>
|
</div>
|
||||||
|
<div className={onStats ? "page-pane" : "page-pane hidden"} aria-hidden={!onStats}>
|
||||||
|
<StatsPage />
|
||||||
|
</div>
|
||||||
<div className={onSettings ? "page-pane" : "page-pane hidden"} aria-hidden={!onSettings}>
|
<div className={onSettings ? "page-pane" : "page-pane hidden"} aria-hidden={!onSettings}>
|
||||||
<SettingsPage />
|
<SettingsPage />
|
||||||
</div>
|
</div>
|
||||||
{!onMonitor && !onSettings ? <Navigate to="/monitor" replace /> : null}
|
{!onMonitor && !onStats && !onSettings ? (
|
||||||
|
<Navigate to="/monitor" replace />
|
||||||
|
) : null}
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,21 +98,6 @@ type NodeStats = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type FleetStatsRow = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
ok: boolean;
|
|
||||||
error?: string | null;
|
|
||||||
latest_funds?: number | null;
|
|
||||||
groups?: number | null;
|
|
||||||
fees_perp?: number | null;
|
|
||||||
fees_option?: number | null;
|
|
||||||
total_fees?: number | null;
|
|
||||||
max_single_loss?: number | null;
|
|
||||||
loss_streak?: number | null;
|
|
||||||
total_pnl?: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CLOSE_REASON_ZH: Record<string, string> = {
|
const CLOSE_REASON_ZH: Record<string, string> = {
|
||||||
fixed_usdt: "固定净盈利达标·双腿全平",
|
fixed_usdt: "固定净盈利达标·双腿全平",
|
||||||
premium_multiple: "权利金倍数达标·双腿全平",
|
premium_multiple: "权利金倍数达标·双腿全平",
|
||||||
@@ -264,28 +249,12 @@ export default function MonitorPage() {
|
|||||||
const [detailStatsLoading, setDetailStatsLoading] = useState(false);
|
const [detailStatsLoading, setDetailStatsLoading] = useState(false);
|
||||||
const [equityPage, setEquityPage] = useState(0);
|
const [equityPage, setEquityPage] = useState(0);
|
||||||
const EQUITY_PAGE_SIZE = 5;
|
const EQUITY_PAGE_SIZE = 5;
|
||||||
const [fleetStats, setFleetStats] = useState<FleetStatsRow[]>([]);
|
|
||||||
const [fleetStatsErr, setFleetStatsErr] = useState("");
|
|
||||||
const [fleetStatsLoading, setFleetStatsLoading] = useState(false);
|
|
||||||
|
|
||||||
const applyNodes = useCallback((list: NodeCard[]) => {
|
const applyNodes = useCallback((list: NodeCard[]) => {
|
||||||
cachedNodes = list;
|
cachedNodes = list;
|
||||||
setNodes(list);
|
setNodes(list);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshStats = useCallback(async () => {
|
|
||||||
setFleetStatsLoading(true);
|
|
||||||
try {
|
|
||||||
const r = await apiFetch<{ nodes: FleetStatsRow[] }>("/api/nodes/stats/all");
|
|
||||||
setFleetStats(r.nodes || []);
|
|
||||||
setFleetStatsErr("");
|
|
||||||
} catch (ex) {
|
|
||||||
setFleetStatsErr(ex instanceof Error ? ex.message : String(ex));
|
|
||||||
} finally {
|
|
||||||
setFleetStatsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (detailId == null) {
|
if (detailId == null) {
|
||||||
setDetailStats(null);
|
setDetailStats(null);
|
||||||
@@ -324,14 +293,7 @@ export default function MonitorPage() {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||||
}
|
}
|
||||||
void refreshStats();
|
}, [applyNodes]);
|
||||||
}, [applyNodes, refreshStats]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void refreshStats();
|
|
||||||
const id = window.setInterval(() => void refreshStats(), 60_000);
|
|
||||||
return () => window.clearInterval(id);
|
|
||||||
}, [refreshStats]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiFetch<{ sse_interval_sec?: number }>("/api/auth/me")
|
apiFetch<{ sse_interval_sec?: number }>("/api/auth/me")
|
||||||
@@ -729,126 +691,6 @@ export default function MonitorPage() {
|
|||||||
<p className="meta">暂无策略机。请到「系统设置」添加并生成 Token。</p>
|
<p className="meta">暂无策略机。请到「系统设置」添加并生成 Token。</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="stats-section">
|
|
||||||
<div className="toolbar stats-toolbar">
|
|
||||||
<h2>数据统计</h2>
|
|
||||||
<div className="toolbar-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn ghost"
|
|
||||||
disabled={fleetStatsLoading}
|
|
||||||
onClick={() => void refreshStats()}
|
|
||||||
>
|
|
||||||
{fleetStatsLoading ? "加载中…" : "刷新统计"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{fleetStatsErr ? <div className="err soft">{fleetStatsErr}</div> : null}
|
|
||||||
{fleetStats.length ? (
|
|
||||||
<div className="monitor-table-wrap">
|
|
||||||
<table className="monitor-table stats-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>机器名</th>
|
|
||||||
<th>最新资金</th>
|
|
||||||
<th>轮次</th>
|
|
||||||
<th>永续手续费</th>
|
|
||||||
<th>期权手续费</th>
|
|
||||||
<th>手续费合计</th>
|
|
||||||
<th>单笔最大亏损</th>
|
|
||||||
<th>连亏次数</th>
|
|
||||||
<th>盈亏</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{fleetStats.map((row) => (
|
|
||||||
<tr key={row.id}>
|
|
||||||
<td>
|
|
||||||
<strong className="machine-name">{row.name}</strong>
|
|
||||||
{!row.ok && row.error ? (
|
|
||||||
<div className="meta" title={row.error}>
|
|
||||||
拉取失败
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{row.latest_funds != null
|
|
||||||
? fmt(row.latest_funds, 2)
|
|
||||||
: "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{row.groups != null ? row.groups : "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{row.fees_perp != null ? fmt(row.fees_perp, 4) : "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{row.fees_option != null
|
|
||||||
? fmt(row.fees_option, 4)
|
|
||||||
: "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{row.total_fees != null ? fmt(row.total_fees, 4) : "—"}
|
|
||||||
</td>
|
|
||||||
<td className={`mono ${pnlClass(row.max_single_loss)}`}>
|
|
||||||
{row.max_single_loss != null
|
|
||||||
? fmt(row.max_single_loss, 2)
|
|
||||||
: "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{row.loss_streak != null ? row.loss_streak : "—"}
|
|
||||||
</td>
|
|
||||||
<td className={`mono ${pnlClass(row.total_pnl)}`}>
|
|
||||||
{row.total_pnl != null ? fmt(row.total_pnl, 2) : "—"}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
{(() => {
|
|
||||||
const okRows = fleetStats.filter((r) => r.ok);
|
|
||||||
const sum = (key: keyof FleetStatsRow) =>
|
|
||||||
okRows.reduce((a, r) => a + (Number(r[key]) || 0), 0);
|
|
||||||
const maxLoss = okRows.length
|
|
||||||
? Math.min(
|
|
||||||
0,
|
|
||||||
...okRows.map((r) => Number(r.max_single_loss) || 0),
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
return (
|
|
||||||
<tr className="stats-total-row">
|
|
||||||
<td>
|
|
||||||
<strong>合计</strong>
|
|
||||||
<span className="meta">
|
|
||||||
{" "}
|
|
||||||
({okRows.length}/{fleetStats.length})
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="mono">{fmt(sum("latest_funds"), 2)}</td>
|
|
||||||
<td className="mono">{sum("groups")}</td>
|
|
||||||
<td className="mono">{fmt(sum("fees_perp"), 4)}</td>
|
|
||||||
<td className="mono">{fmt(sum("fees_option"), 4)}</td>
|
|
||||||
<td className="mono">{fmt(sum("total_fees"), 4)}</td>
|
|
||||||
<td className={`mono ${pnlClass(maxLoss)}`}>
|
|
||||||
{fmt(maxLoss, 2)}
|
|
||||||
</td>
|
|
||||||
<td className="mono">—</td>
|
|
||||||
<td className={`mono ${pnlClass(sum("total_pnl"))}`}>
|
|
||||||
{fmt(sum("total_pnl"), 2)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="meta">
|
|
||||||
{fleetStatsLoading ? "正在拉取各策略机统计…" : "暂无统计数据"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{detailNode && detail ? (
|
{detailNode && detail ? (
|
||||||
<div
|
<div
|
||||||
className="modal-backdrop"
|
className="modal-backdrop"
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { apiFetch } from "../api";
|
||||||
|
|
||||||
|
type FleetStatsRow = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
ok: boolean;
|
||||||
|
error?: string | null;
|
||||||
|
latest_funds?: number | null;
|
||||||
|
groups?: number | null;
|
||||||
|
fees_perp?: number | null;
|
||||||
|
fees_option?: number | null;
|
||||||
|
total_fees?: number | null;
|
||||||
|
max_single_loss?: number | null;
|
||||||
|
loss_streak?: number | null;
|
||||||
|
total_pnl?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmt(v: unknown, digits = 4): string {
|
||||||
|
if (v == null || v === "") return "—";
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n)) return String(v);
|
||||||
|
return n.toFixed(digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pnlClass(v: unknown): string {
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n) || n === 0) return "pnl-flat";
|
||||||
|
return n > 0 ? "pnl-pos" : "pnl-neg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatsPage() {
|
||||||
|
const [fleetStats, setFleetStats] = useState<FleetStatsRow[]>([]);
|
||||||
|
const [fleetStatsErr, setFleetStatsErr] = useState("");
|
||||||
|
const [fleetStatsLoading, setFleetStatsLoading] = useState(false);
|
||||||
|
|
||||||
|
const refreshStats = useCallback(async () => {
|
||||||
|
setFleetStatsLoading(true);
|
||||||
|
try {
|
||||||
|
const r = await apiFetch<{ nodes: FleetStatsRow[] }>("/api/nodes/stats/all");
|
||||||
|
setFleetStats(r.nodes || []);
|
||||||
|
setFleetStatsErr("");
|
||||||
|
} catch (ex) {
|
||||||
|
setFleetStatsErr(ex instanceof Error ? ex.message : String(ex));
|
||||||
|
} finally {
|
||||||
|
setFleetStatsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshStats();
|
||||||
|
const id = window.setInterval(() => void refreshStats(), 60_000);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, [refreshStats]);
|
||||||
|
|
||||||
|
const okRows = fleetStats.filter((r) => r.ok);
|
||||||
|
const sum = (key: keyof FleetStatsRow) =>
|
||||||
|
okRows.reduce((a, r) => a + (Number(r[key]) || 0), 0);
|
||||||
|
const maxLoss = okRows.length
|
||||||
|
? Math.min(0, ...okRows.map((r) => Number(r.max_single_loss) || 0))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="toolbar stats-toolbar">
|
||||||
|
<h2>数据统计</h2>
|
||||||
|
<div className="toolbar-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn ghost"
|
||||||
|
disabled={fleetStatsLoading}
|
||||||
|
onClick={() => void refreshStats()}
|
||||||
|
>
|
||||||
|
{fleetStatsLoading ? "加载中…" : "刷新统计"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{fleetStatsErr ? <div className="err soft">{fleetStatsErr}</div> : null}
|
||||||
|
{fleetStats.length ? (
|
||||||
|
<div className="monitor-table-wrap">
|
||||||
|
<table className="monitor-table stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>机器名</th>
|
||||||
|
<th>最新资金</th>
|
||||||
|
<th>轮次</th>
|
||||||
|
<th>永续手续费</th>
|
||||||
|
<th>期权手续费</th>
|
||||||
|
<th>手续费合计</th>
|
||||||
|
<th>单笔最大亏损</th>
|
||||||
|
<th>连亏次数</th>
|
||||||
|
<th>盈亏</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{fleetStats.map((row) => (
|
||||||
|
<tr key={row.id}>
|
||||||
|
<td>
|
||||||
|
<strong className="machine-name">{row.name}</strong>
|
||||||
|
{!row.ok && row.error ? (
|
||||||
|
<div className="meta" title={row.error}>
|
||||||
|
拉取失败
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
<td className="mono">
|
||||||
|
{row.ok ? fmt(row.latest_funds ?? 0, 2) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="mono">
|
||||||
|
{row.groups != null ? row.groups : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="mono">
|
||||||
|
{row.fees_perp != null ? fmt(row.fees_perp, 4) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="mono">
|
||||||
|
{row.fees_option != null ? fmt(row.fees_option, 4) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="mono">
|
||||||
|
{row.total_fees != null ? fmt(row.total_fees, 4) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className={`mono ${pnlClass(row.max_single_loss)}`}>
|
||||||
|
{row.ok ? fmt(row.max_single_loss ?? 0, 2) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="mono">
|
||||||
|
{row.ok ? String(row.loss_streak ?? 0) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className={`mono ${pnlClass(row.total_pnl)}`}>
|
||||||
|
{row.total_pnl != null ? fmt(row.total_pnl, 2) : "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="stats-total-row">
|
||||||
|
<td>
|
||||||
|
<strong>合计</strong>
|
||||||
|
<span className="meta">
|
||||||
|
{" "}
|
||||||
|
({okRows.length}/{fleetStats.length})
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="mono">{fmt(sum("latest_funds"), 2)}</td>
|
||||||
|
<td className="mono">{sum("groups")}</td>
|
||||||
|
<td className="mono">{fmt(sum("fees_perp"), 4)}</td>
|
||||||
|
<td className="mono">{fmt(sum("fees_option"), 4)}</td>
|
||||||
|
<td className="mono">{fmt(sum("total_fees"), 4)}</td>
|
||||||
|
<td className={`mono ${pnlClass(maxLoss)}`}>
|
||||||
|
{fmt(maxLoss, 2)}
|
||||||
|
</td>
|
||||||
|
<td className="mono">—</td>
|
||||||
|
<td className={`mono ${pnlClass(sum("total_pnl"))}`}>
|
||||||
|
{fmt(sum("total_pnl"), 2)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="meta">
|
||||||
|
{fleetStatsLoading ? "正在拉取各策略机统计…" : "暂无统计数据"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user