개발/주식 자동매매

주식 자동매매 봇 만들기 2편: 모멘텀 전략 10년 백테스트, 결과는 지수도 못 이김

bboxnote 2026. 10. 15. 23:40

봇을 돌리기 전에 먼저 해야 할 일이 있습니다. 이 규칙대로 과거에 사고팔았다면 어땠을지 확인하는 백테스트입니다. 첫 전략으로는 많이들 쓰는 모멘텀(추세추종)을 골랐고, 솔직히 당연히 잘 나올 줄 알았습니다.

아래 숫자는 모두 과거 데이터로 계산한 결과입니다. 같은 수익이 앞으로도 난다는 뜻이 아닙니다.

한눈에 보기

  • 과거 주가는 네이버 차트 데이터로 무료로 받았습니다. 한 번에 일봉 3,000개(약 12년)까지 됩니다.
  • 백테스트는 내 PC 대신 GitHub Actions에서 돌렸습니다. 300종목 10년치가 몇 분이면 끝납니다.
  • 결과는 연 +14.9%로, KODEX 200을 그냥 들고 있는 것(+15.5%)보다 못했습니다. 중간에 -63.9%까지 빠지기도 했습니다.

데이터는 네이버에서

과거 주가는 네이버 차트 데이터로 받았습니다. 한 번에 일봉 3,000개, 대략 12년치까지 받을 수 있고 무료입니다. 받은 건 CSV로 저장해 두고 다시 받지 않습니다.

백테스트는 GitHub Actions에서 돌렸습니다. 코드를 올리면 GitHub 서버가 데이터를 받아 백테스트를 돌리고, 결과 리포트를 별도 브랜치(backtest-results)에 올려 줍니다. 약 300종목 10년치가 몇 분이면 끝나고, 결과가 저장소에 쌓이니 나중에 비교하기도 편합니다.

전략: 오르는 종목을 따라 산다

규칙은 이렇습니다.

  • 최근 6개월(마지막 1개월은 빼고) 가장 많이 오른 종목 10개를 삽니다.
  • 종목이 100일선 아래면 사지 않습니다.
  • KODEX 200이 200일선 아래, 즉 하락장이면 새로 사지 않습니다.
  • 매주 순위를 다시 보고, 20위 밖으로 밀리면 팝니다.
  • -10%면 손절합니다.

비용도 현실적으로 넣었습니다. 수수료 0.015%, 팔 때 증권거래세 0.20%, 원하는 값에 바로 체결되지 않아 생기는 손해(슬리피지) 0.1%입니다.

결과: 지수를 못 이겼습니다

2016.1 ~ 2026.10 모멘텀 전략 KODEX 200 보유
연평균 수익률 +14.9% +15.5%
최대 낙폭 -63.9% -41.2%
샤프 지수 0.59 0.69
매매 횟수 748 0
승률 34% -

수익은 비슷한데, 중간에 반 토막 넘게 빠지는 구간이 있었습니다. 748번이나 사고판 결과가 KODEX 200 하나 사 두고 잊어버린 것보다 못했다는 얘기입니다. 보유 종목 수, 모멘텀 기간, 리밸런싱 주기를 바꿔 12가지 조합을 돌려 봐도 지수를 확실히 이기는 건 없었습니다.

연도별로 보면 이 전략의 성격이 확실히 보입니다.

연도 전략 KODEX 200
2017 +54.7% +27.1%
2018 +1.8% -17.3%
2022 0.0% -24.2%
2023 +62.0% +24.9%
2025 +56.5% +94.2%
2026 -5.6% +44.0%

하락장(2018, 2022)은 잘 피했는데, 지수가 크게 오른 해(2025, 2026)에는 오히려 한참 뒤처졌습니다.

여기서 배운 것

  • "모멘텀은 당연히 되겠지"는 돌려 보기 전의 착각이었습니다.
  • 한 전략의 숫자를 계속 만지면 과거 데이터에만 딱 맞는 결과가 나오기 쉽습니다. 그래서 설정을 더 만지는 대신 성격이 다른 전략들을 같은 조건으로 붙여 보기로 했습니다.

코드로 보면

네이버 차트 데이터는 주소 하나로 받습니다. 응답이 XML이라 item 태그만 정규식으로 뽑았습니다.

CHART_URL = "https://fchart.stock.naver.com/sise.nhn"
_ITEM = re.compile(r'<item data="([^"]+)"')


def fetch_daily_bars(symbol, count=3000):
    res = requests.get(
        CHART_URL,
        params={"symbol": symbol, "timeframe": "day", "count": count, "requestType": 0},
        headers={"User-Agent": "Mozilla/5.0"},
        timeout=20,
    )
    bars = []
    for raw in _ITEM.findall(res.content.decode("euc-kr", errors="replace")):
        day, o, h, low, c, v = raw.split("|")
        if int(v) == 0:  # 거래정지일은 0 으로 와서 뺀다
            continue
        bars.append(Bar(day, float(o), float(h), float(low), float(c), int(v)))
    return bars

모멘텀 점수는 "1개월 전 가격 / 6개월 전 가격 - 1"입니다. 100일선 아래거나 거래대금이 부족한 종목은 점수를 매기기 전에 거릅니다.

def ranking(self, panel, i, universe):
    """i 일 종가까지의 정보로 매수 후보를 점수 순으로 돌려준다."""
    p = self.p  # lookback=126(6개월), skip=21(1개월), stock_ma=100
    scored = []
    for s in universe:
        c = panel.close[s]
        now, past, base = c[i], c[i - p.skip], c[i - p.lookback]
        ma = sma(c, i, p.stock_ma)
        if ma is None or now <= ma:      # 100일선 아래면 제외
            continue
        mom = past / base - 1            # 최근 1개월을 뺀 6개월 수익률
        if mom > 0:
            scored.append((mom, s))
    scored.sort(reverse=True)
    return [s for _, s in scored]

백테스트 엔진에서 제일 신경 쓴 건 신호는 오늘 종가로 계산하고, 주문은 다음 날 시가에 체결하는 규칙입니다. 오늘 종가를 보고 오늘 종가에 산 것처럼 계산하면, 실제로는 할 수 없는 매매가 섞여서 결과가 좋아 보입니다.

다음 편에서는 8가지 전략을 비교합니다. 그중 하나는 연 +32%가 나왔는데, 기뻐할 일이 아니었습니다.

주식 자동매매 봇 만들기