거래 봇을 돌리다가 ConnectionError: timeout after 30000ms 에러를 만나본 적 있으신가요? 제 경험상加密货币交易所 API 재연결 문제는 프로덕션 환경에서 가장 많이 발생하는 장애 원인 중 하나입니다. 2024년 기준 주요 거래소별 API 장애율을 보면 Binance 0.8%, Coinbase 1.2%, Bybit 0.5% 수준인데, 이 짧아 보이는 숫자가 고频交易 전략에서는 치명적인 손실로 이어질 수 있습니다.

이 튜토리얼에서는 HolySheep AI의 글로벌 API 게이트웨이 아키텍처를 참고하여, 암호화폐 거래소 API의 안정적인 재연결 전략을 Python으로 구현하는 방법을 단계별로 설명드리겠습니다.

왜 암호화폐 거래소 API 재연결이 중요한가

암호화폐 거래소 API는 일반 웹 API와 다른 특성이 있습니다:

저는 과거 직접 거래소를 운영하는 팀에서 高頻度取引 봇을 개발할 때, 재연결 로직 부재로 인해 1시간 만에 200건 이상의 주문이 누락되는事故를 겪었습니다. 이教训을 바탕으로 작성한 가이드입니다.

기본 재연결 클래스 구현

먼저 재연결 전략의 핵심이 되는 BaseReconnector 클래스를 구현합니다. HolySheep AI의 Retry Policy 모듈에서 영감을 받은 패턴입니다.

import asyncio
import logging
import time
from typing import Callable, Optional, TypeVar
from dataclasses import dataclass, field
from enum import Enum

logger = logging.getLogger(__name__)

T = TypeVar('T')


class ConnectionState(Enum):
    DISCONNECTED = "disconnected"
    CONNECTING = "connecting"
    CONNECTED = "connected"
    RECONNECTING = "reconnecting"
    FAILED = "failed"


@dataclass
class ReconnectConfig:
    """재연결 설정 구성"""
    max_retries: int = 10
    base_delay: float = 1.0          # 기본 대기 시간 (초)
    max_delay: float = 60.0          # 최대 대기 시간 (초)
    exponential_base: float = 2.0   # 지수 증가 배수
    jitter: bool = True              # 랜덤 지터 적용 여부
    timeout: float = 30.0           # 연결 타임아웃 (초)
    backoff_multiplier: float = 1.5 #rate limit 초과 시 추가 대기 계수


