업비트 자동매매 개발일지 #3 – WebSocket으로 실시간 체결 데이터 수집하기

자동매매 시스템에서 전략보다 먼저 필요한 것은 시장 데이터를 안정적으로 수집하는 일이다.

완성된 캔들 데이터는 일정 시간 동안의 시가·고가·저가·종가와 거래량을 보여준다. 하지만 그 구간 안에서 가격이 어떤 순서로 움직였는지, 순간적으로 거래량이 얼마나 증가했는지까지는 확인하기 어렵다.

특히 짧은 시간의 가격 변화와 체결 흐름을 이용하는 전략에서는 이러한 차이가 진입과 청산 결과를 크게 바꿀 수 있다. 따라서 체결이 발생한 직후의 데이터를 가능한 한 빠르게 수집하고 저장한 뒤, 같은 수준의 데이터를 이용해 백테스트와 모의투자를 진행해야 한다.

이를 위해 서버와 연결을 계속 유지하면서 체결 데이터를 바로 받는 WebSocket 방식의 수집기를 구축했다. 이번 글에서는 그 과정을 정리해보려고 한다.

REST API만으로는 부족했던 이유

이번 시스템에서는 일부 관심 종목이 아니라 260여 개 종목의 체결 데이터를 실시간으로 수집해야 했다.

필요할 때마다 서버에 요청해 응답을 받는 REST API 방식으로 종목별 최근 체결 데이터를 1초마다 조회한다고 가정하면 초당 260여 번의 요청이 필요하다.
하지만 업비트 REST API에는 호출 횟수 제한이 있어 이런 방식은 현실적으로 사용할 수 없다.
호출을 제한에 맞춰 분산하면 전체 종목을 한 번 조회하는 데만 수십 초가 걸린다.

지연 문제도 있다. 일정한 간격으로 데이터를 반복 조회하는 방식을 폴링(Polling)이라고 한다. 조회 요청을 보내고 응답을 받는 사이에 새로운 체결이 발생하면, 현재 응답에 반영되지 않아 다음 조회 때까지 기다려야 할 수 있다. 특히 거래가 활발한 종목은 그 짧은 사이에도 여러 건이 체결된다. 다음 조회에서 데이터를 한꺼번에 받으면 어디까지 저장했는지 확인하면서 중복 데이터와 누락된 데이터를 별도로 처리해야 한다.

반면 서버와 연결을 계속 유지하는 WebSocket은 한 번의 연결에서 여러 종목을 구독할 수 있다.
체결이 발생할 때마다 서버가 데이터를 전달하므로 반복 요청이 필요 없고, REST 폴링보다 훨씬 짧은 지연으로 전 종목의 체결 흐름을 수집할 수 있다.

현재 요청 제한 정책은 업비트 공식 요청 수 제한 문서에서 확인할 수 있다.

업비트 WebSocket 연결과 구독

업비트 시세 WebSocket 주소에 연결한 뒤 trade 데이터를 구독했다. 현재는 활성 종목 중 STABLE 등급으로 분류한 6종목(USD1, USDC, USDE, USDS, USDT, XAUT)만 제외하고 나머지 전 종목의 체결 데이터를 수집하고 있다. 실제 매매 대상은 이보다 좁게 선별하지만, 향후 전략 검증을 위해 데이터는 최대한 넓게 모은다.

이번에 사용하는 공개 시세 WebSocket은 별도의 API 키 없이 이용할 수 있다. API 키는 이후 잔고 조회와 실제 주문 기능을 구현할 때 발급할 예정이다.

subscribe = [
    {"ticket": "autotrade3"},
    {
        "type": "trade",
        "codes": markets,
        "isOnlyRealtime": True,
    },
    {"format": "SIMPLE"},
]

ws = websocket.WebSocket()
ws.connect("wss://api.upbit.com/websocket/v1")
ws.send(json.dumps(subscribe))

SIMPLE 형식은 필드 이름을 짧게 줄여 전송량을 줄인다. 수신한 메시지에서는 종목 코드, 체결가, 거래량, 매수·매도 구분, 체결 시각과 체결 고유번호를 꺼내 사용한다.

받은 데이터를 바로 저장하지 않은 이유

처음에는 체결 메시지를 받을 때마다 PostgreSQL에 저장하는 방법을 생각했다. 하지만 거래가 몰리는 시간에는 저장 요청이 지나치게 자주 발생해 데이터베이스에 부담을 줄 수 있다.

체결 데이터 한 건을 틱(Tick)이라고 부른다. 수신한 틱은 바로 저장하지 않고, 데이터를 메모리에 잠시 모아두는 버퍼(Buffer)에 넣었다. 이후 1초마다 또는 500건이 쌓였을 때 한꺼번에 저장하도록 구성했다.

with self._buffer_lock:
    self._buffer.append(tick)
    should_flush = len(self._buffer) >= 500

if should_flush:
    self._flush()

저장 직전에는 데이터를 날짜별로 나눠 보관하는 PostgreSQL 파티션이 존재하는지 확인한다. 이미 저장된 체결 고유번호와 같은 데이터가 다시 들어오면 해당 데이터는 건너뛰도록 해 중복 저장을 방지했다.

