파이썬 ADX — 봉 30개면 25 판정이 15% 틀린다
파이썬 talib.ADX(high, low, close, 14)는 앞 27개(2n−1)가 NaN이고, 그 뒤 값도 입력한 봉 수에 따라 달라집니다. 삼성전자 10년 일봉으로 최근 500일을 다시 계산하니 봉 30개로 계산한 ADX는 75일(15%)이 "ADX > 25" 판정과 어긋났고, 60개는 22일, 100개부터 0일이었습니다. 값을 소수 둘째 자리까지 맞추려면 약 150개가 필요했습니다. pandas로 직접 짤 때 ewm(alpha=1/14, adjust=False)는 TA-Lib과 같아지지만 rolling(14).mean()은 다른 지표가 됩니다(같은 날 49.31 대 26.46). 키움 ka10081 같은 최신순 응답을 뒤집지 않고 넣으면 26.46이 11.70으로 나옵니다. (TA-Lib 0.6.8 · C 0.6.4 · pandas 2.2.3 · FinanceDataReader 0.9.202, ASOF=2026-10-23, 2026-10-26 실측)
목차
- ADX가 계산하는 것 — 네 단계
- talib.ADX의 NaN 27개
- 실측 ① 입력 봉 수가 판정을 바꾼다
- 실측 ② rolling 평균으로 짜면 다른 지표
- 실측 ③ 최신순 데이터를 그대로 넣으면
- KIS·키움 API로 몇 개를 받아야 하나
- 안전하게 계산하는 코드
- 자주 묻는 질문
1. ADX가 계산하는 것 — 네 단계
ADX(Average Directional Index)는 J. Welles Wilder가 1978년 책에서 소개한 추세 강도 지표입니다. 방향(오를지 내릴지)이 아니라 "지금 한쪽으로 세게 움직이는 중인가"만 0~100으로 나타냅니다. 자동매매에서는 보통 "ADX가 25를 넘을 때만 추세추종 진입" 같은 필터로 씁니다. 25는 관행적으로 쓰는 기준일 뿐 검증된 경계값이 아니라는 점은 먼저 짚어 둡니다.
- 방향 이동: 오늘 고가 − 어제 고가(
+DM), 어제 저가 − 오늘 저가(−DM) 중 큰 쪽 하나만 남기고 나머지는 0. - 진폭:
TR= 고가−저가, |고가−전일종가|, |저가−전일종가| 중 최댓값. - Wilder 평활: 셋을 각각 기간 14로 평활해
+DI = 100·(+DM 평활)/(TR 평활),−DI도 같은 식. 그다음DX = 100·|+DI − −DI| / (+DI + −DI). - ADX:
DX를 다시 기간 14로 Wilder 평활.
문제의 대부분은 3·4단계의 Wilder 평활에서 나옵니다. Wilder 평활은 오늘값 = (어제값 × 13 + 새 값) / 14를 반복하는 재귀식이라 과거가 영원히 조금씩 남습니다. 그래서 "언제부터 계산을 시작했나"가 오늘 값에 영향을 주고, 평활을 두 번(3단계·4단계) 거치는 ADX는 한 번만 거치는 RSI보다 그 영향이 오래 갑니다. TA-Lib 공식 함수 설명도 첫 ADX를 처음 기간 DX의 평균으로 두고 이후 재귀식으로 이어 간다고 적고 있습니다.
2. talib.ADX의 NaN 27개
같은 삼성전자 일봉 2,646개(2016-01-04 ~ 2026-10-23)를 TA-Lib 함수 다섯 개에 넣고 앞부분 NaN 개수를 셌습니다.
rows 2646 2016-01-04 2026-10-23
NaN ADX 27 PDI 14 DX 14 ADXR 40
period 7 ADX NaN 13
period 20 ADX NaN 39
27 bars -> 27 NaN of 27
30 bars -> valid 3
100 bars -> valid 73
| 함수 | 기간 14 NaN | 규칙 | 봉 100개 넣으면 유효값 |
|---|---|---|---|
talib.PLUS_DI · MINUS_DI | 14 | n | 86 |
talib.DX | 14 | n | 86 |
talib.ADX | 27 | 2n − 1 | 73 |
talib.ADXR | 40 | 3n − 2 | 60 |
봉 27개를 넣으면 결과 27개가 전부 NaN이라 봇은 "신호 없음"으로 조용히 넘어갑니다. 예외가 나지 않으니 로그에도 안 남습니다. 그런데 더 위험한 쪽은 NaN이 아니라 NaN이 끝난 직후의 값입니다. 28번째 봉부터 숫자가 나오지만 그 숫자는 아직 수렴 전입니다.
3. 실측 ① 입력 봉 수가 판정을 바꾼다
실제 봇은 10년치를 넣지 않습니다. 증권사 API에서 최근 N개를 받아 마지막 값 하나를 봅니다. 그래서 최근 500거래일 각각에 대해 그날까지의 최근 N개만 넣어 talib.ADX를 다시 계산하고, 10년 전체로 계산한 값과 비교했습니다.
N=28 nan=0 meanerr=7.880 maxerr=32.141 flip25=79
N=30 nan=0 meanerr=7.233 maxerr=33.779 flip25=75
N=40 nan=0 meanerr=4.335 maxerr=20.680 flip25=42
N=50 nan=0 meanerr=2.386 maxerr=11.511 flip25=29
N=60 nan=0 meanerr=1.301 maxerr=6.255 flip25=22
N=100 nan=0 meanerr=0.096 maxerr=0.395 flip25=0
N=150 nan=0 meanerr=0.003 maxerr=0.013 flip25=0
N=200 nan=0 meanerr=0.000 maxerr=0.000 flip25=0
flip25는 "ADX > 25"의 참·거짓이 전체 기간 계산과 달랐던 날 수입니다(500일 중). 봉 30개면 75일, 즉 15%의 날에 필터가 반대로 동작했고, 최대 오차는 33.8포인트였습니다. 마지막 날(2026-10-23) 한 점만 보면 이렇습니다.
lastday N=28 ADX=35.76 (full 26.46)
lastday N=30 ADX=36.46 (full 26.46)
lastday N=50 ADX=28.93 (full 26.46)
lastday N=100 ADX=26.39 (full 26.46)
lastday N=150 ADX=26.46 (full 26.46)
봉 30개로 계산하면 36.46이라 "강한 추세"로 읽히는데, 충분히 긴 데이터로는 26.46입니다. 같은 종목·같은 날·같은 함수인데 입력 길이만 다릅니다. HTS 차트와 봇 값이 다르다는 문의의 상당수가 여기서 나옵니다. 차트 프로그램은 화면에 깔린 수백 개 봉으로 계산하고, 봇은 API로 받은 몇십 개로 계산하기 때문입니다.
이 숫자는 삼성전자 일봉 한 종목, 한 기간의 결과입니다. 변동성이 큰 종목이나 분봉에서는 수렴 속도가 다를 수 있으니, 본인 종목으로 같은 비교를 한 번 돌려 보는 편이 확실합니다. 아래 7절 코드에 그 검사를 넣어 두었습니다.
4. 실측 ② rolling 평균으로 짜면 다른 지표
TA-Lib 설치가 번거로워 pandas로 직접 짜는 경우가 많습니다. 평활 방식 세 가지를 같은 데이터에 돌렸습니다.
LAST 2026-10-23 talib 26.4579 ewmF 26.4579 ewmT 26.4579 sma 49.3051
ewmF-talib absdiff at bar 30 11.0447
ewmF-talib absdiff at bar 60 3.4138
ewmF-talib absdiff at bar 100 0.2066
ewmF-talib absdiff at bar 150 0.0088
ewmF-talib absdiff at bar 200 0.0003
sma-talib max diff last 500: 32.3090 mean 9.5124
ADX>25 days talib 217 sma 289 disagree 126
| 구현 | 2026-10-23 값 | TA-Lib과의 관계 |
|---|---|---|
talib.ADX | 26.46 | 기준 |
ewm(alpha=1/14, adjust=False) | 26.46 | 앞부분만 다름. 오차 1 미만은 80번째 봉부터, 0.01 미만은 149번째 봉부터 계속 유지 |
ewm(alpha=1/14) (pandas 기본 adjust=True) | 26.46 | 위와 거의 같음(0.01 미만은 146번째 봉부터) |
rolling(14).mean() | 49.31 | 수렴하지 않음. 최근 500일 평균 오차 9.5, 최대 32.3, 25 판정 126일 불일치 |
핵심은 alpha=1/14입니다. Wilder 평활은 지수평활에서 alpha = 1/n인 경우와 같습니다. pandas에서 span=14나 com을 잘못 넣으면 alpha = 2/15가 되어 일반 EMA가 되고, rolling(14).mean()은 과거 14개만 똑같이 평균 내는 단순이동평균이라 재귀가 아예 없습니다. 인터넷에 도는 ADX 코드 중 rolling 버전이 꽤 있는데, 이건 "조금 다른 ADX"가 아니라 다른 지표입니다. 25 같은 기준값도 Wilder 방식에서 쓰던 숫자라 rolling 버전에 그대로 옮기면 의미가 없습니다(실측에서 rolling 버전은 25를 넘는 날이 289일로 TA-Lib의 217일보다 33% 많았습니다).
+DI·−DI 교차 횟수도 갈립니다. 최근 500일 동안 TA-Lib 기준 48회, rolling 기준 52회였습니다. DI 교차를 진입 신호로 쓰는 전략이라면 체결 횟수와 비용이 같이 바뀝니다(거래비용 반영 백테스트 참고).
set_unstable_period는 무엇을 하나
talib.set_unstable_period('ADX', 100)
u = talib.ADX(H, L, C, 14)
# unstable100 NaN 127 last 26.4579
TA-Lib에는 앞부분 불안정 구간을 NaN으로 더 가려 주는 설정이 있습니다. 100을 주면 NaN이 27개에서 127개로 늘고, 마지막 값은 그대로입니다. 즉 값을 고쳐 주는 기능이 아니라 못 믿을 구간을 숨기는 기능입니다. 입력을 30개만 주고 이 설정을 켜면 결과가 전부 NaN이 될 뿐이라, 봉 수 문제를 해결하지는 못합니다. 전역 설정이라 다른 코드에도 영향을 주므로 쓰고 나면 set_unstable_period('ADX', 0)으로 되돌리는 편이 안전합니다.
5. 실측 ③ 최신순 데이터를 그대로 넣으면
증권사 차트 응답은 최신 날짜가 맨 앞에 오는 경우가 많습니다. 키움 REST 일봉 ka10081의 공식 명세 응답 예시도 stk_dt_pole_chart_qry 배열이 "dt": "20250908" 다음에 "dt": "20250905"가 오는 최신순입니다.
{
"stk_cd": "005930",
"stk_dt_pole_chart_qry": [
{
"cur_prc": "70100",
"trde_qty": "9263135",
"trde_prica": "648525",
"dt": "20250908",
"open_pric": "69800",
"high_pric": "70500",
"low_pric": "69600",
...
},
{
"cur_prc": "69500",
"dt": "20250905",
...
이 순서 그대로 배열을 만들어 talib.ADX에 넣으면 TA-Lib은 맨 앞을 가장 오래된 봉으로 보고 시간을 거꾸로 계산합니다. 실측입니다.
100 reversed: r[0](newest pos)= nan r[-1]=11.70 true newest 26.46
reversed-150 valid 123 flip25 82 meanabs 13.83
최근 100개를 최신순으로 넣고 "마지막 값 = 오늘"로 읽으면 26.46이 아니라 11.70이 나옵니다. 실제 오늘 위치(r[0])는 워밍업 구간이라 NaN입니다. 150개로 해도 유효한 123일 중 82일이 25 판정에서 갈렸습니다. 에러는 나지 않습니다. 정렬 실수는 같은 계열 지표 전부에서 일어나므로 MACD·볼린저밴드를 함께 쓰는 봇이라면 한곳에서 정렬을 강제하는 편이 낫습니다.
6. KIS·키움 API로 몇 개를 받아야 하나
| 데이터 | 1회 응답 | ADX(14) 관점 |
|---|---|---|
KIS 일봉 FHKST03010100 | 최대 100건 | 1회분이면 유효값 73개, 그중 앞부분은 수렴 전 → 2회 받아 150개 이상 (FHKST03010100 연속 조회) |
KIS 당일분봉 FHKST03010200 | 30건 | 1회분이면 ADX 유효값 3개, 오차 평균 7포인트대 → 분봉 ADX는 여러 번 받아 누적 저장 (분봉 데이터 받는 법) |
키움 일봉 ka10081 | 연속조회(cont-yn·next-key) | 최신순 응답 → 오름차순 정렬 필수, upd_stkpc_tp 1로 수정주가 요청 (ka10081 수정주가 함정) |
KIS 분봉 30건의 오차는 위 일봉 실측(N=30 평균 오차 7.2)을 옮긴 추정입니다. 분봉은 변동 패턴이 달라 숫자가 그대로 같지는 않겠지만, 30개로는 부족하다는 방향은 같습니다. 분봉 ADX를 쓰려면 장중에 받은 봉을 파일이나 DB에 쌓아 두고 150개 이상에서 계산하는 구조가 필요합니다. 고가·저가가 들어가는 지표라 수정주가 여부도 중요합니다. 액면분할 전후가 섞이면 TR이 하루에 수십 배로 튀어 이후 수십 개 봉의 ADX가 왜곡됩니다.
7. 안전하게 계산하는 코드
정렬·중복·봉 수·수렴 구간을 한 함수에서 막습니다. 아래 코드는 위 실측에 쓴 데이터로 실행해 확인했습니다.
import numpy as np
import pandas as pd
import talib
MIN_BARS = 150 # 실측: 100개부터 25 판정 불일치 0일, 150개부터 오차 0.013 이하
def adx_safe(df: pd.DataFrame, n: int = 14, min_bars: int = MIN_BARS) -> pd.DataFrame:
"""df: 날짜 인덱스 + High/Low/Close 열. 최신순으로 와도 된다."""
df = df.sort_index() # ③ 최신순 응답 뒤집기
if df.index.has_duplicates:
raise ValueError("같은 날짜가 두 번 있다 — 연속조회 경계 중복 확인")
if len(df) < min_bars:
raise ValueError(f"봉 {len(df)}개 — ADX({n})는 {min_bars}개 이상 필요")
h, l, c = (df[k].astype(float).to_numpy() for k in ("High", "Low", "Close"))
out = pd.DataFrame({
"adx": talib.ADX(h, l, c, timeperiod=n),
"pdi": talib.PLUS_DI(h, l, c, timeperiod=n),
"mdi": talib.MINUS_DI(h, l, c, timeperiod=n),
}, index=df.index)
out.iloc[: min_bars - 1] = np.nan # ① 수렴 전 구간은 신호에서 제외
return out
def warmup_check(df: pd.DataFrame, n: int = 14, short: int = 30, days: int = 250) -> int:
"""내 종목에서 '짧은 입력'이 25 판정을 몇 번 뒤집는지 센다."""
df = df.sort_index()
h, l, c = (df[k].astype(float).to_numpy() for k in ("High", "Low", "Close"))
full = talib.ADX(h, l, c, n)
flips = 0
for i in range(len(df) - days, len(df)):
a = talib.ADX(h[i-short+1:i+1], l[i-short+1:i+1], c[i-short+1:i+1], n)[-1]
flips += (a > 25) != (full[i] > 25)
return flips
if __name__ == "__main__":
df = pd.read_csv("005930_daily.csv", index_col=0, parse_dates=True)
print(adx_safe(df).tail(3).round(2))
print("30개 입력 시 최근 250일 중 뒤집힌 날:", warmup_check(df))
실행 결과입니다.
adx pdi mdi
Date
2026-10-21 23.59 14.64 32.94
2026-10-22 24.88 13.62 33.13
2026-10-23 26.46 12.79 35.42
30개 입력 시 최근 250일 중 뒤집힌 날: 29
KIS로 받는다면 stck_bsop_date를 인덱스로, stck_hgpr·stck_lwpr·stck_clpr를 High·Low·Close로, 키움 ka10081이라면 dt·high_pric·low_pric·cur_prc로 이름만 바꿔 넣으면 됩니다. 앱키·토큰은 코드에 적지 말고 환경변수에서 읽으십시오. 미래 데이터가 섞이지 않게 신호는 "어제까지 확정된 봉"으로 내는 것이 기본입니다(룩어헤드 편향 찾는 법).
ADX는 과거 가격의 움직임 크기를 요약할 뿐 이후 방향이나 수익을 알려 주지 않습니다. 이 글의 수치는 한 종목·한 기간의 계산 재현이며 특정 전략이나 종목을 권하지 않습니다. 과거 결과는 미래를 보장하지 않으니, 필터를 넣고 뺀 차이는 본인 데이터로 백테스트해 확인하십시오(백테스트 완전 가이드).
8. 자주 묻는 질문
talib.ADX 결과 앞부분이 NaN인 이유는?
TA-Lib의 ADX는 기간 n일 때 lookback이 2n-1입니다. 기본 14면 앞 27개가 NaN이고 28번째 봉부터 값이 나옵니다. +DI·-DI·DX는 앞 14개, ADXR은 앞 40개가 NaN입니다. 봉 27개만 넣으면 결과 27개가 전부 NaN이라 신호가 하나도 나오지 않습니다.
ADX는 봉 몇 개로 계산해야 HTS 값과 맞나요?
삼성전자 10년 일봉으로 최근 500일을 다시 계산해 보니, 입력 봉 30개는 ADX 25 기준 판정이 75일 어긋났고 60개는 22일, 100개부터 0일이었습니다. 오차가 0.01 밑으로 내려가는 데는 약 150개가 필요했습니다. 신호용으로는 최소 100개, 값 자체를 맞추려면 150개 이상을 권합니다.
pandas로 ADX를 직접 짜면 TA-Lib과 다른가요?
평활을 ewm(alpha=1/14, adjust=False)로 하면 앞부분만 다르고 200개쯤부터 TA-Lib과 소수 넷째 자리까지 같아집니다. 반면 rolling(14).mean()으로 평활하면 Wilder 방식이 아니라 다른 지표가 됩니다. 실측에서 같은 날 rolling 구현은 49.31, TA-Lib은 26.46이었고 최근 500일 중 126일이 25 판정에서 갈렸습니다.
증권사 API로 받은 데이터로 ADX를 계산할 때 주의할 점은?
KIS 일봉 FHKST03010100은 1회 100건이라 한 번 받은 데이터로는 앞 27개가 NaN이고 나머지 73개 중 앞부분은 아직 수렴 전입니다. 두 번 이상 받아 150개 이상을 만들고, 응답이 최신순이면 반드시 날짜 오름차순으로 뒤집은 뒤 계산하십시오. 최신순 100개를 그대로 넣고 마지막 값을 오늘로 읽으면 실측에서 26.46이어야 할 값이 11.70으로 나왔고, 오늘 위치의 값은 워밍업에 걸려 NaN이었습니다.
지표 값이 HTS와 다르다면, 계산부터 맞춰 드립니다
쓰시는 증권사와 지표만 알려 주시면 데이터 수집량, 정렬, 수정주가, 워밍업 구간까지 맞춘 자동매매 프로그램으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기