4 minute read

지난 글에서 절대 모멘텀 백테스트를 만들면서, 이 코드에 빠진 것 네 가지를 적어뒀습니다. 오늘은 그중 첫 번째인 거래 비용을 채워 넣습니다. 백테스트에서 비용을 빼먹는 것은 초보만의 실수가 아닙니다. 논문에서도 비용을 반영하면 사라지는 전략이 수두룩합니다.

거래 비용은 세 겹이다

“수수료 얼마 안 되는데”라고 생각하기 쉽지만, 실제로 계좌에서 빠져나가는 돈은 세 종류입니다.

구분 성격 특징
위탁수수료 증권사에 내는 돈 증권사·계좌 조건에 따라 다름. 협상 가능
세금 매도 시 부과 요율과 대상이 제도 변경에 따라 바뀜
슬리피지 시장에 내는 보이지 않는 돈 명세서에 안 찍히지만 가장 클 수 있음

앞의 둘은 명세서에 숫자로 찍히니 확인하기 쉽습니다. 문제는 세 번째입니다.

주의할 점 하나. 수수료율과 세율은 증권사와 제도에 따라 다르고 시간이 지나면 바뀝니다. 아래 코드에서 쓰는 숫자는 설명을 위한 가정치이며, 실제로 쓰실 때는 본인 계좌의 수수료율과 현재 시행 중인 세율을 직접 확인해서 넣으셔야 합니다. 백테스트의 정확도는 이런 확인에서 나옵니다.

슬리피지: 명세서에 없는 비용

슬리피지(slippage)는 내가 기대한 가격과 실제 체결된 가격의 차이입니다. 원인은 크게 둘입니다.

첫째, 호가 스프레드입니다. 매수 호가와 매도 호가 사이에는 항상 틈이 있습니다. 사자마자 팔면 그 틈만큼 손해입니다. 유동성이 풍부한 대형주나 ETF는 이 틈이 얇지만, 거래가 뜸한 소형주는 한 호가만 밀려도 0.5% 이상 벌어질 수 있습니다.

둘째, 시장 충격(market impact)입니다. 내 주문 자체가 가격을 밀어 올립니다. 100만 원어치 사는 것과 10억 원어치 사는 것은 다릅니다. 개인 투자자 규모에서는 대형주라면 무시할 만하지만, 소형주를 다루거나 종가에 몰아서 주문하는 전략이라면 무시할 수 없습니다.

백테스트는 보통 “종가에 정확히 체결됐다”고 가정합니다. 현실에서 종가에 정확히 사는 것은 불가능합니다. 이 간극이 슬리피지입니다.

비용을 코드에 넣기

지난 글의 절대 모멘텀 백테스트에 비용을 붙여 봅니다. 핵심은 매매가 실제로 일어난 시점을 찾아내는 것입니다.

import FinanceDataReader as fdr
import pandas as pd

# 지난 글과 동일한 세팅
kospi = fdr.DataReader('KS11', '2005-01-01')['Close']
monthly = kospi.resample('ME').last()
returns = monthly.pct_change()

signal = (monthly.pct_change(12) > 0).astype(int)
position = signal.shift(1)          # 미래 참조 방지

# 포지션이 바뀐 달 = 매매가 일어난 달
trades = position.diff().abs().fillna(0)

# 편도 비용 가정치 (수수료 + 세금 + 슬리피지). 반드시 본인 조건으로 교체할 것
COST = 0.003   # 0.3%

gross = position * returns
net = gross - trades * COST

position.diff().abs()가 이 코드의 핵심입니다. 포지션이 0에서 1로(매수) 또는 1에서 0으로(매도) 바뀐 달에만 1이 되고, 그대로 유지된 달에는 0입니다. 즉 실제로 주문을 낸 달에만 비용을 뺍니다. 매달 무조건 비용을 빼면 과도하게 보수적인 백테스트가 되고, 아예 안 빼면 낙관적인 백테스트가 됩니다.

얼마나 차이가 나는지 직접 보기

비용 전후를 나란히 놓고 비교합니다.

result = pd.DataFrame({
    'Buy & Hold': returns,
    'Momentum (gross)': gross,
    'Momentum (net)': net
}).dropna()

cum = (1 + result).cumprod()
n_years = len(result) / 12
cagr = cum.iloc[-1] ** (1 / n_years) - 1

