AI를 활용한 재밌는 것들을 개발합니다

2026년 5월 5일 화요일

WebSocket + Vanilla JS로 실시간 자동매매 UI 만들기 [Claude Code · 자동매매 리밸런싱 · 4편]

FastAPI WebSocket Vanilla JS Chart.js SQLite TWR

WebSocket + Vanilla JS로
실시간 자동매매 UI 만들기

모바일 반응형 대시보드, TWR 수익률, 배당금 차트까지 — 프레임워크 없이 구현

📌 시리즈 4편입니다
3편에서 TWAP 분할 주문 엔진과 주문 큐를 완성했습니다. 이번 편에서는 웹 UI, WebSocket 실시간 통신, SQLite 이력 관리, TWR 수익률 계산, Chart.js 대시보드, 텔레그램 알림까지 다룹니다.

왜 Vanilla JS인가 — React 없이 만든 이유

프론트엔드에 React나 Vue 같은 프레임워크를 쓰지 않았습니다. 이유는 단순합니다.

  • 빌드 과정 없이 index.html 파일 하나로 바로 실행
  • Python 백엔드에서 정적 파일로 서빙하기 편함
  • 개인 도구라 번들 크기 최적화 불필요
  • Claude Code가 Vanilla JS를 더 정확하게 생성함

결과적으로 index.html + style.css + app.js 세 파일만으로 모든 UI를 구현했습니다.

FastAPI WebSocket — 실시간 로그 스트리밍

자동매매가 진행되는 동안 백엔드 로그가 실시간으로 브라우저에 표시됩니다. WebSocket으로 서버 → 클라이언트 방향으로 로그를 스트리밍합니다.

# web_server.py — WebSocket 엔드포인트
@app.websocket("/ws/logs")
async def ws_logs(ws: WebSocket):
    await ws.accept()
    # 재연결 시 이전 로그 버퍼 전송 (최대 500개)
    for entry in self._log_buffer:
        await ws.send_text(json.dumps({
            "type": "log", "ts": entry["ts"], "message": entry["message"]
        }))
    self._ws_clients.append(ws)
    try:
        while True:
            await ws.receive_text()  # 연결 유지
    except WebSocketDisconnect:
        self._ws_clients.remove(ws)
// app.js — WebSocket 연결
function connectWS() {
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
  ws = new WebSocket(`${proto}://${location.host}/ws/logs`);
  ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.type === 'log') appendLog(msg.message, msg.ts);
    if (msg.type === 'status') updateRunStatus(msg.status);
  };
  ws.onclose = () => setTimeout(connectWS, 3000); // 자동 재연결
}
로그 버퍼의 역할
모바일에서 페이지를 새로고침하면 WebSocket 연결이 끊깁니다. 서버에 최근 500개 로그를 버퍼에 보관해두고, 재연결 시 즉시 전송합니다. 덕분에 자동매매 중 잠깐 화면을 나갔다 와도 이전 로그를 볼 수 있습니다.

UI 패널 구성 — 탭 기반 SPA

React Router 없이 탭 전환만으로 SPA처럼 동작합니다. 각 탭이 활성화될 때만 해당 데이터를 API로 요청합니다.

패널기능데이터 소스
계좌 현황보유 종목, 목표금액, 비중 설정/api/account
스케줄 설정매도/매수 시간, 분할 횟수/api/schedule
주문 현황접수된 주문, 체결수량, 회차/api/orders
이력자동매매 세션 이력, 주문 상세/api/trading/sessions
포트폴리오TWR 수익률, 배당금 차트/api/dashboard
알림 설정텔레그램 Bot Token/Chat ID/api/notification/settings

주문 현황 — 주문번호 매칭 & 체결 추적

주문을 접수한 후 체결수량을 실시간으로 추적합니다. 핵심은 접수된 주문번호미체결 조회 결과를 매칭하는 것입니다.

# /api/orders — 주문 현황 + 체결수량 계산
async def get_orders():
    submitted = self.om.get_submitted_orders()
    unfilled_map = {}

    # 미체결 조회 API
    raw = await self.am.fetch_unfilled_orders(self.am.selected_account)
    for o in raw:
        for key in ("odno", "ord_no", "ORD_NO", "ordNo"):
            val = str(o.get(key, "")).strip()
            if val and val != "0":
                unfilled_map[val] = int(o.get("rmnd_qty", 0)); break

    orders_out = []
    for o in submitted:
        order_no = o.get("order_no", "")
        if order_no in unfilled_map:
            unfilled = unfilled_map[order_no]
            filled = o["quantity"] - unfilled
        elif order_no:
            unfilled, filled = 0, o["quantity"]  # 전량 체결
        else:
            unfilled, filled = None, None   # 주문번호 미수신
        orders_out.append({**o, "unfilled_qty": unfilled, "filled_qty": filled})

    # 체결수량을 DB에 비동기 저장
    asyncio.create_task(self.hm.update_order_fills(fills))
    return {"orders": orders_out, "sell_total": cfg.sell_split, "buy_total": cfg.buy_split}