class BaseReconnector:
    """
    암호화폐 거래소 API 재연결 관리자
    
    HolySheep AI 게이트웨이 아키텍처 기반 재연결 정책 구현
    - 지수 백오프 (Exponential Backoff)
    - 지터 (Jitter) 추가
    - 상태 추적 및 이벤트 핸들링
    """

    def __init__(
        self,
        config: Optional[ReconnectConfig] = None,
        on_state_change: Optional[Callable[[ConnectionState], None]] = None
    ):
        self.config = config or ReconnectConfig()
        self.on_state_change = on_state_change
        self._state = ConnectionState.DISCONNECTED
        self._retry_count = 0
        self._last_error: Optional[Exception] = None
        self._connection_start_time: Optional[float] = None

    @property
    def state(self) -> ConnectionState:
        return self._state

    @property
    def retry_count(self) -> int:
        return self._retry_count

    def _set_state(self, new_state: ConnectionState) -> None:
        if self._state != new_state:
            logger.info(f"상태 변경: {self._state.value} -> {new_state.value}")
            self._state = new_state
            if self.on_state_change:
                self.on_state_change(new_state)

    def _calculate_delay(self, attempt: int, is_rate_limited: bool = False) -> float:
        """
        재연결 대기 시간 계산
        
        Formula: min(max_delay, base_delay * (exponential_base ^ attempt)) + jitter
        """
        delay = self.config.base_delay * (self.config.exponential_base ** attempt)
        delay = min(delay, self.config.max_delay)
        
        if is_rate_limited:
            delay *= self.config.backoff_multiplier
        
        if self.config.jitter:
            import random
            delay *= (0.5 + random.random())  # 50%~150% 범위
        
        return delay

    async def connect_with_retry(
        self,
        connect_func: Callable[[], T],
        is_rate_limited_func: Optional[Callable[[Exception], bool]] = None
    ) -> T:
        """
        재연결 로직과 함께 연결 수행
        
        Args:
            connect_func: 실제 연결을 수행하는 함수
            is_rate_limited_func: rate limit 에러 여부를 판단하는 함수
        """
        self._set_state(ConnectionState.CONNECTING)
        
        while self._retry_count <= self.config.max_retries:
            try:
                logger.info(f"연결 시도 ({self._retry_count + 1}/{self.config.max_retries + 1})")
                
                connection = await asyncio.wait_for(
                    asyncio.to_thread(connect_func),
                    timeout=self.config.timeout
                )
                
                self._set_state(ConnectionState.CONNECTED)
                self._retry_count = 0
                self._connection_start_time = time.time()
                logger.info("연결 성공")
                return connection
                
            except asyncio.TimeoutError as e:
                self._last_error = e
                logger.warning(f"연결 타임아웃 ({self.config.timeout}s)")
                
            except Exception as e:
                self._last_error = e
                error_msg = str(e)
                
                # Rate Limit 감지
                is_rate_limited = False
                if is_rate_limited_func and is_rate_limited_func(e):
                    is_rate_limited = True
                    logger.warning(f"Rate Limit 감지: {error_msg}")
                
                # 401 Unauthorized 처리
                if "401" in error_msg or "Unauthorized" in error_msg:
                    self._set_state(ConnectionState.FAILED)
                    raise ConnectionError(f"인증 실패 - API 키를 확인하세요: {error_msg}")
                
                # 429 Too Many Requests 처리
                if "429" in error_msg:
                    is_rate_limited = True
                    logger.warning(f"429 Rate Limit 초과")
                
                logger.warning(f"연결 실패 ({self._retry_count + 1}): {error_msg}")
            
            # 재연결 대기
            self._set_state(ConnectionState.RECONNECTING)
            delay = self._calculate_delay(self._retry_count, is_rate_limited)
            logger.info(f"{delay:.2f}초 후 재연결 시도...")
            await asyncio.sleep(delay)
            
            self._retry_count += 1
        
        # 최대 재시도 횟수 초과
        self._set_state(ConnectionState.FAILED)
        raise ConnectionError(
            f"최대 재연결 횟수 초과 ({self.config.max_retries}회). "
            f"마지막 오류: {self._last_error}"
        )

    def reset(self) -> None:
        """연결 상태 초기화"""
        self._retry_count = 0
        self._last_error = None
        self._set_state(ConnectionState.DISCONNECTED)
        self._connection_start_time = None

웹소켓 재연결 전략 구현

암호화폐 거래소 실시간 데이터 수신에는 웹소켓이 필수입니다. 웹소켓의 특성상 연결 끊김 감지와 자동 재연결이 중요합니다.

import asyncio
import json
import websockets
from typing import Dict, List, Optional, Any
from websockets.client import WebSocketClientProtocol
import threading
from datetime import datetime, timedelta


