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:
@@ -68,6 +68,12 @@ PATTERNS = {
|
|||||||
r".*\[(?P<symbol>\w+)\] 오늘 누적 PnL: (?P<pnl>[+\-\d.]+) USDT"
|
r".*\[(?P<symbol>\w+)\] 오늘 누적 PnL: (?P<pnl>[+\-\d.]+) USDT"
|
||||||
),
|
),
|
||||||
|
|
||||||
|
"position_monitor": re.compile(
|
||||||
|
r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"
|
||||||
|
r".*\[(?P<symbol>\w+)\] 포지션 모니터 \| (?P<direction>\w+) \| "
|
||||||
|
r"현재가=(?P<price>[\d.]+) \| PnL=(?P<pnl>[+\-\d.]+) USDT \((?P<pnl_pct>[+\-\d.]+)%\)"
|
||||||
|
),
|
||||||
|
|
||||||
"bot_start": re.compile(
|
"bot_start": re.compile(
|
||||||
r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"
|
r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"
|
||||||
r".*\[(?P<symbol>\w+)\] 봇 시작, 레버리지 (?P<leverage>\d+)x"
|
r".*\[(?P<symbol>\w+)\] 봇 시작, 레버리지 (?P<leverage>\d+)x"
|
||||||
@@ -272,6 +278,15 @@ class LogParser:
|
|||||||
self._set_status("ml_threshold", m.group("threshold"))
|
self._set_status("ml_threshold", m.group("threshold"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 포지션 모니터 (5분 간격 현재가·PnL 갱신)
|
||||||
|
m = PATTERNS["position_monitor"].search(line)
|
||||||
|
if m:
|
||||||
|
symbol = m.group("symbol")
|
||||||
|
self._set_status(f"{symbol}:current_price", m.group("price"))
|
||||||
|
self._set_status(f"{symbol}:unrealized_pnl", m.group("pnl"))
|
||||||
|
self._set_status(f"{symbol}:unrealized_pnl_pct", m.group("pnl_pct"))
|
||||||
|
return
|
||||||
|
|
||||||
# 포지션 복구 (재시작 시)
|
# 포지션 복구 (재시작 시)
|
||||||
m = PATTERNS["position_recover"].search(line)
|
m = PATTERNS["position_recover"].search(line)
|
||||||
if m:
|
if m:
|
||||||
|
|||||||
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 && (
|
{positions.length > 0 && (
|
||||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
<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={{
|
<div key={pos.id} style={{
|
||||||
background: "linear-gradient(135deg,rgba(99,102,241,0.08) 0%,rgba(99,102,241,0.02) 100%)",
|
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,
|
border: "1px solid rgba(99,102,241,0.15)", borderRadius: 14,
|
||||||
@@ -414,17 +427,24 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
||||||
<Badge
|
<Badge
|
||||||
bg={pos.direction === "SHORT" ? "rgba(239,68,68,0.12)" : "rgba(52,211,153,0.12)"}
|
bg={isShort ? "rgba(239,68,68,0.12)" : "rgba(52,211,153,0.12)"}
|
||||||
color={pos.direction === "SHORT" ? S.red : S.green}
|
color={isShort ? S.red : S.green}
|
||||||
>
|
>
|
||||||
{pos.direction} {pos.leverage || 10}x
|
{pos.direction} {pos.leverage || 10}x
|
||||||
</Badge>
|
</Badge>
|
||||||
<span style={{ fontSize: 14, fontWeight: 700, fontFamily: S.mono }}>
|
<span style={{ fontSize: 14, fontWeight: 700, fontFamily: S.mono }}>
|
||||||
{fmt(pos.entry_price)}
|
{fmt(pos.entry_price)}
|
||||||
</span>
|
</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>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user