트레이딩 봇을 개발할 때 가장 큰 도전 중 하나는 과거 데이터를 실시간 스트림처럼 시뮬레이션하는 것입니다. Tardis Machine은 마치 타임머신처럼 과거의 시장 데이터를 미래의 거래 봇에게 "실시간"으로 흘려보내는 기술입니다. 이 튜토리얼에서는 Python 기반 로컬 WebSocket 서버를 구축하여 캔들스틱, 오더북, 체결 데이터를 과거에서 재생하는 방법을 단계별로 설명합니다.

비교표: HolySheep vs 공식 API vs 다른 릴레이 서비스

특징 HolySheep AI 공식 Binance API 기타 릴레이 서비스
로컬 결제 지원 ✅ 해외 신용카드 불필요 ❌ 해외 신용카드 필수 ⚠️ 제한적
다중 모델 통합 ✅ GPT-4.1, Claude, Gemini, DeepSeek 단일 키 ❌ 각 서비스별 별도 키 ⚠️ 2-3개 모델만 지원
WebSocket 스트리밍 ✅ 네이티브 지원 ✅ 실시간 거래 데이터 ⚠️ 지연 발생
트레이딩 봇 백테스트 지원 ✅ AI 분석 + 시뮬레이션 ❌ 별도 시뮬레이터 필요 ⚠️ 기본 히스토리만
가격 (GPT-4.1) $8/MTok $15/MTok $10-12/MTok
무료 크레딧 ✅ 가입 시 제공 ❌ 없음 ⚠️ 제한적
API Gateway 통합 ✅ 단일 엔드포인트 ❌ 다중 서비스 관리 ⚠️ 수동 전환

Tardis Machine이란?

Tardis Machine은 과거의 시장 데이터를 시간 여행시켜 마치 실시간으로 흘러오는 것처럼 시뮬레이션하는 시스템입니다. 핵심 개념은 다음과 같습니다:

아키텍처 개요

┌─────────────────────────────────────────────────────────────────┐
│                      Tardis Machine 아키텍처                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────┐      ┌──────────────┐      ┌──────────────┐  │
│  │  Historical  │      │   Local      │      │  Trading     │  │
│  │   Data       │─────▶│  WebSocket   │─────▶│    Bot       │  │
│  │   Store      │      │   Server     │      │  (기존 코드)  │  │
│  └──────────────┘      └──────────────┘      └──────────────┘  │
│        │                      │                      │         │
│        ▼                      ▼                      ▼         │
│  ┌──────────────┐      ┌──────────────┐      ┌──────────────┐  │
│  │  SQLite/     │      │  asyncio     │      │  Binance    │  │
│  │  Parquet     │      │  Playback    │      │  Compatible │  │
│  └──────────────┘      └──────────────┘      └──────────────┘  │
│                                                                 │
│  시간 흐름:  2024-01-15 09:30 ───────────▶ 2024-01-15 10:00     │
│             (과거 데이터)          (현재 시뮬레이션 시점)         │
└─────────────────────────────────────────────────────────────────┘

필수 설치 패키지

# 핵심 의존성 설치
pip install fastapi uvicorn websockets pandas numpy asyncio aiofiles

선택적 의존성 (성능 최적화)

pip install uvloop orjson # 고성능 이벤트 루프 및 JSON 처리

데이터 저장용

pip install sqlalchemy aiosqlite # 비동기 데이터베이스

1단계: 과거 데이터 캡처 및 저장

먼저 Binance websocket에서 실시간 데이터를 캡처하여 저장하는 스크립트를 작성합니다. HolySheep AI의 다중 모델 지원을 활용하면 캡처된 데이터를 AI로 분석하여 패턴을 식별할 수 있습니다.

# data_capture.py - 과거 데이터 캡처 스크립트
import asyncio
import json
import aiofiles
from datetime import datetime
from pathlib import Path
import websockets

