diff --git a/README.md b/README.md index 11fd21c..eea34b4 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ cointrader/ │ ├── tune_hyperparams.py # Optuna 하이퍼파라미터 자동 탐색 (수동 트리거) │ ├── deploy_model.sh # 모델 파일 LXC 서버 전송 │ └── run_tests.sh # 전체 테스트 실행 +├── dashboard/ +│ ├── api/ # FastAPI 백엔드 (로그 파서 + REST API) +│ └── ui/ # React 프론트엔드 (Vite + Recharts) ├── models/ # 학습된 모델 저장 (.pkl / .onnx) ├── data/ # 과거 데이터 캐시 (.parquet) ├── logs/ # 로그 파일 @@ -241,6 +244,46 @@ MLX로 학습한 모델은 ONNX 포맷으로 export되어 Linux 서버에서 `on --- +## 대시보드 + +봇 로그를 실시간으로 파싱하여 거래 내역, 수익 통계, 차트를 웹에서 조회할 수 있는 모니터링 대시보드입니다. + +### 기술 스택 + +- **프론트엔드**: React 18 + Vite + Recharts, Nginx 정적 서빙 +- **백엔드**: FastAPI + SQLite, 로그 파서(5초 주기 폴링) +- **배포**: Docker Compose 3컨테이너 (`dashboard-ui`, `dashboard-api`, `cointrader`) + +### 주요 화면 + +| 탭 | 내용 | +|----|------| +| **Overview** | 총 수익, 승률, 거래 수, 최대 수익/손실 KPI + 일별 PnL 차트 + 누적 수익 곡선 | +| **Trades** | 전체 거래 내역 — 진입/청산가, 방향, 레버리지, 기술 지표(RSI, MACD, ATR), SL/TP, 순익 상세 | +| **Chart** | XRP/USDT 15분봉 가격 차트 + RSI 지표 + ADX 추세 강도 | + +### API 엔드포인트 + +| 엔드포인트 | 설명 | +|-----------|------| +| `GET /api/position` | 현재 포지션 + 봇 상태 | +| `GET /api/trades` | 청산 거래 내역 (페이지네이션) | +| `GET /api/daily` | 일별 PnL 집계 | +| `GET /api/stats` | 전체 통계 (총 거래, 승률, 수수료 등) | +| `GET /api/candles` | 최근 캔들 + 기술 지표 | +| `GET /api/health` | 헬스 체크 | +| `POST /api/reset` | DB 초기화 + 로그 파서 재시작 | + +### 실행 + +```bash +docker compose up -d +``` + +대시보드는 `http://<서버IP>:8080`에서 접속할 수 있습니다. 봇 로그를 읽기 전용으로 마운트하여 봇 코드를 수정하지 않는 디커플드 설계입니다. + +--- + ## 테스트 ```bash diff --git a/dashboard/api/log_parser.py b/dashboard/api/log_parser.py index 577dd5b..e32f0f4 100644 --- a/dashboard/api/log_parser.py +++ b/dashboard/api/log_parser.py @@ -46,7 +46,7 @@ PATTERNS = { r".*기존 포지션 복구: (?P\w+) \| 진입가=(?P[\d.]+) \| 수량=(?P[\d.]+)" ), - # SHORT 진입: 가격=1.3940, 수량=86.8, SL=1.4040, TP=1.3840 + # SHORT 진입: 가격=1.3940, 수량=86.8, SL=1.4040, TP=1.3840, RSI=42.31, MACD_H=-0.001234, ATR=0.005678 "entry": re.compile( r"(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" r".*(?PSHORT|LONG) 진입: " @@ -54,6 +54,9 @@ PATTERNS = { r"수량=(?P[\d.]+), " r"SL=(?P[\d.]+), " r"TP=(?P[\d.]+)" + r"(?:, RSI=(?P[\d.]+))?" + r"(?:, MACD_H=(?P[+\-\d.]+))?" + r"(?:, ATR=(?P[\d.]+))?" ), # 청산 감지(MANUAL): exit=1.3782, rp=+2.9859, commission=0.0598, net_pnl=+2.9261 @@ -286,7 +289,7 @@ class LogParser: ) return - # 포지션 진입: SHORT 진입: 가격=X, 수량=Y, SL=Z, TP=W + # 포지션 진입: SHORT 진입: 가격=X, 수량=Y, SL=Z, TP=W, RSI=R, MACD_H=M, ATR=A m = PATTERNS["entry"].search(line) if m: self._handle_entry( @@ -296,6 +299,9 @@ class LogParser: qty=float(m.group("qty")), sl=float(m.group("sl")), tp=float(m.group("tp")), + rsi=float(m.group("rsi")) if m.group("rsi") else None, + macd_hist=float(m.group("macd_hist")) if m.group("macd_hist") else None, + atr=float(m.group("atr")) if m.group("atr") else None, ) return @@ -381,7 +387,8 @@ class LogParser: # ── 포지션 진입 핸들러 ─────────────────────────────────────── def _handle_entry(self, ts, direction, entry_price, qty, - leverage=None, sl=None, tp=None, is_recovery=False): + leverage=None, sl=None, tp=None, is_recovery=False, + rsi=None, macd_hist=None, atr=None): if leverage is None: leverage = self._bot_config.get("leverage", 10) @@ -405,11 +412,12 @@ class LogParser: cur = self.conn.execute( """INSERT INTO trades(symbol, direction, entry_time, entry_price, - quantity, leverage, sl, tp, status, extra) - VALUES(?,?,?,?,?,?,?,?,?,?)""", + quantity, leverage, sl, tp, status, extra, rsi, macd_hist, atr) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)""", (self._bot_config.get("symbol", "XRPUSDT"), direction, ts, entry_price, qty, leverage, sl, tp, "OPEN", - json.dumps({"recovery": is_recovery})), + json.dumps({"recovery": is_recovery}), + rsi, macd_hist, atr), ) self.conn.commit() self._current_position = { diff --git a/src/bot.py b/src/bot.py index 3e72449..4567101 100644 --- a/src/bot.py +++ b/src/bot.py @@ -205,7 +205,10 @@ class TradingBot: ) logger.success( f"{signal} 진입: 가격={price}, 수량={quantity}, " - f"SL={stop_loss:.4f}, TP={take_profit:.4f}" + f"SL={stop_loss:.4f}, TP={take_profit:.4f}, " + f"RSI={signal_snapshot['rsi']:.2f}, " + f"MACD_H={signal_snapshot['macd_hist']:.6f}, " + f"ATR={signal_snapshot['atr']:.6f}" ) sl_side = "SELL" if signal == "LONG" else "BUY"