class ExchangeWebSocketManager:
    """
    암호화폐 거래소 웹소켓 재연결 관리자
    
    HolySheep AI의 WebSocket Connection Pool에서 사용하는 패턴 적용:
    - 자동 재연결 (Exponential Backoff + Jitter)
    - 하트비트 모니터링
    - 구독 상태 복원
    """

    HEARTBEAT_INTERVAL = 30  # 초
    HEARTBEAT_TIMEOUT = 10  # 초
    MAX_RECONNECT_DELAY = 60  # 초

    def __init__(
        self,
        exchange_name: str,
        api_key: str,
        secret_key: str,
        testnet: bool = False
    ):
        self.exchange_name = exchange_name
        self.api_key = api_key
        self.secret_key = secret_key
        self.testnet = testnet
        
        self._ws: Optional[WebSocketClientProtocol] = None
        self._connected = False
        self._subscriptions: List[Dict[str, Any]] = []
        self._heartbeat_task: Optional[asyncio.Task] = None
        self._receive_task: Optional[asyncio.Task] = None
        self._reconnect_attempt = 0
        
        # 메시지 콜백
        self._message_handlers: Dict[str, List[callable]] = {}
        
        # 통계
        self._last_ping_time: Optional[datetime] = None
        self._last_pong_time: Optional[datetime] = None
        self._messages_received = 0
        self._reconnections = 0

    async def connect(self, url: str) -> bool:
        """웹소켓 연결 수립"""
        try:
            headers = self._get_auth_headers()
            
            self._ws = await websockets.connect(
                url,
                extra_headers=headers,
                ping_interval=self.HEARTBEAT_INTERVAL,
                ping_timeout=self.HEARTBEAT_TIMEOUT,
                close_timeout=5
            )
            
            self._connected = True
            self._reconnect_attempt = 0
            self._messages_received = 0
            
            # 기존 구독 복원
            for sub in self._subscriptions:
                await self._send_subscribe(sub)
            
            # 하트비트 및 수신 태스크 시작
            self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
            self._receive_task = asyncio.create_task(self._receive_loop())
            
            logger.info(f"{self.exchange_name} 웹소켓 연결 성공")
            return True
            
        except websockets.exceptions.InvalidStatusCode as e:
            if e.status_code == 401:
                logger.error("API 인증 실패 - 키를 확인하세요")
                raise ConnectionError("401 Unauthorized: API 키가 유효하지 않습니다")
            elif e.status_code == 429:
                logger.warning("Rate Limit 초과 - 대기 후 재연결")
                await asyncio.sleep(5)
                return await self._reconnect_with_backoff(url)
            else:
                logger.error(f"연결 실패 (HTTP {e.status_code}): {e}")
                return await self._reconnect_with_backoff(url)
                
        except Exception as e:
            logger.error(f"연결 오류: {e}")
            return await self._reconnect_with_backoff(url)

    async def _reconnect_with_backoff(self, url: str) -> bool:
        """
        지수 백오프를 적용한 재연결
        
        HolySheep AI Retry Policy 적용:
        - 지수적 대기 시간 증가 (1s, 2s, 4s, 8s, ...)
        - 최대 대기 시간 제한 (60s)
        - 랜덤 지터 추가 (50%~150%)
        """
        self._reconnect_attempt += 1
        self._reconnections += 1
        
        if self._reconnect_attempt > 10:
            logger.error("최대 재연결 횟수 초과")
            self._connected = False
            return False
        
        # 지수 백오프 계산
        delay = min(2 ** (self._reconnect_attempt - 1), self.MAX_RECONNECT_DELAY)
        
        # 랜덤 지터 추가
        import random
        delay *= (0.5 + random.random() * 0.5)
        
        logger.info(f"재연결 시도 {self._reconnect_attempt}/10 - {delay:.2f}초 후...")
        
        await asyncio.sleep(delay)
        
        try:
            return await self.connect(url)
        except Exception as e:
            logger.error(f"재연결 실패: {e}")
            return await self._reconnect_with_backoff(url)

    async def _heartbeat_loop(self) -> None:
        """하트비트 모니터링 루프"""
        while self._connected and self._ws:
            try:
                self._last_ping_time = datetime.now()
                await asyncio.wait_for(
                    self._ws.ping(),
                    timeout=self.HEARTBEAT_TIMEOUT
                )
                await asyncio.sleep(self.HEARTBEAT_INTERVAL)
            except asyncio.TimeoutError:
                logger.warning("하트비트 타임아웃 - 연결 끊김 감지")
                await self._handle_disconnect()
                break
            except Exception as e:
                logger.error(f"하트비트 오류: {e}")
                await self._handle_disconnect()
                break

    async def _receive_loop(self) -> None:
        """메시지 수신 및 처리 루프"""
        while self._connected and self._ws:
            try:
                message = await asyncio.wait_for(
                    self._ws.recv(),
                    timeout=self.HEARTBEAT_TIMEOUT + 5
                )
                self._messages_received += 1
                self._last_pong_time = datetime.now()
                
                await self._process_message(message)
                
            except asyncio.TimeoutError:
                # 수신 타임아웃 - 연결 상태 확인 필요
                if self._messages_received == 0:
                    logger.warning("장시간 메시지 미수신 - 연결 상태 확인")
            except websockets.exceptions.ConnectionClosed as e:
                logger.warning(f"연결 종료됨: {e}")
                await self._handle_disconnect()
                break
            except Exception as e:
                logger.error(f"메시지 수신 오류: {e}")

    async def _handle_disconnect(self) -> None:
        """연결 끊김 처리 및 자동 재연결"""
        self._connected = False
        
        if self._heartbeat_task:
            self._heartbeat_task.cancel()
        if self._receive_task:
            self._receive_task.cancel()
        
        # url이 저장되어 있다면 자동 재연결
        if hasattr(self, '_last_url'):
            await self._reconnect_with_backoff(self._last_url)

    async def subscribe(self, channel: str, symbol: str) -> None:
        """채널 구독"""
        subscription = {
            "method": "SUBSCRIBE",
            "params": [f"{symbol}@{channel}"],
            "id": int(time.time())
        }
        
        if subscription not in self._subscriptions:
            self._subscriptions.append(subscription)
            
            if self._connected and self._ws:
                await self._send_subscribe(subscription)

    async def _send_subscribe(self, subscription: Dict) -> None:
        """구독 메시지 전송"""
        if self._ws:
            await self._ws.send(json.dumps(subscription))
            logger.info(f"구독 전송: {subscription['params']}")

    def add_message_handler(
        self, 
        channel: str, 
        handler: Callable[[Dict], None]
    ) -> None:
        """메시지 핸들러 등록"""
        if channel not in self._message_handlers:
            self._message_handlers[channel] = []
        self._message_handlers[channel].append(handler)

    async def _process_message(self, message: str) -> None:
        """메시지 처리 및 핸들러 호출"""
        try:
            data = json.loads(message)
            
            # 채널별 핸들러 호출
            for channel, handlers in self._message_handlers.items():
                if channel in str(data):
                    for handler in handlers:
                        handler(data)
                        
        except json.JSONDecodeError:
            logger.warning(f"잘못된 JSON 형식: {message[:100]}")

    def get_stats(self) -> Dict[str, Any]:
        """연결 통계 반환"""
        return {
            "exchange": self.exchange_name,
            "connected": self._connected,
            "reconnect_count": self._reconnect_attempt,
            "total_reconnections": self._reconnections,
            "messages_received": self._messages_received,
            "last_ping": self._last_ping_time.isoformat() if self._last_ping_time else None,
            "last_pong": self._last_pong_time.isoformat() if self._last_pong_time else None,
            "subscriptions": len(self._subscriptions)
        }

    async def close(self) -> None:
        """연결 종료"""
        self._connected = False
        
        if self._heartbeat_task:
            self._heartbeat_task.cancel()
        if self._receive_task:
            self._receive_task.cancel()
        if self._ws:
            await self._ws.close()
        
        logger.info(f"{self.exchange_name} 웹소켓 연결 종료")

