AlgoLab Blog · 퀀트 지표 구현 · pandas · TA-Lib · 2026

파이썬 MACD — 봉 35개로 계산하면 부호가 10% 틀린다

퀀트·지표 구현 2026-10-22 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약

파이썬 MACD(12·26·9) 값이 HTS나 백테스트와 다르게 나오는 원인은 라이브러리보다 "봉을 몇 개 넣었나"입니다. 삼성전자(005930) 최근 500거래일을 매일 최근 봉 35개로만 계산하면 MACD 히스토그램 부호(골든·데드 상태)가 10년 전체 이력으로 계산한 값과 48일 어긋났고, 60개면 12일, 100개면 1일, 150개부터 0일이었습니다. 반대로 pandas ewm(adjust=True)·adjust=False·talib.MACD의 차이는 데이터 앞쪽 60~117개 봉에서 1원 미만으로 사라져, 10년 백테스트의 골든크로스 수는 95~99회로 거의 같았습니다. 봇에서 확인할 것은 세 가지입니다 — 봉 200개 이상, 날짜 오름차순 정렬, TA-Lib 워밍업 33개 NaN.

목차

  1. 10년 백테스트에선 라이브러리 차이가 거의 없다
  2. 진짜 함정 ① 봇에 넣는 봉 개수
  3. 함정 ② TA-Lib은 33개를 NaN으로 비운다
  4. 함정 ③ 최신순 응답을 그대로 넣으면
  5. 함정 ④ 반환 순서와 adjust 기본값
  6. 봇에 넣는 안전한 MACD 함수
  7. 자주 묻는 질문

1. 10년 백테스트에선 라이브러리 차이가 거의 없다

MACD는 "12일 EMA − 26일 EMA"이고, 시그널은 그 MACD의 9일 EMA, 히스토그램(국내 HTS에서는 흔히 MACD 오실레이터)은 MACD − 시그널입니다. 파이썬으로 옮기는 방법은 크게 셋입니다 — pandas의 ewm(), TA-Lib의 talib.MACD, pandas-ta의 ta.macd(). EMA를 어떻게 시작하느냐(시드)가 서로 다릅니다.

조건: FinanceDataReader 0.9.202 일봉 종가, 2016-10-17 ~ 2026-10-16(기준일 고정, 2,452거래일), pandas 2.2.3, TA-Lib 0.8.1(C 라이브러리 0.8.1), Python 3.10.4. 세 종목에 네 방식을 모두 돌렸습니다.

import numpy as np, FinanceDataReader as fdr, talib

ASOF = "2026-10-16"   # 기준일 고정 — 바꾸면 숫자가 달라진다
c = fdr.DataReader("005930", "2016-10-17", ASOF)["Close"].astype(float)

def macd_pandas(c, adjust):
    m = c.ewm(span=12, adjust=adjust).mean() - c.ewm(span=26, adjust=adjust).mean()
    return m, m.ewm(span=9, adjust=adjust).mean()

m, sig, hist = talib.MACD(c.values, fastperiod=12, slowperiod=26, signalperiod=9)

실행 출력(삼성전자, 마지막 날 값과 앞쪽 봉 값)입니다.

pd_adjTrue   last MACD -9461.8377 signal -2611.0372 hist -6850.8005
pd_adjFalse  last MACD -9461.8377 signal -2611.0372 hist -6850.8005
talib        last MACD -9461.8377 signal -2611.0372 hist -6850.8005
pta_presma   last MACD -9461.8377 signal -2611.0372 hist -6850.8005
bar33:  adjTrue 512.56  adjFalse 582.26  talib 544.55   close 34980
bar100: adjTrue 581.87  adjFalse 584.90  talib 584.47   close 40180
bar200: adjTrue 59.78   adjFalse 59.78   talib 59.78    close 47700

마지막 날 값은 소수 넷째 자리까지 같습니다. 차이는 앞쪽에만 있습니다. 두 방식의 MACD 차이가 1원 이상인 마지막 봉을 찾으면 adjust=True vs False는 62~117번째, talib vs adjust=False는 49~88번째였습니다. 시드의 흔적이 EMA 특성상 지수적으로 사라지기 때문입니다.

종목골든크로스 수 adjTrue / adjFalse / TA-LibTA-Lib과 다른 날짜
삼성전자 00593099 / 98 / 95전부 2016-10-19 ~ 2017-01-25
KODEX 200 06950091 / 90 / 88전부 2016-10-18 ~ 2016-11-23
카카오 035720100 / 99 / 97전부 2016-10-18 ~ 2016-11-11

