파이썬 TWAP 알고리즘 주문 실행

TWAP 알고리즘이란?

TWAP(Time-Weighted Average Price)은 대량 주문을 일정 시간에 걸쳐 균등하게 분할 체결하는 알고리즘 주문 실행 전략입니다. 기관 투자자와 퀀트 트레이더가 시장 충격(Market Impact)을 최소화하기 위해 가장 널리 사용하는 실행 알고리즘 중 하나입니다.

핵심 원리는 단순합니다. 1000만원어치를 한 번에 매수하는 대신, 1시간 동안 5분 간격으로 12번에 나눠서 매수하면 평균 체결가가 시간가중평균가격에 수렴합니다.

TWAP vs VWAP 비교

TWAP과 함께 자주 언급되는 VWAP(거래량가중평균가격)과의 차이를 이해해야 합니다:

항목 TWAP VWAP
분할 기준 시간 균등 거래량 비례
구현 난이도 낮음 중간 (거래량 예측 필요)
적합한 시장 24시간 암호화폐, 유동성 균일 시장 주식 등 장중 거래량 패턴이 뚜렷한 시장
시장 충격 일정하게 분산 유동성 높은 시간대에 집중
예측 가능성 높음 (패턴 단순) 낮음 (거래량 적응)

기본 TWAP 엔진 구현

파이썬으로 구현하는 기본 TWAP 주문 실행 엔진입니다:

import time
import math
from datetime import datetime, timedelta
from dataclasses import dataclass, field