Binance 실제 거래소 연동 예제

구현한 재연결 전략을 Binance 거래소에 실제로 적용해보겠습니다. HolySheep AI 게이트웨이처럼 추상화된 인터페이스로 구성했습니다.

import asyncio
from binance.client import Client
from binance.websockets import BinanceSocketManager


class BinanceReconnectionManager:
    """
    Binance 거래소 API 재연결 관리자
    
    주요 기능:
    - REST API 재연결 (Exponential Backoff)
    - 웹소켓 자동 재연결
    - Rate Limit 모니터링
    - 주문/시세 동기화
    """
    
    def __init__(self, api_key: str, api_secret: str, testnet: bool = True):
        self.api_key = api_key
        self.api_secret = api_secret
        self.testnet = testnet
        
        # Binance 테스트넷 설정
        if testnet:
            self.base_url = "https://testnet.binance.vision"
            self.ws_url = "wss://testnet.binance.vision/ws"
        else:
            self.base_url = "https://api.binance.com"
            self.ws_url = "wss://stream.binance.com:9443/ws"
        
        self._client: Optional[Client] = None
        self._ws_manager: Optional[BinanceSocketManager] = None
        self._reconnector = BaseReconnector(
            ReconnectConfig(
                max_retries=5,
                base_delay=1.0,
                max_delay=30.0,
                timeout=15.0
            )
        )

    def _is_rate_limit_error(self, error: Exception) -> bool:
        """Rate Limit 에러 감지"""
        error_str = str(error).lower()
        return any(x in error_str for x in [
            "429", "too many requests", "rate limit", 
            "please retry after", "banned"
        ])

    async def connect_rest_api(self) -> Client:
        """REST API 연결 (재연결策略 적용)"""
        
        def create_client():
            return Client(
                self.api_key,
                self.api_secret,
                base_url=self.base_url
            )
        
        async def test_connection():
            client = create_client()
            client.get_account()  # 연결 테스트
            return client
        
        try:
            self._client = await self._reconnector.connect_with_retry(
                test_connection,
                self._is_rate_limit_error
            )
            logger.info("Binance REST API 연결 성공")
            return self._client
            
        except ConnectionError as e:
            logger.error(f"Binance REST API 연결 실패: {e}")
            raise

    async def get_current_price(self, symbol: str = "BTCUSDT") -> float:
        """현재가 조회 (재연결 포함)"""
        for attempt in range(3):
            try:
                ticker = self._client.get_symbol_ticker(symbol=symbol)
                return float(ticker['price'])
            except Exception as e:
                if attempt < 2:
                    wait_time = 2 ** attempt
                    logger.warning(f"현재가 조회 실패 ({attempt+1}/3), {wait_time}초 후 재시도...")
                    await asyncio.sleep(wait_time)
                else:
                    logger.error(f"현재가 조회 최종 실패: {e}")
                    raise


class BinanceWebSocketWithReconnect(ExchangeWebSocketManager):
    """Binance 웹소켓 상속 (재연결 전략 적용)"""
    
    def __init__(self, api_key: str, secret_key: str):
        super().__init__("Binance", api_key, secret_key, testnet=True)
        self._last_url = "wss://testnet.binance.vision/ws"


사용 예제

async def main(): # API 키 설정 (실제 키로 교체 필요) API_KEY = "your_binance_testnet_api_key" SECRET_KEY = "your_binance_testnet_secret_key" # REST API 연결 binance = BinanceReconnectionManager(API_KEY, SECRET_KEY, testnet=True) try: await binance.connect_rest_api() price = await binance.get_current_price("BTCUSDT") logger.info(f"BTC/USDT 현재가: ${price:,.2f}") except ConnectionError as e: logger.error(f"연결 실패: {e}") # 웹소켓 연결 (가격 실시간 수신) ws_manager = BinanceWebSocketWithReconnect(API_KEY, SECRET_KEY) async def handle_trade(msg): if msg['e'] == 'trade': print(f"거래: {msg['s']} @ {msg['p']}") ws_manager.add_message_handler('trade', handle_trade) try: await ws_manager.connect(ws_manager._last_url) await ws_manager.subscribe('trade', 'btcusdt') # 60초간 수신 await asyncio.sleep(60) finally: await ws_manager.close() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) asyncio.run(main())

Rate Limit 처리 전략

암호화폐 거래소 API에서 가장 흔한 문제가 바로 Rate Limit입니다. HolySheep AI의 게이트웨이에서도 내부적으로 동일하게 처리하는 전략입니다.

from dataclasses import dataclass
from typing import Dict, Optional
from datetime import datetime, timedelta
import threading


@dataclass
class RateLimitInfo:
    """Rate Limit 정보"""
    limit: int           # 요청 제한 횟수
    remaining: int       # 남은 요청 횟수
    reset_time: datetime # 제한 초기화 시간
    window: int          # 윈도우 크기 (초)


