Skip to main content

모멘텀 전략 만들기

Views 43약 7분생성형 AI 활용
Summary

오른 주식은 계속 오를까?

이동평균선 전략은 한 종목의 가격 흐름을 보고 매수와 매도를 결정했다. 이번 장에서는 관점을 넓혀 여러 종목 중 무엇을 살지 고르는 전략을 만든다.

모멘텀 전략의 출발점은 단순하다.

최근에 강했던 종목이 앞으로도 강할 가능성이 있는가?

예를 들어 최근 6개월 동안 A 종목은 30% 올랐고, B 종목은 5% 올랐고, C 종목은 10% 하락했다고 하자. 모멘텀 전략은 보통 A처럼 최근 수익률이 높은 종목에 더 높은 점수를 준다. 시장에서 이미 강하게 움직인 종목을 따라가는 전략이다.

물론 이 말이 항상 맞는 것은 아니다. 많이 오른 종목은 더 오를 수도 있지만, 이미 너무 올라서 하락할 수도 있다. 그래서 우리는 이 아이디어를 믿는 대신 검증해야 한다. 이번 장의 목표는 "강한 종목을 산다"는 말을 코드로 바꾸고, 백테스트할 수 있는 형태로 만드는 것이다.

모멘텀 전략 만들기AI 생성 이미지

모멘텀을 숫자로 바꾸기

모멘텀은 보통 일정 기간의 수익률로 측정한다. 3개월 수익률, 6개월 수익률, 12개월 수익률이 대표적이다.

일봉 데이터를 사용한다면 대략 다음처럼 계산할 수 있다.

mom_3m = prices.pct_change(63)
mom_6m = prices.pct_change(126)
mom_12m = prices.pct_change(252)

여기서 prices는 날짜를 인덱스로, 종목 코드를 열로 가진 수정종가 데이터프레임이라고 가정한다. 거래일 기준으로 한 달을 약 21일로 보면 3개월은 63일, 6개월은 126일, 12개월은 252일 정도다.

예를 들어 어떤 종목의 현재 가격이 13만 원이고 6개월 전 가격이 10만 원이라면 6개월 모멘텀은 30%다.

momentum = current_price / past_price - 1

모멘텀 점수는 하나의 기간만 사용할 수도 있고, 여러 기간을 평균내서 만들 수도 있다.

momentum_score = (mom_3m + mom_6m + mom_12m) / 3

여러 기간을 함께 쓰면 특정 기간 하나에 지나치게 의존하는 문제를 줄일 수 있다. 다만 기간을 많이 섞는다고 무조건 좋아지는 것은 아니다. 기간 조합도 하나의 가정이다. 결국 검증 대상이다.

상대 모멘텀과 절대 모멘텀

모멘텀 전략에는 크게 두 가지 관점이 있다. 상대 모멘텀과 절대 모멘텀이다.

상대 모멘텀은 여러 종목을 서로 비교한다. "이 종목이 올랐는가?"보다 "다른 종목보다 더 많이 올랐는가?"를 본다. 예를 들어 20개 종목 중 최근 6개월 수익률이 가장 높은 5개 종목을 매수하는 방식이다.

절대 모멘텀은 종목 자신의 수익률이 기준을 넘었는지를 본다. 예를 들어 최근 6개월 수익률이 0%보다 크면 매수하고, 0%보다 작으면 현금으로 대기한다. 시장 전체가 약할 때는 상대적으로 덜 빠진 종목이 상위권에 들어올 수 있다. 하지만 덜 빠졌다는 이유만으로 꼭 매수할 필요는 없다. 절대 모멘텀은 이런 상황을 걸러내는 장치가 된다.

두 규칙을 문장으로 정리하면 다음과 같다.

  • 상대 모멘텀: 최근 수익률이 높은 상위 N개 종목을 산다.

  • 절대 모멘텀: 최근 수익률이 0보다 큰 종목만 산다.

  • 결합 전략: 최근 수익률 상위 N개 중에서도 수익률이 0보다 큰 종목만 산다.

실전에서는 상대 모멘텀과 절대 모멘텀을 함께 쓰는 경우가 많다. 강한 종목을 고르되, 전체 시장이 무너질 때는 매수 비중을 줄이기 위해서다.

예제 데이터 준비하기

여기서는 여러 종목의 가격 데이터가 이미 prices에 들어 있다고 가정하자. 해외 주식이라면 yfinance, 한국 주식이라면 앞 장에서 다룬 데이터 수집 방법을 사용할 수 있다.

import pandas as pd
import numpy as np

