거래소 API 장애, 왜 대비해야 하는가
자동매매 봇을 운영하다 보면 거래소 API 장애는 피할 수 없는 현실입니다. 바이낸스, 업비트 등 주요 거래소도 연간 수십 차례 API 지연이나 다운타임을 경험합니다. 문제는 이 장애가 시장 급변동 시점에 집중된다는 것입니다. 가격이 급등락할 때 API가 먹통이 되면, 손절 주문이 실행되지 않아 계좌에 치명적 손실이 발생할 수 있습니다.
API 장애의 3가지 유형
1. 응답 지연 (Latency Spike)
평소 50ms 이내에 응답하던 API가 수 초 이상 걸리는 경우입니다. 완전한 장애는 아니지만, 고빈도 전략에서는 치명적입니다. 주문 체결 시점이 밀리면서 의도한 가격과 실제 체결가의 괴리(슬리피지)가 커집니다.
2. 부분 장애 (Partial Outage)
주문 API는 정상이지만 잔고 조회가 안 되거나, 현물은 되지만 선물 API가 다운된 경우입니다. 이 상황에서 봇이 잔고를 0으로 인식하고 전량 매도하는 사고가 실제로 발생합니다.
3. 완전 장애 (Full Outage)
API 서버가 완전히 응답하지 않는 경우입니다. 이때 열려있는 포지션을 관리할 수 없으므로, 사전에 거래소 웹에서 조건부 주문(스톱로스)을 설정해두는 것이 필수입니다.
| 장애 유형 | 발생 빈도 | 위험도 | 대응 난이도 |
|---|---|---|---|
| 응답 지연 | 주 1~2회 | ⚠️ 중간 | 낮음 |
| 부분 장애 | 월 2~3회 | 🔴 높음 | 중간 |
| 완전 장애 | 분기 1~2회 | 🔴 매우 높음 | 높음 |
장애 감지 시스템 구현
API 장애를 빠르게 감지하는 것이 대응의 첫 단계입니다. 헬스체크, 응답시간 모니터링, 에러율 추적을 조합하여 구현합니다.
import time
import requests
from collections import deque
class APIHealthMonitor:
def __init__(self, base_url, threshold_ms=2000, error_rate_limit=0.3):
self.base_url = base_url
self.threshold_ms = threshold_ms
self.error_rate_limit = error_rate_limit
self.response_times = deque(maxlen=50)
self.errors = deque(maxlen=50)
def check_health(self):
try:
start = time.time()
resp = requests.get(f"{self.base_url}/api/v3/ping", timeout=5)
latency_ms = (time.time() - start) * 1000
self.response_times.append(latency_ms)
self.errors.append(0 if resp.status_code == 200 else 1)
return {
'status': 'healthy' if self._is_healthy() else 'degraded',
'latency_ms': round(latency_ms, 1),
'avg_latency_ms': round(sum(self.response_times) / len(self.response_times), 1),
'error_rate': sum(self.errors) / len(self.errors) if self.errors else 0
}
except Exception as e:
self.errors.append(1)
return {'status': 'down', 'error': str(e)}
def _is_healthy(self):
if not self.response_times:
return True
avg_latency = sum(self.response_times) / len(self.response_times)
error_rate = sum(self.errors) / len(self.errors) if self.errors else 0
return avg_latency < self.threshold_ms and error_rate < self.error_rate_limit
자동 장애 대응 패턴
재시도 + 지수 백오프
일시적 장애에 대응하는 가장 기본적인 패턴입니다. 실패 시 대기 시간을 점진적으로 늘려 거래소 서버에 과부하를 주지 않으면서 복구를 기다립니다.
import random
def retry_with_backoff(func, max_retries=5, base_delay=1.0):
for attempt in range(max_retries):
try:
return func()
except Exception as e:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f"재시도 {attempt + 1}/{max_retries}, {delay:.1f}초 대기: {e}")
time.sleep(delay)
# 사용 예시
order = retry_with_backoff(
lambda: exchange.create_limit_buy_order('BTC/USDT', 0.01, 50000)
)
서킷 브레이커 패턴
연속 실패가 일정 횟수를 넘으면 API 호출 자체를 중단하고, 일정 시간 후 다시 시도합니다. 계좌 생존 규칙과 동일한 원리로, 장애 상황에서 무분별한 주문 시도가 오히려 손실을 키우는 것을 방지합니다.
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.state = 'CLOSED' # CLOSED=정상, OPEN=차단, HALF_OPEN=시험
self.last_failure_time = 0
def call(self, func):
if self.state == 'OPEN':
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = 'HALF_OPEN'
else:
raise Exception("서킷 브레이커 OPEN - API 호출 차단 중")
try:
result = func()
if self.state == 'HALF_OPEN':
self.state = 'CLOSED'
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = 'OPEN'
print(f"⚠️ 서킷 브레이커 OPEN! {self.recovery_timeout}초 후 재시도")
raise
멀티 거래소 폴백
하나의 거래소가 장애일 때 다른 거래소에서 헤지 포지션을 잡거나, 동일 자산을 다른 거래소에서 매매하는 전략입니다. CCXT 라이브러리를 활용하면 거래소 간 전환이 간편합니다.
import ccxt
class MultiExchangeExecutor:
def __init__(self):
self.exchanges = [
ccxt.binance({'apiKey': '...', 'secret': '...'}),
ccxt.bybit({'apiKey': '...', 'secret': '...'}),
ccxt.okx({'apiKey': '...', 'secret': '...', 'password': '...'})
]
self.breakers = [CircuitBreaker() for _ in self.exchanges]
def execute_order(self, symbol, side, amount):
for i, (exchange, breaker) in enumerate(zip(self.exchanges, self.breakers)):
try:
order_func = (exchange.create_market_buy_order
if side == 'buy'
else exchange.create_market_sell_order)
result = breaker.call(lambda: order_func(symbol, amount))
print(f"✅ {exchange.id}에서 체결 완료")
return result
except Exception as e:
print(f"❌ {exchange.id} 실패: {e}, 다음 거래소로 전환")
continue
raise Exception("모든 거래소 실패 - 수동 개입 필요")
장애 시 알림 체계
API 장애 감지 시 즉각적인 알림이 필수입니다. 텔레그램 봇이나 Discord 웹훅을 활용하여 장애 발생, 포지션 상태, 서킷 브레이커 작동 여부를 실시간으로 전달받아야 합니다.
- ✅ 응답 지연 2초 초과 시 → 경고 알림
- ✅ 에러율 30% 초과 시 → 긴급 알림 + 신규 주문 중단
- ✅ 완전 장애 시 → 긴급 알림 + 열린 포지션 목록 전송
- ✅ 리밸런싱 봇도 장애 시 자동 일시정지
실전 체크리스트
- ✅ 모든 API 호출에 타임아웃(5초) 설정
- ✅ 재시도 로직에 지수 백오프 적용
- ✅ 서킷 브레이커로 연쇄 장애 방지
- ✅ 거래소 웹에서 스톱로스 사전 설정
- ✅ 장애 로그를 파일로 기록하여 사후 분석
- ✅ 월 1회 장애 시뮬레이션(카오스 테스트) 실시
결론
자동매매에서 API 장애 대응은 선택이 아닌 필수입니다. 수익을 내는 전략보다 장애 상황에서 계좌를 지키는 방어 로직이 더 중요합니다. 헬스 모니터링, 서킷 브레이커, 멀티 거래소 폴백을 조합하면 대부분의 장애 상황에서 자동으로 대응할 수 있습니다. 최악의 상황을 대비하는 봇이 장기적으로 살아남는 봇입니다.