Development/Indicator Lab

MACD (Moving Average Convergence Divergence)

MildChoco 2026. 3. 15. 11:21

 

앞의 두 글에서 모멘텀과 ROC를 다뤘습니다. "현재 가격이 N일 전보다 높은가 낮은가"를 보는 가장 원시적인 모멘텀 지표들이었는데, 공통적으로 노이즈에 취약했습니다. 제로라인을 너무 자주 넘나들어서 223회나 거래가 발생했죠.

 

MACD는 이 문제를 EMA(지수이동평균)로 해결합니다. 가격을 직접 비교하는 대신, 두 개의 EMA 차이를 사용해서 모멘텀을 평활화합니다. Gerald Appel이 1970년대에 개발한 이후 가장 널리 쓰이는 모멘텀 지표 중 하나입니다.

 


지표 소개

MACD는 세 가지 요소로 구성됩니다.

MACD Line  = EMA(12) - EMA(26)
Signal Line = EMA(9) of MACD Line
Histogram  = MACD Line - Signal Line

 

MACD Line: 빠른 EMA(12)에서 느린 EMA(26)를 뺀 값입니다. 가격이 상승하면 빠른 EMA가 느린 EMA보다 먼저 올라가므로 MACD가 양수가 됩니다. 이동평균 크로스에서 다뤘던 "두 MA의 차이"를 그대로 숫자로 표현한 것입니다.

Signal Line: MACD Line 자체에 다시 EMA(9)를 씌운 것입니다. MACD의 움직임을 한 번 더 평활화해서, MACD의 "추세"를 보여줍니다.

Histogram: MACD Line과 Signal Line의 차이를 바 차트로 표시합니다. 양수면 MACD가 시그널 위에 있고(상승 모멘텀 강화), 음수면 아래에 있습니다(하락 모멘텀 강화).

 

 

상승장에서 MACD Line(파랑)이 Signal Line(주황) 위에 오래 머무르며, 히스토그램이 대부분 초록색(양수)인 것을 볼 수 있습니다. 모멘텀 차트와 비교하면 제로라인 부근의 잡음이 줄어든 것이 눈에 띕니다.

 

 

하락 구간에서는 MACD Line이 Signal Line 아래에 위치하고, 히스토그램이 빨간색(음수)이 우세합니다.


해석

MACD에서 읽을 수 있는 신호는 크게 세 가지입니다.

 

시그널 크로스: MACD Line이 Signal Line을 위로 돌파하면 상승 신호, 아래로 돌파하면 하락 신호. 가장 기본적인 매매 신호입니다.

 

제로라인 크로스: MACD Line이 0을 넘으면 빠른 EMA가 느린 EMA 위로 올라간 것이므로, 이동평균 골든크로스와 같은 의미입니다. 시그널 크로스보다 느리지만 더 강한 추세 확인 신호로 씁니다.

 

히스토그램 방향: 히스토그램이 줄어들기 시작하면, MACD와 시그널의 간격이 좁혀지고 있다는 뜻이므로 모멘텀이 약해지는 조기 신호가 됩니다.


모멘텀/ROC와의 관계

모멘텀은 현재 가격 - N일 전 가격이었습니다. MACD는 EMA(12) - EMA(26)입니다. 둘 다 "두 값의 차이"로 모멘텀을 측정하지만, 모멘텀은 특정 하루의 가격과 비교하는 반면, MACD는 이동평균과 비교합니다. 이 차이가 평활화 효과를 만들어서 노이즈를 줄여줍니다.

거기에 시그널 라인이라는 2차 평활화까지 추가되어, 모멘텀보다 훨씬 부드러운 신호를 만들어냅니다.


가설 설정

가장 기본적인 시그널 크로스 전략을 테스트합니다.

  • 매수 — MACD Line이 Signal Line 위로 크로스
  • 매도 — MACD Line이 Signal Line 아래로 크로스

모멘텀의 제로라인 크로스(223회)보다 거래 횟수가 상당히 줄어들 것으로 예상됩니다.


백테스트 조건

항목 설정
종목 BTC/USDT (Binance Spot)
기간 2018.01.01 ~ 2025.12.31
타임프레임 일봉 (1D)
MACD 12 / 26 / 9 (기본값)
진입 MACD가 시그널 위로 크로스
청산 MACD가 시그널 아래로 크로스
초기 자본 $10,000
수수료 / 슬리피지 미적용