# 예시: prices는 수정종가 데이터프레임
# index: 날짜
# columns: 종목 코드
prices = prices.dropna(how='all')
prices = prices.ffill()

모멘텀 전략에서는 결측치 처리가 중요하다. 어떤 종목은 상장일이 늦어 과거 데이터가 부족할 수 있다. 데이터가 부족한 종목까지 억지로 비교하면 랭킹이 왜곡될 수 있다.

먼저 6개월 모멘텀을 계산해보자.

lookback = 126
momentum = prices.pct_change(lookback)

이제 특정 날짜의 모멘텀 순위를 볼 수 있다.

date = momentum.index[-1]
momentum.loc[date].sort_values(ascending=False).head(10)

이 코드는 마지막 날짜 기준으로 최근 6개월 수익률이 높은 종목 10개를 보여준다. 여기까지는 분석이다. 다음 단계는 이것을 매수 규칙으로 바꾸는 것이다.

상위 N개 종목을 선택하는 규칙

상대 모멘텀 전략은 매 리밸런싱 날짜마다 모멘텀 점수를 계산하고, 점수가 높은 상위 N개 종목을 선택한다.

가장 단순한 형태는 다음과 같다.

def select_top_n(momentum_row, n=5):
    return momentum_row.dropna().sort_values(ascending=False).head(n).index.tolist()

특정 날짜에 대해 선택 종목을 확인해보자.

selected = select_top_n(momentum.loc[date], n=5)
print(selected)

이제 선택된 종목에 동일한 비중을 부여한다. 5개 종목을 선택한다면 각 종목에 20%씩 투자한다.

def make_weights(momentum_row, n=5):
    selected = select_top_n(momentum_row, n)
    weights = pd.Series(0, index=momentum_row.index, dtype=float)
    if len(selected) > 0:
        weights[selected] = 1 / len(selected)
    return weights

이 함수는 한 날짜의 모멘텀 점수를 받아 포트폴리오 비중을 만든다. 선택되지 않은 종목의 비중은 0이다.

절대 모멘텀 조건 추가하기

이번에는 최근 수익률이 0보다 큰 종목만 사도록 조건을 추가해보자.

def make_weights_with_absolute_momentum(momentum_row, n=5, threshold=0):
    filtered = momentum_row.dropna()
    filtered = filtered[filtered > threshold]
    selected = filtered.sort_values(ascending=False).head(n).index.tolist()

    weights = pd.Series(0, index=momentum_row.index, dtype=float)
    if len(selected) > 0:
        weights[selected] = 1 / len(selected)
    return weights

이 함수는 두 가지 일을 한다.

첫째, 모멘텀 점수가 0보다 큰 종목만 남긴다. 둘째, 그중 상위 N개 종목을 고른다.

만약 조건을 만족하는 종목이 하나도 없다면 모든 비중은 0이 된다. 이때 남은 돈은 현금으로 보유한다고 가정할 수 있다. 단순화를 위해 현금 수익률은 0%로 두자.

이제 전략 문장이 훨씬 명확해졌다.

매월 말 최근 6개월 수익률이 0%보다 큰 종목 중 수익률 상위 5개를 동일비중으로 매수한다.

이 정도 문장이 되어야 백테스트할 수 있다. "강한 종목을 산다"는 말만으로는 검증할 수 없다. 기간, 기준, 종목 수, 리밸런싱 주기가 있어야 한다.

리밸런싱 날짜 만들기

모멘텀 전략은 보통 매일 종목을 바꾸지 않는다. 매월, 매분기, 또는 일정한 주기로 포트폴리오를 다시 구성한다. 이것을 리밸런싱이라고 한다.

월말 리밸런싱 날짜를 만들어보자.

monthly_dates = prices.resample('M').last().index
monthly_dates = monthly_dates.intersection(prices.index)

데이터에 따라 월말이 휴장일이면 실제 거래일과 맞지 않을 수 있다. 더 안정적으로 하려면 월별 마지막 거래일을 다음처럼 구할 수 있다.

monthly_dates = prices.groupby(prices.index.to_period('M')).tail(1).index

이제 각 리밸런싱 날짜마다 비중을 계산한다.

weights_list = []

for dt in monthly_dates:
    w = make_weights_with_absolute_momentum(momentum.loc[dt], n=5, threshold=0)
    w.name = dt
    weights_list.append(w)

target_weights = pd.DataFrame(weights_list)

target_weights는 월말마다 어떤 종목을 몇 퍼센트 살지 담은 표다. 하지만 일간 수익률 백테스트를 하려면 이 월별 비중을 매일의 비중으로 확장해야 한다.