class MarketDataCapture:
    def __init__(self, symbol: str = "btcusdt", output_dir: str = "./historical_data"):
        self.symbol = symbol.lower()
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.buffer = []
        self.capture_active = False
        
    async def connect_binance_websocket(self):
        """Binance WebSocket 스트림 연결"""
        # 캔들스틱 (1분), 오더북, 체결 데이터 스트림
        streams = [
            f"{self.symbol}@kline_1m",
            f"{self.symbol}@depth20@100ms",
            f"{self.symbol}@trade"
        ]
        uri = f"wss://stream.binance.com:9443/stream?streams={'/'.join(streams)}"
        return uri
    
    async def save_to_file(self, data_type: str, data: dict):
        """데이터를 날짜별 파일로 저장"""
        timestamp = datetime.fromisoformat(data.get('event_time', datetime.now().isoformat()))
        date_str = timestamp.strftime("%Y-%m-%d")
        filename = self.output_dir / f"{self.symbol}_{data_type}_{date_str}.jsonl"
        
        async with aiofiles.open(filename, mode='a') as f:
            await f.write(json.dumps(data) + '\n')
    
    async def process_message(self, message: dict):
        """WebSocket 메시지 처리 및 저장"""
        try:
            stream = message.get('stream', '')
            data = message.get('data', {})
            
            if 'kline' in stream:
                await self.save_to_file('kline', {
                    'event_time': data['k']['t'],
                    'symbol': data['s'],
                    'open': float(data['k']['o']),
                    'high': float(data['k']['h']),
                    'low': float(data['k']['l']),
                    'close': float(data['k']['c']),
                    'volume': float(data['k']['v']),
                    'is_closed': data['k']['x']
                })
            elif 'depth' in stream:
                await self.save_to_file('orderbook', {
                    'event_time': data.get('E'),
                    'symbol': data['s'],
                    'bids': [[float(p), float(q)] for p, q in data.get('b', [])],
                    'asks': [[float(p), float(q)] for p, q in data.get('a', [])]
                })
            elif 'trade' in stream:
                await self.save_to_file('trade', {
                    'event_time': data['T'],
                    'symbol': data['s'],
                    'price': float(data['p']),
                    'quantity': float(data['q']),
                    'is_buyer_maker': data['m']
                })
        except Exception as e:
            print(f"처리 오류: {e}")
    
    async def start_capture(self, duration_minutes: int = 60):
        """지정된 시간 동안 데이터 캡처 시작"""
        uri = await self.connect_binance_websocket()
        print(f"📡 {uri}에 연결 중...")
        
        self.capture_active = True
        capture_start = datetime.now()
        
        async with websockets.connect(uri) as ws:
            print(f"✅ 캡처 시작: {self.symbol} ({duration_minutes}분간)")
            
            while self.capture_active:
                elapsed = (datetime.now() - capture_start).total_seconds() / 60
                if elapsed >= duration_minutes:
                    print(f"⏰ 지정된 시간({duration_minutes}분) 완료")
                    break
                    
                try:
                    message = await asyncio.wait_for(ws.recv(), timeout=30)
                    data = json.loads(message)
                    await self.process_message(data)
                    
                    if len(self.buffer) % 100 == 0 and self.buffer:
                        print(f"📊 캡처됨: {len(self.buffer)}건")
                        
                except asyncio.TimeoutError:
                    print("⚠️ 30초 타임아웃, 재연결 시도...")
                except Exception as e:
                    print(f"❌ 오류 발생: {e}")
                    await asyncio.sleep(5)
    
    def stop_capture(self):
        """캡처 중지"""
        self.capture_active = False

async def main():
    capture = MarketDataCapture(symbol="ethusdt", output_dir="./data")
    
    # 1시간 동안 데이터 캡처 (테스트용으로 5분으로 줄임)
    await capture.start_capture(duration_minutes=5)
    
    print("📁 저장된 파일 목록:")
    for f in capture.output_dir.glob("*.jsonl"):
        print(f"   - {f.name}")

if __name__ == "__main__":
    asyncio.run(main())

2단계: Tardis Machine WebSocket 서버 구현

이제 캡처한 과거 데이터를 실시간 스트림처럼 재생하는 WebSocket 서버를 구현합니다. HolySheep AI의 스트리밍 지원을 활용하면 AI 기반 시장 분석과 백테스트 결과를 실시간으로 받을 수 있습니다.

