파이썬 마켓메이킹 전략

마켓메이킹이란? 수익 원리와 핵심 개념

마켓메이킹(Market Making)은 매수·매도 양쪽에 주문을 동시에 걸어 스프레드 차이로 수익을 내는 퀀트 전략입니다. 거래소에 유동성을 공급하는 대가로 스프레드 수익을 얻는 구조이며, 전통 금융에서 암호화폐 시장까지 폭넓게 활용됩니다.

핵심 수익 공식은 단순합니다:

수익 = (매도호가 - 매수호가) × 체결 수량 - 재고 리스크 손실

하지만 실전에서는 재고 리스크(Inventory Risk), 역선택(Adverse Selection), 변동성 급등 등 다양한 위험을 관리해야 합니다. 이 글에서는 파이썬으로 마켓메이킹 봇을 구현하는 전체 과정을 다룹니다.

Avellaneda-Stoikov 모델: 최적 호가 계산

학술적으로 가장 널리 쓰이는 마켓메이킹 모델은 Avellaneda-Stoikov(2008) 모델입니다. 이 모델은 재고 위험을 고려한 최적 매수·매도 호가를 수학적으로 도출합니다.

import numpy as np

class AvellanedaStoikov:
    """Avellaneda-Stoikov 최적 마켓메이킹 모델"""

    def __init__(self, gamma=0.1, sigma=0.02, k=1.5, T=1.0):
        """
        gamma: 리스크 회피 계수 (클수록 보수적)
        sigma: 자산 변동성
        k: 주문 체결 강도 파라미터
        T: 전략 운용 기간
        """
        self.gamma = gamma
        self.sigma = sigma
        self.k = k
        self.T = T

    def reservation_price(self, mid_price, inventory, t):
        """재고 조정된 기준가격 계산"""
        remaining = self.T - t
        return mid_price - inventory * self.gamma * (self.sigma ** 2) * remaining

    def optimal_spread(self, t):
        """최적 스프레드 계산"""
        remaining = self.T - t
        spread = (self.gamma * (self.sigma ** 2) * remaining
                  + (2 / self.gamma) * np.log(1 + self.gamma / self.k))
        return spread

    def get_quotes(self, mid_price, inventory, t):
        """최적 매수·매도 호가 반환"""
        r = self.reservation_price(mid_price, inventory, t)
        spread = self.optimal_spread(t)
        bid = r - spread / 2
        ask = r + spread / 2
        return round(bid, 2), round(ask, 2)

# 사용 예시
model = AvellanedaStoikov(gamma=0.1, sigma=0.015, k=1.5)
mid = 50000  # 현재 중간가
bid, ask = model.get_quotes(mid, inventory=2, t=0.5)
print(f"매수호가: {bid:,.0f} | 매도호가: {ask:,.0f} | 스프레드: {ask-bid:,.0f}")

gamma(리스크 회피 계수)가 핵심 파라미터입니다. 값이 클수록 재고에 민감하게 반응하여 포지션을 빠르게 해소하려 합니다. 백테스트 가이드를 참고해 최적 gamma를 탐색하세요.

파이썬 마켓메이킹 봇 구현

실전 마켓메이킹 봇은 (1) 시장 데이터 수신, (2) 호가 계산, (3) 주문 관리, (4) 재고 관리의 루프로 동작합니다.

import ccxt
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("MarketMaker")

