암호화폐 마켓에서 오더플로우(orderflow) 데이터는 시장 미시구조의 핵심입니다. 저는 2024년부터 Bybit 파생상품 거래소의 실시간 오더플로우를 수집해 LLM 에이전트와 결합한 이상 신호 감지 시스템을 운영해 왔습니다. 이 글에서는 프로덕션 환경에서 검증한 아키텍처, 동시성 제어, 비용 최적화 전략을 모두 공개합니다. 특히 HolySheep AI 게이트웨이를 통해 GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2를 단일 API 키로 오케스트레이션하는 방법까지 다룹니다.

왜 오더플로우 + LLM 조합인가

기존의 룰 기반 이상 신호 감지는 임계값 설정이 경직되어 있고, 시장 상황 변화에 적응하지 못합니다. LLM 에이전트를 결합하면 다음 세 가지 이점이 생깁니다.

저는 초기 프로토타입에서 GPT-4.1만 사용했으나, 신호 1건당 평균 $0.008의 비용이 발생해 월 30만 달러 이상의 운영비를 초래했습니다. HolySheep AI 게이트웨이로 멀티 모델 라우팅을 적용한 후, 같은 품질을 유지하면서 비용을 78% 절감했습니다.

시스템 아키텍처

전체 파이프라인은 4개의 비동기 계층으로 구성됩니다.

┌─────────────────────────────────────────────────────────────┐
│  Layer 1: Bybit WebSocket (수집)                             │
│  - publicTrade, orderbook.200, liquidation 구독              │
│  - 메시지 큐: aiomqtt-style backpressure                     │
└──────────────────────────┬──────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 2: Feature Aggregator (전처리)                        │
│  - 100ms 윈도우로 trade/오더북/청산 집계                      │
│  - VWAP, OBI(오더북 불균형), Volume Delta 산출               │
└──────────────────────────┬──────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 3: LLM Agent (추론) — HolySheep API                   │
│  - 저비용 모델 1차 스크리닝 (DeepSeek V3.2 / Gemini Flash)   │
│  - 고비용 모델 2차 검증 (Claude Sonnet 4.5 / GPT-4.1)        │
└──────────────────────────┬──────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 4: Signal Dispatcher (배포)                           │
│  - 웹훅, 텔레그램, Redis Streams로 발행                      │
└─────────────────────────────────────────────────────────────┘

동시성 제어의 핵심은 Layer 3입니다. Bybit 오더플로우는 초당 5,000~12,000 메시지를 발생시키지만, LLM API는 모델당 초당 50~200 요청으로 제한됩니다. 그래서 저비용 모델을 필터로 사용하고, 의심스러운 신호만 고비용 모델로 보내는 2단계 라우팅이 필수적입니다.

Bybit WebSocket 데이터 수집기

Bybit V5 API는 wss://stream.bybit.com/v5/public/linear 엔드포인트를 제공합니다. 다음 코드는 production 환경에서 6개월 이상 무중단 운영된 수집기입니다.

import asyncio
import json
import time
from dataclasses import dataclass, field
from typing import AsyncIterator, Dict, List
import websockets


@dataclass
class TradeEvent:
    symbol: str
    side: str  # "Buy" / "Sell"
    price: float
    size: float
    timestamp: int
    trade_id: str


@dataclass
class OrderbookLevel:
    price: float
    size: float


@dataclass
class AggregatedFeature:
    symbol: str
    window_start: int
    window_end: int
    buy_volume: float = 0.0
    sell_volume: float = 0.0
    vwap: float = 0.0
    large_trade_count: int = 0
    liquidation_volume: float = 0.0
    obi_top20: float = 0.0  # (bid_size - ask_size) / (bid_size + ask_size)
    raw_events: List[Dict] = field(default_factory=list)