# tardis_server.py - Tardis Machine WebSocket 서버
import asyncio
import json
import heapq
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Set, Optional
from dataclasses import dataclass, field
from collections import defaultdict
import websockets
from websockets.server import WebSocketServerProtocol

@dataclass(order=True)
class TimedEvent:
    event_time: float  # Unix timestamp (sort key)
    event_type: str = field(compare=False)
    symbol: str = field(compare=False)
    data: dict = field(compare=False)

class TardisMachine:
    """시계 방향/time-warp market data replay engine"""
    
    def __init__(self, data_dir: str = "./historical_data", playback_speed: float = 1.0):
        self.data_dir = Path(data_dir)
        self.playback_speed = playback_speed  # 1.0 = realtime, 10.0 = 10x speed
        self.clients: Set[WebSocketServerProtocol] = set()
        self.subscriptions: Dict[str, Set[WebSocketServerProtocol]] = defaultdict(set)
        self.event_queue: list[TimedEvent] = []
        self.is_playing = False
        self.current_time = 0.0
        self.start_time = 0.0
        self.end_time = 0.0
        
    def load_historical_data(self, symbol: str = None, date: str = None):
        """저장된 과거 데이터 로드 및 인덱싱"""
        print(f"📂 데이터 로딩 중: {self.data_dir}")
        
        files = list(self.data_dir.glob("*.jsonl"))
        all_events = []
        
        for file_path in files:
            if symbol and symbol not in file_path.name:
                continue
            if date and date not in file_path.name:
                continue
                
            print(f"   로딩: {file_path.name}")
            
            with open(file_path, 'r') as f:
                for line in f:
                    try:
                        data = json.loads(line.strip())
                        event_time = data.get('event_time', 0) / 1000  # ms to seconds
                        
                        # 이벤트 타입 결정
                        if 'open' in data:  # kline
                            event_type = 'kline'
                        elif 'bids' in data:  # orderbook
                            event_type = 'depth'
                        elif 'price' in data:  # trade
                            event_type = 'trade'
                        else:
                            continue
                        
                        heapq.heappush(all_events, TimedEvent(
                            event_time=event_time,
                            event_type=event_type,
                            symbol=data.get('symbol', ''),
                            data=data
                        ))
                    except json.JSONDecodeError:
                        continue
        
        self.event_queue = all_events
        
        if self.event_queue:
            self.start_time = self.event_queue[0].event_time
            self.end_time = self.event_queue[-1].event_time
            print(f"✅ {len(self.event_queue):,}개 이벤트 로드됨")
            print(f"   시간 범위: {datetime.fromtimestamp(self.start_time)} ~ {datetime.fromtimestamp(self.end_time)}")
        else:
            print("⚠️ 로드된 데이터 없음")
    
    async def broadcast_to_subscribers(self, event: TimedEvent):
        """구독자에게 이벤트 브로드캐스트"""
        subscribers = self.subscriptions.get(event.event_type, set())
        subscribers |= self.subscriptions.get('all', set())
        
        if not subscribers:
            return
        
        # Binance 호환 형식으로 변환
        message = self._format_binance_message(event)
        
        # 모든 구독자에게 전송
        disconnected = set()
        for client in subscribers:
            try:
                await client.send(json.dumps(message))
            except websockets.exceptions.ConnectionClosed:
                disconnected.add(client)
        
        # 연결 끊긴 클라이언트 제거
        for client in disconnected:
            await self.remove_client(client)
    
    def _format_binance_message(self, event: TimedEvent) -> dict:
        """Binance WebSocket 형식으로 변환"""
        data = event.data
        
        if event.event_type == 'kline':
            return {
                "stream": f"{data['symbol'].lower()}@kline_1m",
                "data": {
                    "e": "kline",
                    "E": int(event.event_time * 1000),
                    "s": data['symbol'],
                    "k": {
                        "t": int(event.event_time * 1000),
                        "o": str(data['open']),
                        "h": str(data['high']),
                        "l": str(data['low']),
                        "c": str(data['close']),
                        "v": str(data['volume']),
                        "x": data.get('is_closed', False)
                    }
                }
            }
        elif event.event_type == 'depth':
            return {
                "stream": f"{data['symbol'].lower()}@depth20@100ms",
                "data": {
                    "e": "depthUpdate",
                    "E": int(event.event_time * 1000),
                    "s": data['symbol'],
                    "b": [[str(p), str(q)] for p, q in data.get('bids', [])],
                    "a": [[str(p), str(q)] for p, q in data.get('asks', [])]
                }
            }
        elif event.event_type == 'trade':
            return {
                "stream": f"{data['symbol'].lower()}@trade",
                "data": {
                    "e": "trade",
                    "E": int(event.event_time * 1000),
                    "s": data['symbol'],
                    "p": str(data['price']),
                    "q": str(data['quantity']),
                    "m": data.get('is_buyer_maker', False)
                }
            }
        return {}
    
    async def playback_loop(self):
        """이벤트 재생 루프"""
        if not self.event_queue:
            print("⚠️ 재생할 이벤트가 없음")
            return
        
        print(f"▶️ 재생 시작 (속도: {self.playback_speed}x)")
        self.is_playing = True
        self.current_time = self.start_time
        
        processed_count = 0
        last_print_time = asyncio.get_event_loop().time()
        
        while self.event_queue and self.is_playing:
            # 가장 빠른 시간의 이벤트 가져오기
            next_event = heapq.heappop(self.event_queue)
            
            # 재생 속도에 따른 대기 시간 계산
            event_timestamp = next_event.event_time
            if event_timestamp > self.current_time:
                real_wait = (event_timestamp - self.current_time) / self.playback_speed
                if real_wait > 0:
                    await asyncio.sleep(min(real_wait, 1.0))  # 최대 1초 대기
            
            self.current_time = event_timestamp
            await self.broadcast_to_subscribers(next_event)
            processed_count += 1
            
            # 1초마다 상태 출력
            now = asyncio.get_event_loop().time()
            if now - last_print_time >= 1.0:
                elapsed = datetime.fromtimestamp(self.current_time)
                print(f"   📊 {elapsed} | 처리: {processed_count:,} | 남은: {len(self.event_queue):,}")
                last_print_time = now
        
        print(f"✅ 재생 완료: {processed_count:,}개 이벤트")
        self.is_playing = False
    
    async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
        """WebSocket 클라이언트 핸들러"""
        client_id = id(websocket)
        print(f"🔗 클라이언트 연결: {client_id}")
        self.clients.add(websocket)
        
        try:
            async for message in websocket:
                try:
                    data = json.loads(message)
                    await self.handle_client_message(websocket, data)
                except json.JSONDecodeError:
                    await websocket.send(json.dumps({"error": "Invalid JSON"}))
        except websockets.exceptions.ConnectionClosed:
            print(f"🔌 클라이언트断开: {client_id}")
        finally:
            await self.remove_client(websocket)
    
    async def handle_client_message(self, websocket: WebSocketServerProtocol, data: dict):
        """클라이언트 메시지 처리"""
        msg_type = data.get('type', '')
        
        if msg_type == 'subscribe':
            streams = data.get('streams', [])
            for stream in streams:
                self.subscriptions[stream].add(websocket)
                print(f"📥 구독: {stream} (총 {len(self.subscriptions[stream])}명)")
            await websocket.send(json.dumps({
                "type": "subscribed",
                "streams": streams
            }))
        
        elif msg_type == 'unsubscribe':
            streams = data.get('streams', [])
            for stream in streams:
                self.subscriptions[stream].discard(websocket)
            await websocket.send(json.dumps({
                "type": "unsubscribed",
                "streams": streams
            }))
        
        elif msg_type == 'control':
            action = data.get('action', '')
            if action == 'play':
                if not self.is_playing:
                    asyncio.create_task(self.playback_loop())
            elif action == 'pause':
                self.is_playing = False
            elif action == 'speed':
                self.playback_speed = data.get('speed', 1.0)
                print(f"⚡ 재생 속도: {self.playback_speed}x")
    
    async def remove_client(self, websocket: WebSocketServerProtocol):
        """클라이언트 제거"""
        self.clients.discard(websocket)
        for stream in self.subscriptions:
            self.subscriptions[stream].discard(websocket)
    
    async def start_server(self, host: str = "localhost", port: int = 8765):
        """서버 시작"""
        print(f"🚀 Tardis Machine 서버 시작: ws://{host}:{port}")
        self.load_historical_data(symbol="ethusdt")  # 기본값: ETH/USDT
        
        async with websockets.serve(self.handle_client, host, port):
            print("✅ WebSocket 서버 실행 중...")
            print("   클라이언트가 연결되기를 기다리는 중...")
            
            # 무한 대기 (Ctrl+C로 종료)
            await asyncio.Future()

