← 기사 목록으로
July 20, 2026
5분 소요

구현 부족(Implementation shortfall)과 DIY TCA: 체결이 실제로 얼마의 비용을 발생시키는지 측정하기

구현 부족(Implementation shortfall)과 DIY TCA: 체결이 실제로 얼마의 비용을 발생시키는지 측정하기
#execution
#tca
#implementation-shortfall
#마크아웃
#거래-비용
#알고트레이딩
#backtest
#시장-미시구조

모든 기관 트레이딩 데스크에는 거래 비용 분석(TCA) 파이프라인이 있다. 크립토 봇을 운영하는 사람 중에는 이를 갖춘 이가 거의 없다. 전형적인 구성은 체결을 로깅하고, 수수료를 합산한 뒤, 백테스트 PnL과 라이브 PnL의 차이를 "슬리피지"라고 부른다 — 지연, 스프레드, 임팩트, 역선택, 그리고 가격이 달아나버려 체결되지 못한 모든 주문을 한꺼번에 흡수해버리는, 설명되지 않는 단일 잔차다. 하나의 숫자로 측정하는 비용은 고칠 수 없다. 이를 제대로 측정하는 기법은 1988년부터 존재해왔고, 독점적인 것도 아니며, 자신의 체결 로그를 대상으로 하면 대략 200줄의 Python이면 된다. 이 글에서는 그것을 구축한다.

성과는 더 예쁜 대시보드가 아니다. 백테스트에는 비용 모델 — 슬리피지 상수, 체결 확률, 임팩트 계수 — 이 들어 있고, 그 안의 모든 파라미터는 현재 추측에 불과하다. 라이브 체결에 대한 TCA는 그 파라미터를 보정할 수 있는 유일한 실측 기준이다. 우리는 그 루프의 시뮬레이션 쪽을 체결 시뮬레이션: 종가 환상에서 큐 인식 현실로 가는 사다리에서 구축했다. 이 글에서는 측정 쪽을 구축한다.

종이 대 현실: Perold가 실제로 측정한 것

이 기초적인 트릭은 André Perold(1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4–9)에서 비롯된다. 두 개의 포트폴리오를 병렬로 운영한다. **종이 포트폴리오(paper portfolio)**는 모든 의사결정을 즉시, 무제한 크기로, 비용 없이, 의사결정 시점에 형성된 가격으로 체결한다. **실제 포트폴리오(real portfolio)**는 봇이 실제로 한 것이다: 부분 체결, 쫓아간 호가, 취소된 잔량, 수수료. 구현 부족은 이 둘의 수익률 차이다.

이 정의가 중요한 이유는, 그것이 무엇을 숨기기를 거부하는가에 있다. 수수료 명세서는 커미션을 보여준다. 체결가 대비 지정가 리포트는 아무것도 보여주지 않는다(구조상 지정가보다 나쁘게 체결되는 일은 없다). 종이 포트폴리오는 모든 것에 대해 비용을 청구한다: 의사결정과 도착 사이의 드리프트, 넘어간 스프레드, 유발한 임팩트, 그리고 — 결정적으로 — 가격이 달아나는 동안 끝내 체결되지 못한 주문들. Wagner와 Edwards(1993, "Best Execution," Financial Analysts Journal 49(1), 65–71)는 눈에 보이는 수수료를 빙산의 일각이라 불렀다. 회전율이 있는 어떤 것이든, 수면 아래 부분이 지배한다.

표기법을 정리하자. 부모 주문(parent order): 방향 s{+1,1}s \in \{+1, -1\} (매수/매도), 크기 XX. 의사결정가(decision price) PdP_d는 신호가 발화한 순간 전략이 본 미드(mid)다. 체결은 (pj,xj)(p_j, x_j)로 도착하며 총 체결 수량은 x=jxjXx = \sum_j x_j \le X다. 시계(horizon) TT(부모 주문 완료, 취소, 또는 타임아웃)에서 미드는 PTP_T다. 명시적 수수료는 FF다. 통화 단위의 구현 부족:

IS=sjxj(pjPd)execution cost  +  s(Xx)(PTPd)opportunity cost  +  F,\mathrm{IS} = \underbrace{s \sum_j x_j\,(p_j - P_d)}_{\text{execution cost}} \;+\; \underbrace{s\,(X - x)(P_T - P_d)}_{\text{opportunity cost}} \;+\; F,

종이 명목금액 XPdX \cdot P_d로 나누어 베이시스 포인트로 정규화한다. 양수면 비용을 지불한 것이다. 첫 번째 항은 종이 포트폴리오 대비 체결이 발생시킨 비용이고, 두 번째는 Perold의 기회비용 — 가격이 달아나버린 지점의 가격으로 표시한 미체결 잔량 — 이며, 세 번째는 거래소 명세서가 인정하는 유일한 부분이다.

관례에 대한 한 가지 주석: 업계에서는 "도착가(arrival price)"라고 말하고, 대부분의 주식 TCA에서 도착이란 주문이 시장에 도달한 순간의 미드를 의미한다. 봇의 경우 의사결정 시점과 도착 시점은 자체 내부 지연에 레이트 리밋 큐잉을 더한 만큼 차이가 나며 — 이는 실재하고 측정 가능한 비용이다. 그래서 우리는 두 타임스탬프와 두 가격을 모두 보존하고, 분해를 통해 그것들을 분리한다.

분해: 지연, 임팩트, 타이밍, 기회, 수수료

단일 IS 숫자는 체결이 비싸다는 것을 알려준다. 그런지는 알려주지 않으며, 각 구성 요소에 대한 해법은 완전히 다르다 — 지연 비용과 임팩트 비용을 같은 변경으로 해결하지 못한다. Robert Kissell의 확장 구현 부족(Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6–16; 저서 The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013에서 책 분량으로 전개)은 총합을 각 구성 요소로 풀어내며, 각각은 주문 생애주기의 서로 다른 단계에 귀속된다. 도착 미드 P0P_0(첫 거래소 승인 시점의 미드)를 사용하는 실무자 버전:

IS=sX(P0Pd)delay  +  sjxj(pjP0)trading cost  +  s(Xx)(PTP0)opportunity  +  F.\mathrm{IS} = \underbrace{s\,X\,(P_0 - P_d)}_{\text{delay}} \;+\; \underbrace{s \sum_j x_j\,(p_j - P_0)}_{\text{trading cost}} \;+\; \underbrace{s\,(X - x)(P_T - P_0)}_{\text{opportunity}} \;+\; F.

이 항등식은 Perold의 정의로 정확히 접혀 되돌아간다 — 항들을 전개하면 P0P_0가 소거된다. 각 조각은 뚜렷한 주인을 가진다:

지연 비용(Delay cost) sX(P0Pd)sX(P_0 - P_d): 신호 발화와 첫 자식 주문(child order)이 거래소에 실제로 활성화되는 사이의 가격 드리프트. 이는 당신의 인프라 — 직렬화, 네트워크, 레이트 리밋 큐, 리스크 체크 — 다. 신호가 진짜 단기 알파를 가지고 있다면, 지연 비용은 그 알파가 가장 먼저 새어나가는 지점이다. 일관되게 양수인 평균은 당신이 도착하기 전에 시장이 당신에게 유리한 방향으로 움직인다는 뜻이다 — 감쇠하는 모멘텀 알파거나, 같은 신호를 더 빠르게 트레이딩하는 누군가다.

트레이딩 비용(Trading cost) sjxj(pjP0)s\sum_j x_j(p_j - P_0): 도착가 대비 당신의 체결이 지불한 것 — 넘어간 스프레드에 시장 임팩트, 그리고 스케줄 내 드리프트를 더한 것. 이는 실행 알고리즘의 성적표이며, 실행 연구가 실제로 모델링하는 대상이다. Almgren, Thum, Hauptmann, Li(2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58–62)는 이를 씨티그룹 데스크의 미국 주식 주문 약 700,000건(2001년 12월–2003년 6월)에 대해 측정했고, 트레이딩 비용이 일간 변동성 및 참여율에 따라 스케일링됨을 발견했다 — 일시적 임팩트는 거래 속도에 대해 지수가 3/5에 가까운 멱법칙을 따르고, 영구적 임팩트는 크기에 대해 거의 선형이다. 보정할 때 그들의 함수 형태를 재사용할 것이다.