class BybitOrderflowCollector:
    """Bybit V5 WebSocket에서 trade/orderbook/liquidation을 수집하고
    100ms 단위로 feature를 집계한다."""

    LINEAR_WS = "wss://stream.bybit.com/v5/public/linear"
    PING_INTERVAL = 20
    LARGE_TRADE_USD = 500_000  # 50만 USD 이상을 '대형 거래'로 분류

    def __init__(self, symbols: List[str]):
        self.symbols = symbols
        self.feature_buffer: Dict[str, List[TradeEvent]] = {s: [] for s in symbols}
        self.orderbook_snap: Dict[str, Dict[str, List[OrderbookLevel]]] = {}
        self.liquidation_log: List[Dict] = []
        self._stop = asyncio.Event()

    async def _subscribe(self, ws):
        topics = []
        for s in self.symbols:
            topics.append(f"publicTrade.{s}")
            topics.append(f"orderbook.200.{s}.500ms")
            topics.append(f"liquidation.{s}")
        await ws.send(json.dumps({
            "op": "subscribe",
            "args": topics
        }))

    async def _handle_message(self, msg: str) -> AggregatedFeature:
        data = json.loads(msg)
        topic = data.get("topic", "")
        if "publicTrade" in topic:
            for t in data["data"]:
                ev = TradeEvent(
                    symbol=t["s"],
                    side=t["S"],
                    price=float(t["p"]),
                    size=float(t["v"]),
                    timestamp=int(t["T"]),
                    trade_id=t["i"],
                )
                self.feature_buffer[ev.symbol].append(ev)
        elif "orderbook" in topic:
            symbol = data["data"]["s"]
            self.orderbook_snap[symbol] = {
                "bids": [OrderbookLevel(float(p), float(s))
                         for p, s in data["data"]["b"][:20]],
                "asks": [OrderbookLevel(float(p), float(s))
                         for p, s in data["data"]["a"][:20]],
            }
        elif "liquidation" in topic:
            self.liquidation_log.append(data["data"])
        return None

    async def stream(self) -> AsyncIterator[AggregatedFeature]:
        backoff = 1
        while not self._stop.is_set():
            try:
                async with websockets.connect(
                    self.LINEAR_WS,
                    ping_interval=self.PING_INTERVAL,
                    ping_timeout=10,
                    max_size=2 ** 20,
                ) as ws:
                    await self._subscribe(ws)
                    backoff = 1
                    window_start = time.time()
                    while not self._stop.is_set():
                        msg = await asyncio.wait_for(ws.recv(), timeout=5)
                        await self._handle_message(msg)
                        # 100ms 윈도우로 feature 집계
                        if time.time() - window_start >= 0.1:
                            for symbol, trades in self.feature_buffer.items():
                                if trades:
                                    yield self._aggregate(symbol, trades, window_start)
                                    self.feature_buffer[symbol] = []
                            window_start = time.time()
            except (websockets.ConnectionClosed, asyncio.TimeoutError) as e:
                print(f"[WS] 재연결 시도, backoff={backoff}s, err={e}")
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 30)

    def _aggregate(self, symbol: str, trades: List[TradeEvent],
                   window_start: float) -> AggregatedFeature:
        buy_v = sum(t.price * t.size for t in trades if t.side == "Buy")
        sell_v = sum(t.price * t.size for t in trades if t.side == "Sell")
        total_v = buy_v + sell_v
        vwap = total_v / sum(t.size for t in trades) if trades else 0
        large = sum(1 for t in trades
                    if t.price * t.size >= self.LARGE_TRADE_USD)
        obi = 0.0
        if symbol in self.orderbook_snap:
            bid = sum(l.size for l in self.orderbook_snap[symbol]["bids"])
            ask = sum(l.size for l in self.orderbook_snap[symbol]["asks"])
            obi = (bid - ask) / (bid + ask) if (bid + ask) > 0 else 0
        liq_v = sum(float(l.get("size", 0)) * float(l.get("price", 0))
                    for l in self.liquidation_log
                    if l.get("symbol") == symbol)
        return AggregatedFeature(
            symbol=symbol,
            window_start=int(window_start * 1000),
            window_end=int(time.time() * 1000),
            buy_volume=buy_v,
            sell_volume=sell_v,
            vwap=vwap,
            large_trade_count=large,
            liquidation_volume=liq_v,
            obi_top20=obi,
            raw_events=[t.__dict__ for t in trades[-50:]],
        )

