diff --git a/backend/app/api/fleet.py b/backend/app/api/fleet.py
index b31e308..a6e42cb 100644
--- a/backend/app/api/fleet.py
+++ b/backend/app/api/fleet.py
@@ -220,12 +220,26 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di
return None
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 {
"ok": True,
"mode": settings.mode,
"env_name": settings.env_name,
"exchange": exchange_name,
"sim": settings.is_sim,
+ "latest_funds": latest_funds,
"market_connected": bool(snap.connected) if snap else False,
"pair": snap.pair.to_dict() if snap and snap.pair else None,
"index_px": (
diff --git a/control/backend/app/api/nodes.py b/control/backend/app/api/nodes.py
index fdcf762..d89bcdd 100644
--- a/control/backend/app/api/nodes.py
+++ b/control/backend/app/api/nodes.py
@@ -278,6 +278,26 @@ async def node_status(
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]:
base = {
"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):
base["error"] = _http_detail(data) if data else f"HTTP {code}"
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(
{
"ok": True,
- "latest_funds": data.get("latest_funds"),
+ "latest_funds": latest_funds,
"groups": data.get("groups"),
"fees_perp": data.get("fees_perp"),
"fees_option": data.get("fees_option"),
"total_fees": data.get("total_fees"),
- "max_single_loss": data.get("max_single_loss"),
- "loss_streak": data.get("loss_streak"),
+ "max_single_loss": max_loss,
+ "loss_streak": loss_streak,
"total_pnl": data.get("total_pnl"),
"mode": data.get("mode"),
}
diff --git a/control/frontend/src/App.tsx b/control/frontend/src/App.tsx
index 362ab45..ca5d100 100644
--- a/control/frontend/src/App.tsx
+++ b/control/frontend/src/App.tsx
@@ -10,6 +10,7 @@ import { apiFetch, clearSession, getToken, getUsername } from "./api";
import LoginPage from "./pages/Login";
import MonitorPage from "./pages/Monitor";
import SettingsPage from "./pages/Settings";
+import StatsPage from "./pages/Stats";
function Shell({ children }: { children: ReactNode }) {
const user = getUsername();
@@ -29,6 +30,9 @@ function Shell({ children }: { children: ReactNode }) {
暂无策略机。请到「系统设置」添加并生成 Token。
)} -| 机器名 | -最新资金 | -轮次 | -永续手续费 | -期权手续费 | -手续费合计 | -单笔最大亏损 | -连亏次数 | -盈亏 | -
|---|---|---|---|---|---|---|---|---|
|
- {row.name}
- {!row.ok && row.error ? (
-
- 拉取失败
-
- ) : null}
- |
- - {row.latest_funds != null - ? fmt(row.latest_funds, 2) - : "—"} - | -- {row.groups != null ? row.groups : "—"} - | -- {row.fees_perp != null ? fmt(row.fees_perp, 4) : "—"} - | -- {row.fees_option != null - ? fmt(row.fees_option, 4) - : "—"} - | -- {row.total_fees != null ? fmt(row.total_fees, 4) : "—"} - | -- {row.max_single_loss != null - ? fmt(row.max_single_loss, 2) - : "—"} - | -- {row.loss_streak != null ? row.loss_streak : "—"} - | -- {row.total_pnl != null ? fmt(row.total_pnl, 2) : "—"} - | -
| - 合计 - - {" "} - ({okRows.length}/{fleetStats.length}) - - | -{fmt(sum("latest_funds"), 2)} | -{sum("groups")} | -{fmt(sum("fees_perp"), 4)} | -{fmt(sum("fees_option"), 4)} | -{fmt(sum("total_fees"), 4)} | -- {fmt(maxLoss, 2)} - | -— | -- {fmt(sum("total_pnl"), 2)} - | -
- {fleetStatsLoading ? "正在拉取各策略机统计…" : "暂无统计数据"} -
- )} -| 机器名 | +最新资金 | +轮次 | +永续手续费 | +期权手续费 | +手续费合计 | +单笔最大亏损 | +连亏次数 | +盈亏 | +
|---|---|---|---|---|---|---|---|---|
|
+ {row.name}
+ {!row.ok && row.error ? (
+
+ 拉取失败
+
+ ) : null}
+ |
+ + {row.ok ? fmt(row.latest_funds ?? 0, 2) : "—"} + | ++ {row.groups != null ? row.groups : "—"} + | ++ {row.fees_perp != null ? fmt(row.fees_perp, 4) : "—"} + | ++ {row.fees_option != null ? fmt(row.fees_option, 4) : "—"} + | ++ {row.total_fees != null ? fmt(row.total_fees, 4) : "—"} + | ++ {row.ok ? fmt(row.max_single_loss ?? 0, 2) : "—"} + | ++ {row.ok ? String(row.loss_streak ?? 0) : "—"} + | ++ {row.total_pnl != null ? fmt(row.total_pnl, 2) : "—"} + | +
| + 合计 + + {" "} + ({okRows.length}/{fleetStats.length}) + + | +{fmt(sum("latest_funds"), 2)} | +{sum("groups")} | +{fmt(sum("fees_perp"), 4)} | +{fmt(sum("fees_option"), 4)} | +{fmt(sum("total_fees"), 4)} | ++ {fmt(maxLoss, 2)} + | +— | ++ {fmt(sum("total_pnl"), 2)} + | +
+ {fleetStatsLoading ? "正在拉取各策略机统计…" : "暂无统计数据"} +
+ )} +