타이밍 리스크(Timing risk): 평균 분해의 항은 아니지만 그 주변의 분산이다. 부모 주문을 시간에 걸쳐 분산하면 기대 임팩트는 줄지만 변동성에 노출된다. 잔여 포지션이 q(t)q(t)인 스케줄에 대해 비용의 표준편차는 σ0Tq(t)2dt\sigma \sqrt{\int_0^T q(t)^2\,dt}로 스케일링된다. 이것이 바로 Almgren–Chriss 프레임워크가 최적화하는 트레이드오프다. TCA 리포트에서는 부모 주문 전반에 걸친 IS의 분산으로 나타난다 — 모든 평균 옆에 표준편차를 보고하라. 그렇지 않으면 평균이 모든 주목을 받고 꼬리(tail)가 당신의 돈을 모두 가져갈 것이다.

기회비용(Opportunity cost) s(Xx)(PTP0)s(X - x)(P_T - P_0): 종가로 표시한 미체결 수량. 패시브 전략의 경우 이는 흔히 가장 크고 가장 덜 검토되는 구성 요소이며, 프레임워크 전체를 정직하게 만드는 항이다 — 이것을 생략하는 것이 사람들이 TCA로 스스로를 속이는 가장 흔한 방식이므로, 함정 섹션에서 더 다룬다.

수수료(Fees) FF: 명시적인 부분. 크립토에서는 할인 이후의 수수료를 로깅하고(VIP 등급, 체결 시점 가격으로 환산한 토큰 리베이트), 메이커 리베이트는 부호를 유지하라 — 음수 수수료는 노이즈가 아니라 데이터다.

예제 풀이

X=10X = 10 BTC 매수. 신호가 Pd=60,000P_d = 60{,}000에서 발화; 종이 명목금액 $600,000. 첫 자식 주문이 미드 P0=60,012P_0 = 60{,}012에서 승인됨. 이후 2분간 8 BTC가 VWAP 60,072에 체결되고, 가격이 추세적으로 달아나며, 알고리즘이 지정가를 준수하여 남은 2 BTC는 미드가 PT=60,240P_T = 60{,}240일 때 취소된다. 체결 명목금액에 대한 혼합 수수료 2.5 bps.

구성 요소 공식 USD 종이 명목금액의 bps
지연 10×(60,01260,000)10 \times (60{,}012 - 60{,}000) 120 2.0
트레이딩 비용 8×(60,07260,012)8 \times (60{,}072 - 60{,}012) 480 8.0
기회 2×(60,24060,012)2 \times (60{,}240 - 60{,}012) 456 7.6
수수료 8×60,072×2.5e-48 \times 60{,}072 \times 2.5\text{e-}4 120 2.0
총 IS 1,176 19.6

거래소 명세서는 $120을 보여준다. 의사결정을 구현하는 실제 비용은 $1,176이었다 — 10배이며, 가장 큰 두 구성 요소는 수수료 기반 회계에는 보이지 않는다. 그 중 대략 40%는 결코 거래되지 않은 수량에서 왔다. 체결만 분석한 TCA 리포트는 이 부모 주문을 8 bps로 채점하고 괜찮다고 할 것이다.

종이 포트폴리오에서 실제 포트폴리오로 이어지는 구현 부족의 워터폴 분해

마크아웃: 역선택의 가격 매기기

구현 부족은 부모 주문을 채점한다. 개별 체결의 품질에 대해서는 아무것도 말하지 않는다 — 구체적으로, 당신이 모르는 무언가를 아는 상대방과 체계적으로 거래하는지에 대해서는. 그것은 **마크아웃(markouts)**으로 측정한다: 각 체결을, 체결이 일어난 뒤 고정된 시계에서의 미드로 표시한다.

πτ=s(mtf+τpf)mtf×104 bps,\pi_\tau = \frac{s\,(m_{t_f + \tau} - p_f)}{m_{t_f}} \times 10^4 \ \text{bps},