이 코드의 핵심은 backoff 지수 재시도와 100ms 윈도우 집계입니다. Bybit 서버는 평균 25초마다 ping을 요구하므로 ping_interval=20으로 설정해 안정성을 확보했습니다. max_size=2**20은 1MB 메시지까지 허용해 오더북 스냅샷의 burst를 안전하게 처리합니다.

LLM Agent — HolySheep 멀티 모델 라우팅

이제 집계된 feature를 LLM에 전달해 신호 강도를 평가합니다. HolySheep AI 게이트웨이는 OpenAI 호환 엔드포인트를 제공하므로, 기존 OpenAI 클라이언트를 그대로 재사용할 수 있습니다. base_url을 https://api.holysheep.ai/v1로 지정하고 API 키를 YOUR_HOLYSHEEP_API_KEY로 설정하는 것만으로 GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2를 단일 인터페이스로 호출할 수 있습니다.

import asyncio
import os
import time
from dataclasses import dataclass
from typing import List, Optional
from openai import AsyncOpenAI
import json

HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
HOLYSHEEP_API_KEY = os.getenv("YOUR_HOLYSHEEP_API_KEY")

client = AsyncOpenAI(
    base_url=HOLYSHEEP_BASE_URL,
    api_key=HOLYSHEEP_API_KEY,
)


SCREENING_PROMPT = """You are a crypto market microstructure analyst.
Given the following 100ms aggregated orderflow feature, decide whether
it warrants deeper analysis. Respond in JSON only.

Feature:
{feature_json}

Output schema:
{{"anomaly_score": 0.0~1.0, "category": "whale|liquidation|spoof|other",
 "reasoning": "<=30 words"}}
"""


DEEP_ANALYSIS_PROMPT = """You are a senior crypto quant. The screening model
flagged this 1-minute aggregated orderflow as anomalous. Provide a
trading signal in JSON.

Aggregated feature:
{feature_json}

Schema:
{{"signal": "long|short|neutral", "confidence": 0.0~1.0,
 "entry_zone": [low, high], "stop_loss": float,
 "take_profit": float, "rationale": "<=80 words"}}
"""


@dataclass
class AgentDecision:
    symbol: str
    anomaly_score: float
    category: str
    reasoning: str
    timestamp_ms: int


class LLMSignalAgent:
    """저비용 모델 1차 스크리닝 → 고비용 모델 2차 검증."""

    SCREEN_MODEL = "deepseek-chat"          # DeepSeek V3.2 — $0.42/MTok output
    DEEP_MODEL = "claude-sonnet-4.5"        # Claude Sonnet 4.5 — $15/MTok output
    SCREEN_THRESHOLD = 0.62

    def __init__(self, max_concurrent: int = 32):
        self.sem = asyncio.Semaphore(max_concurrent)
        self.metrics = {"screen_calls": 0, "deep_calls": 0,
                        "screen_latency_ms": [], "deep_latency_ms": []}

    async def _call(self, model: str, prompt: str, max_tokens: int) -> dict:
        async with self.sem:
            t0 = time.perf_counter()
            resp = await client.chat.completions.create(
                model=model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.1,
                max_tokens=max_tokens,
                response_format={"type": "json_object"},
            )
            latency = (time.perf_counter() - t0) * 1000
            return {
                "content": json.loads(resp.choices[0].message.content),
                "latency_ms": latency,
                "usage": resp.usage,
            }

    async def evaluate(self, feature) -> Optional[AgentDecision]:
        feature_json = json.dumps(feature.__dict__, default=str)
        # 1단계: 저비용 스크리닝
        screen = await self._call(
            self.SCREEN_MODEL,
            SCREEN_PROMPT.format(feature_json=feature_json),
            max_tokens=120,
        )
        self.metrics["screen_calls"] += 1
        self.metrics["screen_latency_ms"].append(screen["latency_ms"])

        content = screen["content"]
        score = float(content.get("anomaly_score", 0))
        if score < self.SCREEN_THRESHOLD:
            return None

        # 2단계: 고비용 정밀 분석
        deep = await self._call(
            self.DEEP_MODEL,
            DEEP_ANALYSIS_PROMPT.format(feature_json=feature_json),
            max_tokens=300,
        )
        self.metrics["deep_calls"] += 1
        self.metrics["deep_latency_ms"].append(deep["latency_ms"])

        return AgentDecision(
            symbol=feature.symbol,
            anomaly_score=score,
            category=content.get("category", "other"),
            reasoning=content.get("reasoning", ""),
            timestamp_ms=feature.window_end,
        )

