Python으로 구현하는 모멘텀 트레이딩 전략
EMA 크로스오버, RSI, MACD를 조합한 복합 모멘텀 신호 생성과 한국 시장 최적화 파라미터
모멘텀 전략 개요
모멘텀 전략은 가격의 추세와 속도를 측정하여 상승 추세에 매수, 하락 추세에 매도하는 방법이다. "강한 것은 더 강해지고, 약한 것은 더 약해진다"는 시장의 관성을 활용한다.
이 글에서는 세 가지 기술 지표를 조합한 복합 모멘텀 전략을 구현한다.
| 지표 | 역할 | 측정 대상 |
|---|---|---|
| EMA 크로스오버 | 추세 방향 판단 | 이동평균 교차 |
| RSI | 과매수/과매도 판단 | 가격 변동의 속도와 크기 |
| MACD | 추세 강도와 전환 판단 | 이동평균 수렴/발산 |
단일 지표만으로는 거짓 신호가 빈번하다. 2개 이상의 지표가 동일 방향을 가리킬 때만 진입하는 것이 핵심이다.
의존성
# pyproject.toml
[project.dependencies]
ta = ">=0.11" # 기술 분석 라이브러리
pandas = ">=2.0"
numpy = ">=1.24"import pandas as pd
import numpy as np
from ta.trend import EMAIndicator, MACD as MACDIndicator
from ta.momentum import RSIIndicatorEMA 골든크로스 / 데드크로스
이동평균 크로스오버란
이동평균 크로스오버는 가장 기본적인 추세 추종 신호다. 단기 이동평균이 장기 이동평균을 상향 돌파하면 골든크로스(매수), 하향 돌파하면 데드크로스(매도)다.
| 패턴 | 조건 | 의미 |
|---|---|---|
| 골든크로스 | 단기 MA가 장기 MA를 상향 돌파 | 강세 전환 - 매수 신호 |
| 데드크로스 | 단기 MA가 장기 MA를 하향 돌파 | 약세 전환 - 매도 신호 |
SMA vs EMA
단순이동평균(SMA)은 모든 데이터에 동일한 가중치를 부여하지만, **지수이동평균(EMA)**은 최근 데이터에 더 높은 가중치를 부여한다. 변동성이 큰 한국 시장에서는 EMA가 더 빠르게 추세 변화에 반응한다.
일반적인 MA 조합
| 조합 | 단기 | 장기 | 특성 |
|---|---|---|---|
| 빠른 크로스 | 5일 | 20일 | 민감, 거짓 신호 많음 |
| 표준 크로스 | 20일 | 60일 | 중단기 추세 |
| 클래식 크로스 | 50일 | 200일 | 장기 추세, 신뢰도 높음 |
한국 시장 최적 설정
한국 시장은 미국 대비 변동성이 크므로 단기 MA를 약간 짧게 설정하는 것이 효과적이다.
- 코스피 대형주: 20일/60일 EMA (변동성 보통)
- 코스닥 중소형주: 10일/30일 EMA (변동성 높음)
Python 구현
def ma_crossover_signals(
df: pd.DataFrame,
fast: int = 20,
slow: int = 60,
) -> pd.DataFrame:
"""이동평균 크로스오버 신호 생성.
Args:
df: OHLCV DataFrame (columns: open, high, low, close, volume)
fast: 단기 이동평균 기간
slow: 장기 이동평균 기간
Returns:
signal 컬럼이 추가된 DataFrame (1=매수, -1=매도, 0=중립)
"""
df = df.copy()
# EMA 계산
df["ema_fast"] = EMAIndicator(
close=df["close"], window=fast
).ema_indicator()
df["ema_slow"] = EMAIndicator(
close=df["close"], window=slow
).ema_indicator()
# 크로스오버 감지
df["ma_cross"] = 0
# 골든크로스: fast가 slow 위로 교차
df.loc[
(df["ema_fast"] > df["ema_slow"])
& (df["ema_fast"].shift(1) <= df["ema_slow"].shift(1)),
"ma_cross",
] = 1
# 데드크로스: fast가 slow 아래로 교차
df.loc[
(df["ema_fast"] < df["ema_slow"])
& (df["ema_fast"].shift(1) >= df["ema_slow"].shift(1)),
"ma_cross",
] = -1
return df교차 감지의 핵심은 현재 봉과 직전 봉의 상대적 위치 변화다.
- 골든크로스: 직전 봉에서
fast <= slow이고, 현재 봉에서fast > slow - 데드크로스: 직전 봉에서
fast >= slow이고, 현재 봉에서fast < slow
RSI (Relative Strength Index)
RSI 개요
RSI는 가격 변동의 속도와 크기를 측정하여 0~100 사이의 값을 반환하는 오실레이터다. 특정 기간 동안의 상승폭 평균과 하락폭 평균의 비율로 계산한다.
계산 공식
RSI가 높으면 상승 압력이 강했다는 의미이고, 낮으면 하락 압력이 강했다는 의미다.
신호 해석
| RSI 범위 | 상태 | 행동 |
|---|---|---|
| 과매수 (Overbought) | 매도 고려 | |
| 과매도 (Oversold) | 매수 고려 | |
| 중립 | 다른 지표 참조 |
주의할 점은 RSI가 과매수/과매도에 진입했다고 바로 매매하는 것이 아니라, 해당 영역을 벗어나는 시점을 포착하는 것이다.
- 매수 시점: RSI가 과매도 영역(30 이하)에서 위로 돌파할 때
- 매도 시점: RSI가 과매수 영역(70 이상)에서 아래로 돌파할 때
한국 시장 파라미터
| 조건 | RSI 기간 | 과매수 | 과매도 | 비고 |
|---|---|---|---|---|
| 추세장 (상승/하락 뚜렷) | 14 | 80 | 20 | 추세 따라가기 |
| 박스권 (횡보) | 14 | 70 | 30 | 표준 설정 |
| 단기 매매 | 7 | 70 | 30 | 민감하게 반응 |
| 코스닥 중소형 | 9 | 75 | 25 | 변동성 보정 |
추세장에서는 과매수/과매도 기준을 80/20으로 넓히면 추세를 더 오래 따라갈 수 있다. 박스권에서는 표준 70/30이 효과적이다.
RSI 다이버전스
다이버전스는 가격과 RSI의 방향이 반대로 움직이는 현상으로, 추세 전환의 강력한 선행 지표다.
| 유형 | 가격 | RSI | 의미 |
|---|---|---|---|
| 강세 다이버전스 | 저점 갱신 | 저점 상승 | 하락 추세 약화 - 반등 가능 |
| 약세 다이버전스 | 고점 갱신 | 고점 하락 | 상승 추세 약화 - 하락 가능 |
Python 구현
def rsi_signals(
df: pd.DataFrame,
window: int = 14,
overbought: float = 70,
oversold: float = 30,
) -> pd.DataFrame:
"""RSI 기반 매매 신호 생성.
Args:
df: OHLCV DataFrame
window: RSI 계산 기간
overbought: 과매수 기준
oversold: 과매도 기준
Returns:
rsi, rsi_signal 컬럼이 추가된 DataFrame
"""
df = df.copy()
df["rsi"] = RSIIndicator(
close=df["close"], window=window
).rsi()
df["rsi_signal"] = 0
# 과매도 탈출 → 매수 (RSI가 oversold 아래에서 위로)
df.loc[
(df["rsi"] > oversold)
& (df["rsi"].shift(1) <= oversold),
"rsi_signal",
] = 1
# 과매수 이탈 → 매도 (RSI가 overbought 위에서 아래로)
df.loc[
(df["rsi"] < overbought)
& (df["rsi"].shift(1) >= overbought),
"rsi_signal",
] = -1
return dfMACD (Moving Average Convergence Divergence)
MACD 구성 요소
MACD는 세 가지 요소로 구성된다.
| 요소 | 계산 | 기본값 |
|---|---|---|
| MACD 선 | EMA(fast) - EMA(slow) | EMA(12) - EMA(26) |
| Signal 선 | EMA(MACD, signal) | EMA(MACD, 9) |
| 히스토그램 | MACD - Signal | - |
MACD 선은 두 EMA의 차이를 나타내고, Signal 선은 MACD의 이동평균이다. 히스토그램은 이 둘의 거리를 시각화한 것이다.
신호 해석
| 패턴 | 조건 | 의미 |
|---|---|---|
| MACD 골든크로스 | MACD가 Signal 위로 교차 | 매수 신호 |
| MACD 데드크로스 | MACD가 Signal 아래로 교차 | 매도 신호 |
| 0선 돌파 | MACD가 0 위로 돌파 | 상승 추세 확인 |
| 히스토그램 확대 | 히스토그램 양봉 증가 | 추세 강화 |
히스토그램 해석
히스토그램은 추세의 강도 변화를 가장 민감하게 반영한다.
- 양봉 증가: MACD가 Signal에서 멀어짐 - 상승 추세 강화
- 양봉 감소: MACD가 Signal에 수렴 - 상승 추세 약화 (데드크로스 임박)
- 음봉 증가: 하락 추세 강화
- 음봉 감소: 하락 추세 약화 (골든크로스 임박)
한국 시장 파라미터
| 시장 | fast | slow | signal | 비고 |
|---|---|---|---|---|
| 코스피 대형주 | 12 | 26 | 9 | 표준 설정 |
| 코스닥 | 8 | 21 | 5 | 빠른 반응 |
| 일봉 단기 | 6 | 13 | 5 | 스윙 트레이딩 |
코스닥 종목은 변동성이 크므로 표준 (12, 26, 9) 대신 **(8, 21, 5)**로 설정해 빠르게 반응하도록 조정한다.
Python 구현
def macd_signals(
df: pd.DataFrame,
fast: int = 12,
slow: int = 26,
signal: int = 9,
) -> pd.DataFrame:
"""MACD 크로스오버 신호 생성.
Args:
df: OHLCV DataFrame
fast: 단기 EMA 기간
slow: 장기 EMA 기간
signal: 시그널 EMA 기간
Returns:
macd, macd_signal_line, macd_hist, macd_signal 컬럼 추가
"""
df = df.copy()
macd = MACDIndicator(
close=df["close"],
window_fast=fast,
window_slow=slow,
window_sign=signal,
)
df["macd"] = macd.macd()
df["macd_signal_line"] = macd.macd_signal()
df["macd_hist"] = macd.macd_diff()
df["macd_signal"] = 0
# MACD 골든크로스
df.loc[
(df["macd"] > df["macd_signal_line"])
& (df["macd"].shift(1) <= df["macd_signal_line"].shift(1)),
"macd_signal",
] = 1
# MACD 데드크로스
df.loc[
(df["macd"] < df["macd_signal_line"])
& (df["macd"].shift(1) >= df["macd_signal_line"].shift(1)),
"macd_signal",
] = -1
return df복합 모멘텀 신호
전략 로직
단일 지표의 한계를 극복하기 위해, 2개 이상의 지표가 동일 방향을 가리킬 때만 진입한다.
| 매수 조건 (모두 충족) | 매도 조건 (모두 충족) |
|---|---|
| MA 골든크로스 또는 fast > slow | MA 데드크로스 또는 fast < slow |
| RSI < 70 (과매수 아님) | RSI > 30 (과매도 아님) |
| MACD 골든크로스 또는 히스토그램 양전환 | MACD 데드크로스 또는 히스토그램 음전환 |
각 지표의 방향성을 +1(강세) 또는 -1(약세)로 수치화한 뒤 합산하여 momentum_score를 계산한다. 이 점수가 min_confirmations 이상이면 매수, -min_confirmations 이하이면 매도 신호를 발생시킨다.
Python 구현
def combined_momentum_signal(
df: pd.DataFrame,
ma_fast: int = 20,
ma_slow: int = 60,
rsi_window: int = 14,
rsi_overbought: float = 70,
rsi_oversold: float = 30,
macd_fast: int = 12,
macd_slow: int = 26,
macd_signal: int = 9,
min_confirmations: int = 2,
) -> pd.DataFrame:
"""복합 모멘텀 신호 생성.
min_confirmations개 이상의 지표가 동일 방향일 때만 신호 발생.
"""
df = ma_crossover_signals(df, ma_fast, ma_slow)
df = rsi_signals(df, rsi_window, rsi_overbought, rsi_oversold)
df = macd_signals(df, macd_fast, macd_slow, macd_signal)
# 각 지표의 현재 방향성
df["ma_direction"] = np.where(
df["ema_fast"] > df["ema_slow"], 1, -1
)
df["rsi_direction"] = np.where(
df["rsi"] < 50, -1, 1
) # RSI 50 기준
df["macd_direction"] = np.where(
df["macd_hist"] > 0, 1, -1
)
# 방향성 합산
df["momentum_score"] = (
df["ma_direction"]
+ df["rsi_direction"]
+ df["macd_direction"]
)
# 최종 신호
df["signal"] = 0
df.loc[
df["momentum_score"] >= min_confirmations, "signal"
] = 1 # 매수
df.loc[
df["momentum_score"] <= -min_confirmations, "signal"
] = -1 # 매도
return df신호 발생 예시
| momentum_score | 구성 | 판정 |
|---|---|---|
| EMA 강세 + RSI 강세 + MACD 강세 | 강력 매수 | |
| 지표 혼조 | 신호 없음 (min_confirmations=2) | |
| EMA 약세 + MACD 약세 | 매도 | |
| 모든 지표 약세 | 강력 매도 |
한국 시장 파라미터 세트
시장 특성에 맞는 세 가지 파라미터 프리셋을 제공한다.
코스피 표준 (kospi_standard)
대형주 중심의 안정적인 트레이딩에 적합하다.
"kospi_standard": {
"ma_fast": 20, "ma_slow": 60,
"rsi_window": 14, "rsi_overbought": 70, "rsi_oversold": 30,
"macd_fast": 12, "macd_slow": 26, "macd_signal": 9,
"min_confirmations": 2,
}코스닥 공격적 (kosdaq_aggressive)
변동성이 큰 중소형주를 빠르게 대응한다.
"kosdaq_aggressive": {
"ma_fast": 10, "ma_slow": 30,
"rsi_window": 9, "rsi_overbought": 75, "rsi_oversold": 25,
"macd_fast": 8, "macd_slow": 21, "macd_signal": 5,
"min_confirmations": 2,
}보수적 (conservative)
장기 추세를 따르며, 3개 지표가 모두 일치할 때만 진입한다.
"conservative": {
"ma_fast": 50, "ma_slow": 200,
"rsi_window": 14, "rsi_overbought": 80, "rsi_oversold": 20,
"macd_fast": 12, "macd_slow": 26, "macd_signal": 9,
"min_confirmations": 3, # 3개 모두 일치 시만
}파라미터 비교
| 항목 | 코스피 표준 | 코스닥 공격적 | 보수적 |
|---|---|---|---|
| EMA 단기/장기 | 20/60 | 10/30 | 50/200 |
| RSI 기간 | 14 | 9 | 14 |
| RSI 과매수/과매도 | 70/30 | 75/25 | 80/20 |
| MACD (fast/slow/signal) | 12/26/9 | 8/21/5 | 12/26/9 |
| 최소 확인 수 | 2개 | 2개 | 3개 |
| 신호 빈도 | 중간 | 높음 | 낮음 |
| 거짓 신호 빈도 | 중간 | 높음 | 낮음 |
전체 파라미터 딕셔너리
MOMENTUM_PARAMS = {
"kospi_standard": {
"ma_fast": 20, "ma_slow": 60,
"rsi_window": 14, "rsi_overbought": 70, "rsi_oversold": 30,
"macd_fast": 12, "macd_slow": 26, "macd_signal": 9,
"min_confirmations": 2,
},
"kosdaq_aggressive": {
"ma_fast": 10, "ma_slow": 30,
"rsi_window": 9, "rsi_overbought": 75, "rsi_oversold": 25,
"macd_fast": 8, "macd_slow": 21, "macd_signal": 5,
"min_confirmations": 2,
},
"conservative": {
"ma_fast": 50, "ma_slow": 200,
"rsi_window": 14, "rsi_overbought": 80, "rsi_oversold": 20,
"macd_fast": 12, "macd_slow": 26, "macd_signal": 9,
"min_confirmations": 3,
},
}사용 예:
params = MOMENTUM_PARAMS["kospi_standard"]
result = combined_momentum_signal(df, **params)포지션 사이징
고정 비율 방식
1회 거래의 최대 손실을 전체 예산의 일정 비율로 제한한다. 손절가까지의 거리에 따라 매수 수량이 결정된다.
def calculate_position_size(
budget: float,
price: float,
stop_loss_pct: float,
risk_per_trade_pct: float = 1.0,
) -> int:
"""고정비율 포지션 사이징.
Args:
budget: 총 투자 가능 금액
price: 현재 주가
stop_loss_pct: 손절 비율 (예: -3.0)
risk_per_trade_pct: 1회 거래 최대 손실 비율 (예산 대비, 기본 1%)
Returns:
매수 수량 (정수)
"""
max_loss_amount = budget * (risk_per_trade_pct / 100)
loss_per_share = price * abs(stop_loss_pct / 100)
if loss_per_share == 0:
return 0
shares = int(max_loss_amount / loss_per_share)
# 종목당 최대 비중 10% 제한
max_shares = int(budget * 0.10 / price)
return min(shares, max_shares)예시 계산:
예산: 1억 원, 주가: 50,000원, 손절:
손절 / 익절 전략
리스크 파라미터
RISK_PARAMS = {
"stop_loss_pct": -3.0, # 손절: -3%
"take_profit_pct": 6.0, # 익절: +6% (R:R = 1:2)
"trailing_stop_pct": -2.0, # 트레일링 스톱: 고점 대비 -2%
"max_holding_days": 20, # 최대 보유 기간: 20 영업일
"max_position_pct": 10.0, # 종목당 최대 비중: 10%
}| 항목 | 값 | 설명 |
|---|---|---|
| 손절 | -3% | 진입가 대비 3% 하락 시 강제 청산 |
| 익절 | +6% | 진입가 대비 6% 상승 시 수익 실현 |
| 트레일링 스톱 | -2% | 보유 중 최고가 대비 2% 하락 시 청산 |
| 최대 보유 | 20영업일 | 약 1개월 후 강제 청산 |
| 최대 비중 | 10% | 한 종목에 전체 자산의 10%까지만 투자 |
손절:익절 비율(R:R)이 1:2이면, 승률이 34% 이상일 때 장기적으로 수익이 난다. 모멘텀 전략의 평균 승률은 40~55% 범위이므로 1:2 비율이 적절하다.
트레일링 스톱
트레일링 스톱은 주가가 상승할 때 손절선도 함께 올라가는 방식이다. 수익이 난 상태에서 급락 시 이익을 보전할 수 있다.
백테스트 고려사항
실전 적용 전 반드시 과거 데이터로 백테스트를 수행해야 한다. 다음 사항을 반영하지 않으면 과도하게 낙관적인 결과가 나온다.
거래 비용
한국 시장의 거래 비용 구조:
| 항목 | 비율 |
|---|---|
| 매수 수수료 | 0.015% |
| 매도 수수료 | 0.015% |
| 증권거래세 (코스피, 2026년) | 0.18% |
| 왕복 총 비용 | 약 0.21% |
슬리피지 (호가 단위)
한국 시장은 주가 범위에 따라 호가 단위가 다르다.
| 주가 범위 | 호가 단위 |
|---|---|
| 1,000원 미만 | 1원 |
| 5,000원 미만 | 5원 |
| 10,000원 미만 | 10원 |
| 50,000원 미만 | 50원 |
| 50,000원 이상 | 100원 |
슬리피지는 시장가 주문 시 실제 체결가와 예상가의 차이로, 백테스트에서 반드시 반영해야 한다.
주의사항
| 항목 | 설명 |
|---|---|
| Look-ahead bias | 지표 계산에 미래 데이터 사용 금지 (shift 확인) |
| 생존자 편향 | 상장 폐지 종목을 포함한 데이터로 백테스트 |
| 최소 데이터 | 200일 이동평균 사용 시 최소 250일 데이터 필요 |
| 거래량 제약 | 일평균 거래량의 1% 이하로 주문 제한 (시장 충격 방지) |
Look-ahead bias는 백테스트에서 가장 흔한 실수다. 현재 봉의 종가로 계산한 지표를 현재 봉에서 매매 판단에 사용하는 것은 미래 정보를 활용하는 것이다. 반드시
shift(1)이상의 지연을 적용해야 한다.
전략 요약
핵심 정리
- EMA 크로스오버로 추세 방향을 파악한다
- RSI로 과매수/과매도 상태를 확인하고, 극단적 영역에서의 진입을 방지한다
- MACD 히스토그램으로 추세 강도와 전환 시점을 판단한다
- 2개 이상의 지표가 일치할 때만 진입하여 거짓 신호를 필터링한다
- 한국 시장 특성에 맞게 코스피/코스닥별 파라미터를 차별 적용한다
- 고정 비율 포지션 사이징으로 1회 거래 리스크를 1%로 제한한다
- **손절(-3%), 익절(+6%), 트레일링 스톱(-2%)**으로 리스크를 관리한다