From a063bd9d10b6c319389a9dd11137c81ee3f7009c Mon Sep 17 00:00:00 2001 From: agentson Date: Tue, 24 Feb 2026 06:48:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=EC=97=90=20=EB=AA=A8=EC=9D=98=ED=88=AC?= =?UTF-8?q?=EC=9E=90/=EC=8B=A4=EC=A0=84=ED=88=AC=EC=9E=90=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=B0=B0=EC=A7=80=20=ED=91=9C=EC=8B=9C=20(#237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/status 응답에 MODE 환경변수 기반 mode 필드 추가 - 대시보드 헤더에 모드 배지 표시 (live=빨간색 깜빡임, paper=노란색) - 모드 관련 테스트 3개 추가 (total 26 passed) Co-Authored-By: Claude Sonnet 4.6 --- src/dashboard/app.py | 1 + src/dashboard/static/index.html | 27 +++++++++++++++++++++++++++ tests/test_dashboard.py | 27 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) 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"