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:
43
README.md
43
README.md
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user