async def main():
    tardis = TardisMachine(
        data_dir="./data",
        playback_speed=10.0  # 10배속 재생
    )
    await tardis.start_server(host="0.0.0.0", port=8765)

if __name__ == "__main__":
    asyncio.run(main())

3단계: 거래 봇 연동

기존 Binance WebSocket 클라이언트 코드를 그대로 사용하여 Tardis Machine에 연결할 수 있습니다. HolySheep AI의 다중 모델 통합 기능을 활용하면 AI 기반 거래 시그널 생성도 가능합니다.

# trading_bot.py - Tardis Machine에 연결하는 거래 봇
import asyncio
import json
import websockets
from datetime import datetime
from typing import Dict, List, Optional
import numpy as np

class SimpleTradingBot:
    """단순 이동평균 교차 거래 봇"""
    
    def __init__(self, symbol: str = "ethusdt", short_window: int = 5, long_window: int = 20):
        self.symbol = symbol
        self.short_window = short_window
        self.long_window = long_window
        self.prices: List[float] = []
        self.position = 0  # 0: 없음, 1: 매수, -1: 매도
        self.trades: List[dict] = []
        
        # HolySheep AI API 설정
        self.holysheep_api_key = "YOUR_HOLYSHEEP_API_KEY"
        self.holysheep_base_url = "https://api.holysheep.ai/v1"
    
    async def analyze_with_ai(self, price_data: dict) -> Optional[str]:
        """HolySheep AI를 활용한 시장 분석"""
        try:
            import openai
            client = openai.AsyncOpenAI(
                api_key=self.holysheep_api_key,
                base_url=self.holysheep_base_url
            )
            
            response = await client.chat.completions.create(
                model="gpt-4.1",
                messages=[
                    {"role": "system", "content": "당신은 전문 트레이딩 분석가입니다. 현재 가격 데이터 기반 매매 신호를 'BUY', 'SELL', 'HOLD'로만 응답하세요."},
                    {"role": "user", "content": f"현재 가격: {price_data.get('close', 0)}, 이전 종가: {price_data.get('prev_close', 0)}, 거래량: {price_data.get('volume', 0)}"}
                ],
                max_tokens=10,
                temperature=0.1
            )
            return response.choices[0].message.content.strip()
        except Exception as e:
            print(f"AI 분석 오류: {e}")
            return None
    
    def calculate_sma(self, prices: List[float], window: int) -> Optional[float]:
        """단순 이동평균 계산"""
        if len(prices) < window:
            return None
        return np.mean(prices[-window:])
    
    def generate_signal(self) -> str:
        """매매 신호 생성 (이동평균 교차)"""
        if len(self.prices) < self.long_window:
            return "HOLD"
        
        short_ma = self.calculate_sma(self.prices, self.short_window)
        long_ma = self.calculate_sma(self.prices, self.long_window)
        
        if short_ma is None or long_ma is None:
            return "HOLD"
        
        # 골든크로스 / 데드크로스 감지
        prev_short = self.prices[-2] if len(self.prices) > 1 else short_ma
        prev_long = self.prices[-len(self.prices)+1] if len(self.prices) > 1 else long_ma
        
        if prev_short <= prev_long and short_ma > long_ma:
            return "BUY"
        elif prev_short >= prev_long and short_ma < long_ma:
            return "SELL"
        
        return "HOLD"
    
    def execute_trade(self, signal: str, price: float):
        """거래 실행 시뮬레이션"""
        if signal == "BUY" and self.position <= 0:
            self.position = 1
            self.trades.append({
                "time": datetime.now().isoformat(),
                "type": "BUY",
                "price": price,
                "position": self.position
            })
            print(f"🟢 매수 실행: ${price:.2f}")
            
        elif signal == "SELL" and self.position >= 0:
            self.position = -1
            self.trades.append({
                "time": datetime.now().isoformat(),
                "type": "SELL",
                "price": price,
                "position": self.position
            })
            print(f"🔴 매도 실행: ${price:.2f}")
    
    async def handle_kline(self, data: dict):
        """캔들스틱 데이터 처리"""
        kline = data.get('k', {})
        close_price = float(kline.get('c', 0))
        volume = float(kline.get('v', 0))
        
        if close_price > 0:
            self.prices.append(close_price)
            
            # 이동평균 계산을 위해 최근 50개만 유지
            if len(self.prices) > 50:
                self.prices.pop(0)
            
            # 매매 신호 생성
            signal = self.generate_signal()
            
            if signal != "HOLD":
                self.execute_trade(signal, close_price)
            
            # HolySheep AI 분석 (10개 캔들마다)
            if len(self.prices) % 10 == 0:
                ai_signal = await self.analyze_with_ai({
                    "close": close_price,
                    "prev_close": self.prices[-2] if len(self.prices) > 1 else close_price,
                    "volume": volume
                })
                if ai_signal:
                    print(f"🤖 AI 신호: {ai_signal}")
    
    async def handle_depth(self, data: dict):
        """오더북 데이터 처리"""
        bids = data.get('b', [])
        asks = data.get('a', [])
        
        if bids and asks:
            best_bid = float(bids[0][0])
            best_ask = float(asks[0][0])
            spread = (best_ask - best_bid) / best_bid * 100
            
            if len(self.prices) > 0:
                spread_indicator = "📊 스프레드: {:.4f}%".format(spread)
                if spread > 0.5:
                    spread_indicator = "⚠️ 스프레드 확대: {:.4f}%".format(spread)
    
    async def connect_to_tardis(self, url: str = "ws://localhost:8765"):
        """Tardis Machine에 연결"""
        print(f"🔗 Tardis Machine 연결 중: {url}")
        
        subscribe_msg = {
            "type": "subscribe",
            "streams": [
                f"{self.symbol.lower()}@kline_1m",
                f"{self.symbol.lower()}@depth20@100ms"
            ]
        }
        
        async with websockets.connect(url) as ws:
            await ws.send(json.dumps(subscribe_msg))
            print(f"✅ 구독 완료: {subscribe_msg['streams']}")
            
            # 재생 시작 요청
            await ws.send(json.dumps({
                "type": "control",
                "action": "play",
                "speed": 10.0
            }))
            print("▶️ 데이터 재생 시작 (10x 속도)")
            
            async for message in ws:
                try:
                    data = json.loads(message)
                    
                    # Binance 스트림 타입 확인
                    if 'stream' in data:
                        stream = data.get('stream', '')
                        payload = data.get('data', {})
                        
                        if 'kline' in stream:
                            await self.handle_kline(payload)
                        elif 'depth' in stream:
                            await self.handle_depth(payload)
                    else:
                        # 제어 메시지 처리
                        print(f"📨 서버 메시지: {data}")
                        
                except json.JSONDecodeError:
                    print(f"❌ JSON 파싱 오류: {message[:100]}")
    
    def print_summary(self):
        """거래 요약 출력"""
        print("\n" + "="*50)
        print("📊 거래 봇 백테스트 결과")
        print("="*50)
        print(f"총 거래 횟수: {len(self.trades)}")
        
        if self.trades:
            total_pnl = 0
            for i in range(0, len(self.trades)-1, 2):
                if i+1 < len(self.trades):
                    buy_trade = self.trades[i]
                    sell_trade = self.trades[i+1]
                    pnl = sell_trade['price'] - buy_trade['price']
                    total_pnl += pnl
                    print(f"   거래 {i//2+1}: 매수가 ${buy_trade['price']:.2f} → 매도가 ${sell_trade['price']:.2f} (P&L: ${pnl:.2f})")
            
            print(f"\n총 손익: ${total_pnl:.2f}")
            print(f"최종 포지션: {'매수' if self.position == 1 else '매도' if self.position == -1 else '중립'}")

