
앞의 두 글에서 모멘텀과 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 |