# 연평균 매매 횟수 (회전율)
n_trades = trades.loc[result.index].sum()
print(f"총 매매 횟수: {n_trades:.0f}회, 연평균: {n_trades / n_years:.1f}회")
print(cagr.round(4))

cum.plot(figsize=(12, 5), title='Trading Cost Impact')

여기서 봐야 할 숫자가 연평균 매매 횟수입니다. 비용이 성과를 갉아먹는 정도는 대략 이렇게 계산됩니다.

연간 비용 부담 ≈ 연평균 매매 횟수 × 편도 비용

연 4회 매매에 편도 0.3%면 연 1.2%입니다. CAGR이 8%인 전략이라면 성과의 15%가 비용으로 사라지는 셈입니다. 매달 종목을 갈아치우는 상대 모멘텀 전략이라면 연 24회(매수 12 + 매도 12)까지 갈 수 있고, 그러면 연 7% 이상이 비용입니다. 이 지점에서 죽는 전략이 정말 많습니다.

실제 결과

같은 코스피 데이터(2005년 1월~2026년 8월)로 비용 전후를 비교한 결과입니다.

  CAGR
Buy & Hold 9.66%
모멘텀 (비용 전) 5.91%
모멘텀 (편도 0.3% 반영) 5.37%

총 매매 횟수는 21년 반 동안 37회, 연평균 1.71회였습니다. 비용으로 깎인 성과는 연 0.54%p인데, 앞에서 말한 어림셈 “연평균 매매 횟수 × 편도 비용 = 1.71 × 0.3% = 0.51%p”와 거의 일치합니다. 어림셈이 쓸 만하다는 뜻입니다.

절대 모멘텀은 매매가 드문 전략이라 비용 타격이 작은 편입니다. 그래도 5.91%에서 5.37%로, 원래도 Buy & Hold에 뒤지던 성과가 더 뒤집니다. 매달 종목을 갈아치우는 전략이었다면 연 20회 넘는 매매에 연 6%p 이상이 비용으로 사라졌을 겁니다. 비용을 빼먹은 백테스트가 왜 위험한지, 숫자로 확인한 셈입니다.

비용을 줄이는 방향

비용을 알고 나면 전략 설계가 달라집니다.

  • 매매를 줄인다 — 신호가 애매할 때 무조건 갈아타지 않고 완충 구간을 두는 방법이 있습니다. 예를 들어 “수익률 > 0”이 아니라 “수익률 > +1%면 매수, < -1%면 매도, 그 사이면 유지”로 바꾸면 휩쏘 구간의 잦은 매매가 줄어듭니다.
  • 유동성 있는 자산을 쓴다 — 거래대금이 큰 ETF나 대형주는 슬리피지가 작습니다.
  • 리밸런싱 주기를 늘린다 — 월 1회를 분기 1회로 바꾸면 비용이 3분의 1이 됩니다. 대신 신호 반응이 느려집니다. 이 맞교환을 데이터로 비교해 보는 것 자체가 좋은 백테스트 연습입니다.
  • 비용을 보수적으로 잡는다 — 실제보다 조금 크게 잡아서도 살아남는 전략만 채택합니다. 비용을 낙관적으로 잡아 통과시킨 전략은 실전에서 반드시 실망시킵니다.

정리

  • 거래 비용은 수수료, 세금, 슬리피지 세 겹이고, 명세서에 안 찍히는 슬리피지가 종종 가장 크다.
  • 비용은 position.diff().abs()실제 매매가 일어난 시점에만 부과해야 한다.
  • 성과 훼손 정도는 대략 “연평균 매매 횟수 × 편도 비용”이며, 회전율이 높은 전략일수록 치명적이다.
  • 수수료율·세율은 조건과 제도에 따라 다르므로 반드시 본인 기준으로 확인해서 넣는다.

다음 글에서는 두 번째 빠진 조각, 과최적화(오버피팅)를 다룹니다. 지난 글에서 모멘텀 기간을 12개월로 정한 것은 제 임의였습니다. 6개월, 9개월, 3개월을 다 돌려보고 제일 좋은 것을 고르면 어떻게 될까요? 그게 왜 위험한지 같은 코드로 직접 확인해 보겠습니다.


이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.

Updated: