티스토리 뷰

FastAPI + React로 봇 관리 대시보드 만들기

터미널 로그를 뚫어져라 쳐다보는 것도 한계가 있었다. "지금 포지션이 뭐지?", "오늘 수익률이 얼마지?", "봇이 살아있긴 한 거야?" — 이런 질문에 매번 로그를 뒤져야 했다. 웹 대시보드를 만들기로 했다.

API 서버: FastAPI

FastAPI를 선택한 이유는 간단하다. Python이니까 봇 코드와 같은 언어로 작성할 수 있고, 비동기 지원이 기본이고, WebSocket도 된다. Swagger 문서가 자동으로 생성되는 것도 좋았다.

from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(title="CryptoBot API")

@app.get("/api/portfolio")
async def get_portfolio():
    """현재 포트폴리오 상태"""
    positions = db.get_open_positions()
    balance = trader.get_balance()
    return {"balance": balance, "positions": positions}

@app.get("/api/trades")
async def get_trades(limit: int = 50):
    """최근 매매 내역"""
    return db.get_recent_trades(limit)

@app.post("/api/bot/stop")
async def stop_bot():
    """봇 긴급 정지"""
    scheduler.pause()
    return {"status": "stopped"}

API 엔드포인트는 크게 4개 카테고리로 나눴다. 포트폴리오 조회, 매매 내역, 봇 상태/제어, 전략 파라미터 조회/수정. REST API로 기본 CRUD를 처리하고, 실시간 데이터는 WebSocket으로 밀어준다.

실시간 WebSocket

대시보드에서 가장 중요한 건 실시간성이다. 1분마다 갱신되는 봇 상태를 브라우저에서 바로 볼 수 있어야 한다.

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    try:
        while True:
            # 봇의 매 틱 결과를 실시간 전송
            data = await tick_queue.get()
            await websocket.send_json({
                "type": "tick",
                "timestamp": data["timestamp"],
                "prices": data["prices"],
                "signals": data["signals"],
                "portfolio_value": data["total_value"]
            })
    except WebSocketDisconnect:
        pass

봇의 메인 루프가 틱을 처리할 때마다 결과를 asyncio Queue에 넣고, WebSocket 핸들러가 이걸 꺼내서 클라이언트에 전송한다. 연결이 끊어져도 봇은 영향을 받지 않는 구조다.

React 대시보드

프론트엔드는 React + Recharts로 만들었다. 핵심 화면은 3개다.

  • Overview — 총 자산, 일일 수익률, 봇 상태(실행중/정지), 현재 포지션 요약
  • Trades — 매매 히스토리 테이블 + 필터(코인별, 기간별, 승/패)
  • Settings — 전략 파라미터 수정, 봇 시작/정지, 코인 추가/제거

포트폴리오 가치 변화를 라인 차트로 보여주고, 코인별 비중을 파이 차트로 표시한다. 매매 히스토리는 수익이면 초록, 손실이면 빨강으로 색을 입혔다. 단순하지만 한눈에 상태를 파악할 수 있다.

봇 제어 패널

대시보드에서 제일 많이 쓰는 기능은 봇 제어다. 시작/정지 버튼, K값 슬라이더, 손절률 조정. 파라미터를 바꾸면 API를 통해 즉시 반영되고, 다음 틱부터 새 파라미터가 적용된다. 긴급 상황에서는 "전체 매도" 버튼으로 모든 포지션을 시장가 청산할 수 있다.

// 긴급 전체 매도
const handleEmergencySell = async () => {
  if (!confirm('정말 모든 포지션을 매도하시겠습니까?')) return;
  await fetch('/api/bot/emergency-sell', { method: 'POST' });
};

confirm 한 번 더 물어보는 게 별거 아닌 것 같지만, 새벽에 졸린 눈으로 대시보드를 열었을 때 실수로 누르는 걸 막아준다.

모바일 대응

코인은 24시간 돌아간다. 밖에서도 봇 상태를 확인해야 한다. CSS Grid + Flexbox로 반응형을 잡았다. 복잡한 차트는 모바일에서 숨기고, 핵심 수치(총 자산, 현재 포지션, 봇 상태)만 보여주는 모바일 뷰를 별도로 만들었다. 폰에서 봇을 정지시킬 수 있다는 것만으로도 심리적 안정감이 크다.

대시보드가 생기니까 봇 관리가 확 편해졌다. 하지만 실전 운영은 대시보드만으로 해결되지 않는 문제들이 있었다. 다음 글에서는 운영 중 터진 장애들과 리스크 관리, 그리고 코드 리팩토링 이야기를 한다.


반응형
댓글