어긋난 크로스는 전부 데이터 첫 석 달 안에 있었습니다. 10년짜리 백테스트라면 어느 라이브러리를 써도 결과가 사실상 같다는 뜻입니다. TA-Lib 쪽 크로스가 적은 이유도 단순합니다 — 앞 33개 봉이 NaN이라 그 구간 크로스를 세지 않습니다. 시작 시점 자체가 결과를 바꾸는 문제는 백테스트 시작일 민감도 실측에서 따로 다뤘습니다.

2. 진짜 함정 ① 봇에 넣는 봉 개수

백테스트는 10년치를 한 번에 넣지만, 실거래 봇은 보통 API로 최근 봉 몇 개만 받아 계산합니다. 그런데 EMA는 과거 전체를 기억하는 지표라, 짧게 자른 데이터로 계산하면 "앞쪽 봉" 문제가 매일 다시 생깁니다. 1장에서 본 시드 오차가 백테스트에선 처음 석 달에 한 번이지만, 봇에선 매 봉마다 일어나는 셈입니다.

최근 500거래일 각 날짜에서 "그날까지 최근 N개 봉만" 넣어 계산한 히스토그램 부호를 전체 이력 값과 비교했습니다(adjust=False).

full_m, full_s = macd_pandas(c, adjust=False)
for N in [35, 60, 100, 150, 200]:
    miss = 0
    for t in range(len(c) - 500, len(c)):
        m, s = macd_pandas(c.iloc[t-N+1:t+1], adjust=False)
        miss += (np.sign(m.iloc[-1] - s.iloc[-1]) != np.sign(full_m.iloc[t] - full_s.iloc[t]))
    print(N, miss)
005930  window 35:  hist sign mismatch 48/500, max MACD err 2.151% of price
005930  window 60:  hist sign mismatch 12/500, max MACD err 0.316% of price
005930  window 100: hist sign mismatch 1/500,  max MACD err 0.011% of price
005930  window 150: hist sign mismatch 0/500,  max MACD err 0.000% of price
005930  window 200: hist sign mismatch 0/500,  max MACD err 0.000% of price
넣은 봉 수삼성전자KODEX 200카카오
35개48일 / 50038일26일
60개12일2일10일
100개1일1일0일
150개 · 200개0일0일0일

부호가 틀린 날은 "골든크로스 상태인데 데드로 본" 날입니다. 히스토그램 부호 전환으로 매매하는 봇이라면 그날 주문이 백테스트와 다르게 나갑니다. 35개는 10일에 하루꼴(삼성전자 9.6%)입니다.

48일 12일 1일 0일 0일 35봉 60봉 100봉 150봉 200봉 삼성전자 최근 500거래일 · 히스토그램 부호가 전체 이력 값과 다른 날 KIS FHKST03010100 1회 100건 · 당일분봉 FHKST03010200 1회 30건
입력 봉 수별 MACD 히스토그램 부호 오류 (MACD 12·26·9, adjust=False)

이 숫자가 API 호출 설계와 바로 이어집니다. 한국투자증권 일봉 FHKST03010100(inquire-daily-itemchartprice)은 1회 호출에 최대 100건이라, 한 번만 받으면 "100개" 줄에 해당합니다. 오차가 작긴 하지만 0은 아닙니다. 두 번 받아 200개를 만드는 방법은 KIS 일봉 FHKST03010100 3년치 받는 법에 있습니다. 분봉은 더 빡빡합니다. KIS 당일분봉 FHKST03010200은 1회 30건이라(분봉 데이터 수집 — 당일 30건·과거 120건), 30개로 MACD를 내면 위 표의 35개보다도 나쁩니다. 키움 REST는 ka10080(분봉)·ka10081(일봉)로 받습니다(키움 ka10081·ka10080 응답 키). 1회 응답 건수는 키움 공식 명세로 확인하십시오.

3. 함정 ② TA-Lib은 33개를 NaN으로 비운다

talib.MACD는 느린 EMA(26)와 시그널(9)이 모두 채워지기 전 구간을 계산하지 않습니다. 워밍업 길이는 (26−1)+(9−1) = 33입니다. 실측도 같았습니다.

>>> int(np.isnan(talib.MACD(c.values, 12, 26, 9)[0]).sum())
33
>>> np.isnan(talib.MACD(c.values[-33:], 12, 26, 9)[2]).all()   # 최근 봉 33개
True                                                           # 히스토그램 전부 NaN
>>> np.isnan(talib.MACD(c.values[-34:], 12, 26, 9)[1][-1])     # 34개
False                                                          # 마지막 값부터 나온다

