시작하기 전에: 실제 발생 오류로 배우는教训

프로덕션 환경에서 OKX WebSocket에 연결할 때 가장 흔히 마주치는 오류들을 먼저 확인합시다:

# 오류 1: ConnectionError: timeout
import websocket

ws = websocket.WebSocketApp("wss://ws.okx.com:8443/ws/v5/public")

결과: ConnectionError: timeout - 방화벽 또는 네트워크 설정 문제

오류 2: 401 Unauthorized (REST API)

import requests response = requests.get( "https://www.okx.com/api/v5/market/ticker?instId=BTC-USDT-SWAP", headers={"OK-ACCESS-KEY": "your_key"} )

결과: {"code": "501", "msg": "Authentication failed"}

오류 3: WebSocket ping/pong timeout

결과: websocket app died: pong timeout 30.012s

원인: 연결 유지 핑 미발송 또는 서버 응답 지연

본 가이드에서는 이러한 오류들을 선제적으로 방지하면서 OKX永续合约(Perpetual Swap)의 실시간 데이터 스트림을 안정적으로获取하는 방법을 설명합니다.

OKX永续合约 데이터 구조 이해

OKX의永续合约는 USDT-Margined Perpetual과 Coin-Margined Perpetual 두 가지 유형이 있습니다. 대부분의 트레이더는 USDT-Margined Perpetual을 사용합니다:

# OKX永续合约 심볼 구조
BTC-USDT-SWAP     # BTC/USDT USDT 마진永续合约
ETH-USDT-SWAP     # ETH/USDT USDT 마진永续合约
SOL-USDT-SWAP     # SOL/USDT USDT 마진永续合约

주요 데이터 피드 채널

1. trades: 실시간 체결

2. candles: K선/캔들스틱

3. books: 주문서(호가창)

4. mark-price: 마크 프라이스

5. funding: 펀딩レ이트

WebSocket 연결实战

OKX는 wss://ws.okx.com:8443/ws/v5/public 엔드포인트를 제공합니다. 아래는 여러永续合约의 실시간 데이터를 Subscribe하는 완전한 예제입니다:

# okx_perpetual_stream.py
import json
import websocket
import threading
import time
from datetime import datetime

class OKXPerpetualStream:
    def __init__(self):
        self.ws = None
        self.url = "wss://ws.okx.com:8443/ws/v5/public"
        self.subscribed = False
        
    def connect(self):
        """WebSocket 연결 및 자동 재연결 포함"""
        while True:
            try:
                self.ws = websocket.WebSocketApp(
                    self.url,
                    on_message=self.on_message,
                    on_error=self.on_error,
                    on_close=self.on_close,
                    on_open=self.on_open
                )
                # 핑Interval 20초 (OKX 권장)
                self.ws.run_forever(ping_interval=20, ping_timeout=10)
            except Exception as e:
                print(f"[{datetime.now()}] 연결 끊김: {e}")
                time.sleep(5)  # 재연결 전 5초 대기
                
    def on_open(self, ws):
        """연결 성공 시永续合约 Subscribe"""
        print(f"[{datetime.now()}] OKX WebSocket 연결됨")
        
        # 여러永续合约의 실시간 데이터 Subscribe
        subscribe_msg = {
            "op": "subscribe",
            "args": [
                {
                    "channel": "tickers",
                    "instId": "BTC-USDT-SWAP"
                },
                {
                    "channel": "candle1m",
                    "instId": "BTC-USDT-SWAP"
                },
                {
                    "channel": "funding",
                    "instId": "BTC-USDT-SWAP"
                },
                {
                    "channel": "trades",
                    "instId": "BTC-USDT-SWAP"
                }
            ]
        }
        ws.send(json.dumps(subscribe_msg))
        print(f"[{datetime.now()}] 구독 요청 전송 완료")
        
    def on_message(self, ws, message):
        """실시간 메시지 처리"""
        data = json.loads(message)
        
        # Subscribe 확인 메시지
        if "event" in data:
            if data["event"] == "subscribe":
                print(f"[{datetime.now()}] 구독 성공: {data.get('arg', {})}")
            return
        
        # 실제 데이터 수신
        if "data" in data:
            arg = data.get("arg", {})
            channel = arg.get("channel")
            inst_id = arg.get("instId")
            
            for item in data["data"]:
                self.process_data(channel, inst_id, item)
                
    def process_data(self, channel, inst_id, item):
        """채널별 데이터 처리"""
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        
        if channel == "tickers":
            # 실시간 티커 (가격,成交量,Funding等)
            print(f"[{timestamp}] {inst_id} | "
                  f"Last: {item.get('last', 'N/A')} | "
                  f"24h Change: {item.get('last', 'N/A')} | "
                  f"Volume: {item.get('vol24h', 'N/A')}")
                  
        elif channel == "candle1m":
            # 1분 K선
            print(f"[{timestamp}] {inst_id} Candle | "
                  f"O:{item[1]} H:{item[2]} L:{item[3]} C:{item[4]}")
                  
        elif channel == "funding":
            # 펀딩レ이트
            next_funding_time = datetime.fromtimestamp(
                int(item['nextFundingTime']) / 1000
            ).strftime('%Y-%m-%d %H:%M:%S')
            print(f"[{timestamp}] {inst_id} | "
                  f"Funding Rate: {item['fundingRate']} | "
                  f"Next: {next_funding_time}")
                  
        elif channel == "trades":
            # 실시간 체결
            side = "매수(BUY)" if item['side'] == 'buy' else "매도(SELL)"
            print(f"[{timestamp}] {inst_id} Trade | "
                  f"Price: {item['px']} | {side} | "
                  f"Size: {item['sz']}")
                  
    def on_error(self, ws, error):
        """오류 처리"""
        print(f"[{datetime.now()}] WebSocket 오류: {error}")
        
    def on_close(self, ws, close_status_code, close_msg):
        """연결 종료 처리"""
        print(f"[{datetime.now()}] 연결 종료: {close_status_code} - {close_msg}")
        
    def start(self):
        """백그라운드 스레드로 연결 시작"""
        thread = threading.Thread(target=self.connect, daemon=True)
        thread.start()
        return thread


if __name__ == "__main__":
    stream = OKXPerpetualStream()
    stream_thread = stream.start()
    
    print("OKX永续合约 실시간 데이터 스트림 시작...")
    print("종료하려면 Ctrl+C를 누르세요")
    
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n스트림 종료")
        exit(0)
# 테스트 실행
$ python okx_perpetual_stream.py

예상 출력:

[2025-01-15 10:30:01.123] OKX WebSocket 연결됨