ADX 필터 백테스트 — 25로 거르면 횡보 손실이 줄까
20일 돌파 전략에 ADX(14) 필터를 20·25·30으로 걸어 국내 22종목 10년(2016-01 ~ 2026-10-30)을 돌려 보니, 필터는 횡보장 손실을 줄이지 못하고 거래 수만 줄였습니다. 거래는 1,050건 → 759 → 522 → 375건으로 줄었지만 승률은 29.5% → 28.3% → 27.8% → 24.8%로 오히려 내려갔고, 횡보 구간(60일 효율비율 0.2 미만) 거래의 기대값은 필터 없음 1.34%, ADX 25에서도 1.22%로 그대로였습니다. ADX 25의 거래당 기대값 2.72%(필터 없음 2.00%)는 좋아 보이지만, 같은 개수를 무작위로 뺀 1,000번 중 18.4%가 그만큼 나와서 운과 구별되지 않습니다. 줄어든 것은 손실이 아니라 시장에 들어가 있던 시간이고, 그래서 종목별 MDD 중앙값만 −45.2% → −29.0%로 좋아졌습니다. (FinanceDataReader 수정주가, 왕복 비용 0.3%, 배당 미포함, 과거 결과는 미래를 보장하지 않음)
목차
- 무엇을 돌렸나 — 진입·청산·ADX 계산
- 결과 — 거래는 줄고 승률도 줄었다
- ADX 구간별로 쪼개 보면 — 25~30만 좋고 40 이상은 최악
- 횡보 손실이 줄었나 — ADX와 무관한 잣대로 확인
- '거래가 줄어서'인가 '나쁜 거래가 빠져서'인가 — 무작위 제거 1,000회
- 봇에 넣는다면 — 코드와 판단 기준
- 한계
- 자주 묻는 질문
1. 무엇을 돌렸나 — 진입·청산·ADX 계산
이 글은 손절 후 재진입 대기, ATR 손절 vs 고정 −5%와 같은 틀을 씁니다. 청산 쪽을 바꿔 본 세 편에 이어 이번에는 진입 쪽 필터 하나만 바꿉니다.
| 항목 | 설정 |
|---|---|
| 진입 | 종가가 직전 20거래일 고가를 넘으면 다음 날 시가 매수 |
| ADX 필터 | 신호일 ADX(14) ≥ 20 / 25 / 30 일 때만 진입 (없음 = 모든 돌파 진입) |
| 청산 | ① 진입가 −5% 손절(장중 저가 터치, 갭이면 시가) ② 종가가 직전 10일 저가 이탈 → 다음 날 시가 |
| 비용 | 왕복 0.3% (수수료·거래세·슬리피지 뭉뚱그린 가정) |
| 종목·기간 | 2016년 이전 상장 KOSPI·KOSDAQ 22종목, 2016-01-01 ~ 2026-10-30 (ASOF 고정) |
| 데이터 | FinanceDataReader fdr.DataReader 수정주가, pandas 2.2.3 · numpy |
ADX는 TA-Lib 없이 와일더 평활로 직접 계산했습니다. 삼성전자(005930) 2026-10-23 값이 ADX 26.46 · +DI 12.79 · −DI 35.42로, 파이썬 ADX 계산 글에서 talib.ADX로 낸 값과 소수 둘째 자리까지 같습니다. 그 글에서 본 것처럼 봉이 짧으면 25 판정이 흔들리므로, 2014년부터 받아 앞 100개 봉은 신호에서 뺐습니다.
def wilder_adx(df, n=14):
"""TA-Lib ADX 와 같은 계산: 첫 값은 n개 합/평균, 이후 와일더 평활."""
h, l, c = (df[k].values for k in ("High", "Low", "Close"))
...
atr[i] = atr[i - 1] - atr[i - 1] / n + tr[i] # +DM, -DM 도 같은 식
dx = 100 * np.abs(pdi - mdi) / (pdi + mdi)
adx[2 * n - 1] = np.nanmean(dx[n:2 * n])
adx[i] = (adx[i - 1] * (n - 1) + dx[i]) / n
adx[:100] = np.nan # 초기값 영향 구간은 버린다
# 진입 조건 — 필터는 이 한 줄이 전부다
ok = c[i] > hi20[i] and (th is None or adx[i] >= th)
2. 결과 — 거래는 줄고 승률도 줄었다
ASOF 2026-10-30 tickers 22 stop -5% cost 0.3%
filter trades win% stop_hit% expectancy avg_hold med_cum med_mdd
none 1050 29.5% 49.9% 2.00% 17.6d -7.8% -45.2%
ADX20 759 28.3% 53.4% 2.09% 16.8d -0.2% -42.5%
ADX25 522 27.8% 55.6% 2.72% 16.4d 1.5% -29.0%
ADX30 375 24.8% 58.7% 2.04% 14.9d -17.9% -30.7%
흔히 기대하는 그림은 "ADX로 횡보장을 거르면 허탕 돌파가 빠져 승률이 오른다"입니다. 실제로는 반대였습니다.
- 승률이 내려갔습니다. 29.5% → 24.8%. 손절(−5%)로 끝난 비율은 49.9% → 58.7%로 올랐습니다.
- 보유 기간이 짧아졌습니다. 17.6일 → 14.9일. ADX가 높을 때 돌파했다는 건 이미 많이 오른 뒤라는 뜻이라, 되돌림에 손절이 먼저 걸립니다.
- 기대값은 25에서만 올랐습니다. 2.00% → 2.72%. 20과 30은 필터 없음과 거의 같습니다. 기준값을 5 바꿀 때마다 결과가 오르내리는 모양이라 "25가 정답"이라고 읽기 어렵습니다.
종목별 누적수익 1등 횟수는 필터 없음 6 · ADX20 4 · ADX25 9 · ADX30 3이었습니다. 25가 가장 많이 이겼지만 22종목 중 9개입니다. SK하이닉스는 필터 없음 +884.6%가 ADX 25에서 +136.3%로, 삼성SDI는 +162.4%가 −39.2%로 무너졌고, 반대로 삼성전기는 +637.0% → +1070.4%, 에스엠은 −37.2% → +68.9%로 좋아졌습니다. 큰 추세 한두 번을 탔느냐 놓쳤느냐가 종목 결과를 가릅니다.
3. ADX 구간별로 쪼개 보면 — 25~30만 좋고 40 이상은 최악
필터 없음으로 들어간 1,050건을 신호일 ADX 값으로 나눴습니다.
adx_bin trades win% expectancy sum_ret
0-20 592 28.9% 1.44% 851.3%
20-25 239 30.5% 2.16% 515.8%
25-30 123 35.0% 6.18% 759.7%
30-40 75 28.0% 0.52% 39.3%
40- 21 9.5% -3.38% -71.0%
두 가지가 보입니다. 첫째, "ADX 20 미만 = 버릴 거래"가 아닙니다. 0~20 구간 592건의 기대값은 1.44%로 플러스였고, 누적 합(851.3%)은 다섯 구간 중 가장 큽니다. 필터는 이 돈을 같이 버립니다. 둘째, ADX는 높을수록 좋은 게 아니라 25~30 한 칸만 튀고 30을 넘으면 꺾입니다. 40 이상 21건은 승률 9.5%입니다. ADX는 추세가 '얼마나 오래, 세게 이어졌나'를 재는 후행 지표라, 값이 높을 때의 돌파는 추세 끝물에 올라탄 진입이 많습니다. "ADX ≥ 30이 더 엄격하니 더 안전하다"는 직관이 여기서 틀립니다.
참고로 돌파 신호의 98.7%는 이미 +DI > −DI였습니다. 20일 신고가를 낸 날이면 방향 조건은 거의 저절로 맞기 때문에, "ADX 25 + (+DI > −DI)"를 걸어도 결과(214건, 기대값 3.47%)가 ADX 25 하나와 거의 같습니다. 방향 조건은 돌파 전략에서 추가 정보가 없습니다.
4. 횡보 손실이 줄었나 — ADX와 무관한 잣대로 확인
"ADX가 낮으면 횡보"로 정의하고 ADX 필터의 효과를 재면 동어반복이 됩니다. 그래서 횡보 여부는 ADX와 상관없는 60일 효율비율(ER, Kaufman)로 따로 판정했습니다. ER = |60일 전 대비 변화| ÷ 60일간 일별 변화 절댓값의 합이고, 0.2 미만을 횡보로 봤습니다.
filter regime trades win% expectancy sum_ret
none chop 870 28.9% 1.34% 1165.4%
none trend 180 32.8% 5.17% 929.7%
ADX20 chop 526 27.8% 1.22% 639.6%
ADX20 trend 233 29.6% 4.07% 947.6%
ADX25 chop 289 27.3% 1.22% 352.9%
ADX25 trend 233 28.3% 4.59% 1069.4%
ADX30 chop 161 20.5% -1.26% -202.2%
ADX30 trend 214 28.0% 4.52% 966.6%
- 필터 없음에서 횡보 구간 거래가 870건(83%)입니다. 국내 개별 종목의 20일 돌파는 대부분 박스권 안에서 나옵니다.
- ADX 25를 걸어도 남은 거래의 55%(289/522)가 여전히 횡보 구간입니다. 그리고 그 거래들의 기대값은 1.22%로 필터 전(1.34%)과 같습니다. 횡보 거래를 '더 좋은 횡보 거래'로 바꾸지 못하고 개수만 줄였습니다.
- ADX 30에서는 횡보 구간 거래가 −1.26%로 손실이 됐습니다. 박스권에서 ADX가 30을 넘는 순간은 대개 박스 상단을 한 번 세게 찌른 직후라, 돌파가 아니라 고점 매수가 됩니다.
추세 구간(ER ≥ 0.2) 거래는 필터와 상관없이 4~5%대로 좋습니다. 결국 수익은 '추세 구간에 있었느냐'가 만들고, ADX 필터는 그것을 사전에 골라내지 못했습니다.
5. '거래가 줄어서'인가 '나쁜 거래가 빠져서'인가 — 무작위 제거 1,000회
필터를 걸면 거래가 줄고, 거래가 적으면 결과는 운에 더 크게 흔들립니다. 그래서 필터 없음 1,050건에서 ADX가 기준 미만인 것만 뺀 결과와 같은 개수를 무작위로 뺀 결과 1,000번을 비교했습니다(np.random.default_rng(7)).
ADX20: kept 458/1050 kept_exp 2.72% dropped_exp 1.44% random_exp p5 0.66% p50 1.97% p95 3.36% share_random>=kept 20.9%
ADX25: kept 219/1050 kept_exp 3.32% dropped_exp 1.65% random_exp p5 -0.14% p50 1.84% p95 4.75% share_random>=kept 18.4%
ADX30: kept 96/1050 kept_exp -0.33% dropped_exp 2.23% random_exp p5 -0.94% p50 1.46% p95 6.87% share_random>=kept 87.3%
ADX 25로 남긴 219건의 기대값 3.32%는 무작위로 219건을 남긴 경우의 18.4%가 넘었습니다. 통상 쓰는 5% 기준에 한참 못 미치니, "ADX 25가 나쁜 거래를 골라냈다"고 말할 근거가 없습니다. ADX 30은 무작위보다도 나빴습니다(87.3%).
왜 2장(522건)과 여기(219건)의 거래 수가 다른가. 2장은 필터를 실제로 건 상태로 다시 돌린 결과입니다. ADX 25 미만이라 건너뛴 날 뒤에 추세가 이어지면 며칠 뒤 ADX가 25를 넘은 날 다른 돌파로 들어갑니다. 즉 필터는 거래를 빼기만 하는 게 아니라 진입을 늦춥니다. 5장은 같은 거래 목록에서 빼기만 해서 '선별력'만 따로 잰 것입니다. 두 방식 모두 결론은 같습니다.
꼬리 거래 의존도도 확인했습니다. 에코프로(086520)를 빼고, 남은 거래 중 상위 1%까지 빼면 기대값은 필터 없음 0.11% · ADX20 −0.11% · ADX25 0.42% · ADX30 −0.24%로 전부 0 근처입니다. 이 전략의 수익은 소수의 대형 추세에서 나오고, ADX 필터 차이는 그 몇 건을 탔느냐의 차이에 가깝습니다. 비슷한 함정은 분할매도 백테스트(승률은 오르고 기대값은 1/9)에서도 봤습니다.
MDD가 좋아진 것(종목별 중앙값 −45.2% → −29.0%)은 사실이지만 이유는 단순합니다. 거래가 절반이니 시장에 들어가 있던 날도 절반입니다. 같은 효과는 포지션 크기를 줄여도 얻습니다. MDD 하나로 필터를 고르면 '덜 투자해서 덜 잃은 것'을 '좋은 전략'으로 착각합니다(몬테카를로로 MDD 분포 보기).
6. 봇에 넣는다면 — 코드와 판단 기준
그래도 ADX 필터를 쓰고 싶다면, 기준값 하나를 고르기 전에 아래 세 가지를 같이 출력하게 만드는 것이 안전합니다.
# 필터 효과 판정 3종 — 위 백테스트의 [2]·[3]·[4] 출력과 같은 계산
rng = np.random.default_rng(7)
base = [t for v in res[None].values() for t in v] # 필터 없음 거래 목록
r0 = np.array([t["ret"] for t in base]); a0 = np.array([t["adx"] for t in base])
keep = a0 >= 25
print("남긴/버린 기대값", r0[keep].mean(), r0[~keep].mean()) # 버린 쪽도 플러스면 돈을 같이 버린 것
sims = [r0[rng.choice(len(r0), keep.sum(), replace=False)].mean() for _ in range(1000)]
print("무작위가 이긴 비율", np.mean(np.array(sims) >= r0[keep].mean())) # 5% 넘으면 운과 구별 불가
- 버린 거래의 기대값이 플러스라면, 필터는 손실이 아니라 수익을 버리고 있습니다(이번 실측: 1.65%).
- 무작위 제거와 비교해 5% 안에 들지 못하면, 기준값의 효과는 운과 구별되지 않습니다.
- ADX와 무관한 횡보 잣대(효율비율, 200일선 위·아래 등)로 남은 거래를 다시 나눠, 횡보 구간 거래가 실제로 줄었는지 봅니다.
실시간 봇에서는 ADX를 어제까지 확정된 일봉으로 계산해야 합니다. KIS Developers 일봉 응답이라면 stck_bsop_date·stck_hgpr·stck_lwpr·stck_clpr, 키움 REST ka10081이라면 dt·high_pric·low_pric·cur_prc를 High·Low·Close로 넣으면 됩니다. 장중 현재가로 오늘 ADX를 계산하면 백테스트와 다른 값이 나옵니다(미래참조 편향). 기준값을 20·25·30 중 '가장 좋았던 값'으로 고르는 것 자체도 과최적화이니, 고르고 싶다면 워크포워드로 다시 확인하십시오.
전체 재현 코드: /downloads/adx_filter_backtest.py (pip install finance-datareader pandas numpy). ASOF = "2026-10-30"으로 고정해 이 글의 숫자가 그대로 나오며, 최신 데이터로 보려면 ASOF = None으로 바꾸면 됩니다(숫자는 달라집니다).
7. 한계
- 생존 편향 — 22종목은 지금 상장돼 있는 종목입니다. 중간에 상장폐지된 종목이 빠져 있어 전체 수치가 부풀려졌을 수 있습니다(생존편향 실측).
- 전략 하나 — 20일 돌파 + −5% 손절 + 10일 저가 이탈 한 조합입니다. 이동평균 교차나 다른 청산 규칙에서는 ADX 필터의 효과가 다를 수 있습니다.
- ADX 기간 14 고정 — 기간을 바꾸면 구간별 결과가 달라질 수 있습니다.
- 배당 미포함, 비용은 왕복 0.3% 가정 — 실제 체결 슬리피지는 종목·시간대에 따라 다릅니다. 거래세율도 연도마다 달랐습니다.
- 종목별 동일 비중, 거래 단위 복리 — 포트폴리오 자금 배분·동시 보유 제약은 반영하지 않았습니다.
이 글은 특정 종목이나 지표를 권하지 않으며, 과거 백테스트 결과는 미래 수익을 보장하지 않습니다. 실제 적용 전에는 본인 종목·기간으로 다시 확인하십시오.
8. 자주 묻는 질문
ADX 25 필터를 걸면 승률이 오르나요?
이번 실측에서는 내려갔습니다. 20일 돌파 22종목 10년 기준 승률이 필터 없음 29.5%에서 ADX 25일 때 27.8%, ADX 30일 때 24.8%였고, -5% 손절로 끝난 비율은 49.9%에서 58.7%까지 올랐습니다. ADX가 높을 때의 돌파는 이미 많이 오른 뒤의 진입이 많기 때문입니다.
ADX 20 미만 구간의 거래는 버려도 되나요?
이번 데이터에서는 아닙니다. 신호일 ADX 0~20 구간 592건의 거래당 기대값이 1.44%로 플러스였고 누적 합도 다섯 구간 중 가장 컸습니다. 필터를 걸면 이 수익도 같이 버립니다.
ADX 필터 효과가 진짜인지 어떻게 확인하나요?
필터 없음 거래 목록에서 기준 미만만 뺀 결과와, 같은 개수를 무작위로 뺀 결과 1,000번을 비교합니다. 무작위가 이긴 비율이 5%를 넘으면 운과 구별되지 않습니다. 이번 ADX 25는 18.4%였습니다. 버린 거래의 기대값이 플러스인지도 함께 봐야 합니다.
ADX 필터를 걸면 MDD가 줄던데 좋은 것 아닌가요?
종목별 MDD 중앙값은 -45.2%에서 -29.0%로 줄었지만 거래 수가 절반으로 줄어 시장에 있던 기간이 짧아진 효과가 큽니다. 포지션 크기를 줄여도 비슷한 효과가 나므로 MDD 하나로 필터를 고르면 안 됩니다.
내 전략의 필터, 효과가 있는지부터 재 드립니다
ADX·이평·거래량 필터를 넣고 뺀 결과를 무작위 비교까지 붙여 백테스트하고, 키움·KIS API로 실제 주문까지 이어지는 봇으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기