그래서 KIS 당일분봉 30건을 한 번 받아 talib.MACD에 넣으면 결과 전체가 NaN입니다. hist[-1] > 0 같은 비교는 NaN이면 항상 False라서 에러 없이 "신호 없음"이 됩니다. 봇이 장중 내내 주문을 한 건도 안 내는데 로그엔 아무 문제가 없는 경우입니다. 반대로 pandas ewm은 첫 봉부터 숫자를 내 주므로 NaN은 안 생기지만, 그 숫자가 2장의 "35개" 줄처럼 틀릴 뿐입니다. NaN이 안 나오는 쪽이 더 위험할 수 있습니다.

참고로 TA-Lib 0.8.1 파이썬 래퍼의 시그니처는 MACD(real, fastperiod=-0x80000000, slowperiod=-0x80000000, signalperiod=-0x80000000)이고, 인자를 빼고 부르면 C 라이브러리 기본값을 씁니다. 0.8.1 실측으로는 talib.MACD(c.values)와 talib.MACD(c.values, 12, 26, 9)가 같았습니다. 다만 TA-Lib은 BBANDS 기본 기간을 0.8.1에서 5→20으로 바꾼 전례가 있으니(파이썬 볼린저밴드 — 기본 기간 함정) 기간은 항상 적으십시오.

4. 함정 ③ 최신순 응답을 그대로 넣으면

증권사 API 응답 배열이 최신 날짜부터 오는 경우가 있습니다(TR마다 다르니 응답의 날짜 필드로 직접 확인하십시오). 이걸 정렬 없이 ewm에 넣으면 EMA가 시간을 거꾸로 흐르며 계산됩니다. 일부러 뒤집어 넣어 봤습니다.

rev = c.iloc[::-1]                        # 최신 → 과거 순서
mr, sr = macd_pandas(rev, adjust=False)
print(mr.loc[c.index[-1]])                # 가장 최근 날짜의 MACD
005930  reversed input: value at newest date MACD 0.00 vs correct -9461.84
005930  reversed hist sign mismatch last250: 168/250
069500  reversed hist sign mismatch last250: 176/250
035720  reversed hist sign mismatch last250: 116/250

가장 최근 날짜가 배열 맨 앞이 되니 그날의 EMA는 "첫 값" 그 자체이고, MACD는 정확히 0.00이 됩니다. 최근 250일 중 116~176일(46~70%)의 히스토그램 부호가 틀렸습니다. 동전 던지기보다 나쁜 신호입니다. 날짜 문자열(stck_bsop_date, 키움 dt 등)을 pd.to_datetime으로 바꿔 인덱스로 잡고 sort_index()를 한 줄 넣으면 끝나는 문제입니다.

5. 함정 ④ 반환 순서와 adjust 기본값

라이브러리를 바꾸거나 섞어 쓸 때 조용히 틀어지는 두 군데입니다.

도구반환 순서주의
talib.MACD(macd, macdsignal, macdhist) 튜플앞 33개 NaN
pandas-ta ta.macd()MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9두 번째 열이 히스토그램
pandas ewm()직접 조립adjust 기본 True

talib은 두 번째가 시그널, pandas-ta는 두 번째 열이 히스토그램입니다(pandas_ta/momentum/macd.py의 data 딕셔너리 순서). df.iloc[:, 1]처럼 위치로 꺼내던 코드를 라이브러리만 바꿔 쓰면 "MACD가 시그널을 상향 돌파" 조건이 "MACD가 히스토그램을 상향 돌파"로 바뀝니다. 열 이름으로 꺼내십시오.

adjust는 pandas 문서상 ewm의 기본값이 True입니다. 흔히 말하는 재귀식 EMA("오늘 EMA = α×오늘 종가 + (1−α)×어제 EMA")는 adjust=False입니다. 1장에서 봤듯 긴 데이터에서는 결과가 수렴하지만, 2장처럼 봉을 짧게 넣는 봇에선 둘의 오차가 다릅니다. 삼성전자 마지막 날을 최근 봉 30개로만 계산하면 MACD가 전체 이력 대비 adjust=True는 +516.92원, adjust=False는 −114.04원 어긋났습니다. 어느 쪽을 쓰든 백테스트와 봇이 같은 쪽이어야 합니다. RSI도 같은 구조의 평활 방식 차이가 있습니다(파이썬 RSI 계산 차이).

6. 봇에 넣는 안전한 MACD 함수

위 함정을 한 함수에 막아 둔 형태입니다. 증권사 응답을 이미 DataFrame으로 바꿨다고 가정합니다(숫자가 문자열로 오니 pd.to_numeric 먼저).