수집 지연을 따로 기록하기

업비트의 체결 시각만 저장하면 네트워크나 프로그램에서 얼마나 지연됐는지 알기 어렵다. 그래서 데이터베이스에 실제 수집 시각을 저장하는 collected_at 컬럼을 별도로 만들고, WebSocket 메시지를 받은 즉시 현재 시각을 기록했다.

체결 시각과 수신 시각의 차이를 비교하면 WebSocket 지연을 측정할 수 있고, 백테스트와 실시간 매매 결과가 달라지는 원인을 분석할 때도 도움이 된다.

연결이 끊어지는 상황에 대비하기

WebSocket은 계속 연결되어 있을 것 같지만 실제 운영에서는 인터넷 장애, 서버 응답 지연, 프로그램 재시작 등으로 언제든 끊길 수 있다.

현재 수집기는 연결이 종료되거나 30초 동안 새로운 데이터가 들어오지 않는 상황을 감지하면 기존 연결을 닫고 3초 뒤 자동으로 다시 연결한다. 일정 시간 동안 응답이 없는 상태를 타임아웃(Timeout)이라고 한다.

while self._running:
    try:
        self._connect_once()
    except Exception as error:
        print(f"WebSocket 오류: {error}")

    if self._running:
        time.sleep(3)

새로운 종목이 상장되면 10분 주기의 마켓 동기화에서 이를 감지한다. 누락 가능성이 있는 최근 데이터는 REST API로 보충하고, WebSocket을 재연결해 새로운 종목까지 구독한다.

Windows 서비스와 재부팅 데이터 복구

수집기를 터미널에서 직접 실행하면 창을 닫거나 PC를 재부팅했을 때 수집도 함께 중단된다. 이를 방지하기 위해 일반 프로그램을 Windows 서비스로 등록하고 자동 재시작을 관리하는 NSSM(Non-Sucking Service Manager)을 사용해 WebSocket 수집 프로그램을 서비스로 등록했다. PC가 부팅되면 자동으로 실행되고, 비정상 종료되면 5초 뒤 다시 시작된다.

Windows 서비스에서 자동으로 실행 중인 WebSocket 수집 프로그램
WebSocket 수집 프로그램을 Windows 서비스로 등록한 모습. 현재 실행 중이며 PC 부팅 시 자동으로 시작된다.

하지만 Windows 서비스만으로는 PC가 꺼져 있던 시간의 체결 데이터를 복구할 수 없다. WebSocket은 다시 연결된 시점 이후의 데이터만 전달하기 때문이다.

그래서 부팅 시 PostgreSQL에 저장된 마지막 체결 시각을 먼저 확인한다. 마지막 체결부터 현재까지 비어 있는 구간은 업비트 REST 최근 체결 API로 종목별 보충한 뒤 WebSocket 연결을 시작하도록 구성했다.

PC 재부팅
    ↓
Windows 서비스 자동 시작
    ↓
PostgreSQL 마지막 틱 시각 확인
    ↓
업비트 REST API로 누락 데이터 다시 수집
    ↓
WebSocket 연결 및 실시간 수집 재개

REST API로 다시 수집한 데이터와 기존 데이터가 겹치더라도 체결 고유번호를 기준으로 중복을 제거한다. 덕분에 짧은 재부팅이나 네트워크 중단이 발생해도 누락 구간을 보충하고 실시간 수집을 이어갈 수 있다.

WebSocket 한 연결이 만드는 데이터 흐름

업비트 WebSocket
    ↓ 실시간 체결 수신
체결 데이터 수집기
    ├─ 메모리에 잠시 모음 → PostgreSQL ticks 테이블에 저장
    ├─ 캔들 생성기로 전달 → 분봉 생성
    └─ 현재가를 메모리에 보관 → 모니터링 화면에 전달

하나의 체결 데이터는 데이터베이스 저장에만 사용되지 않는다. 실시간 캔들을 만들고, 현재가를 갱신하며, 이후 매매 전략이 판단할 기초 데이터가 된다.

마치며

처음에는 WebSocket 주소에 연결하고 받은 데이터를 저장하면 끝이라고 생각했다. 막상 운영해 보니 연결보다 재연결, 중복 처리, 재부팅 후 데이터 복구가 더 큰 일이었다.

이번 작업으로 실시간 시장 데이터를 PostgreSQL에 안정적으로 쌓을 수 있는 기반을 마련했다. 다음 글에서는 수집한 틱 데이터로 여러 시간 단위의 캔들을 생성하는 과정을 정리해보려고 한다.

참고 자료: 업비트 WebSocket 공식 가이드

이 글은 개인적인 개발 과정과 경험을 기록한 글이며, 특정 투자 또는 수익을 권유하거나 보장하지 않습니다.

개발일지 이어보기
이전 글: 업비트 자동매매 개발일지 #2 – 개발 환경 세팅
다음 글: 업비트 자동매매 개발일지 #4 – API 키 발급과 안전한 관리 방법