코드 구현

import pandas as pd
import numpy as np
import mplfinance as mpf
import matplotlib.pyplot as plt

# ===== 설정 =====
DATA_FILE = "BTCUSDT_1d.csv"
FAST_PERIOD = 12        # 빠른 EMA
SLOW_PERIOD = 26        # 느린 EMA
SIGNAL_PERIOD = 9       # 시그널 EMA
INITIAL_CAPITAL = 10000
ZOOM_DAYS = 180
# ================

# 데이터 로드
df = pd.read_csv(DATA_FILE, index_col="timestamp", parse_dates=True)

# MACD 계산
df["ema_fast"] = df["close"].ewm(span=FAST_PERIOD, adjust=False).mean()
df["ema_slow"] = df["close"].ewm(span=SLOW_PERIOD, adjust=False).mean()
df["macd"] = df["ema_fast"] - df["ema_slow"]
df["signal"] = df["macd"].ewm(span=SIGNAL_PERIOD, adjust=False).mean()
df["histogram"] = df["macd"] - df["signal"]

# 백테스트: 시그널 크로스
capital = INITIAL_CAPITAL
holdings = 0
buy_price = 0
in_position = False
trades = []

start_idx = SLOW_PERIOD + SIGNAL_PERIOD

for i in range(start_idx, len(df)):
    macd = df["macd"].iloc[i]
    macd_prev = df["macd"].iloc[i - 1]
    sig = df["signal"].iloc[i]
    sig_prev = df["signal"].iloc[i - 1]
    close = df["close"].iloc[i]

    if not in_position:
        # MACD가 시그널 위로 크로스 → 매수
        if macd > sig and macd_prev <= sig_prev:
            buy_price = close
            holdings = capital / buy_price
            capital = 0
            in_position = True
    else:
        # MACD가 시그널 아래로 크로스 → 매도
        if macd < sig and macd_prev >= sig_prev:
            sell_price = close
            capital = holdings * sell_price
            pnl = (sell_price - buy_price) / buy_price * 100
            trades.append({
                "buy": buy_price,
                "sell": sell_price,
                "pnl_pct": round(pnl, 2)
            })
            holdings = 0
            in_position = False

# 마지막 포지션 정리
if in_position:
    last_price = df["close"].iloc[-1]
    capital = holdings * last_price
    pnl = (last_price - buy_price) / buy_price * 100
    trades.append({
        "buy": buy_price,
        "sell": last_price,
        "pnl_pct": round(pnl, 2)
    })

# 결과 출력
final_capital = capital
total_return = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
win_trades = [t for t in trades if t["pnl_pct"] > 0]
lose_trades = [t for t in trades if t["pnl_pct"] <= 0]

first_price = df["close"].dropna().iloc[0]
last_price = df["close"].dropna().iloc[-1]
bnh_return = (last_price - first_price) / first_price * 100
bnh_capital = INITIAL_CAPITAL * (1 + bnh_return / 100)

print("=" * 50)
print(f"MACD ({FAST_PERIOD}/{SLOW_PERIOD}/{SIGNAL_PERIOD}) 시그널 크로스 백테스트")
print("=" * 50)
print(f"초기 자본:    ${INITIAL_CAPITAL:,.0f}")
print(f"최종 자본:    ${final_capital:,.0f}")
print(f"총 수익률:    {total_return:+.2f}%")
print(f"총 거래 수:   {len(trades)}회")
print(f"승리:         {len(win_trades)}회")
print(f"패배:         {len(lose_trades)}회")
if trades:
    print(f"승률:         {len(win_trades)/len(trades)*100:.1f}%")
print("-" * 50)
print(f"바이앤홀드:   ${bnh_capital:,.0f} ({bnh_return:+.2f}%)")
print("=" * 50)

# 차트 헬퍼: 캔들 + MACD 서브플롯
def plot_macd(df_plot, title):
    fig, axes = mpf.plot(df_plot, type="candle", style="charles",
        title=title, volume=False, figsize=(14, 8), returnfig=True)

    ax_macd = fig.add_axes([0.12, 0.08, 0.76, 0.25])

    macd_data = df_plot["macd"].dropna()
    sig_data = df_plot["signal"].dropna()
    hist_data = df_plot["histogram"].dropna()

    x = range(len(hist_data))
    ax_macd.bar(x, hist_data.values,
        color=["#43A047" if v >= 0 else "#E53935" for v in hist_data.values],
        width=1.0, alpha=0.6)
    ax_macd.plot(range(len(macd_data)), macd_data.values,
        color="#1E88E5", linewidth=1.2, label="MACD")
    ax_macd.plot(range(len(sig_data)), sig_data.values,
        color="#FF8F00", linewidth=1.2, label="Signal")
    ax_macd.axhline(y=0, color="black", linewidth=0.8)
    ax_macd.legend(loc="upper left", fontsize=8)
    ax_macd.set_ylabel("MACD")
    ax_macd.set_xlim(0, len(hist_data))

    axes[0].set_position([0.12, 0.38, 0.76, 0.55])
    return fig

# 차트 1 — 상승장
df_bull = df.loc["2020-10-01":"2021-04-30"].copy()
plot_macd(df_bull,
    f"BTC/USDT  MACD ({FAST_PERIOD}/{SLOW_PERIOD}/{SIGNAL_PERIOD}) (2020.10 ~ 2021.04, Bull)")

# 차트 2 — 최근
df_zoom = df.tail(ZOOM_DAYS).copy()
plot_macd(df_zoom,
    f"BTC/USDT  MACD ({FAST_PERIOD}/{SLOW_PERIOD}/{SIGNAL_PERIOD}) (Last {ZOOM_DAYS}D)")

plt.show()

사용법

FAST_PERIOD, SLOW_PERIOD, SIGNAL_PERIOD를 조절하면 다른 설정도 테스트할 수 있습니다. 기본값 12/26/9가 가장 널리 쓰입니다.


결과 분석

 

모멘텀/ROC(223회)의 절반 이하인 104회로 거래 횟수가 줄었습니다. EMA 평활화의 효과입니다. 승률도 32.3% → 37.5%로 개선되었고, 수익률은 +1031% → +1066%로 비슷한 수준을 유지합니다.

 

거래가 절반으로 줄었는데 수익률이 비슷하다는 건, 모멘텀에서 발생했던 불필요한 거래(진입했다가 바로 손절)가 MACD에서는 걸러졌다는 뜻입니다. 평활화가 노이즈를 줄여준 것이죠.

 

다만 104회도 8년 기간으로 보면 한 달에 한 번 이상 거래하는 셈이라, 여전히 많은 편입니다. 이전에 다뤘던 이동평균 크로스(SMA 20/60: 28회)나 터틀트레이딩(58회)에 비하면 아직 노이즈가 남아있습니다. MACD가 모멘텀보다는 개선됐지만, 단독 매매 신호로 쓰기에는 여전히 한계가 있습니다.


정리

MACD는 두 EMA의 차이(모멘텀)에 시그널 라인(2차 평활화)을 추가한 지표입니다. 모멘텀/ROC의 노이즈 문제를 EMA로 해결했고, 시그널 크로스라는 명확한 매매 기준을 제공합니다.

 

다만 MACD도 추세추종 지표입니다. 추세가 있는 시장에서 잘 작동하지만, 횡보장에서는 잦은 크로스로 인한 손실이 발생합니다. 이 한계는 모멘텀/ROC와 본질적으로 같습니다 — 평활화로 줄였을 뿐 완전히 없앤 건 아닙니다.

 

다음 글에서는 추세추종이 아닌, 과매수/과매도라는 새로운 개념의 오실레이터인 RSI를 다룹니다.


본 포스팅은 지표의 원리와 백테스트 과정을 정리한 기술 블로그이며, 특정 자산의 매매를 권유하지 않습니다.
백테스트 결과는 과거 데이터 기반이며 실제 수익을 보장하지 않습니다. 모든 투자의 책임은 본인에게 있습니다.

'Development > Indicator Lab' 카테고리의 다른 글

스토캐스틱 (Stochastic Oscillator)  (0) 2026.03.17
RSI (Relative Strength Index)  (3) 2026.03.16
모멘텀 (Momentum)  (0) 2026.03.14
ROC (Rate of Change)  (0) 2026.03.13
터틀트레이딩 — MA 필터와 조합 비교  (3) 2026.03.12