@dataclass
class TWAPOrder:
    symbol: str
    side: str           # "buy" or "sell"
    total_quantity: float
    duration_minutes: int
    interval_seconds: int = 30
    filled_quantity: float = 0
    fills: list = field(default_factory=list)

    @property
    def num_slices(self) -> int:
        return max(1, (self.duration_minutes * 60) // self.interval_seconds)

    @property
    def slice_quantity(self) -> float:
        remaining = self.total_quantity - self.filled_quantity
        remaining_slices = self.num_slices - len(self.fills)
        if remaining_slices <= 0:
            return remaining
        return remaining / remaining_slices

    @property
    def progress(self) -> float:
        return self.filled_quantity / self.total_quantity if self.total_quantity > 0 else 0

class TWAPEngine:
    """TWAP 알고리즘 주문 실행 엔진"""

    def __init__(self, exchange):
        self.exchange = exchange
        self.active_orders = {}

    def create_twap(self, symbol: str, side: str,
                    total_qty: float, duration_min: int,
                    interval_sec: int = 30) -> TWAPOrder:
        order = TWAPOrder(
            symbol=symbol,
            side=side,
            total_quantity=total_qty,
            duration_minutes=duration_min,
            interval_seconds=interval_sec,
        )
        self.active_orders[symbol] = order
        return order

    def execute_slice(self, order: TWAPOrder) -> dict:
        """단일 슬라이스 실행"""
        qty = order.slice_quantity
        if qty <= 0:
            return {"status": "complete"}

        try:
            result = self.exchange.create_market_order(
                symbol=order.symbol,
                side=order.side,
                amount=qty,
            )
            fill = {
                "time": datetime.now().isoformat(),
                "quantity": qty,
                "price": result.get("average", result.get("price", 0)),
                "order_id": result.get("id"),
            }
            order.fills.append(fill)
            order.filled_quantity += qty
            return {"status": "filled", "fill": fill}

        except Exception as e:
            return {"status": "error", "error": str(e)}

    def run(self, order: TWAPOrder):
        """전체 TWAP 실행 루프"""
        print(f"TWAP 시작: {order.symbol} {order.side} "
              f"{order.total_quantity} / {order.duration_minutes}분")

        for i in range(order.num_slices):
            result = self.execute_slice(order)
            print(f"  슬라이스 {i+1}/{order.num_slices}: {result['status']} "
                  f"({order.progress:.1%} 완료)")

            if order.filled_quantity >= order.total_quantity:
                break

            if i < order.num_slices - 1:
                time.sleep(order.interval_seconds)

        avg_price = self._calc_avg_price(order)
        print(f"TWAP 완료: 평균가 {avg_price:,.2f}")
        return order

    def _calc_avg_price(self, order: TWAPOrder) -> float:
        if not order.fills:
            return 0
        total_cost = sum(f["quantity"] * f["price"] for f in order.fills)
        total_qty = sum(f["quantity"] for f in order.fills)
        return total_cost / total_qty if total_qty > 0 else 0

랜덤화 TWAP: 예측 방지

기본 TWAP은 일정 간격으로 주문하기 때문에 다른 트레이더가 패턴을 예측하고 앞서 매매(front-running)할 수 있습니다. 랜덤화를 추가하면 이를 방지합니다:

import random

class RandomizedTWAP(TWAPEngine):
    """시간·수량 랜덤화 TWAP"""

    def __init__(self, exchange, time_jitter: float = 0.3,
                 qty_jitter: float = 0.2):
        super().__init__(exchange)
        self.time_jitter = time_jitter  # ±30% 시간 변동
        self.qty_jitter = qty_jitter    # ±20% 수량 변동

    def run(self, order: TWAPOrder):
        print(f"랜덤 TWAP 시작: {order.symbol} {order.side}")

        for i in range(order.num_slices):
            # 수량 랜덤화
            base_qty = order.slice_quantity
            jittered_qty = base_qty * (1 + random.uniform(
                -self.qty_jitter, self.qty_jitter))
            remaining = order.total_quantity - order.filled_quantity
            actual_qty = min(jittered_qty, remaining)

            if actual_qty <= 0:
                break

            # 임시로 슬라이스 수량 오버라이드
            original_qty = order.slice_quantity
            result = self.execute_slice(order)

            print(f"  슬라이스 {i+1}: qty={actual_qty:.4f} "
                  f"({order.progress:.1%})")

            if order.filled_quantity >= order.total_quantity:
                break

            # 시간 랜덤화
            if i < order.num_slices - 1:
                base_sleep = order.interval_seconds
                jittered_sleep = base_sleep * (1 + random.uniform(
                    -self.time_jitter, self.time_jitter))
                time.sleep(max(1, jittered_sleep))

        return order

적응형 TWAP: 시장 상황 반영

스프레드와 변동성에 따라 주문 속도를 조절하는 적응형 TWAP입니다:

import numpy as np

class AdaptiveTWAP(TWAPEngine):
    """시장 상황 적응형 TWAP"""

    def __init__(self, exchange, spread_threshold: float = 0.001,
                 vol_window: int = 20):
        super().__init__(exchange)
        self.spread_threshold = spread_threshold
        self.vol_window = vol_window
        self.price_history = []

    def assess_market(self, symbol: str) -> dict:
        """현재 시장 상태 평가"""
        ob = self.exchange.fetch_order_book(symbol, limit=5)
        best_bid = ob["bids"][0][0]
        best_ask = ob["asks"][0][0]
        mid = (best_bid + best_ask) / 2
        spread = (best_ask - best_bid) / mid

        self.price_history.append(mid)
        if len(self.price_history) > self.vol_window:
            self.price_history.pop(0)

        volatility = 0
        if len(self.price_history) >= 5:
            returns = np.diff(np.log(self.price_history))
            volatility = np.std(returns)

        return {
            "spread": spread,
            "volatility": volatility,
            "mid_price": mid,
            "bid_depth": sum(b[1] for b in ob["bids"][:5]),
            "ask_depth": sum(a[1] for a in ob["asks"][:5]),
        }

    def get_urgency(self, market: dict) -> float:
        """
        시장 상태에 따른 긴급도 (0.5~2.0)
        스프레드 좁고 변동성 낮으면 → 천천히 (< 1.0)
        스프레드 넓고 변동성 높으면 → 빠르게 (> 1.0)
        """
        spread_score = market["spread"] / self.spread_threshold
        vol_score = 1 + market["volatility"] * 100

        urgency = (spread_score + vol_score) / 2
        return max(0.5, min(2.0, urgency))

    def run(self, order: TWAPOrder):
        print(f"적응형 TWAP 시작: {order.symbol}")

        i = 0
        while order.filled_quantity < order.total_quantity:
            market = self.assess_market(order.symbol)
            urgency = self.get_urgency(market)

            result = self.execute_slice(order)
            print(f"  슬라이스 {i+1}: urgency={urgency:.2f} "
                  f"spread={market['spread']:.4%} "
                  f"({order.progress:.1%})")

            if order.filled_quantity >= order.total_quantity:
                break

            # 긴급도에 따라 대기 시간 조절
            wait = order.interval_seconds / urgency
            time.sleep(max(1, wait))
            i += 1

        return order

TWAP 성과 측정: 실행 품질 분석

TWAP 실행의 품질을 평가하는 지표들입니다:

def analyze_execution(order: TWAPOrder, market_twap: float) -> dict:
    """
    TWAP 실행 품질 분석
    market_twap: 같은 기간 시장의 실제 TWAP
    """
    if not order.fills:
        return {"error": "체결 없음"}

    prices = [f["price"] for f in order.fills]
    quantities = [f["quantity"] for f in order.fills]
    total_cost = sum(p * q for p, q in zip(prices, quantities))
    total_qty = sum(quantities)
    exec_avg = total_cost / total_qty

    # Implementation Shortfall (구현 부족)
    arrival_price = order.fills[0]["price"]
    impl_shortfall = (exec_avg - arrival_price) / arrival_price
    if order.side == "sell":
        impl_shortfall *= -1

    # TWAP 슬리피지
    twap_slippage = (exec_avg - market_twap) / market_twap
    if order.side == "sell":
        twap_slippage *= -1

    return {
        "실행평균가": f"{exec_avg:,.2f}",
        "시장TWAP": f"{market_twap:,.2f}",
        "도착가격": f"{arrival_price:,.2f}",
        "TWAP슬리피지": f"{twap_slippage:.4%}",
        "구현부족": f"{impl_shortfall:.4%}",
        "체결횟수": len(order.fills),
        "가격표준편차": f"{np.std(prices):,.2f}",
        "총체결수량": f"{total_qty:.4f}",
    }

실전 적용 시 주의사항

  • 최소 주문 수량: 거래소마다 최소 주문 수량이 있습니다. 슬라이스 크기가 최소 수량 미만이면 주문이 거부됩니다. 사전에 확인하고 슬라이스 수를 조절하세요.
  • API 호출 제한: 대부분 거래소는 분당 API 호출 수를 제한합니다. 간격을 최소 10초 이상으로 설정하는 것이 안전합니다.
  • 부분 체결 처리: 시장가 주문도 유동성 부족 시 부분 체결될 수 있습니다. 미체결분을 다음 슬라이스에 포함하는 로직이 필요합니다.
  • 네트워크 장애 대응: 실행 중 연결이 끊기면 현재 진행 상태를 저장하고, 재연결 후 이어서 실행할 수 있어야 합니다.
  • 슬리피지 최적화와 병행: TWAP으로 시간 분산을 하더라도 각 슬라이스의 슬리피지를 모니터링하고, 비정상적으로 높으면 실행을 일시 중단하세요.

핵심 정리

TWAP은 대량 주문의 시장 충격을 최소화하는 가장 기본적이고 효과적인 알고리즘 주문 실행 전략입니다. 기본 TWAP에 랜덤화를 추가하면 프론트러닝을 방지할 수 있고, 적응형 TWAP은 시장 상황에 따라 실행 속도를 최적화합니다. 자동매매 봇에 TWAP 엔진을 통합하면 실행 품질이 크게 향상됩니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux