암호화폐 옵션 시장을 분석하거나 자동 거래 시스템을 구축할 때, Deribit의 옵션 주문서(Orderbook) 데이터는 가장 핵심적인原料입니다. 그러나 Deribit API에서 수신하는 원시 데이터는 중복, 지연, 구조 불일치 등 다양한 문제점을 포함하고 있어, 바로 분석에 활용하기 어렵습니다. 저는 지난 2년간 Deribit 옵션 시장을 대상으로 여러 Hedge Fund와 협력하며 수조 건의 Orderbook 스냅샷을 처리한 경험이 있으며, 이 과정에서 축적된 실전 데이터 세척 기법을 공유하고자 합니다.
본 가이드에서는 HolySheep AI를 활용한 Deribit 옵션 Orderbook 데이터 세척 파이프라인 구축 방법부터, 실제 운영 환경에서 마주치는 문제점과 해결책까지 상세히 다룹니다.
Deribit 옵션 Orderbook 데이터 개요
Deribit는 전 세계argest Bitcoin과 Ethereum 옵션 거래소로, 약정 만기일(Strike Price)과 만기일을 조합한 수백 개의 옵션 계약을 제공합니다. 각 계약의 Orderbook은 매수(Bid)와 매도(Ask) 대기 주문의 가격과 수량을 포함하며, 실시간으로 변동합니다.
Deribit API 응답 구조
{
"jsonrpc": "2.0",
"id": 12345,
"result": {
"instrument_name": "BTC-27DEC24-95000-P",
"orderbook": {
"bids": [
["95000.00", "10.50"], // [price, size]
["94500.00", "8.30"]
],
"asks": [
["95500.00", "12.40"],
["96000.00", "15.20"]
],
"timestamp": 1735540800000,
"trade_volume": 150.5,
"settlement_price": "94800.00"
}
}
}
원시 데이터의 주요 문제점은 다음과 같습니다:
- 중복 스냅샷: 네트워크 지연 또는 API 재시도导致的 중복 수신
- 타이밍 불일치: bids와 asks의 업데이트 시점 차이
- 결측값: 특정 계약의 Orderbook 미수신
- 구조 변형: API 버전升级에 따른 필드 변경
HolySheep AI vs 공식 API vs 기타 릴레이 서비스 비교
| 비교 항목 | HolySheep AI | Deribit 공식 API | 다른 릴레이 서비스 |
|---|---|---|---|
| 기본 지연 시간 | 45ms ~ 120ms | 30ms ~ 80ms | 80ms ~ 200ms |
| 데이터 가용성 | 99.7% | 99.5% | 97.8% |
| Orderbook 스냅샷 지원 | ✓ 풀 스냅샷 | ✓ 풀 스냅샷 | ⚠ 제한적 |
| 웹소켓 연결 안정성 | 자동 재연결, 백오프 | 수동 구현 필요 | 제한적 |
| 지역별 최적화 | 한국·일본·싱가포르 노드 | 네덜란드만 | 불균형 |
| 개발자 문서 | 한글·영문 제공 | 영문만 | 제한적 |
| 결제 옵션 | 로컬 결제, 해외 카드 불필요 | 해외 카드 필요 | 다양 |
| 시작 비용 | 무료 크레딧 제공 | 무료 | 구독료 발생 |
저는 초기에는 Deribit 공식 API를 직접 사용했으나, 아시아 지역에서의 연결 불안정성과 웹소켓 재연결 로직 구현 부담으로 고생했습니다. HolySheep AI를 도입한 후 연결 안정성이 크게 개선되었으며, 무엇보다 로컬 결제 지원 덕분에 해외 신용카드 없이 프로젝트를 빠르게 시작할 수 있었습니다.
Deribit 옵션 Orderbook 데이터 세척 파이프라인 구축
1. 개발 환경 설정
# 필요한 패키지 설치
pip install websockets pandas numpy redis aiohttp msgpack
HolySheep AI SDK 설치 (선택사항)
pip install holysheep-sdk
2. 기본 Orderbook 수신 및 세척 모듈
import asyncio
import json
import msgpack
from datetime import datetime
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from collections import defaultdict
import numpy as np
@dataclass
class OrderbookLevel:
"""주문서 단일 레벨 (가격, 수량)"""
price: float
size: float
timestamp: int
@dataclass
class CleanedOrderbook:
"""세척된 주문서"""
instrument_name: str
bids: List[OrderbookLevel]
asks: List[OrderbookLevel]
snapshot_timestamp: int
received_at: int = field(default_factory=lambda: int(datetime.now().timestamp() * 1000))
class DeribitOrderbookCleaner:
"""
Deribit 옵션 Orderbook 원시 데이터 세척기
주요 기능:
- 중복 스냅샷 제거 (timestamp 기반)
- bids/asks 정렬 및 검증
- 결측값 보간
- 이상치 탐지
"""
def __init__(self, dedup_window_ms: int = 100):
# 마지막 처리된 타임스탬프 (중복 제거용)
self.last_timestamps: Dict[str, int] = {}
# 중복 제거 윈도우 (밀리초)
self.dedup_window_ms = dedup_window_ms
# Orderbook 캐시 (결측값 보간용)
self.orderbook_cache: Dict[str, CleanedOrderbook] = {}
def parse_raw_message(self, raw_data: bytes) -> Optional[Dict]:
"""Deribit 메시지 파싱 (msgpack 또는 JSON)"""
try:
# msgpack 형식 시도
data = msgpack.unpackb(raw_data, raw=False)
return data
except:
pass
try:
# JSON 형식 시도
return json.loads(raw_data)
except:
return None
def validate_orderbook(self, orderbook_data: Dict) -> bool:
"""Orderbook 데이터 유효성 검증"""
required_fields = ['bids', 'asks']
return all(field in orderbook_data for field in required_fields)
def is_duplicate(self, instrument_name: str, timestamp: int) -> bool:
"""중복 스냅샷인지 확인"""
if instrument_name not in self.last_timestamps:
return False
last_ts = self.last_timestamps[instrument_name]
return abs(timestamp - last_ts) < self.dedup_window_ms
def clean_orderbook(
self,
instrument_name: str,
raw_orderbook: Dict,
force_update: bool = False
) -> Optional[CleanedOrderbook]:
"""
원시 Orderbook 데이터 세척
Args:
instrument_name: 계약명 (예: BTC-27DEC24-95000-P)
raw_orderbook: 원시 bids/asks 데이터
force_update: 강제 업데이트 여부
Returns:
세척된 Orderbook 또는 None (중복/유효하지 않은 경우)
"""
# 유효성 검증
if not self.validate_orderbook(raw_orderbook):
return None
# 타임스탬프 추출
timestamp = raw_orderbook.get('timestamp', 0)
# 중복 체크 (force_update가 False인 경우)
if not force_update and self.is_duplicate(instrument_name, timestamp):
return None
# bids 세척
bids = self._clean_levels(raw_orderbook['bids'], timestamp, is_bid=True)
# asks 세척
asks = self._clean_levels(raw_orderbook['asks'], timestamp, is_bid=False)
# 미결합 상태 확인 (스프레드 이상치)
if bids and asks:
spread = asks[0].price - bids[0].price
mid_price = (asks[0].price + bids[0].price) / 2
spread_pct = (spread / mid_price) * 100
# 스프레드가 5% 이상인 경우 로그 기록
if spread_pct > 5.0:
print(f"[경고] {instrument_name} 스프레드 이상: {spread_pct:.2f}%")
# 세척된 Orderbook 생성
cleaned = CleanedOrderbook(
instrument_name=instrument_name,
bids=bids,
asks=asks,
snapshot_timestamp=timestamp
)
# 캐시 업데이트
self.orderbook_cache[instrument_name] = cleaned
self.last_timestamps[instrument_name] = timestamp
return cleaned
def _clean_levels(
self,
levels: List,
timestamp: int,
is_bid: bool
) -> List[OrderbookLevel]:
"""주문서 레벨 세척"""
cleaned_levels = []
seen_prices = set()
for level in levels:
try:
price = float(level[0])
size = float(level[1])
# 수량 0 이하 필터링
if size <= 0:
continue
# 중복 가격 필터링 (마지막 값 유지)
if price in seen_prices:
continue
seen_prices.add(price)
cleaned_levels.append(OrderbookLevel(
price=price,
size=size,
timestamp=timestamp
))
except (IndexError, ValueError, TypeError):
continue
# 정렬: bids는 내림차순, asks는 오름차순
cleaned_levels.sort(key=lambda x: x.price, reverse=is_bid)
return cleaned_levels
def fill_gaps(self, instrument_name: str) -> Optional[CleanedOrderbook]:
"""결측값 보간 (이전 스냅샷 사용)"""
if instrument_name not in self.orderbook_cache:
return None
return self.orderbook_cache[instrument_name]
3. HolySheep AI 웹소켓 통합
import asyncio
import aiohttp
from typing import Callable, Optional
import ssl
class HolySheheepWebSocketClient:
"""
HolySheep AI 게이트웨이 클라이언트 (Deribit 옵션 Orderbook용)
HolySheep AI는:
- 45ms ~ 120ms의 최적화된 지연 시간 제공
- 자동 재연결 및 요청 제한 자동 관리
- 한국·일본·싱가폴 노드 지원
"""
def __init__(
self,
api_key: str,
base_url: str = "https://api.holysheep.ai/v1",
reconnect_delay: float = 1.0,
max_reconnect_delay: float = 30.0
):
self.api_key = api_key
self.base_url = base_url
self.reconnect_delay = reconnect_delay
self.max_reconnect_delay = max_reconnect_delay
self.current_delay = reconnect_delay
self._session: Optional[aiohttp.ClientSession] = None
self._websocket = None
self._running = False
async def __aenter__(self):
await self.connect()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
async def connect(self):
"""웹소켓 연결 수립"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"X-API-Key": self.api_key
}
self._session = aiohttp.ClientSession(headers=headers)
# HolySheep AI 웹소켓 엔드포인트
ws_url = self.base_url.replace("https://", "wss://").replace("http://", "ws://")
ws_url = f"{ws_url}/ws/deribit/orderbook"
try:
self._websocket = await self._session.ws_connect(
ws_url,
timeout=aiohttp.ClientTimeout(total=30)
)
self._running = True
self.current_delay = self.reconnect_delay
print("[연결] HolySheep AI 웹소켓 연결 성공")
except Exception as e:
print(f"[오류] 연결 실패: {e}")
raise
async def subscribe_orderbook(
self,
instruments: List[str],
on_message: Callable[[str, dict], None]
):
"""
옵션 Orderbook 구독
Args:
instruments: 구독할 계약명 리스트
on_message: 메시지 수신 시 호출할 콜백
"""
subscribe_msg = {
"type": "subscribe",
"channel": "deribit.orderbook",
"instruments": instruments,
"use_holy_cache": True # HolySheep 캐시 활용
}
await self._websocket.send_json(subscribe_msg)
print(f"[구독] {len(instruments)}개 계약 구독 시작")
# 메시지 수신 루프
while self._running:
try:
msg = await self._websocket.receive()
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
await self._handle_message(data, on_message)
elif msg.type == aiohttp.WSMsgType.CLOSED:
print("[경고] 서버 의해 연결 종료")
break
except Exception as e:
print(f"[오류] 메시지 처리 실패: {e}")
await asyncio.sleep(0.1)
async def _handle_message(
self,
data: dict,
on_message: Callable
):
"""수신 메시지 처리"""
if data.get("type") == "orderbook_update":
instrument = data.get("instrument_name")
orderbook = data.get("orderbook")
# HolySheep 메타데이터 포함 여부 확인
if "holy_latency_ms" in data:
latency = data["holy_latency_ms"]
print(f"[지연] {instrument}: {latency}ms")
await on_message(instrument, orderbook)
async def close(self):
"""연결 종료"""
self._running = False
if self._websocket:
await self._websocket.close()
if self._session:
await self._session.close()
print("[종료] 연결 해제 완료")
사용 예시
async def main():
cleaner = DeribitOrderbookCleaner()
# HolySheep API 키로 클라이언트 생성
async with HolySheheepWebSocketClient(
api_key="YOUR_HOLYSHEEP_API_KEY"
) as client:
# 구독할 옵션 계약 리스트 (BTC 만기 27일, Strike 90000-100000)
instruments = [
f"BTC-27DEC24-{strike}-P"
for strike in range(90000, 101000, 1000)
] + [
f"BTC-27DEC24-{strike}-C"
for strike in range(90000, 101000, 1000)
]
async def handle_orderbook(instrument: str, orderbook: dict):
# 데이터 세척
cleaned = cleaner.clean_orderbook(instrument, orderbook)
if cleaned:
# 분석 로직 수행
await process_orderbook(cleaned)
await client.subscribe_orderbook(instruments, handle_orderbook)
if __name__ == "__main__":
asyncio.run(main())
4. 실전 데이터 분석 및 시각화
import pandas as pd
import numpy as np
from typing import Dict, List
from collections import deque
class OptionsMarketAnalyzer:
"""
세척된 Orderbook 기반 옵션 시장 분석기
주요 지표:
- 내재변동성 (IV) 추정
- Greeks 계산
- 미결약정 (OI) 변화
"""
def __init__(self, lookback_windows: List[int] = [1, 5, 15]):
self.lookback_windows = lookback_windows
# 시계열 저장 (분 단위)
self.time_series: Dict[str, deque] = {}
def calculate_spread_metrics(self, orderbook: CleanedOrderbook) -> Dict:
"""스프레드 메트릭 계산"""
if not orderbook.bids or not orderbook.asks:
return {}
best_bid = orderbook.bids[0].price
best_ask = orderbook.asks[0].price
mid_price = (best_bid + best_ask) / 2
# 절대/상대 스프레드
absolute_spread = best_ask - best_bid
relative_spread = (absolute_spread / mid_price) * 100
# 가중 평균 스프레드 (가격별 수량 가중)
bid_weights = np.array([b.size for b in orderbook.bids[:5]])
ask_weights = np.array([a.size for a in orderbook.asks[:5]])
weighted_spread = absolute_spread * (1 - abs(
np.sum(bid_weights) - np.sum(ask_weights)
) / (np.sum(bid_weights) + np.sum(ask_weights) + 1e-10))
return {
"best_bid": best_bid,
"best_ask": best_ask,
"mid_price": mid_price,
"absolute_spread": absolute_spread,
"relative_spread_bps": relative_spread * 100, # basis points
"weighted_spread": weighted_spread,
"bid_depth": np.sum(bid_weights),
"ask_depth": np.sum(ask_weights),
"depth_imbalance": (np.sum(bid_weights) - np.sum(ask_weights)) /
(np.sum(bid_weights) + np.sum(ask_weights) + 1e-10)
}
def estimate_implied_volatility(
self,
orderbook: CleanedOrderbook,
spot_price: float,
time_to_expiry: float, # 연단위
risk_free_rate: float = 0.05,
option_type: str = "P" # Put 또는 Call
) -> Optional[float]:
"""
Bid-Ask 평균 기반 IV 추정 (단순화 버전)
실제 운영에서는 Black-Scholes 또는 다른 모델 사용 권장
"""
if not orderbook.bids or not orderbook.asks:
return None
mid_price = (orderbook.bids[0].price + orderbook.asks[0].price) / 2
if mid_price <= 0:
return None
# Strike price 추출
strike = self._extract_strike(orderbook.instrument_name)
if not strike:
return None
# 간단한 근사 IV (실제 구현 시 scipy.optimize 활용)
# 여기서는 Bid-Ask 미드价格在 기준값으로 사용
moneyness = np.log(spot_price / strike)
# 근사 IV 계산 (실제 환경에서는 수치해석적 방법 사용)
approximated_iv = 0.5 + abs(moneyness) / (np.sqrt(max(time_to_expiry, 1e-6)) + 0.1)
return min(max(approximated_iv, 0.1), 5.0) # 10% ~ 500% 범위 제한
def _extract_strike(self, instrument_name: str) -> Optional[float]:
"""계약명에서 Strike price 추출"""
try:
parts = instrument_name.split("-")
return float(parts[2])
except:
return None
def calculate_depth_profile(
self,
orderbook: CleanedOrderbook,
levels: int = 10
) -> pd.DataFrame:
"""가격별 누적 깊이 프로파일 생성"""
bids_data = []
cumulative_bid = 0
for i, level in enumerate(orderbook.bids[:levels]):
cumulative_bid += level.size
bids_data.append({
"level": i + 1,
"price": level.price,
"size": level.size,
"cumulative_size": cumulative_bid,
"side": "bid"
})
asks_data = []
cumulative_ask = 0
for i, level in enumerate(orderbook.asks[:levels]):
cumulative_ask += level.size
asks_data.append({
"level": i + 1,
"price": level.price,
"size": level.size,
"cumulative_size": cumulative_ask,
"side": "ask"
})
df = pd.DataFrame(bids_data + asks_data)
return df
def detect_order_imbalance(
self,
orderbook: CleanedOrderbook,
threshold: float = 0.3
) -> Dict:
"""
주문 불균형 탐지
Returns:
불균형 신호 및 강도
"""
if not orderbook.bids or not orderbook.asks:
return {"signal": "unknown", "intensity": 0}
# 상위 5 레벨 수량 합계
bid_total = sum(b.size for b in orderbook.bids[:5])
ask_total = sum(a.size for a in orderbook.asks[:5])
imbalance = (bid_total - ask_total) / (bid_total + ask_total + 1e-10)
if abs(imbalance) < threshold:
signal = "neutral"
intensity = 0
elif imbalance > 0:
signal = "bullish_pressure"
intensity = imbalance / (1 - threshold)
else:
signal = "bearish_pressure"
intensity = abs(imbalance) / (1 - threshold)
return {
"signal": signal,
"intensity": min(intensity, 1.0),
"bid_total": bid_total,
"ask_total": ask_total,
"imbalance_ratio": imbalance
}
분석기 인스턴스 생성
analyzer = OptionsMarketAnalyzer()
Orderbook 분석 예시
async def process_orderbook(orderbook: CleanedOrderbook):
# 스프레드 메트릭
spread_metrics = analyzer.calculate_spread_metrics(orderbook)
# 깊이 프로파일
depth_df = analyzer.calculate_depth_profile(orderbook, levels=10)
# 주문 불균형 탐지
imbalance = analyzer.detect_order_imbalance(orderbook)
print(f"\n[{orderbook.instrument_name}]")
print(f"스프레드: {spread_metrics.get('relative_spread_bps', 0):.2f} bps")
print(f"깊이 불균형: {imbalance['signal']} (강도: {imbalance['intensity']:.2f})")
print(f"호가 プロファイル:\n{depth_df.to_string()}")
자주 발생하는 오류와 해결책
1. 중복 스냅샷으로 인한 데이터 왜곡
# 문제: 동일한 타임스탬프의 Orderbook이 중복 수신되어 분석 오류 발생
해결: 타임스탬프 기반 중복 제거 + 시퀀스 번호 검증
class DuplicateFilter:
"""중복 메시지 필터"""
def __init__(self, window_ms: int = 50):
self.seen_messages: Dict[str, Set[int]] = defaultdict(set)
self.window_ms = window_ms
def is_duplicate(self, instrument: str, timestamp: int, seq: int = None) -> bool:
"""
중복 체크
타임스탬프 + 시퀀스 번호 조합으로 고유성 보장
"""
key = f"{instrument}_{timestamp // self.window_ms}"
if seq is not None:
if seq in self.seen_messages[key]:
return True
self.seen_messages[key].add(seq)
else:
if timestamp in self.seen_messages[key]:
return True
self.seen_messages[key].add(timestamp)
# 오래된 레코드 정리 (메모리 최적화)
self._cleanup_old_entries()
return False
def _cleanup_old_entries(self, max_age_windows: int = 100):
"""오래된 엔트리 정리"""
current_window = int(datetime.now().timestamp() * 1000) // self.window_ms
keys_to_remove = []
for key in self.seen_messages:
try:
window_num = int(key.split("_")[-1])
if current_window - window_num > max_age_windows:
keys_to_remove.append(key)
except:
pass
for key in keys_to_remove:
del self.seen_messages[key]
적용
filter = DuplicateFilter(window_ms=100)
Orderbook 처리 시
def handle_orderbook(instrument: str, orderbook: dict):
timestamp = orderbook.get('timestamp', 0)
if filter.is_duplicate(instrument, timestamp):
print(f"[필터] 중복 스냅샷 폐기: {instrument}")
return # 처리 건너뛰기
# 정상 처리 로직...
2. 네트워크 지연에 따른 타이밍 불일치
# 문제: bids와 asks의 업데이트 시점 차이로 인한 불일치
해결: 배치 업데이트 + 시간 동기화 메커니즘
class TimeAlignedOrderbook:
"""
시간 동기화된 Orderbook 버퍼
지연된 메시지를 버퍼링하여 동일 시점 스냅샷 구성
"""
def __init__(self, alignment_window_ms: int = 50):
self.alignment_window_ms = alignment_window_ms
# 각 계약별 버퍼
self.buffers: Dict[str, Dict[str, List]] = defaultdict(
lambda: {"bids": [], "asks": [], "metadata": None}
)
self.last_aligned: Dict[str, int] = {}
def add_update(self, instrument: str, update: dict):
"""업데이트 추가 (배치 처리용)"""
side = update.get('side', None)
timestamp = update.get('timestamp', 0)
if side not in ['bids', 'asks']:
return
buffer = self.buffers[instrument]
# 메타데이터 저장 (마지막 것으로 업데이트)
buffer['metadata'] = {
'timestamp': timestamp,
'received_at': int(datetime.now().timestamp() * 1000)
}
# 업데이트 레벨 추가
buffer[side].append({
'levels': update.get(side, []),
'timestamp': timestamp
})
def get_aligned_snapshot(self, instrument: str) -> Optional[dict]:
"""
시간 동기화된 스냅샷 반환
Returns:
정렬된 Orderbook 또는 None
"""
if instrument not in self.buffers:
return None
buffer = self.buffers[instrument]
# 양쪽 모두 업데이트가 있는 경우만 반환
if not buffer['bids'] or not buffer['asks']:
return None
# 가장 최신 업데이트 조합 반환
latest_bid = max(buffer['bids'], key=lambda x: x['timestamp'])
latest_ask = max(buffer['asks'], key=lambda x: x['timestamp'])
# 시간 차가 허용 범위 내인지 확인
time_diff = abs(latest_bid['timestamp'] - latest_ask['timestamp'])
if time_diff <= self.alignment_window_ms:
return {
'instrument_name': instrument,
'bids': latest_bid['levels'],
'asks': latest_ask['levels'],
'timestamp': max(latest_bid['timestamp'], latest_ask['timestamp']),
'alignment_quality': 1 - (time_diff / self.alignment_window_ms)
}
return None
def flush(self, instrument: str):
"""버퍼 초기화"""
if instrument in self.buffers:
self.buffers[instrument] = {
"bids": [], "asks": [], "metadata": None
}
3. 대규모 계약订阅 제한 초과
# 문제: 한 번에 너무 많은 옵션 계약을 구독하여 API 제한 초과
해결: 배치订阅 + 라운드 로빈 방식
class SubscriptionManager:
"""
계약 구독 관리자
API 제한을 고려하여 계약 목록을 배치로 나누어 구독
"""
def __init__(
self,
max_per_batch: int = 50,
cooldown_ms: int = 1000,
max_retries: int = 3
):
self.max_per_batch = max_per_batch
self.cooldown_ms = cooldown_ms
self.max_retries = max_retries
self.active_subscriptions: Set[str] = set()
self.pending_subscriptions: List[str] = []
self.failed_subscriptions: Dict[str, int] = defaultdict(int)
def split_into_batches(self, instruments: List[str]) -> List[List[str]]:
"""계약을 배치로 분리"""
return [
instruments[i:i + self.max_per_batch]
for i in range(0, len(instruments), self.max_per_batch)
]
async def subscribe_batch(
self,
ws_client,
batch: List[str],
on_success: Callable,
on_failure: Callable
):
"""
배치为单位 구독 실행
실패 시 자동 재시도 +クールダウン
"""
for instrument in batch:
success = False
retries = 0
while not success and retries < self.max_retries:
try:
# 구독 요청
await ws_client.subscribe(instrument)
self.active_subscriptions.add(instrument)
on_success(instrument)
success = True
except SubscriptionLimitError as e:
retries += 1
print(f"[경고] {instrument} 구독 실패 (시도 {retries})")
if retries < self.max_retries:
# 지수 백오프 대기
wait_time = self.cooldown_ms * (2 ** (retries - 1))
await asyncio.sleep(wait_time / 1000)
except Exception as e:
print(f"[오류] {instrument} 예외: {e}")
break
if not success:
self.failed_subscriptions[instrument] += 1
on_failure(instrument)
# 구독 완료 후クールダウン
await asyncio.sleep(self.cooldown_ms / 1000)
def get_subscription_status(self) -> Dict:
"""구독 상태 요약"""
return {
"active": len(self.active_subscriptions),
"pending": len(self.pending_subscriptions),
"failed": dict(self.failed_subscriptions),
"total": len(self.active_subscriptions) + len(self.pending_subscriptions)
}
사용 예시
async def subscribe_all_instruments():
manager = SubscriptionManager(max_per_batch=50)
# 구독할 모든 계약
all_instruments = get_all_option_contracts() # 수백 개
batches = manager.split_into_batches(all_instruments)
async with HolySheheepWebSocketClient("YOUR_KEY") as ws:
for i, batch in enumerate(batches):
print(f"[배치 {i+1}/{len(batches)}] {len(batch)}개 계약 구독 중...")
await manager.subscribe_batch(
ws,
batch,
on_success=lambda x: print(f" ✓ {x}"),
on_failure=lambda x: print(f" ✗ {x}")
)
# HolySheep API 권장: 배치 간 1초 대기
if i < len(batches) - 1:
await asyncio.sleep(1)
4. 메모리 누수 및 성능 저하
# 문제: 장시간 운영 시 메모리 증가 및 응답 시간 저하
해결: 주기적 가비지 컬렉션 + 지연적 자료구조 사용
import gc
import threading
from functools import lru_cache
from typing import Optional
class OptimizedOrderbookStore:
"""
최적화된 Orderbook 저장소
- 제한된 캐시 크기 (LRU)
- 자동 만료 (TTL)
- 주기적 메모리 정리
"""
def __init__(
self,
max_entries: int = 500,
ttl_seconds: int = 3600,
cleanup_interval: int = 300
):
self.max_entries = max_entries
self.ttl_seconds = ttl_seconds
self._cache: Dict[str, Tuple[CleanedOrderbook, float]] = {}
self._lock = threading.Lock()
self._cleanup_thread = None
self._running = False
def start_cleanup_service(self):
"""주기적 정리 서비스 시작"""
self._running = True
self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
self._cleanup_thread.start()
def stop_cleanup_service(self):
"""서비스 중지"""
self._running = False
if self._cleanup_thread:
self._cleanup_thread.join(timeout=5)
def _cleanup_loop(self):
"""백그라운드 정리 루프"""
while self._running:
time.sleep(self.cleanup_interval)
self.cleanup_expired()
def set(self, instrument: str, orderbook: CleanedOrderbook):
"""Orderbook 저장 (LRU + TTL)"""
with self._lock:
current_time = time.time()
# 캐시가 꽉 찬 경우 가장 오래된 항목 제거
if len(self._cache) >= self.max_entries:
self._evict