여기서 pfp_f는 시각 tft_f의 체결가이고 mtm_t는 시각 tt의 미드다. 이는 시계 τ\tau에서 체결의 단위당 시가평가(mark-to-market) PnL이며, τ=0\tau = 0에서 깔끔한 해부학을 가진다: 메이커 체결은 ++하프 스프레드에서 시작하고(당신은 매수 호가에 샀고 미드는 그 위에 있다), 테이커 체결은 -하프 스프레드에서 시작한다. τ\tau가 커질수록 일어나는 일이 거래의 정보 함량이다:

  • τ=1\tau = 1s는 스나이핑과 오래된 호가 픽오프(stale-quote pickoff)를 분리한다. 메이커 체결이 체결 후 1초 만에 이미 손실 상태라면, 더 빠른 참여자들이 당신의 호가가 잘못 가격 매겨지는 순간에 그것을 때리고 있는 것이다 — 당신의 호가 갱신 루프가 그들의 트리거 루프보다 느리다. 이 마크아웃은 전략 진단이 아니라 지연 진단이다.
  • τ=10\tau = 10s는 고전적 역선택을 측정한다: 체결 후 당신의 가격을 관통하는 지속적 움직임이 뒤따르는 경우. 마켓 메이커에게 이것은 스프레드 캡처가 이겨야 하는 비용이다. 주식 미시구조에서는 관련된 양을 실현 스프레드(realized spread)라 부르며, SEC Rule 605 보고에서 5분 시계로 제도화되어 있다. 크립토는 더 빠르게 움직인다. 10–60s가 이에 상응하는 구간이다.
  • τ=60\tau = 60s는 체결이 미시구조 시계를 넘어 당신에게 불리한 모멘텀을 담고 있는지 알려준다 — 테이커 전략의 경우, 60s에서의 신호 알파가 진입을 위해 지불한 스프레드 플러스 임팩트를 초과하는지. 4-4 bps에서 시작해 결코 0을 넘지 못하는 테이커 마크아웃 곡선은, 알파가 감당할 수 없는 진입을 위해 비용을 지불하는 전략이다.

메이커/테이커 비대칭성은 두 곡선 안에 담긴 패시브 트레이딩의 경제학 전체다. +2+2 bps(하프 스프레드)에서 시작해 60s까지 1-1 bps로 감쇠하는 메이커 곡선이 말하는 것: 당신은 스프레드를 잡지만 그보다 더 많이 되돌려주며, 어떤 리베이트 등급도 그것을 고치지 못한다. 같은 곡선이 +0.7+0.7 bps에 안착하면 호가 엔진이 제 몫을 하고 있는 것이다. 이것은 또한 순진한 터치 체결(touch-fill) 백테스트가 정확히 가정으로 없애버리는 것이다 — 가격이 지정가에 닿을 때마다 당신을 체결시키는 시뮬레이터는 체결된다는 것이 틀린 것과 상관되어 있다는 사실을 무시하며, 이것이 체결 시뮬레이션 사다리의 큐 인식 단계가 가정이 아니라 입력으로서 측정된 마크아웃을 필요로 하는 이유다.

역선택 감쇠를 보여주는 메이커 및 테이커 체결의 시계 대비 마크아웃 곡선

계산은 자신의 미드 스트림에 대한 merge_asof다:

import pandas as pd

def markouts(fills: pd.DataFrame, mids: pd.DataFrame,
             horizons=("1s", "10s", "60s")) -> pd.DataFrame:
    """fills: [ts, price, qty, side, liquidity]; mids: [ts, mid].
    Both UTC-indexed and sorted. Mid stream must be from YOUR captured
    feed, not candles reconstructed later."""
    fills = fills.sort_values("ts").reset_index(drop=True)
    mids  = mids.sort_values("ts")
    out = fills.copy()
    for h in horizons:
        probe = fills[["ts"]].copy()
        probe["ts"] = probe["ts"] + pd.Timedelta(h)
        m = pd.merge_asof(probe, mids, on="ts", direction="backward")
        out[f"mo_{h}"] = (fills["side"] * (m["mid"].values - fills["price"])
                          / fills["price"] * 1e4)
    return out