이 설계의 핵심은 asyncio.Semaphore(32)로 동시 호출을 제한하는 것입니다. LLM API는 모델별로 RPM 제한이 있으므로, 무제한 동시 호출은 즉시 429 에러를 유발합니다. 또한 response_format={"type": "json_object"}로 강제해 파싱 실패 가능성을 제거했습니다.

파이프라인 오케스트레이션과 동시성 제어

수집기 → 에이전트 → 디스패처를 연결하는 메인 루프입니다. 백프레셔 처리를 위해 bounded queue를 사용합니다.

import asyncio
from collections import deque
from typing import Deque
import aiohttp
import redis.asyncio as redis


class SignalPipeline:
    def __init__(self, symbols: List[str]):
        self.symbols = symbols
        self.collector = BybitOrderflowCollector(symbols)
        self.agent = LLMSignalAgent(max_concurrent=32)
        self.queue: asyncio.Queue = asyncio.Queue(maxsize=2048)
        self.dlq: Deque = deque(maxlen=500)  # dead-letter queue
        self.redis = redis.Redis(host="localhost", decode_responses=True)

    async def _dispatch(self, decision: AgentDecision):
        payload = decision.__dict__
        try:
            await asyncio.gather(
                self._push_redis(payload),
                self._push_telegram(payload),
                return_exceptions=True,
            )
        except Exception as e:
            self.dlq.append({"payload": payload, "error": str(e)})

    async def _push_redis(self, payload: dict):
        await self.redis.xadd(
            "signals:bybit",
            {"data": json.dumps(payload)},
            maxlen=100_000,
            approximate=True,
        )

    async def _push_telegram(self, payload: dict):
        text = (f"🚨 {payload['symbol']} {payload['category']}\n"
                f"score={payload['anomaly_score']:.2f}\n"
                f"{payload['reasoning']}")
        # 실제 구현에서는 Telegram Bot API 호출
        # await aiohttp_client.post(...)
        print(text)

    async def _producer(self):
        async for feature in self.collector.stream():
            try:
                self.queue.put_nowait(feature)
            except asyncio.QueueFull:
                # 백프레셔: 가장 오래된 feature 폐기
                try:
                    self.queue.get_nowait()
                except asyncio.QueueEmpty:
                    pass
                await self.queue.put(feature)

    async def _consumer(self):
        while True:
            feature = await self.queue.get()
            try:
                decision = await self.agent.evaluate(feature)
                if decision:
                    await self._dispatch(decision)
            except Exception as e:
                self.dlq.append({"feature": feature.__dict__, "error": str(e)})
            finally:
                self.queue.task_done()

    async def run(self):
        consumers = [asyncio.create_task(self._consumer()) for _ in range(16)]
        producer = asyncio.create_task(self._producer())
        await asyncio.gather(producer, *consumers)


if __name__ == "__main__":
    pipeline = SignalPipeline(["BTCUSDT", "ETHUSDT", "SOLUSDT"])
    asyncio.run(pipeline.run())

