feat: log technical indicators on entry and add dashboard docs to README

- Include RSI, MACD_H, ATR in bot entry log so the log parser can
  extract and store them in the trades DB for dashboard display
- Update log parser regex and _handle_entry() to persist indicator values
- Add dashboard section to README (tech stack, screens, API endpoints)
- Add dashboard/ directory to project structure in README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-05 21:08:16 +09:00
parent aef161002d
commit 39e55368fd
3 changed files with 61 additions and 7 deletions

View File

@@ -57,6 +57,9 @@ cointrader/
│ ├── tune_hyperparams.py # Optuna 하이퍼파라미터 자동 탐색 (수동 트리거) │ ├── tune_hyperparams.py # Optuna 하이퍼파라미터 자동 탐색 (수동 트리거)
│ ├── deploy_model.sh # 모델 파일 LXC 서버 전송 │ ├── deploy_model.sh # 모델 파일 LXC 서버 전송
│ └── run_tests.sh # 전체 테스트 실행 │ └── run_tests.sh # 전체 테스트 실행
├── dashboard/
│ ├── api/ # FastAPI 백엔드 (로그 파서 + REST API)
│ └── ui/ # React 프론트엔드 (Vite + Recharts)
├── models/ # 학습된 모델 저장 (.pkl / .onnx) ├── models/ # 학습된 모델 저장 (.pkl / .onnx)
├── data/ # 과거 데이터 캐시 (.parquet) ├── data/ # 과거 데이터 캐시 (.parquet)
├── logs/ # 로그 파일 ├── 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 ```bash

View File

@@ -46,7 +46,7 @@ PATTERNS = {
r".*기존 포지션 복구: (?P<direction>\w+) \| 진입가=(?P<entry_price>[\d.]+) \| 수량=(?P<qty>[\d.]+)" r".*기존 포지션 복구: (?P<direction>\w+) \| 진입가=(?P<entry_price>[\d.]+) \| 수량=(?P<qty>[\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( "entry": 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<direction>SHORT|LONG) 진입: " r".*(?P<direction>SHORT|LONG) 진입: "
@@ -54,6 +54,9 @@ PATTERNS = {
r"수량=(?P<qty>[\d.]+), " r"수량=(?P<qty>[\d.]+), "
r"SL=(?P<sl>[\d.]+), " r"SL=(?P<sl>[\d.]+), "
r"TP=(?P<tp>[\d.]+)" r"TP=(?P<tp>[\d.]+)"
r"(?:, RSI=(?P<rsi>[\d.]+))?"
r"(?:, MACD_H=(?P<macd_hist>[+\-\d.]+))?"
r"(?:, ATR=(?P<atr>[\d.]+))?"
), ),
# 청산 감지(MANUAL): exit=1.3782, rp=+2.9859, commission=0.0598, net_pnl=+2.9261 # 청산 감지(MANUAL): exit=1.3782, rp=+2.9859, commission=0.0598, net_pnl=+2.9261
@@ -286,7 +289,7 @@ class LogParser:
) )
return 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) m = PATTERNS["entry"].search(line)
if m: if m:
self._handle_entry( self._handle_entry(
@@ -296,6 +299,9 @@ class LogParser:
qty=float(m.group("qty")), qty=float(m.group("qty")),
sl=float(m.group("sl")), sl=float(m.group("sl")),
tp=float(m.group("tp")), 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 return
@@ -381,7 +387,8 @@ class LogParser:
# ── 포지션 진입 핸들러 ─────────────────────────────────────── # ── 포지션 진입 핸들러 ───────────────────────────────────────
def _handle_entry(self, ts, direction, entry_price, qty, 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: if leverage is None:
leverage = self._bot_config.get("leverage", 10) leverage = self._bot_config.get("leverage", 10)
@@ -405,11 +412,12 @@ class LogParser:
cur = self.conn.execute( cur = self.conn.execute(
"""INSERT INTO trades(symbol, direction, entry_time, entry_price, """INSERT INTO trades(symbol, direction, entry_time, entry_price,
quantity, leverage, sl, tp, status, extra) quantity, leverage, sl, tp, status, extra, rsi, macd_hist, atr)
VALUES(?,?,?,?,?,?,?,?,?,?)""", VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(self._bot_config.get("symbol", "XRPUSDT"), direction, ts, (self._bot_config.get("symbol", "XRPUSDT"), direction, ts,
entry_price, qty, leverage, sl, tp, "OPEN", 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.conn.commit()
self._current_position = { self._current_position = {

View File

@@ -205,7 +205,10 @@ class TradingBot:
) )
logger.success( logger.success(
f"{signal} 진입: 가격={price}, 수량={quantity}, " 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" sl_side = "SELL" if signal == "LONG" else "BUY"