class MarketMaker:
    def __init__(self, exchange, symbol, model, order_size=0.01):
        self.exchange = exchange
        self.symbol = symbol
        self.model = model
        self.order_size = order_size
        self.inventory = 0
        self.active_orders = []
        self.pnl = 0

    def get_mid_price(self):
        """오더북에서 중간가 계산"""
        ob = self.exchange.fetch_order_book(self.symbol, limit=5)
        best_bid = ob['bids'][0][0]
        best_ask = ob['asks'][0][0]
        return (best_bid + best_ask) / 2

    def cancel_all(self):
        """기존 주문 전체 취소"""
        for oid in self.active_orders:
            try:
                self.exchange.cancel_order(oid, self.symbol)
            except Exception:
                pass
        self.active_orders = []

    def place_quotes(self, bid, ask):
        """매수·매도 주문 동시 제출"""
        try:
            buy = self.exchange.create_limit_buy_order(
                self.symbol, self.order_size, bid
            )
            sell = self.exchange.create_limit_sell_order(
                self.symbol, self.order_size, ask
            )
            self.active_orders = [buy['id'], sell['id']]
            logger.info(f"주문: BID {bid:,.0f} | ASK {ask:,.0f}")
        except Exception as e:
            logger.error(f"주문 실패: {e}")

    def check_fills(self):
        """체결 확인 및 재고·PnL 업데이트"""
        for oid in self.active_orders:
            try:
                order = self.exchange.fetch_order(oid, self.symbol)
                if order['status'] == 'closed':
                    if order['side'] == 'buy':
                        self.inventory += order['filled']
                        self.pnl -= order['filled'] * order['price']
                    else:
                        self.inventory -= order['filled']
                        self.pnl += order['filled'] * order['price']
            except Exception:
                pass

    def run(self, duration=3600, interval=5):
        """메인 루프 실행"""
        start = time.time()
        steps = int(duration / interval)

        for step in range(steps):
            t = (time.time() - start) / duration
            self.check_fills()
            self.cancel_all()

            mid = self.get_mid_price()
            bid, ask = self.model.get_quotes(mid, self.inventory, t)
            self.place_quotes(bid, ask)

            logger.info(
                f"[{step}] mid={mid:,.0f} inv={self.inventory:.4f} "
                f"pnl={self.pnl:,.0f}"
            )
            time.sleep(interval)

이 코드는 ccxt 라이브러리를 사용해 바이낸스, 업비트 등 다양한 거래소에 적용할 수 있습니다. interval(갱신 주기)은 시장 상황에 따라 1~10초로 조정합니다.

재고 리스크 관리: 마켓메이커의 생존 전략

마켓메이킹에서 가장 큰 위험은 한쪽 방향으로 재고가 쌓이는 것입니다. 가격이 불리한 방향으로 움직이면 스프레드 수익을 초과하는 손실이 발생합니다.

리스크 관리 기법 설명 효과
호가 비대칭 조정 재고가 많으면 매도호가를 공격적으로 낮춤 재고 자연 해소
재고 한도 설정 최대 보유량 초과 시 한쪽 주문 중단 극단적 손실 방지
변동성 기반 스프레드 변동성 급등 시 스프레드 확대 역선택 리스크 감소
헤지 주문 재고 일정량 초과 시 시장가로 해소 빠른 포지션 정리
class InventoryManager:
    """재고 리스크 관리 모듈"""

    def __init__(self, max_inventory=10, skew_factor=0.0005):
        self.max_inventory = max_inventory
        self.skew_factor = skew_factor

    def adjust_quotes(self, bid, ask, inventory):
        """재고에 따라 호가 비대칭 조정"""
        skew = inventory * self.skew_factor * (ask - bid)
        adjusted_bid = bid - skew
        adjusted_ask = ask - skew
        return adjusted_bid, adjusted_ask

    def should_pause_side(self, inventory):
        """재고 한도 초과 시 한쪽 주문 중단"""
        if inventory >= self.max_inventory:
            return 'buy'   # 매수 중단
        elif inventory <= -self.max_inventory:
            return 'sell'  # 매도 중단
        return None

    def need_hedge(self, inventory, threshold=0.8):
        """긴급 헤지 필요 여부"""
        ratio = abs(inventory) / self.max_inventory
        return ratio >= threshold

MDD 관리 전략과 결합하면 전체 자산 수준의 리스크 관리도 가능합니다.

스프레드 동적 조정과 변동성 필터

고정 스프레드는 시장 상황 변화에 취약합니다. 실전에서는 실시간 변동성에 따라 스프레드를 동적으로 조정해야 합니다.

from collections import deque

