본문 바로가기
inpilot.dev

수수료를 넣자 전략이 죽었다 — 슬리피지·비용 모델링

2026-04-08 · 3 min read

$ backtest --cost-bps 20

CAGR 30% → 4%

비용 0으로 돌린 백테스트는 백테스트가 아니다

일봉 전략을 돌렸을 때 연 30%가 나왔다. 비용을 넣었더니 연 4%가 됐다. 거래 빈도가 높을수록 이 차이가 커진다. 하루 한 번 리밸런싱하는 전략은 비용이 수익의 대부분을 먹는다.

문제는 이걸 안 넣어도 백테스트가 잘 돌아간다는 것이다. 숫자만 예쁘게 나오고, 실계좌에 붙였을 때 비로소 드러난다.

국내 주식 왕복 비용 구성

항목대략언제
위탁수수료증권사·이벤트마다 다름. 0%~0.015% 수준매수·매도 양쪽
증권거래세매도 시 부과. 세율은 시장·연도별로 다름매도만
스프레드호가 단위의 절반 정도가 기본체결 시
시장충격주문 크기 / 유동성에 비례크게 낼수록

여기서 두 가지를 강조하고 싶다.

세금은 매도에만 붙는다. 왕복 비용을 "수수료 × 2"로 계산하면 과소평가된다. 그리고 세율·수수료율은 계속 바뀌므로 코드에 상수로 박지 말고 설정으로 뺀다. 내 백테스트 설정 파일에는 이렇게 들어 있다.

costs:
  commission_bps: 1.5      # 왕복 각각
  tax_sell_bps: 20         # 매도만. 현재 기준값을 확인해서 넣을 것
  half_spread_bps: 5

숫자 자체보다 어떤 값을 썼는지 결과에 같이 기록하는 게 중요하다. 비용 가정이 다른 두 백테스트를 비교하면 아무 의미가 없다.

스프레드: 체결가를 불리하게 민다

중간가에 체결됐다고 가정하면 안 된다. 사면 매도호가에, 팔면 매수호가에 닿는다.

def fill_price(mid: float, side: str, half_spread_bps: float) -> float:
    sign = 1 if side == "buy" else -1
    return mid * (1 + sign * half_spread_bps / 10_000)

국내 주식은 가격대별로 호가 단위가 다르다. 저가주는 1원 단위라 1,000원짜리 종목의 한 틱이 10bp다. 스프레드를 고정 bp로 두면 저가주에서 과소평가된다. 가격대별 호가 단위를 반영하는 게 더 정확하다.

시장충격: 크게 사면 더 비싸게 산다

내 주문이 호가를 밀어 올린다. 소액이면 무시해도 되는데, 유동성 낮은 종목에서는 이게 제일 큰 비용이다.

간단한 근사로 제곱근 모델을 쓴다.

def impact_bps(order_value: float, adv: float, k: float = 10) -> float:
    """adv = 최근 평균 거래대금"""
    return k * math.sqrt(order_value / adv)

k는 추정치라 정확하지 않다. 그래서 나는 이걸 정밀한 예측이 아니라 필터로 쓴다. 하루 거래대금의 일정 비율을 넘는 주문은 아예 못 내게 막는다.

MAX_PARTICIPATION = 0.01     # ponytail: 거래대금의 1%까지만. 실측 후 조정
if order_value > adv * MAX_PARTICIPATION:
    order_value = adv * MAX_PARTICIPATION

이 제한이 없으면 백테스트가 "일 거래대금 3천만원짜리 종목을 1억어치 매수" 같은 불가능한 거래로 수익을 만든다. 유동성 필터를 안 걸면 백테스트 수익의 상당 부분이 살 수 없는 종목에서 나온다.

체결 자체를 못 할 수도 있다

지정가 주문은 가격이 안 오면 체결이 안 된다. 백테스트에서 전량 체결을 가정하면 실전보다 유리해진다. 최소한 두 케이스는 처리해야 한다.

  • 상한가/하한가: 그날 거래 자체가 안 되거나 한 방향만 체결
  • 거래정지: 봉이 없다. 청산 신호가 나와도 못 판다
if bar.halted or bar.close >= bar.upper_limit:
    return Fill(qty=0, reason="unfillable")

"못 판 채로 다음 날로 넘어가는" 경로가 백테스트에 없으면, 리스크가 실제보다 작게 측정된다.

민감도로 확인한다

정확한 비용을 맞추려 애쓰는 것보다, 비용에 얼마나 민감한지를 보는 게 낫다.

for bps in [0, 10, 20, 30, 50]:
    r = backtest(strategy, cost_bps=bps)
    print(f"{bps:>3}bp → CAGR {r.cagr:6.1%}  MDD {r.mdd:6.1%}  거래 {r.trades}")

비용 10bp만 올려도 수익이 음수가 되는 전략은, 비용 추정이 조금만 틀려도 실전에서 손실이다. 비용 가정에 둔감한 전략만 살린다. 이 표 하나가 "이 전략을 실계좌에 붙일지" 판단을 거의 결정해준다.

검증

def test_cost_always_hurts():
    """비용은 어떤 방향이든 수익을 깎아야 한다"""
    assert backtest(s, cost_bps=20).cagr < backtest(s, cost_bps=0).cagr
 
def test_sell_taxed_buy_not():
    assert cost_of(side="sell", value=1_000_000) > cost_of(side="buy", value=1_000_000)

첫 테스트가 실패한다면 비용이 어딘가에서 부호가 뒤집혀 수익으로 잡히고 있는 것이다. 웃긴 버그 같지만 실제로 났었다.

한 줄 요약

수수료·매도세·스프레드·충격을 다 넣고, 거래대금 참여율을 제한하고, 비용 민감도 표로 전략을 거른다.