이종관
글 목록으로

Python으로 구현하는 모멘텀 트레이딩 전략

EMA 크로스오버, RSI, MACD를 조합한 복합 모멘텀 신호 생성과 한국 시장 최적화 파라미터

2026년 2월 21일·22 min read·
quant
quant
momentum
rsi
macd
moving-average
python
trading

모멘텀 전략 개요

모멘텀 전략은 가격의 추세와 속도를 측정하여 상승 추세에 매수, 하락 추세에 매도하는 방법이다. "강한 것은 더 강해지고, 약한 것은 더 약해진다"는 시장의 관성을 활용한다.

이 글에서는 세 가지 기술 지표를 조합한 복합 모멘텀 전략을 구현한다.

지표역할측정 대상
EMA 크로스오버추세 방향 판단이동평균 교차
RSI과매수/과매도 판단가격 변동의 속도와 크기
MACD추세 강도와 전환 판단이동평균 수렴/발산

단일 지표만으로는 거짓 신호가 빈번하다. 2개 이상의 지표가 동일 방향을 가리킬 때만 진입하는 것이 핵심이다.

의존성

toml
# pyproject.toml
[project.dependencies]
ta = ">=0.11"       # 기술 분석 라이브러리
pandas = ">=2.0"
numpy = ">=1.24"
python
import pandas as pd
import numpy as np
from ta.trend import EMAIndicator, MACD as MACDIndicator
from ta.momentum import RSIIndicator

EMA 골든크로스 / 데드크로스

이동평균 크로스오버란

이동평균 크로스오버는 가장 기본적인 추세 추종 신호다. 단기 이동평균이 장기 이동평균을 상향 돌파하면 골든크로스(매수), 하향 돌파하면 데드크로스(매도)다.

패턴조건의미
골든크로스단기 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 구현

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 사이의 값을 반환하는 오실레이터다. 특정 기간 동안의 상승폭 평균과 하락폭 평균의 비율로 계산한다.

계산 공식

RS=평균 상승폭평균 하락폭(N일 기간)RS = \frac{\text{평균 상승폭}}{\text{평균 하락폭}} \quad (N\text{일 기간})

RSI=1001001+RSRSI = 100 - \frac{100}{1 + RS}

RSI가 높으면 상승 압력이 강했다는 의미이고, 낮으면 하락 압력이 강했다는 의미다.

신호 해석

RSI 범위상태행동
RSI>70RSI > 70과매수 (Overbought)매도 고려
RSI<30RSI < 30과매도 (Oversold)매수 고려
30RSI7030 \leq RSI \leq 70중립다른 지표 참조

주의할 점은 RSI가 과매수/과매도에 진입했다고 바로 매매하는 것이 아니라, 해당 영역을 벗어나는 시점을 포착하는 것이다.

  • 매수 시점: RSI가 과매도 영역(30 이하)에서 위로 돌파할 때
  • 매도 시점: RSI가 과매수 영역(70 이상)에서 아래로 돌파할 때

한국 시장 파라미터

조건RSI 기간과매수과매도비고
추세장 (상승/하락 뚜렷)148020추세 따라가기
박스권 (횡보)147030표준 설정
단기 매매77030민감하게 반응
코스닥 중소형97525변동성 보정

추세장에서는 과매수/과매도 기준을 80/20으로 넓히면 추세를 더 오래 따라갈 수 있다. 박스권에서는 표준 70/30이 효과적이다.

RSI 다이버전스

다이버전스는 가격과 RSI의 방향이 반대로 움직이는 현상으로, 추세 전환의 강력한 선행 지표다.

유형가격RSI의미
강세 다이버전스저점 갱신저점 상승하락 추세 약화 - 반등 가능
약세 다이버전스고점 갱신고점 하락상승 추세 약화 - 하락 가능

Python 구현

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 df

MACD (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에 수렴 - 상승 추세 약화 (데드크로스 임박)
  • 음봉 증가: 하락 추세 강화
  • 음봉 감소: 하락 추세 약화 (골든크로스 임박)

한국 시장 파라미터

시장fastslowsignal비고
코스피 대형주12269표준 설정
코스닥8215빠른 반응
일봉 단기6135스윙 트레이딩

코스닥 종목은 변동성이 크므로 표준 (12, 26, 9) 대신 **(8, 21, 5)**로 설정해 빠르게 반응하도록 조정한다.

Python 구현

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 > slowMA 데드크로스 또는 fast < slow
RSI < 70 (과매수 아님)RSI > 30 (과매도 아님)
MACD 골든크로스 또는 히스토그램 양전환MACD 데드크로스 또는 히스토그램 음전환

각 지표의 방향성을 +1(강세) 또는 -1(약세)로 수치화한 뒤 합산하여 momentum_score를 계산한다. 이 점수가 min_confirmations 이상이면 매수, -min_confirmations 이하이면 매도 신호를 발생시킨다.

Python 구현

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구성판정
+3+3EMA 강세 + RSI 강세 + MACD 강세강력 매수
+1+1지표 혼조신호 없음 (min_confirmations=2)
2-2EMA 약세 + MACD 약세매도
3-3모든 지표 약세강력 매도

한국 시장 파라미터 세트

시장 특성에 맞는 세 가지 파라미터 프리셋을 제공한다.

코스피 표준 (kospi_standard)

대형주 중심의 안정적인 트레이딩에 적합하다.

python
"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)

변동성이 큰 중소형주를 빠르게 대응한다.

python
"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개 지표가 모두 일치할 때만 진입한다.

python
"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/6010/3050/200
RSI 기간14914
RSI 과매수/과매도70/3075/2580/20
MACD (fast/slow/signal)12/26/98/21/512/26/9
최소 확인 수2개2개3개
신호 빈도중간높음낮음
거짓 신호 빈도중간높음낮음

전체 파라미터 딕셔너리

python
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,
    },
}

사용 예:

python
params = MOMENTUM_PARAMS["kospi_standard"]
result = combined_momentum_signal(df, **params)

포지션 사이징

고정 비율 방식

1회 거래의 최대 손실을 전체 예산의 일정 비율로 제한한다. 손절가까지의 거리에 따라 매수 수량이 결정된다.

python
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원, 손절: 3%-3\%

최대 허용 손실=1×1%=100만 원\text{최대 허용 손실} = 1\text{억} \times 1\% = 100\text{만 원}

주당 손실=50,000×3%=1,500\text{주당 손실} = 50{,}000 \times 3\% = 1{,}500\text{원}

매수 수량=1001,500=666\text{매수 수량} = \frac{100\text{만}}{1{,}500} = 666\text{주}

최대 비중=1×10%50,000=200\text{최대 비중} = \frac{1\text{억} \times 10\%}{50{,}000} = 200\text{주}

최종=min(666,  200)=200\text{최종} = \min(666,\; 200) = 200\text{주}


손절 / 익절 전략

리스크 파라미터

python
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) 이상의 지연을 적용해야 한다.


전략 요약

핵심 정리

  1. EMA 크로스오버로 추세 방향을 파악한다
  2. RSI로 과매수/과매도 상태를 확인하고, 극단적 영역에서의 진입을 방지한다
  3. MACD 히스토그램으로 추세 강도와 전환 시점을 판단한다
  4. 2개 이상의 지표가 일치할 때만 진입하여 거짓 신호를 필터링한다
  5. 한국 시장 특성에 맞게 코스피/코스닥별 파라미터를 차별 적용한다
  6. 고정 비율 포지션 사이징으로 1회 거래 리스크를 1%로 제한한다
  7. **손절(-3%), 익절(+6%), 트레일링 스톱(-2%)**으로 리스크를 관리한다