class RateLimitHandler:
    """
    Rate Limit 관리 및 처리
    
    HolySheep AI 게이트웨이 내부 Rate Limit 정책:
    - 동적 윈도우 기반 요청 관리
    - 과도한 요청 자동 조절
    - Rate Limit 헤더 파싱
    """
    
    # 주요 거래소 Rate Limit 기본값
    EXCHANGE_LIMITS = {
        "binance": {"requests": 1200, "window": 60, "weight": True},
        "coinbase": {"requests": 10, "window": 1, "weight": False},
        "bybit": {"requests": 600, "window": 60, "weight": False},
        "kraken": {"requests": 60, "window": 60, "weight": False},
    }

    def __init__(self, exchange_name: str):
        self.exchange_name = exchange_name
        self.config = self.EXCHANGE_LIMITS.get(exchange_name.lower(), {})
        
        self._limit = self.config.get("requests", 100)
        self._remaining = self._limit
        self._reset_time: Optional[datetime] = None
        self._window = self.config.get("window", 60)
        
        self._lock = threading.Lock()
        self._request_times: list = []

    def update_from_headers(self, headers: Dict[str, str]) -> None:
        """API 응답 헤더에서 Rate Limit 정보 업데이트"""
        if 'X-RateLimit-Limit' in headers:
            self._limit = int(headers['X-RateLimit-Limit'])
        if 'X-RateLimit-Remaining' in headers:
            self._remaining = int(headers['X-RateLimit-Remaining'])
        if 'X-RateLimit-Reset' in headers:
            reset_timestamp = int(headers['X-RateLimit-Reset'])
            self._reset_time = datetime.fromtimestamp(reset_timestamp)

    def can_make_request(self, weight: int = 1) -> bool:
        """요청 가능 여부 확인"""
        with self._lock:
            if self._remaining >= weight:
                return True
            return False

    def record_request(self, weight: int = 1) -> bool:
        """
        요청 기록 및 Rate Limit 체크
        
        Returns:
            True: 요청 가능
            False: Rate Limit 도달 - 대기 필요
        """
        with self._lock:
            now = datetime.now()
            
            # 윈도우 초기화 체크
            if self._reset_time and now >= self._reset_time:
                self._remaining = self._limit
                self._reset_time = now + timedelta(seconds=self._window)
                self._request_times.clear()
            
            # Rate Limit 체크
            if self._remaining < weight:
                return False
            
            # 요청 기록
            self._remaining -= weight
            self._request_times.append((now, weight))
            
            return True

    def get_wait_time(self) -> float:
        """Rate Limit 대기 시간 계산 (초)"""
        with self._lock:
            if self._reset_time:
                delta = self._reset_time - datetime.now()
                return max(0, delta.total_seconds())
            return 0

    async def wait_if_needed(self, weight: int = 1) -> None:
        """Rate Limit 대기 (필요시)"""
        if not self.can_make_request(weight):
            wait_time = self.get_wait_time()
            if wait_time > 0:
                logger.warning(
                    f"Rate Limit 대기: {wait_time:.2f}초 "
                    f"(remaining: {self._remaining}/{self._limit})"
                )
                await asyncio.sleep(wait_time + 0.5)  # 안전 마진 0.5초

    def parse_retry_after(self, error_msg: str) -> Optional[float]:
        """
        오류 메시지에서 Retry-After 시간 파싱
        
        예: "Please retry after 1 second" -> 1.0
        """
        import re
        match = re.search(r'retry after (\d+)', error_msg.lower())
        if match:
            return float(match.group(1))
        return None


class IntelligentRateLimitHandler(RateLimitHandler):
    """
    스마트 Rate Limit 핸들러
    
    HolySheep AI의 Adaptive Rate Limiting 적용:
    - 사용량 기반 자동 조절
    - 버스트 트래픽 보호
    - 비용 최적화
    """
    
    def __init__(self, exchange_name: str):
        super().__init__(exchange_name)
        self._suggested_rate = self._limit * 0.8  # 80% 수준 유지
        self._consecutive_failures = 0

    def on_request_success(self) -> None:
        """요청 성공 처리"""
        self._consecutive_failures = 0
        # 사용량 회복 시 권장 레이트 상향
        if self._remaining > self._limit * 0.5:
            self._suggested_rate = min(
                self._suggested_rate * 1.1, 
                self._limit * 0.9
            )

    def on_rate_limit_hit(self) -> None:
        """Rate Limit 도달 처리"""
        self._consecutive_failures += 1
        # 연속 실패 시 권장 레이트 하향
        if self._consecutive_failures >= 2:
            self._suggested_rate = self._suggested_rate * 0.5
            logger.warning(
                f"Rate Limit 연속 실패 감지 - "
                f"권장 레이트 {self._suggested_rate:.0f}/window로 감소"
            )

    def get_adaptive_delay(self) -> float:
        """적응형 대기 시간 반환"""
        # 기본 대기 시간: 윈도우 / 권장 레이트
        base_delay = self._window / max(self._suggested_rate, 1)
        # 연속 실패 시 추가 대기
        failure_penalty = self._consecutive_failures * 0.5
        return base_delay + failure_penalty

완전한 거래 봇 통합 예제

지금까지 구현한 모든 컴포넌트를 하나의 완전한 거래 봇으로 통합합니다. HolySheep AI 게이트웨이 스타일로 추상화된 구조입니다.

import asyncio
from enum import Enum
from typing import Dict, Any, List
from dataclasses import dataclass
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


class OrderSide(Enum):
    BUY = "BUY"
    SELL = "SELL"


@dataclass
class OrderRequest:
    symbol: str
    side: OrderSide
    quantity: float
    price: float = 0  # 0이면 시장가 주문