SQLite HistoryManager — 이력 & 수익률

모든 자동매매 이력은 SQLite에 저장합니다. aiosqlite로 비동기로 DB를 다룹니다.

# DB 테이블 구조
CREATE TABLE rebalancing_history (
    id INTEGER PRIMARY KEY,
    account_no TEXT,
    session_id TEXT,
    executed_at TEXT
);

CREATE TABLE order_execution_log (
    id INTEGER PRIMARY KEY,
    session_id TEXT,
    stock_code TEXT, stock_name TEXT,
    order_type TEXT,  -- "sell" / "buy"
    round_no INTEGER, total_rounds INTEGER,
    ordered_qty INTEGER, filled_qty INTEGER,
    order_no TEXT,
    submitted_at TEXT
);

CREATE TABLE portfolio_snapshot (
    id INTEGER PRIMARY KEY,
    account_no TEXT,
    snapshot_date TEXT,  -- YYYY-MM-DD
    total_eval INTEGER,
    deposit INTEGER
);

TWR — 시간가중수익률 계산

연금계좌는 매달 추가 납입이 있어 단순 수익률이 의미 없습니다. TWR(Time-Weighted Return)으로 입출금 영향을 제거한 순수 운용 성과를 측정합니다.

async def calculate_twr(self, account_no):
    snapshots = await self.get_portfolio_snapshots(account_no)
    cashflows = await self.get_cashflow_list(account_no)

    cf_map = {}  # 날짜별 입출금 합계
    for cf in cashflows:
        sign = 1 if cf["flow_type"] == "deposit" else -1
        cf_map[cf["flow_date"]] = cf_map.get(cf["flow_date"], 0) + sign * cf["amount"]

    twr = 1.0
    for i in range(1, len(snapshots)):
        prev = snapshots[i-1]["total_eval"]
        curr = snapshots[i]["total_eval"]
        cf = cf_map.get(snapshots[i]["snapshot_date"], 0)
        adjusted_prev = prev + cf
        if adjusted_prev > 0:
            twr *= (curr / adjusted_prev)

    twr_pct = round((twr - 1) * 100, 2)
    return {"twr": twr_pct, "annualized_twr": ...}

Chart.js 대시보드 — 수익률 & 배당금

포트폴리오 현황 패널에 두 가지 차트를 추가했습니다.

평가금액 추이 / 일별 수익률 차트

대시보드를 열 때마다 당일 스냅샷을 자동 저장(upsert)합니다. 쌓인 스냅샷으로 시간에 따른 평가금액 변화와 일별 수익률을 차트로 보여줍니다.

월별 배당금 바차트

키움 API로 배당금 수령 내역을 동기화합니다. 대시보드 진입 시 백그라운드에서 자동으로 동기화해 새 배당금이 있으면 차트를 즉시 갱신합니다.

// app.js — 대시보드 로드 (배당금 백그라운드 동기화)
async function loadDashboard() {
  const [data, chartData] = await Promise.all([
    api('GET', '/api/dashboard'),
    api('GET', '/api/portfolio/chart').catch(() => ({ snapshots: [], dividends: [] })),
  ]);

  // 화면 먼저 렌더링
  renderPortfolioChart(_chartMode);
  renderDividendChart(chartData.dividends);

  // 배당금 동기화는 백그라운드에서
  api('POST', '/api/dividend/sync').then(result => {
    if (result.saved > 0) renderDividendChart(result.dividends);
  }).catch(() => {});
}

텔레그램 알림 — 실시간 상황 공유

자동매매 중 스마트폰으로 상황을 확인하고 싶어 텔레그램 알림을 붙였습니다.

알림 종류내용
리밸런싱 시작매도 시작 / 매수 시작
완료전체 리밸런싱 완료
주문 실패종목명 + 실패 사유
배당금종목명 + 수령액
장 시작 전08:50 포트폴리오 현황 (ON/OFF)
텔레그램 Bot 설정 방법
① @BotFather 에서 /newbot 명령으로 Bot Token 발급
② @userinfobot 에서 Chat ID 확인
③ 앱의 알림 설정 패널에 Bot Token, Chat ID 입력 후 연결 테스트
✅ 4편 완성 체크리스트
✔ FastAPI WebSocket 실시간 로그 스트리밍
✔ 로그 버퍼 (재연결 시 500개 복원)
✔ Vanilla JS 탭 기반 SPA
✔ 주문번호 매칭 체결수량 추적
✔ SQLite HistoryManager (이력, 스냅샷, 배당금)
✔ TWR 수익률 계산
✔ Chart.js 평가금액/수익률/배당금 차트
✔ 배당금 API 자동 동기화
✔ 텔레그램 알림 (5종)

댓글 없음:

댓글 쓰기

가장 많이 본 글