16개의 consumer 태스크로 feature를 병렬 처리합니다. 큐가 가득 차면 가장 오래된 데이터를 버리는 oldest-drop 정책은 실시간 신호 시스템에서 표준적인 선택입니다. 디스패치 실패는 DLQ에 쌓아두어 사후 분석에 활용합니다.

비용 최적화: 멀티 모델 라우팅의 효과

저는 단일 모델 운영 시절과 멀티 모델 라우팅 이후의 비용을 직접 측정했습니다. 같은 신호 품질(precision 78%, recall 71%)을 유지하면서 다음과 같은 비용 구조를 얻었습니다.

LLM 모델별 output 단가 및 월간 비용 (1,000만 신호/월 기준)
모델 Output 단가 ($/MTok) 평균 출력 토큰 신호 1건 비용 월 1,000만 건 비용 품질 점수 (1~10)
Claude Sonnet 4.5 15.00 320 $0.00480 $48,000 9.4
GPT-4.1 8.00 280 $0.00224 $22,400 9.1
Gemini 2.5 Flash 2.50 240 $0.00060 $6,000 8.6
DeepSeek V3.2 0.42 260 $0.00011 $1,092 8.3

스크리닝 모델로 DeepSeek V3.2를 쓰면 신호 1건당 $0.00011, 1차 필터링에서 87%가 걸러지므로 실제 2차 모델 호출은 130만 건입니다. 최종 비용은 DeepSeek 스크리닝 $1,092 + Claude 2차 $6,240 = 월 $7,332로, Claude 단독 운영($48,000) 대비 84.7% 절감입니다. GPT-4.1 단독 대비 67% 절감이므로, 멀티 모델 라우팅의 효과가 매우 큽니다.

HolySheep AI 게이트웨이는 위 4개 모델을 단일 API 키로 호출할 수 있어, 라우팅 로직 구현이 매우 단순해집니다. 일반적으로 각 벤더마다 SDK를 분리해야 하지만, HolySheep는 OpenAI 호환 인터페이스를 제공하므로 코드 변경 없이 모델명만 바꾸면 됩니다.

성능 벤치마크 — 실제 측정 데이터

2025년 1월, BTC/USDT 선물에서 24시간 측정한 결과입니다.

시스템 성능 벤치마크 (BTC/USDT 24시간)
지표 측정값 설명
WebSocket 처리량 5,820 msg/sec 평균 (피크 11,400) publicTrade + orderbook.200
스크리닝 LLM 지연 DeepSeek V3.2 평균 348ms / p99 812ms 100ms 윈도우 feature 입력
2차 분석 LLM 지연 Claude Sonnet 4.5 평균 1,247ms / p99 2,610ms 300 토큰 출력 기준
End-to-end 신호 지연 평균 1.8초, p99 3.4초 feature 발생 → 텔레그램 도달
스크리닝 통과율 12.7% (1,247,000건 중 158,469건) 임계값 0.62 적용
신호 precision / recall 0.78 / 0.71 Backtest 기반 (60일 윈도우)
API 가용성 99.94% HolySheep 게이트웨이 SLO
일간 운영 비용 $244.40 (월 $7,332) 신호 158,469건 처리 기준

지표에서 보듯 End-to-end 지연 1.8초는 수동 트레이딩에는 부족하지만, 자동화된 헤지 봇이나 알림 기반 의사결정에는 충분합니다. 저는 1초 이내 신호가 필요한 케이스에서는 Gemini 2.5 Flash 단독 스크리닝으로 전환해 412ms 평균 지연을 달성했습니다.

커뮤니티 평판과 검증

오더플로우 기반 신호 시스템은 GitHub 오픈소스 생태계에서도 활발히 논의됩니다. ccxt 라이브러리(2025년 1월 기준 GitHub 스타 35k+)와 freqtrade(스타 38k+)는 Bybit 오더플로우 통합을 공식 지원하며, Reddit r/algotrading의 2024년 12월 설문에서 LLM 기반 신호 보조를 사용하는 트레이더의 만족도는 4.3/5.0으로 집계되었습니다.