def markout_report(mo: pd.DataFrame) -> pd.DataFrame:
    """Qty-weighted markouts by liquidity flag. Weighting matters:
    a 0.001 BTC fill and a 2 BTC fill are not equal evidence."""
    cols = [c for c in mo.columns if c.startswith("mo_")]
    def agg(g):
        w = g["qty"] / g["qty"].sum()
        return pd.Series({c: (g[c] * w).sum() for c in cols}
                         | {"n": len(g), "qty": g["qty"].sum()})
    return mo.groupby("liquidity").apply(agg)

심볼별, 시간대별, 미드로부터의 호가 거리별로 리포트를 더 세분화하라. 메이커 전략에서 가장 실행 가능한 절단은 체결 시점 큐 포지션별 마크아웃이다: 신선한 큐의 앞쪽(front-of-fresh-queue) 체결은 레벨이 당신을 관통해 쓸려나간 체결과 매우 다르게 가격 매겨진다.

파이프라인: 무엇을 로깅할 것인가

TCA는 수학 계층이 아니라 로깅 계층에서 죽는다. 위의 수학은 대부분의 봇이 버리는 숫자를 필요로 하며, 그 중 어느 것도 나중에 거래소 히스토리에서 재구성할 수 없다. 타협 불가능한 것들:

  • 신호 시점에, 자신의 피드에서 나온 의사결정 미드. 거래소 캔들 종가가 아니고, 나중의 재구성도 아니다. 벤치마크는 "내 전략이 결정할 때 믿었던 가격"이다 — 그 순간의 당신의 프로세스만이 그것을 안다.
  • 두 타임스탬프 모두: 의사결정 시점과 첫 승인(first-ack) 시점, 그렇지 않으면 지연 비용은 측정 불가능하며 조용히 트레이딩 비용에 병합된다.
  • 거래소 타임스탬프, 수수료, 메이커/테이커 플래그가 있는 모든 체결 — 당신의 로컬 수신 시각은 자체 인바운드 지연으로 오염되어 있다.
  • 취소되고 만료된 부모 주문을, 다른 모든 것과 마찬가지로 로깅. 체결이 0인 부모 주문이 테이블에서 가장 비싼 행이다.
  • 100–250ms 세분성의 지속적 미드 스트림(또는 L1 스트림), 마크아웃을 계산할 만큼 충분히 오래 보존. 이미 체결 시뮬레이터를 위해 오더북을 기록하고 있다면 이것은 공짜다.

두 개의 플랫 테이블이면 충분하다:

PARENTS = {
    "parent_id":    "str",
    "strategy":     "str",
    "symbol":       "str",
    "venue":        "str",
    "algo":         "str",        # twap | pov | sniper | quote | ...
    "side":         "int8",       # +1 buy, -1 sell
    "qty":          "float64",    # parent size, base units
    "limit_px":     "float64",    # NaN for unconstrained
    "decision_ts":  "datetime64[ns, UTC]",
    "decision_mid": "float64",    # mid your feed showed at decision_ts
    "arrival_ts":   "datetime64[ns, UTC]",   # first exchange ack
    "arrival_mid":  "float64",
    "end_ts":       "datetime64[ns, UTC]",   # filled / cancelled / expired
    "terminal_mid": "float64",
    "mkt_volume":   "float64",    # market volume over [arrival_ts, end_ts]
    "sigma_bps":    "float64",    # realized vol estimate at decision time
}

FILLS = {
    "parent_id": "str",
    "ts":        "datetime64[ns, UTC]",  # exchange timestamp
    "price":     "float64",
    "qty":       "float64",
    "fee":       "float64",      # quote ccy, post-discount, signed
    "liquidity": "str",          # maker | taker
    "venue":     "str",
}

귀속(attribution) 함수는 분해를 그대로 옮긴 것이다:

import numpy as np