daily_weights = target_weights.reindex(prices.index).ffill().fillna(0)

여기서 ffill()은 다음 리밸런싱 전까지 기존 비중을 유지한다는 뜻이다.

포트폴리오 수익률 계산하기

먼저 종목별 일간 수익률을 계산한다.

returns = prices.pct_change().fillna(0)

주의할 점이 있다. 오늘 종가로 모멘텀을 계산해 오늘 종가에 매수했다고 가정하면 미래 데이터를 사용한 것과 비슷한 문제가 생길 수 있다. 실제로는 오늘 장이 끝난 뒤 신호를 계산하고 다음 거래일부터 포지션을 가져간다고 보는 편이 안전하다.

그래서 비중을 하루 뒤로 미룬다.

portfolio_returns = (daily_weights.shift(1) * returns).sum(axis=1)

누적 수익률은 다음처럼 계산한다.

cum_returns = (1 + portfolio_returns).cumprod()

벤치마크와 비교하려면 전체 종목을 동일비중으로 보유하는 전략을 만들 수 있다.

benchmark_returns = returns.mean(axis=1)
benchmark_cum = (1 + benchmark_returns).cumprod()

이제 두 곡선을 함께 그려본다.

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 5))
plt.plot(cum_returns, label='Momentum Strategy')
plt.plot(benchmark_cum, label='Equal Weight Benchmark')
plt.legend()
plt.grid(True)
plt.show()

그래프를 볼 때는 마지막 수익률만 보지 말자. 중간에 얼마나 크게 흔들렸는지, 오랫동안 시장보다 못했던 구간은 없었는지도 함께 봐야 한다.

거래비용 반영하기

모멘텀 전략은 리밸런싱할 때마다 종목을 사고판다. 따라서 거래비용이 중요하다. 특히 리밸런싱 주기가 짧을수록 비용의 영향이 커진다.

거래비용은 비중 변화량으로 간단히 추정할 수 있다. 예를 들어 어떤 종목의 비중이 0%에서 20%로 바뀌면 20%만큼 매수한 것이다. 20%에서 0%로 바뀌면 20%만큼 매도한 것이다.

turnover = daily_weights.diff().abs().sum(axis=1)
cost_rate = 0.001  # 거래금액의 0.1% 비용 가정
cost = turnover * cost_rate

portfolio_returns_after_cost = portfolio_returns - cost
cum_returns_after_cost = (1 + portfolio_returns_after_cost).cumprod()

이 방식은 단순화된 계산이다. 실제로는 수수료, 세금, 매수·매도 호가 차이, 체결 가격 차이까지 고려해야 한다. 하지만 처음에는 이 정도만 반영해도 비용을 무시한 백테스트가 얼마나 낙관적인지 확인할 수 있다.

리밸런싱 주기 실험하기

모멘텀 전략에서 중요한 질문은 이것이다.

얼마나 자주 종목을 바꿀 것인가?

매주 바꾸면 시장 변화에 빠르게 반응할 수 있다. 하지만 거래가 많아지고 비용이 커진다. 매년 바꾸면 비용은 줄지만 변화에 둔감해질 수 있다. 그래서 월별, 분기별, 반기별 리밸런싱을 비교해볼 필요가 있다.

리밸런싱 주기를 함수로 만들면 실험하기 쉽다.

def get_rebalance_dates(prices, freq='M'):
    return prices.groupby(prices.index.to_period(freq)).tail(1).index


def backtest_momentum(prices, lookback=126, n=5, freq='M', threshold=0, cost_rate=0.001):
    returns = prices.pct_change().fillna(0)
    momentum = prices.pct_change(lookback)
    rebalance_dates = get_rebalance_dates(prices, freq=freq)

    weights_list = []
    for dt in rebalance_dates:
        w = make_weights_with_absolute_momentum(momentum.loc[dt], n=n, threshold=threshold)
        w.name = dt
        weights_list.append(w)

    target_weights = pd.DataFrame(weights_list)
    daily_weights = target_weights.reindex(prices.index).ffill().fillna(0)

    portfolio_returns = (daily_weights.shift(1) * returns).sum(axis=1)
    turnover = daily_weights.diff().abs().sum(axis=1)
    cost = turnover * cost_rate
    net_returns = portfolio_returns - cost

    cumulative = (1 + net_returns).cumprod()
    return net_returns, cumulative, daily_weights

이제 월별, 분기별 리밸런싱을 비교할 수 있다.

