diff --git a/control/frontend/src/pages/Monitor.tsx b/control/frontend/src/pages/Monitor.tsx index 2ba1f08..56e26ef 100644 --- a/control/frontend/src/pages/Monitor.tsx +++ b/control/frontend/src/pages/Monitor.tsx @@ -1,4 +1,9 @@ -import { useCallback, useEffect, useState } from "react"; +import { + useCallback, + useEffect, + useState, + type ReactNode, +} from "react"; import { apiFetch, clearSession, getToken, type NodeCard } from "../api"; function pickStrategy(n: NodeCard) { @@ -630,197 +635,229 @@ export default function MonitorPage() { -
-

策略详情

- -
-
-
状态
-
- {detailRunning ? "运行中" : "已停"} · {detail.phase} -
+
+
+

策略详情

+
+ + + {(() => { + const r = riskLines(detail.strat); + const rows: { k: string; v: ReactNode }[] = [ + { k: "定仓", v: r.sizing }, + ...(r.lossPct != null + ? [{ k: "风险比例", v: r.lossPct }] + : []), + { k: "出场", v: r.exit }, + ...(r.openRatio + ? [{ k: "开仓比例", v: r.openRatio }] + : []), + { k: "杠杆", v: r.leverage }, + { + k: "状态", + v: `${detailRunning ? "运行中" : "已停"} · ${detail.phase}`, + }, + { + k: "模式 / 交易所", + v: `${detail.mode} / ${detail.exchange}`, + }, + { k: "轮次", v: String(detail.rounds ?? "—") }, + { + k: "组 ID", + v: ( + + {String( + detail.strat.group_id || + detail.position.group_id || + "—", + )} + + ), + }, + { + k: "保证金", + v: String(detail.strat.perp_margin_mode || "—"), + }, + { + k: "名义 永续/期权", + v: `${fmt(detail.strat.perp_qty_eth, 4)} / ${fmt(detail.strat.option_qty_eth, 4)} ETH`, + }, + { + k: "出场目标", + v: `${fmt(detail.strat.exit_target_usdt, 2)} USDT`, + }, + { k: "指数", v: fmt(detail.index_px, 2) }, + { + k: "合约对", + v: ( + + {detail.pair + ? `${detail.pair.expiry_ymd || "?"} @ ${detail.pair.strike ?? "?"}` + : "—"} + + ), + }, + ]; + if (detail.strat.last_error) { + rows.push({ + k: "最近错误", + v: ( + + {String(detail.strat.last_error)} + + ), + }); + } + return rows.map((row) => ( + + + + + )); + })()} + +
{row.k}{row.v}
-
-
模式 / 交易所
-
- {detail.mode} / {detail.exchange} -
-
-
-
轮次
-
{detail.rounds ?? "—"}
-
-
-
组 ID
-
- {String(detail.strat.group_id || detail.position.group_id || "—")} -
-
-
-
保证金
-
{String(detail.strat.perp_margin_mode || "—")}
-
-
-
名义 永续/期权
-
- {fmt(detail.strat.perp_qty_eth, 4)} /{" "} - {fmt(detail.strat.option_qty_eth, 4)} ETH -
-
-
-
出场目标
-
{fmt(detail.strat.exit_target_usdt, 2)} USDT
-
-
-
指数
-
{fmt(detail.index_px, 2)}
-
-
-
合约对
-
- {detail.pair - ? `${detail.pair.expiry_ymd || "?"} @ ${detail.pair.strike ?? "?"}` - : "—"} -
-
- {detail.strat.last_error ? ( -
-
最近错误
-
{String(detail.strat.last_error)}
-
+
+ +
+

整体统计

+ {detailStatsLoading ? ( +

正在从策略机拉取统计…

) : null} -
-
+ {detailStatsErr ? ( +

{detailStatsErr}

+ ) : null} + {detailStats ? ( +
+ + + + + + + + + + + + + + + + + + + + + + + {(detailStats.show_slip ?? + detailStats.mode !== "LIVE") ? ( + + + + + ) : null} + + + + + +
组数 / 胜率 + {detailStats.groups} /{" "} + {(detailStats.win_rate * 100).toFixed(1)}% +
总盈亏 + {fmt(detailStats.total_pnl, 2)} USDT +
永续手续费 + {fmt(detailStats.fees_perp ?? 0, 4)} +
期权手续费 + {fmt(detailStats.fees_option ?? 0, 4)} +
手续费合计 + {fmt(detailStats.total_fees, 4)} +
滑点合计 + {fmt(detailStats.total_slip ?? 0, 4)} +
平仓原因 + {Object.keys(detailStats.close_reasons || {}) + .length === 0 + ? "—" + : Object.entries(detailStats.close_reasons) + .sort((a, b) => b[1] - a[1]) + .map(([k, n]) => ( +
+ {closeReasonZh(k)} × {n} +
+ ))} +
+
+ ) : !detailStatsLoading && !detailStatsErr ? ( +

暂无统计

+ ) : null} + +
-

整体统计

+

按组盈亏

{detailStatsLoading ? ( -

正在从策略机拉取统计…

- ) : null} - {detailStatsErr ? ( -

{detailStatsErr}

- ) : null} - {detailStats ? ( +

加载中…

+ ) : equityNewestFirst.length ? ( <> -
-
-
组数 / 胜率
-
- {detailStats.groups} /{" "} - {(detailStats.win_rate * 100).toFixed(1)}% -
+
+ + + + + + + + + {equityPageRows.map((x) => ( + + + + + ))} + +
盈亏
{x.group_id} + {fmt(x.realized_pnl, 2)} +
+
+
+ + 第 {equityPageSafe + 1}/{equityPageCount} 页 · 每页{" "} + {EQUITY_PAGE_SIZE} 行 · 共 {equityNewestFirst.length} 组 + +
+ +
-
-
总盈亏
-
- {fmt(detailStats.total_pnl, 2)} USDT -
-
-
-
永续手续费
-
- {fmt(detailStats.fees_perp ?? 0, 4)} -
-
-
-
期权手续费
-
- {fmt(detailStats.fees_option ?? 0, 4)} -
-
-
-
手续费合计
-
- {fmt(detailStats.total_fees, 4)} -
-
- {(detailStats.show_slip ?? detailStats.mode !== "LIVE") ? ( -
-
滑点合计
-
- {fmt(detailStats.total_slip ?? 0, 4)} -
-
- ) : null} -
-
平仓原因
-
- {Object.keys(detailStats.close_reasons || {}).length === - 0 - ? "—" - : Object.entries(detailStats.close_reasons) - .sort((a, b) => b[1] - a[1]) - .map(([k, n]) => ( -
- {closeReasonZh(k)} × {n} -
- ))} -
-
-
- {equityNewestFirst.length ? ( - <> -

按组盈亏

-
- - - - - - - - - {equityPageRows.map((x) => ( - - - - - ))} - -
盈亏
{x.group_id} - {fmt(x.realized_pnl, 2)} -
-
-
- - 第 {equityPageSafe + 1}/{equityPageCount} 页 · 每页{" "} - {EQUITY_PAGE_SIZE} 行 · 共 {equityNewestFirst.length}{" "} - 组 - -
- - -
-
- - ) : ( -

暂无已平仓组

- )} + - ) : null} + ) : ( +

暂无已平仓组

+ )}
diff --git a/control/frontend/src/styles.css b/control/frontend/src/styles.css index 38fa86f..65954f8 100644 --- a/control/frontend/src/styles.css +++ b/control/frontend/src/styles.css @@ -454,27 +454,44 @@ td { .modal-backdrop { position: fixed; inset: 0; - background: rgba(4, 8, 14, 0.92); + background: rgba(4, 8, 14, 0.88); display: flex; + align-items: center; + justify-content: center; + padding: 16px; z-index: 50; + overflow: auto; } .modal-panel { - width: 100%; - height: 100%; - max-height: none; + width: min(920px, 100%); + max-height: calc(100vh - 32px); overflow: auto; background: var(--panel); - border: none; - border-radius: 0; - padding: 20px 24px 28px; + border: 1px solid var(--line); + border-radius: 12px; + padding: 16px 18px 18px; display: flex; flex-direction: column; - gap: 16px; + gap: 12px; + margin: auto; } .modal-panel.running { - box-shadow: inset 0 0 0 2px rgba(60, 179, 113, 0.45); + border-color: rgba(60, 179, 113, 0.65); + box-shadow: 0 0 0 1px rgba(60, 179, 113, 0.2); +} + +.modal-grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +@media (max-width: 760px) { + .modal-grid-2 { + grid-template-columns: 1fr; + } } .pager { @@ -482,12 +499,12 @@ td { align-items: center; justify-content: space-between; gap: 12px; - margin-top: 10px; + margin-top: 8px; } .pager-meta { color: var(--muted); - font-size: 0.85rem; + font-size: 0.8rem; font-variant-numeric: tabular-nums; } @@ -501,15 +518,51 @@ td { justify-content: space-between; align-items: flex-start; gap: 12px; + flex-shrink: 0; } .modal-head h3 { - margin: 0 0 4px; + margin: 0 0 2px; } .modal-section h4 { - margin: 0 0 8px; - font-size: 0.95rem; + margin: 0 0 6px; + font-size: 0.9rem; +} + +.kv-table-wrap { + border: 1px solid var(--line); + border-radius: 8px; + overflow: hidden; +} + +.kv-table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; +} + +.kv-table th { + width: 38%; + background: rgba(255, 255, 255, 0.03); + color: var(--muted); + font-weight: 500; + text-align: left; + padding: 6px 10px; + border-bottom: 1px solid var(--line); + white-space: nowrap; +} + +.kv-table td { + padding: 6px 10px; + border-bottom: 1px solid var(--line); + text-align: left; + font-variant-numeric: tabular-nums; +} + +.kv-table tr:last-child th, +.kv-table tr:last-child td { + border-bottom: none; } .detail-kv {