def is_decomposition(p: pd.Series, fills: pd.DataFrame) -> dict:
    """Expanded implementation shortfall for one parent, bps of paper
    notional. Sign convention: positive = cost."""
    s, X = p["side"], p["qty"]
    paper = X * p["decision_mid"]
    x = fills["qty"].sum()
    delay = s * X * (p["arrival_mid"] - p["decision_mid"])
    trade = s * ((fills["price"] - p["arrival_mid"]) * fills["qty"]).sum()
    oppty = s * (X - x) * (p["terminal_mid"] - p["arrival_mid"])
    fees  = fills["fee"].sum()
    bps   = lambda v: 1e4 * v / paper
    return {
        "parent_id": p["parent_id"],
        "delay_bps": bps(delay), "trade_bps": bps(trade),
        "oppty_bps": bps(oppty), "fees_bps":  bps(fees),
        "is_bps":    bps(delay + trade + oppty + fees),
        "fill_ratio": x / X,
        "participation": x / max(p["mkt_volume"], x),
    }

def tca_table(parents: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
    fg = dict(tuple(fills.groupby("parent_id")))
    empty = fills.iloc[0:0]
    rows = [is_decomposition(p, fg.get(p["parent_id"], empty))
            for _, p in parents.iterrows()]
    return parents.merge(pd.DataFrame(rows), on="parent_id")

그리고 귀속 쿼리들, 여기가 TCA가 회계이기를 멈추고 연구가 되기 시작하는 지점이다:

tca = tca_table(parents, fills)

def wavg(g: pd.DataFrame, col: str) -> float:
    w = g["qty"] * g["decision_mid"]          # notional weights
    return (g[col] * w).sum() / w.sum()

comp = ["delay_bps", "trade_bps", "oppty_bps", "fees_bps", "is_bps"]
by_strat = tca.groupby("strategy").apply(
    lambda g: pd.Series({c: wavg(g, c) for c in comp}
                        | {"is_std": g["is_bps"].std(), "n": len(g)}))

done = tca[tca["fill_ratio"] > 0.99]
done["pov_bin"] = pd.qcut(done["participation"], 6)
impact_curve = done.groupby("pov_bin").apply(lambda g: wavg(g, "trade_bps"))

tca["hour"] = tca["arrival_ts"].dt.hour
by_hour = tca.groupby("hour").apply(lambda g: wavg(g, "is_bps"))

by_venue = tca.groupby("venue").apply(
    lambda g: pd.Series({"is_bps": wavg(g, "is_bps"),
                         "is_std": g["is_bps"].std(),
                         "fill_ratio": g["fill_ratio"].mean(), "n": len(g)}))

쿼리 4는 스마트 주문 라우터(smart order router)가 최적화하는 입력 테이블이다 — 거래소별 TCA 없는 라우팅은 수수료 스케줄에 의한 라우팅, 즉 가장 작은 비용 구성 요소에 의한 라우팅이다(크립토에서의 스마트 주문 라우팅은 이 테이블을 출발점으로 삼는다). 마크아웃 모듈과 함께, 이것이 약속했던 약 200줄이다.

루프 닫기: 백테스트 비용 모델 보정하기

여기가 파이프라인이 그 값을 하는 지점이다. 백테스트는 숫자를 단언한다: slippage_bps = 5, 체결 확률 곡선, 임팩트 계수. 그 하나하나가 라이브 실행에 대한 주장이고, TCA 테이블이 곧 라이브 실행이다. 루프: TCA로 측정하고, 비용 모델을 적합(fit)하고, 적합된 모델로 백테스트를 돌리고, 트레이딩하고, 다시 측정한다.

라이브 체결에서 TCA 귀속을 거쳐 백테스트 비용 모델로 이어지는 닫힌 보정 루프

테이커 비용의 경우, 새로 발명하는 대신 Almgren et al.(2005)에서 함수 형태를 빌려 오라. 그들의 결과 — 비용이 일간 변동성 곱하기 참여율의 멱승에 비례한다 — 는 2-파라미터 모델을 준다:

trade_bpsσbps=a(xV)b,\frac{\text{trade\_bps}}{\sigma_{\text{bps}}} = a \cdot \left(\frac{x}{V}\right)^{b},

주식에서 추정된 b0.6b \approx 0.6을 사전분포(prior)로 삼는다. 원시 부모 주문이 아니라 비닝된 평균으로 적합하라 — 개별 부모 주문 비용은 노이즈가 지배하고(그것이 타이밍 리스크 항이다), 원시 데이터에 대한 로그-로그 회귀는 기꺼이 노이즈를 적합해버린다:

done = tca[(tca["fill_ratio"] > 0.99) & (tca["participation"] > 0)]
done["norm_cost"] = done["trade_bps"] / done["sigma_bps"]
bins = done.groupby(pd.qcut(done["participation"], 8)).agg(
    pov=("participation", "mean"), cost=("norm_cost", "mean"))
bins = bins[bins["cost"] > 0]                # can't log a negative bin
b, log_a = np.polyfit(np.log(bins["pov"]), np.log(bins["cost"]), 1)

def taker_cost_bps(participation, sigma_bps, a=np.exp(log_a), b=b):
    return a * sigma_bps * participation ** b

그다음 오차 막대(error bar)에 대해 정직하라. Almgren의 팀은 700,000건의 주문을 가지고도 일시적 임팩트 지수를 0.600±0.0380.600 \pm 0.038로 보고했다. 2,000개의 부모 주문을 가진 봇이 심볼별로 bb를 자유롭게 추정하지는 못한다. 실용적 방식: σ\sigma로 정규화한 뒤 심볼 전반에 걸쳐 풀링하고, bb를 0.6 쪽으로 강하게 수축(shrink)하고(또는 그냥 고정하고 aa만 적합하고), 매월 재적합하라. 적합된 aa가 위로 드리프트한다면, 당신의 발자국(footprint)이 커졌거나 시장이 얇아진 것이다 — 어느 쪽이든 백테스트가 알아야 했다.

메이커 전략의 경우 보정 대상이 다르며 다른 두 모듈에서 나온다:

  • 체결 확률: 체결 시뮬레이터의 큐 모델은 (스프레드 거리, 큐 포지션) 셀별로 P(fill)P(\text{fill})을 예측한다. 당신의 부모 주문 로그는 셀별 실현 체결 비율을 제공한다. 불일치는 격자 참조가 붙은 시뮬레이터 버그다.
  • 역선택: 시뮬레이터의 암묵적 "체결은 교환 가능하다"는 가정을, 측정된 마크아웃 테이블로 대체하라 — 시각 hh에서 거리 dd의 시뮬레이션된 메이커 체결은 측정된 π10s(d,h)\pi_{10s}(d, h)를 즉각적 시가평가 헤어컷으로 담는다. 이 단 하나의 변경이 환각하는 메이커 백테스트와 실제를 추적하는 백테스트의 차이다. 이는 체결 시뮬레이션 글에서 표시했던 빠진 보정 입력이었다.
  • 비용 분산: IS의 평균이 아니라 분포를 백테스트에 공급하라. 평균만 이동시키는 비용 모델은 타이밍 리스크가 만들어내는 드로다운을 재현할 수 없다. 부모 주문별 IS에 적합된 로그정규(lognormal)조차 상수보다 낫다.

전체 슬리피지 곡선 처리 — 함수 형태, 레짐 조건화, 멱법칙이 깨지는 시점 — 는 그 자체로 별개의 글이다: 백테스트를 위한 슬리피지 곡선과 비용 모델. 여기서의 요점은 아키텍처적이다: 그 글의 모델들은 이 글의 테이블 없이는 적합 불가능하다.

함정

TCA 문헌은 수십 년 되었고, 그것을 조작하는 방법들도 마찬가지다. 세 가지 실패 모드가 대부분의 자기기만을 차지한다.

벤치마크 조작(Benchmark gaming). 도착가 외의 어떤 벤치마크든 껴안을 수 있다. 고전적인 것은 VWAP다: 구간 VWAP에 대해 채점되는 알고리즘은 포지션이 도착가 대비 20 bps 출혈하는 동안에도 VWAP을 1 bp 이내로 추적할 수 있는데, 벤치마크가 당신이 밀어붙이고 있는 가격을 따라 드리프트하기 때문이며 — 유의미한 참여율에서는 당신 자신의 프린트가 VWAP이므로, 그것을 추적하는 것은 자가 채점 숙제다. 우리는 벤치마크 정치학을 TWAP 대 VWAP 대 POV에서 해부했다. TCA 쪽 규칙은 더 단순하다: 벤치마크는 트레이딩 전에 선택되며, 스케줄러가 자신의 스케줄링 벤치마크에 대해 채점될 때조차 IS-대-도착가는 항상 계산된다. 더 미묘한 변종은 도착 조작이다: decision_ts를 설정하는 구성 요소가 단기 모멘텀을 볼 수 있다면, 지연 항을 미화하도록 "의사결정"의 타이밍을 잡을 수 있다. 의사결정 타임스탬프는 신호 계층에 속하며, 어떤 실행 로직이 실행되기 전에 로깅되어야 한다.

체결만 분석하는 생존자 편향(Survivorship bias). 비용 분석을 체결에 조건화하면 패시브 실행은 공짜처럼 보인다. 구체적으로: 미드보다 한 틱 아래에 낸 패시브 매수 부모 주문 100개. 60개가 체결되고 — 패시브이므로 — 도착가보다 평균 3 bps 더 좋은 가격에 체결된다: 측정된 "비용" 3-3 bps, 자랑스러워할 만한 리포트. 결코 체결되지 못한 40개는 정확히 가격이 위로 떠올라간 경우였다. 그것들을 취소 시점에 25 bps 불리하게 표시하면 정직한 숫자는 0.6×(3)+0.4×25=+8.20.6 \times (-3) + 0.4 \times 25 = +8.2 bps다. 체결만 분석한 리포트와 참 리포트는 11 bps 만큼, 그리고 부호까지 다르다. 이것은 극단적 사례가 아니다 — 이것이 패시브 트레이딩의 메커니즘이다: 체결된다는 것은 가격이 당신을 관통해 오는 것과 상관되어 있으며, 이는 터치 체결 백테스트를 환상으로 만드는 것과 같은 조건화다. 기회비용은 IS의 선택적 개선이 아니다. 그것은 전체 측정을 선택 편향으로부터 방어하는 항이다. 같은 편향에는 테이커 변종이 있다: 놓친 IOC 주문, 레이트 리밋이나 리스크 체크로 거부된 주문 — 놓침이 로깅되지 않으면 놓침의 비용은 측정되지 않으며, 그것은 정확히 당신의 전략이 그 거래를 가장 원했던 빠른 시장에서 가장 크다.

잡다한 자충수(foot-guns), 간략히: 거래소 캔들에서 재구성한 도착 미드(당신의 피드와 캔들은 정확히 중요한 순간에 불일치한다); 명목금액 가중치 없이 부모 주문별 bps를 평균 내기(천 개의 먼지(dust) 체결이 하나의 진짜 주문을 표수로 이겨버리기); 할인 전 수수료 로깅; 체결과 다른 거래소에서 취한 마크아웃 미드(역선택으로 가장한 거래소 간 베이시스); 그리고 무기한 선물(perp)에서는 펀딩 누적이 실행 윈도우로 새어들게 두기 — 펀딩은 비용이지만 실행 비용은 아니며, 둘을 섞으면 두 분석 모두를 오염시킨다.

이번 주에 할 일

봇에 두 개의 로그 테이블을 추가하라 — 위의 스키마는 컬럼 목록이지 프로젝트가 아니다. 아무것도 백필(backfill)하지 마라. 2주간의 정직한 로그가 1년치의 재구성보다 낫다. 분해와 세 개의 마크아웃 시계를 실행하라. 어떤 구성 요소가 지배하는지(거의 결코 수수료가 아니다), 당신의 패시브 체결이 스프레드 캡처를 넘어 역선택되는지, 그리고 백테스트의 비용 상수가 측정된 곡선에서 얼마나 떨어져 있는지 알게 될 것이다. 그다음 적합된 곡선을 시뮬레이터에 다시 연결하고, 애초에 이 전략을 돌리라고 말했던 그 백테스트를 재실행하라. Perold의 종이 포트폴리오는, 38년이 지난 지금도, 당신의 실제 포트폴리오가 가진 유일한 정직한 상대다.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

시장에서 앞서 나가세요

뉴스레터를 구독하여 독점적인 AI 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

귀하의 개인정보를 존중합니다. 언제든지 구독을 취소할 수 있습니다.