feat(dashboard): show unrealized PnL on position cards (5min update)
Parse position monitor logs (5min interval) to update current_price, unrealized_pnl and unrealized_pnl_pct in bot_status. Position cards now display USDT amount and percentage, colored green/red. Falls back to entry/current price calculation if monitor data unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
111
dashboard/ui/dist/assets/index-50uRhrJe.js
vendored
Normal file
111
dashboard/ui/dist/assets/index-50uRhrJe.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
dashboard/ui/dist/index.html
vendored
Normal file
20
dashboard/ui/dist/index.html
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Trading Dashboard</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #08080f; }
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-50uRhrJe.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -403,7 +403,20 @@ export default function App() {
|
||||
{/* 오픈 포지션 — 복수 표시 */}
|
||||
{positions.length > 0 && (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{positions.map((pos) => (
|
||||
{positions.map((pos) => {
|
||||
const curP = parseFloat(botStatus[`${pos.symbol}:current_price`] || 0);
|
||||
const entP = parseFloat(pos.entry_price || 0);
|
||||
const isShort = pos.direction === "SHORT";
|
||||
const uPnl = botStatus[`${pos.symbol}:unrealized_pnl`];
|
||||
const uPnlPct = botStatus[`${pos.symbol}:unrealized_pnl_pct`];
|
||||
const pnlPct = uPnlPct != null
|
||||
? parseFloat(uPnlPct)
|
||||
: (entP > 0 && curP > 0
|
||||
? ((isShort ? entP - curP : curP - entP) / entP * 100 * (pos.leverage || 10))
|
||||
: null);
|
||||
const pnlUsdt = uPnl != null ? parseFloat(uPnl) : null;
|
||||
const pnlColor = pnlPct > 0 ? S.green : pnlPct < 0 ? S.red : S.text3;
|
||||
return (
|
||||
<div key={pos.id} style={{
|
||||
background: "linear-gradient(135deg,rgba(99,102,241,0.08) 0%,rgba(99,102,241,0.02) 100%)",
|
||||
border: "1px solid rgba(99,102,241,0.15)", borderRadius: 14,
|
||||
@@ -414,17 +427,24 @@ export default function App() {
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
||||
<Badge
|
||||
bg={pos.direction === "SHORT" ? "rgba(239,68,68,0.12)" : "rgba(52,211,153,0.12)"}
|
||||
color={pos.direction === "SHORT" ? S.red : S.green}
|
||||
bg={isShort ? "rgba(239,68,68,0.12)" : "rgba(52,211,153,0.12)"}
|
||||
color={isShort ? S.red : S.green}
|
||||
>
|
||||
{pos.direction} {pos.leverage || 10}x
|
||||
</Badge>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, fontFamily: S.mono }}>
|
||||
{fmt(pos.entry_price)}
|
||||
</span>
|
||||
{pnlPct !== null && (
|
||||
<span style={{ fontSize: 13, fontWeight: 700, fontFamily: S.mono, color: pnlColor }}>
|
||||
{pnlUsdt != null ? `${pnlUsdt > 0 ? "+" : ""}${pnlUsdt.toFixed(4)}` : ""}
|
||||
{` (${pnlPct > 0 ? "+" : ""}${pnlPct.toFixed(2)}%)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user