class CryptoTradingBot:
    """
    암호화폐 거래 봇 - 완전한 재연결 전략 통합
    
    HolySheep AI 게이트웨이 기반 아키텍처:
    1. REST API 재연결 (주문, 잔고查询)
    2. 웹소켓 재연결 (시세, 체결 실시간 수신)
    3. Rate Limit 적응형 처리
    4. 상태 관리 및 자동 복구
    """
    
    def __init__(self, api_key: str, api_secret: str, exchange: str = "binance"):
        self.exchange = exchange
        self.api_key = api_key
        self.api_secret = api_secret
        
        # 컴포넌트 초기화
        self._init_components()
        
        # 상태
        self._is_running = False
        self._last_order_result: Optional[Dict] = None
        
    def _init_components(self):
        """필요 컴포넌트 초기화"""
        # REST API 재연결 관리자
        self.rest_manager = BinanceReconnectionManager(
            self.api_key, 
            self.api_secret,
            testnet=True
        )
        
        # Rate Limit 핸들러
        self.rate_limiter = IntelligentRateLimitHandler(self.exchange)
        
        # 웹소켓 관리자
        self.ws_manager = BinanceWebSocketWithReconnect(
            self.api_key,
            self.api_secret
        )
        
        # 구독 채널 목록
        self._subscribed_symbols: List[str] = []

    async def start(self) -> None:
        """거래 봇 시작"""
        logger.info("=" * 50)
        logger.info("암호화폐 거래 봇 시작")
        logger.info("=" * 50)
        
        self._is_running = True
        
        # 1단계: REST API 연결
        try:
            await self.rest_manager.connect_rest_api()
            logger.info("✓ REST API 연결 완료")
        except Exception as e:
            logger.error(f"✗ REST API 연결 실패: {e}")
            raise
        
        # 2단계: 잔고 조회
        await self._print_balance()
        
        # 3단계: 웹소켓 연결
        await self._start_websocket()
        
        logger.info("✓ 거래 봇 초기화 완료")
        
        # 메인 루프 (60초간 실행)
        try:
            await self._trading_loop(duration=60)
        finally:
            await self.stop()

    async def _start_websocket(self) -> None:
        """웹소켓 연결 및 시세 구독"""
        
        def on_price_update(msg: Dict[str, Any]):
            if 'e' in msg and msg['e'] == 'trade':
                symbol = msg['s']
                price = float(msg['p'])
                logger.info(f"[시세] {symbol}: ${price:,.2f}")
        
        # 시세 핸들러 등록
        self.ws_manager.add_message_handler('trade', on_price_update)
        
        # 웹소켓 연결
        ws_url = "wss://testnet.binance.vision/ws"
        try:
            await self.ws_manager.connect(ws_url)
            logger.info("✓ 웹소켓 연결 완료")
            
            # 주요 통화 구독
            for symbol in ['btcusdt', 'ethusdt']:
                await self.ws_manager.subscribe('trade', symbol)
                self._subscribed_symbols.append(symbol)
            
            logger.info(f"✓ 구독 완료: {self._subscribed_symbols}")
            
        except Exception as e:
            logger.warning(f"웹소켓 연결 실패 (백그라운드 재연결 계속): {e}")

    async def _trading_loop(self, duration: int = 60) -> None:
        """거래 메인 루프"""
        interval = 10  # 10초마다 거래 체크
        iterations = duration // interval
        
        for i in range(iterations):
            if not self._is_running:
                break
                
            logger.info(f"\n[루프 {i+1}/{iterations}] 거래 체크...")
            
            # Rate Limit 체크
            await self.rate_limiter.wait_if_needed()
            
            # 주문 가능 여부 확인
            if self.rate_limiter.can_make_request():
                await self._simulate_trade()
                self.rate_limiter.on_request_success()
            else:
                wait_time = self.rate_limiter.get_wait_time()
                logger.info(f"Rate Limit 대기 중... ({wait_time:.1f}초)")
                await asyncio.sleep(wait_time)
            
            # 통계 출력
            stats = self.ws_manager.get_stats()
            logger.info(f"[통계] 수신 메시지: {stats['messages_received']}, "
                       f"재연결 횟수: {stats['total_reconnections']}")
            
            await asyncio.sleep(interval)

    async def _simulate_trade(self) -> None:
        """시뮬레이션 거래 실행"""
        try:
            # 현재가 조회
            price = await self.rest_manager.get_current_price("BTCUSDT")
            logger.info(f"BTC/USDT 현재가: ${price:,.2f}")
            
            # 주문 요청
            order = OrderRequest(
                symbol="BTCUSDT",
                side=OrderSide.BUY,
                quantity=0.001,  # 0.001 BTC
                price=price
            )
            
            logger.info(f"주문 시도: {order.side.value} {order.quantity} {order.symbol