ret_m, cum_m, w_m = backtest_momentum(prices, lookback=126, n=5, freq='M')
ret_q, cum_q, w_q = backtest_momentum(prices, lookback=126, n=5, freq='Q')

plt.figure(figsize=(12, 5))
plt.plot(cum_m, label='Monthly')
plt.plot(cum_q, label='Quarterly')
plt.legend()
plt.grid(True)
plt.show()

결과가 월별 전략이 더 좋게 나올 수도 있고, 분기별 전략이 더 안정적으로 나올 수도 있다. 중요한 것은 어느 하나를 정답으로 외우지 않는 것이다. 투자 대상, 기간, 비용 가정, 시장 국면에 따라 결과는 달라진다.

성과를 어떻게 해석할까

모멘텀 전략을 평가할 때는 최소한 다음 항목을 확인해야 한다.

  • 누적 수익률: 최종적으로 얼마나 벌었는가?

  • 변동성: 수익률이 얼마나 흔들렸는가?

  • MDD: 최고점 대비 최대 손실은 어느 정도였는가?

  • 샤프 비율: 위험 대비 수익은 괜찮았는가?

  • 회전율: 포트폴리오가 얼마나 자주 바뀌었는가?

  • 벤치마크 대비 성과: 그냥 시장을 산 것보다 나았는가?

단순히 누적 수익률이 높다고 좋은 전략은 아니다. 수익률은 높지만 MDD가 너무 크면 실제로 버티기 어렵다. 백테스트에서는 3년 동안 손실 구간을 견딜 수 있을 것처럼 보이지만, 실제 계좌에서 그 시간을 견디는 것은 완전히 다른 문제다.

회전율도 꼭 봐야 한다. 회전율이 높다는 것은 종목을 자주 갈아탄다는 뜻이다. 비용이 커지고, 세금이 늘고, 실제 체결에서 불리해질 수 있다. 백테스트에서 조금 더 높은 수익률을 얻기 위해 너무 잦은 거래를 요구한다면 실전 전략으로는 부적합할 수 있다.

모멘텀 전략의 장점과 약점

모멘텀 전략의 장점은 명확하다. 규칙이 단순하고, 여러 종목을 비교하기 쉽고, 가격 데이터만으로도 구현할 수 있다. "최근 강한 종목을 산다"는 아이디어는 직관적이며 코드로 바꾸기도 쉽다.

하지만 약점도 크다.

첫째, 급격한 반전장에 약하다. 상승하던 종목이 갑자기 하락하면 모멘텀 전략은 늦게 반응할 수 있다.

둘째, 인기 종목에 몰릴 수 있다. 최근에 많이 오른 종목은 이미 시장의 기대를 크게 반영했을 수 있다.

셋째, 횡보장에서는 잦은 손실이 날 수 있다. 추세가 이어지지 않고 위아래로 흔들리는 시장에서는 사고팔기를 반복하며 비용만 쌓일 수 있다.

넷째, 종목군 선택에 따라 결과가 크게 달라진다. 현재 살아남은 우량 종목만으로 백테스트하면 과거에 사라진 종목의 손실을 반영하지 못할 수 있다. 이 문제는 뒤에서 백테스트의 함정을 다룰 때 더 자세히 살펴본다.

이번 장의 핵심

모멘텀 전략은 예측 모델이 아니다. "최근에 오른 종목이 계속 오를 것이다"라는 가설을 규칙으로 만든 것이다. 핵심은 그럴듯한 이야기를 데이터로 검증 가능한 형태로 바꾸는 데 있다.

이번 장에서 만든 기본 구조는 다음과 같다.

  1. 3개월, 6개월, 12개월 수익률로 모멘텀을 계산한다.

  2. 상대 모멘텀으로 종목 간 순위를 매긴다.

  3. 절대 모멘텀으로 약한 시장에서 매수를 줄인다.

  4. 상위 N개 종목을 동일비중으로 편입한다.

  5. 정해진 주기로 리밸런싱한다.

  6. 거래비용을 반영해 성과를 다시 계산한다.

이제 우리는 단일 종목의 매수·매도 신호를 넘어, 여러 종목 중 무엇을 살지 선택하는 전략을 만들었다. 다음 장에서는 가격 데이터가 아니라 재무 지표를 이용해 전략을 만들어본다. PER, PBR, ROE 같은 지표를 코드로 바꾸면 가치 투자 아이디어도 백테스트 가능한 규칙이 된다.

책 목록

처음 만나는 AI 퀀트 투자: 파이썬과 AI로 투자 아이디어를 검증하는 법

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기