feat: 대시보드 Circuit Breaker 게이지 추가 (#196)
Some checks failed
CI / test (pull_request) Has been cancelled

- trading_cycle()의 L7 context에 portfolio_pnl_pct_{market} 저장 추가
  → 대시보드가 최신 pnl_pct를 DB에서 직접 조회 가능해짐
- /api/status 응답에 circuit_breaker 섹션 추가
  (threshold_pct, current_pnl_pct, status: ok/warning/tripped/unknown)
  - warning: CB 임계값까지 1% 이내 (-2.0% 이하)
  - tripped: 임계값(-3.0%) 이하
- 대시보드 헤더에 CB 게이지 추가 (점멸 도트 + 진행 바 + 수치)
  - ok: 녹색, warning: 오렌지 점멸, tripped: 빨간 점멸
- CB 상태 테스트 4개 추가 (ok/warning/tripped/unknown)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-21 21:13:53 +09:00
parent 2d5912dc08
commit 342511a6ed
4 changed files with 172 additions and 0 deletions

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import os
import sqlite3
from datetime import UTC, datetime, timezone
from pathlib import Path
@@ -79,6 +80,36 @@ def create_dashboard_app(db_path: str) -> FastAPI:
total_pnl += market_status[market]["total_pnl"]
total_decisions += market_status[market]["decision_count"]
cb_threshold = float(os.getenv("CIRCUIT_BREAKER_PCT", "-3.0"))
pnl_pct_rows = conn.execute(
"""
SELECT key, value
FROM contexts
WHERE layer = 'L7_REALTIME'
AND key LIKE 'portfolio_pnl_pct_%'
ORDER BY updated_at DESC
LIMIT 20
"""
).fetchall()
current_pnl_pct: float | None = None
if pnl_pct_rows:
values = [
json.loads(row["value"]).get("pnl_pct")
for row in pnl_pct_rows
if json.loads(row["value"]).get("pnl_pct") is not None
]
if values:
current_pnl_pct = round(min(values), 4)
if current_pnl_pct is None:
cb_status = "unknown"
elif current_pnl_pct <= cb_threshold:
cb_status = "tripped"
elif current_pnl_pct <= cb_threshold + 1.0:
cb_status = "warning"
else:
cb_status = "ok"
return {
"date": today,
"markets": market_status,
@@ -87,6 +118,11 @@ def create_dashboard_app(db_path: str) -> FastAPI:
"total_pnl": round(total_pnl, 2),
"decision_count": total_decisions,
},
"circuit_breaker": {
"threshold_pct": cb_threshold,
"current_pnl_pct": current_pnl_pct,
"status": cb_status,
},
}
@app.get("/api/playbook/{date_str}")