import numpy as np, pandas as pd

MIN_BARS = 200          # 실측: 150개부터 부호 오류 0 → 여유 포함

def safe_macd(df, fast=12, slow=26, signal=9):
    """df: 'date'(YYYYMMDD 문자열), 'close' 열. 마지막 값만 쓴다."""
    s = (df.assign(date=pd.to_datetime(df["date"], format="%Y%m%d"))
           .drop_duplicates("date")
           .set_index("date")["close"]
           .astype(float)
           .sort_index())                       # 함정 ③ 최신순 응답 방지
    if len(s) < MIN_BARS:                        # 함정 ① 봉 부족
        raise ValueError(f"MACD 봉 부족: {len(s)}개 < {MIN_BARS}")
    m = (s.ewm(span=fast, adjust=False).mean()
         - s.ewm(span=slow, adjust=False).mean())
    sig = m.ewm(span=signal, adjust=False).mean()
    hist = m - sig
    if np.isnan(hist.iloc[-1]):                  # 함정 ② NaN이면 조용히 넘어가지 않는다
        raise ValueError("MACD NaN")
    return float(m.iloc[-1]), float(sig.iloc[-1]), float(hist.iloc[-1])

정리 — 10년 백테스트에서 talib·pandas·pandas-ta는 사실상 같은 MACD를 냅니다. 봇에서 값이 갈리는 건 봉 개수(200개 이상), 정렬(sort_index), NaN 처리(TA-Lib 33개), 반환 순서(열 이름으로) 네 곳입니다.

이 글의 수치는 2016-10-17 ~ 2026-10-16 일봉 기준의 과거 실측이며, MACD 크로스가 앞으로의 수익이나 방향을 알려 준다는 뜻이 아닙니다. 특정 종목이나 전략을 권하지 않습니다. 라이브러리 기본값과 증권사 API 응답 건수는 바뀔 수 있으니 설치한 버전의 소스와 각 증권사 공식 문서로 확인하십시오. 백테스트 숫자가 실거래와 어긋나는 다른 원인은 백테스트와 실거래가 벌어지는 7가지 이유에 모았습니다.

7. 자주 묻는 질문

MACD 계산에 봉은 몇 개가 필요하나요?

12·26·9 설정 기준으로 실측상 150개 이상이면 전체 이력으로 계산한 값과 부호가 어긋난 날이 0일이었습니다. 삼성전자·KODEX 200·카카오 최근 500거래일에서 봉 35개로 계산하면 히스토그램 부호가 26~48일 틀렸고, 60개는 2~12일, 100개는 0~1일이었습니다. 봇에는 여유 있게 200개 이상을 넣는 것을 권합니다.

pandas ewm에서 adjust=True와 adjust=False 중 무엇을 써야 하나요?

일반적인 재귀식 EMA와 같은 것은 adjust=False입니다. pandas의 ewm 기본값은 adjust=True라서 초반 구간 값이 다르게 나옵니다. 다만 실측상 두 방식의 차이는 60~117번째 봉 이후 1원 미만으로 줄어들어, 충분히 긴 데이터라면 최근 값은 사실상 같습니다. 백테스트와 봇에서 같은 쪽을 쓰는 것이 더 중요합니다.

talib.MACD 결과 앞부분이 NaN인 이유는 무엇인가요?

TA-Lib MACD는 느린 EMA 26개와 시그널 9개를 채우는 워밍업 구간(26−1+9−1=33개)을 NaN으로 비워 둡니다. 그래서 봉이 33개 이하면 결과 전체가 NaN입니다. KIS 당일분봉 FHKST03010200은 1회 호출에 30건이라, 한 번만 받아 계산하면 신호가 아예 나오지 않습니다.

증권사 HTS의 MACD 값과 파이썬 값이 다르면 무엇부터 확인하나요?

입력 봉 개수, 날짜 정렬 순서, 기간 설정(12·26·9), 가격 기준(종가), 수정주가 여부, 미완성 현재 봉 포함 여부 순으로 맞춥니다. 특히 API 응답을 날짜 오름차순으로 정렬하지 않고 넣으면 값이 완전히 틀립니다. HTS의 정확한 산식과 시드 방식은 해당 증권사 도움말에서 확인하십시오.

백테스트 값과 봇 값이 같은지, 같이 맞춰 드립니다

쓰시는 지표와 조건을 알려 주시면 계산식을 한 벌로 통일해 백테스트와 실거래 봇에 똑같이 넣어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기