async def main():
    bot = SimpleTradingBot(
        symbol="ETHUSDT",
        short_window=5,
        long_window=20
    )
    
    try:
        await bot.connect_to_tardis("ws://localhost:8765")
    except KeyboardInterrupt:
        print("\n⏹️ 봇 종료")
        bot.print_summary()

if __name__ == "__main__":
    asyncio.run(main())

고급 기능: 오더북 리플레이 및 체결 데이터]

# advanced_replay.py - 고급 오더북/체결 데이터 리플레이
import asyncio
import json
import aiofiles
from pathlib import Path
from typing import List, Dict
from dataclasses import dataclass
from collections import deque

@dataclass
class OrderBookLevel:
    price: float
    quantity: float

class OrderBookReplayer:
    """오더북 상태 추적 및 리플레이"""
    
    def __init__(self, depth_levels: int = 20):
        self.depth_levels = depth_levels
        self.bids: deque = deque(maxlen=depth_levels)  # 내림차순 정렬
        self.asks: deque = deque(maxlen=depth_levels)   # 오름차순 정렬
        self.last_update_id = 0
    
    def apply_delta(self, bids: List[List[float]], asks: List[List[float]]):
        """오더북 델타 적용"""
        # 기존 레벨 매핑
        bid_map = {level.price: level for level in self.bids}
        ask_map = {level.price: level for level in self.asks}
        
        # bids 업데이트
        for price, qty in bids:
            if qty == 0:
                bid_map.pop(price, None)
            else:
                bid_map[price] = OrderBookLevel(price, qty)
        
        # asks 업데이트
        for price, qty in asks:
            if qty == 0:
                ask_map.pop(price, None)
            else:
                ask_map[price] = OrderBookLevel(price, qty)
        
        # 정렬된 deque 재생성
        self.bids = deque(
            sorted(bid_map.values(), key=lambda x: -x.price),
            maxlen=self.depth_levels
        )
        self.asks = deque(
            sorted(ask_map.values(), key=lambda x: x.price),
            maxlen=self.depth_levels
        )
    
    def get_mid_price(self) -> float:
        """중간 가격 계산"""
        if self.bids and self.asks:
            return (self.bids[0].price + self.asks[0].price) / 2
        return 0.0
    
    def get_spread(self) -> float:
        """스프레드 계산 (bps)"""
        if self.bids and self.asks:
            mid = self.get_mid_price()
            if mid > 0:
                return (self.asks[0].price - self.bids[0].price) / mid * 10000
        return 0.0
    
    def to_dict(self) -> dict:
        """딕셔너리 변환"""
        return {
            "bids": [[level.price, level.quantity] for level in self.bids],
            "asks": [[level.price, level.quantity] for level in self.asks],
            "mid_price": self.get_mid_price(),
            "spread_bps": self.get_spread()
        }

class TradeReplayer:
    """체결 데이터 리플레이 및 VWAP 계산"""
    
    def __init__(self, window_size: int = 100):
        self.window_size = window_size
        self.recent_trades: deque = deque(maxlen=window_size)
        self.vwap = 0.0
    
    def add_trade(self, price: float, quantity: float):
        """체결 추가"""
        self.recent_trades.append({
            "price": price,
            "quantity": quantity,
            "value": price * quantity
        })
        self.calculate_vwap()
    
    def calculate_vwap(self):
        """VWAP (표면加权平均価格) 계산"""
        if not self.recent_trades:
            self.vwap = 0.0
            return
        
        total_value = sum(t['value'] for t in self.recent_trades)
        total_volume = sum(t['quantity'] for t in self.recent_trades)
        
        self.vwap = total_value / total_volume if total_volume > 0 else 0.0