class VolatilityFilter:
    """실시간 변동성 기반 스프레드 조정"""

    def __init__(self, window=100, min_spread=0.001, max_spread=0.01):
        self.prices = deque(maxlen=window)
        self.min_spread = min_spread
        self.max_spread = max_spread

    def update(self, price):
        self.prices.append(price)

    def realized_vol(self):
        """실현 변동성 계산"""
        if len(self.prices) < 10:
            return 0.01
        arr = np.array(self.prices)
        returns = np.diff(np.log(arr))
        return np.std(returns) * np.sqrt(len(returns))

    def adjusted_spread(self, base_spread):
        """변동성 비례 스프레드 조정"""
        vol = self.realized_vol()
        spread = base_spread * (1 + vol * 50)
        return np.clip(spread, self.min_spread, self.max_spread)

    def is_safe_to_quote(self, vol_threshold=0.05):
        """변동성 폭등 시 호가 제출 중단"""
        return self.realized_vol() < vol_threshold

변동성이 임계값을 초과하면 호가 제출을 중단하는 킬 스위치는 마켓메이킹 봇의 필수 안전장치입니다. 급등·급락 구간에서 무방비로 주문이 체결되면 큰 손실로 이어집니다.

백테스트: 마켓메이킹 전략 검증

마켓메이킹 백테스트는 일반 방향성 전략과 다릅니다. 체결 시뮬레이션이 핵심이며, 오더북 데이터가 필요합니다.

class MMBacktester:
    """마켓메이킹 백테스트 엔진"""

    def __init__(self, model, inv_manager, vol_filter):
        self.model = model
        self.inv_mgr = inv_manager
        self.vol_filter = vol_filter

    def simulate(self, prices, timestamps):
        """틱 데이터 기반 시뮬레이션"""
        results = []
        inventory = 0
        cash = 0
        T = len(prices)

        for i, (price, ts) in enumerate(zip(prices, timestamps)):
            t = i / T
            self.vol_filter.update(price)

            if not self.vol_filter.is_safe_to_quote():
                continue

            bid, ask = self.model.get_quotes(price, inventory, t)
            bid, ask = self.inv_mgr.adjust_quotes(bid, ask, inventory)

            # 체결 시뮬레이션 (단순화)
            if price <= bid:  # 매수 체결
                inventory += 1
                cash -= bid
            elif price >= ask:  # 매도 체결
                inventory -= 1
                cash += ask

            mark_to_market = cash + inventory * price
            results.append({
                'timestamp': ts,
                'price': price,
                'inventory': inventory,
                'cash': cash,
                'mtm_pnl': mark_to_market
            })

        return results

백테스트 오버피팅 방지법을 반드시 참고하세요. 마켓메이킹은 특히 체결 가정에 따라 결과가 크게 달라지므로, 보수적인 시뮬레이션이 중요합니다.

실전 배포 시 주의사항

마켓메이킹 봇을 실전에 배포할 때 반드시 고려해야 할 사항들입니다:

  • 지연시간(Latency): 호가 갱신 속도가 수익에 직결됩니다. 거래소 서버와 가까운 위치에 봇을 배치하세요.
  • API Rate Limit: 거래소별 초당 요청 한도를 반드시 확인하고, 주문 갱신 주기를 조정합니다.
  • 수수료 구조: Maker 수수료가 음수(리베이트)인 거래소를 선택하면 수익성이 크게 향상됩니다.
  • 장애 대응: 네트워크 단절, API 오류 시 모든 미체결 주문을 즉시 취소하는 로직이 필수입니다.
  • 모니터링: 재고, PnL, 체결률, 스프레드를 실시간 대시보드로 관찰하세요.

마켓메이킹은 고빈도 매매(HFT)의 기초이자, 퀀트 트레이딩에서 가장 체계적인 전략 중 하나입니다. Avellaneda-Stoikov 모델을 기반으로 재고 관리와 변동성 필터를 결합하면, 안정적인 스프레드 수익을 추구할 수 있습니다.

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