diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 40ba5c3..6124ad9 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -111,6 +111,7 @@ def create_dashboard_app(db_path: str) -> FastAPI: return { "date": today, + "mode": os.getenv("MODE", "paper"), "markets": market_status, "totals": { "trade_count": total_trades, diff --git a/src/dashboard/static/index.html b/src/dashboard/static/index.html index 120b9b2..ff32ab8 100644 --- a/src/dashboard/static/index.html +++ b/src/dashboard/static/index.html @@ -43,6 +43,19 @@ font-size: 12px; transition: border-color 0.2s; } .refresh-btn:hover { border-color: var(--accent); color: var(--accent); } + .mode-badge { + padding: 3px 10px; border-radius: 5px; font-size: 12px; font-weight: 700; + letter-spacing: 0.5px; + } + .mode-badge.live { + background: rgba(224, 85, 85, 0.15); color: var(--red); + border: 1px solid rgba(224, 85, 85, 0.4); + animation: pulse-warn 2s ease-in-out infinite; + } + .mode-badge.paper { + background: rgba(232, 160, 64, 0.15); color: var(--warn); + border: 1px solid rgba(232, 160, 64, 0.4); + } /* CB Gauge */ .cb-gauge-wrap { @@ -225,6 +238,7 @@

🐍 The Ouroboros

+ --
CB -- @@ -512,9 +526,22 @@ } document.getElementById('card-pnl-sub').textContent = `결정 ${t.decision_count ?? 0}건`; renderCbGauge(d.circuit_breaker); + renderModeBadge(d.mode); } catch {} } + function renderModeBadge(mode) { + const el = document.getElementById('mode-badge'); + if (!el) return; + if (mode === 'live') { + el.textContent = '🔴 실전투자'; + el.className = 'mode-badge live'; + } else { + el.textContent = '🟡 모의투자'; + el.className = 'mode-badge paper'; + } + } + async function fetchPerformance() { try { const r = await fetch('/api/performance?market=all'); diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index dd3b8cb..38d872e 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -413,3 +413,30 @@ def test_status_circuit_breaker_unknown_when_no_data(tmp_path: Path) -> None: cb = body["circuit_breaker"] assert cb["status"] == "unknown" assert cb["current_pnl_pct"] is None + + +def test_status_mode_paper(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """MODE=paper일 때 status 응답에 mode=paper가 포함돼야 한다.""" + monkeypatch.setenv("MODE", "paper") + app = _app(tmp_path) + get_status = _endpoint(app, "/api/status") + body = get_status() + assert body["mode"] == "paper" + + +def test_status_mode_live(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """MODE=live일 때 status 응답에 mode=live가 포함돼야 한다.""" + monkeypatch.setenv("MODE", "live") + app = _app(tmp_path) + get_status = _endpoint(app, "/api/status") + body = get_status() + assert body["mode"] == "live" + + +def test_status_mode_default_paper(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """MODE 환경변수가 없으면 mode 기본값은 paper여야 한다.""" + monkeypatch.delenv("MODE", raising=False) + app = _app(tmp_path) + get_status = _endpoint(app, "/api/status") + body = get_status() + assert body["mode"] == "paper"