
앞의 두 글에서 밴드형 지표(볼린저 밴드, 켈트너 채널)를 다뤘습니다. 둘 다 "이동평균 ± 변동성"으로 가격의 위아래에 밴드를 그리는 구조였습니다. 이번 글에서는 ATR을 밴드가 아닌 추세 방향 판단과 동적 손절에 사용하는 슈퍼트렌드(SuperTrend)를 다룹니다.
지표 소개
슈퍼트렌드는 가격의 중간값(HL/2)을 기준으로 ATR 배수만큼 위아래에 밴드를 설정하고, 가격이 밴드를 돌파하면 추세 방향을 전환합니다.
기본 상단밴드 = (고가 + 저가) / 2 + K × ATR(N)
기본 하단밴드 = (고가 + 저가) / 2 - K × ATR(N)
일반적으로 N=14, K=3을 사용합니다.
핵심은 밴드가 한 방향으로만 이동한다는 것입니다. 상승 추세에서는 하단밴드(지지선)가 올라가기만 하고 내려가지 않습니다. 하락 추세에서는 상단밴드(저항선)가 내려가기만 하고 올라가지 않습니다. 가격이 이 밴드를 뚫으면 추세가 전환됩니다.
이전 지표들과의 비교
6편의 ATR은 변동성의 크기만 측정했습니다. 볼린저 밴드와 켈트너 채널은 가격을 밴드로 감쌌지만 방향성은 없었습니다. 슈퍼트렌드는 ATR을 사용하되, 추세 방향(상승/하락)과 동적 스톱 레벨을 동시에 제공합니다.
차트에서는 상승 추세 구간은 초록색 선(하단밴드 = 지지선), 하락 추세 구간은 빨간색 선(상단밴드 = 저항선)으로 표시됩니다.

초록색 선이 가격 아래에서 따라올라가는 것이 보입니다. 이것이 동적 지지선 역할을 하며, 가격이 이 선 아래로 내려가면 빨간색으로 전환됩니다.

추세 전환 시점(초록↔빨강)이 매매 신호가 됩니다. ATR 배수(K)가 클수록 전환이 느려지고(덜 민감), 작을수록 빨라집니다(더 민감).
해석
슈퍼트렌드에서 읽을 수 있는 정보는 두 가지입니다.
추세 방향: 초록(상승) / 빨강(하락). 가장 기본적인 해석입니다. 초록이면 롱 포지션 유지, 빨강이면 숏 포지션 유지(또는 현금 보유).
동적 스톱: 슈퍼트렌드 라인 자체가 트레일링 스톱 역할을 합니다. 상승 추세에서 초록색 선은 "여기 아래로 떨어지면 추세가 끝난 것"이라는 기준선입니다. ATR 기반이므로 변동성에 따라 스톱 거리가 자동 조절됩니다.
가설 설정
슈퍼트렌드의 가장 기본적인 사용법은 방향 전환 매매입니다.
- 진입 — 하락 → 상승 전환 (빨강 → 초록)
- 청산 — 상승 → 하락 전환 (초록 → 빨강)
ATR 배수(K)에 따른 차이를 비교합니다. K=2(민감), K=3(기본), K=4(둔감).
백테스트 조건
| 항목 | 설정 |
|---|---|
| 종목 | BTC/USDT (Binance Spot) |
| 기간 | 2018.01.01 ~ 2025.12.31 |
| 타임프레임 | 일봉 (1D) |
| ATR 기간 | 14일 |
| ATR 배수 | 2.0 / 3.0 / 4.0 (비교) |
| 진입 | 하락 → 상승 전환 |
| 청산 | 상승 → 하락 전환 |
| 초기 자본 | $10,000 |
| 수수료 / 슬리피지 | 미적용 |
코드 구현
import pandas as pd
import numpy as np
import mplfinance as mpf
import matplotlib.pyplot as plt
# ===== 설정 =====
DATA_FILE = "BTCUSDT_1d.csv"
ST_PERIOD = 14 # ATR 기간
ST_MULTS = [2.0, 3.0, 4.0] # ATR 배수 비교
INITIAL_CAPITAL = 10000
ZOOM_DAYS = 180
# ================
# 데이터 로드
df = pd.read_csv(DATA_FILE, index_col="timestamp", parse_dates=True)
# ATR 계산
high_low = df["high"] - df["low"]
high_close = (df["high"] - df["close"].shift(1)).abs()
low_close = (df["low"] - df["close"].shift(1)).abs()
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df["atr"] = tr.rolling(window=ST_PERIOD).mean()
# 슈퍼트렌드 계산 함수
def calc_supertrend(df, period, multiplier):
hl2 = (df["high"] + df["low"]) / 2
atr = df["atr"]
upper_basic = hl2 + multiplier * atr
lower_basic = hl2 - multiplier * atr
upper_band = upper_basic.copy()
lower_band = lower_basic.copy()
supertrend = pd.Series(index=df.index, dtype=float)
direction = pd.Series(index=df.index, dtype=int) # 1=up, -1=down
for i in range(period, len(df)):
if pd.isna(upper_basic.iloc[i]):
continue
# 상단밴드: 현재 기본값이 이전 상단밴드보다 낮고, 이전 종가가 이전 상단밴드보다 낮으면 유지
if i > period and not pd.isna(upper_band.iloc[i - 1]):
if upper_basic.iloc[i] < upper_band.iloc[i - 1] or df["close"].iloc[i - 1] > upper_band.iloc[i - 1]:
upper_band.iloc[i] = upper_basic.iloc[i]
else:
upper_band.iloc[i] = upper_band.iloc[i - 1]
# 하단밴드: 현재 기본값이 이전 하단밴드보다 높고, 이전 종가가 이전 하단밴드보다 높으면 유지
if i > period and not pd.isna(lower_band.iloc[i - 1]):
if lower_basic.iloc[i] > lower_band.iloc[i - 1] or df["close"].iloc[i - 1] < lower_band.iloc[i - 1]:
lower_band.iloc[i] = lower_basic.iloc[i]
else:
lower_band.iloc[i] = lower_band.iloc[i - 1]
# 방향 결정
if i == period:
direction.iloc[i] = 1 if df["close"].iloc[i] > upper_band.iloc[i] else -1
else:
prev_dir = direction.iloc[i - 1]
if prev_dir == -1 and df["close"].iloc[i] > upper_band.iloc[i]:
direction.iloc[i] = 1
elif prev_dir == 1 and df["close"].iloc[i] < lower_band.iloc[i]:
direction.iloc[i] = -1
else:
direction.iloc[i] = prev_dir
# 슈퍼트렌드 값
if direction.iloc[i] == 1:
supertrend.iloc[i] = lower_band.iloc[i]
else:
supertrend.iloc[i] = upper_band.iloc[i]
return supertrend, direction
# 백테스트 함수: 슈퍼트렌드 방향 전환으로 매매
def run_st_backtest(df, direction, initial_capital):
capital = initial_capital
holdings = 0
buy_price = 0
in_position = False
trades = []
equity_curve = []
for i in range(1, len(df)):
close = df["close"].iloc[i]
d = direction.iloc[i]
d_prev = direction.iloc[i - 1]
if pd.isna(d) or pd.isna(d_prev):
equity_curve.append(capital + holdings * close)
continue
if not in_position:
# 하락 → 상승 전환 → 매수
if d == 1 and d_prev == -1:
buy_price = close
holdings = capital / buy_price
capital = 0
in_position = True
else:
# 상승 → 하락 전환 → 매도
if d == -1 and d_prev == 1:
sell_price = close
capital = holdings * sell_price
pnl = (sell_price - buy_price) / buy_price * 100
trades.append({"pnl_pct": round(pnl, 2)})
holdings = 0
in_position = False
equity_curve.append(capital + holdings * close)
if in_position:
last_price = df["close"].iloc[-1]
capital = holdings * last_price
pnl = (last_price - buy_price) / buy_price * 100
trades.append({"pnl_pct": round(pnl, 2)})
holdings = 0
eq = pd.Series(equity_curve)
peak = eq.cummax()
mdd = ((eq - peak) / peak * 100).min()
return capital, trades, mdd
# 바이앤홀드
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("=" * 65)
print(f"SuperTrend (ATR {ST_PERIOD}) 백테스트 — ATR 배수 비교")
print(f"진입: 하락→상승 전환, 청산: 상승→하락 전환")
print(f"Data: {df.index[0].strftime('%Y-%m-%d')} ~ {df.index[-1].strftime('%Y-%m-%d')}")
print("=" * 65)
print(f" {'Mult':<8} {'Capital':>10} {'Return':>10} {'MDD':>8} {'Trades':>7} {'Win':>5} {'Lose':>5} {'WinRate':>8}")
print("-" * 65)
best_mult = None
best_st = None
best_dir = None
for mult in ST_MULTS:
st, direction = calc_supertrend(df, ST_PERIOD, mult)
capital, trades, mdd = run_st_backtest(df, direction, INITIAL_CAPITAL)
total_return = (capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
wins = len([t for t in trades if t["pnl_pct"] > 0])
losses = len([t for t in trades if t["pnl_pct"] <= 0])
win_rate = wins / len(trades) * 100 if trades else 0
print(f" ×{mult:<6} ${capital:>9,.0f} {total_return:>+9.2f}% {mdd:>+7.2f}% {len(trades):>6} {wins:>5} {losses:>5} {win_rate:>7.1f}%")
if mult == 3.0: # 차트용 기본값
best_mult = mult
best_st = st
best_dir = direction
print("-" * 65)
print(f" {'B&H':<8} ${bnh_capital:>9,.0f} {bnh_return:>+9.2f}%")
print("=" * 65)
# 슈퍼트렌드를 차트용 컬럼에 저장
df["st"] = best_st
df["st_dir"] = best_dir
df["st_up"] = df["st"].where(df["st_dir"] == 1, np.nan)
df["st_down"] = df["st"].where(df["st_dir"] == -1, np.nan)
# 차트 1 — 상승장
df_bull = df.loc["2020-10-01":"2021-04-30"].copy()
ap_bull = [
mpf.make_addplot(df_bull["st_up"], color="#43A047", width=1.5),
mpf.make_addplot(df_bull["st_down"], color="#E53935", width=1.5),
]
fig1, axes1 = mpf.plot(df_bull, type="candle", style="charles",
title=f"BTC/USDT SuperTrend (ATR {ST_PERIOD}, ×{best_mult}) (2020.10 ~ 2021.04, Bull)",
volume=False, figsize=(14, 8),
addplot=ap_bull, returnfig=True)
# 차트 2 — 최근 구간
df_zoom = df.tail(ZOOM_DAYS).copy()
ap_zoom = [
mpf.make_addplot(df_zoom["st_up"], color="#43A047", width=1.5),
mpf.make_addplot(df_zoom["st_down"], color="#E53935", width=1.5),
]
fig2, axes2 = mpf.plot(df_zoom, type="candle", style="charles",
title=f"BTC/USDT SuperTrend (ATR {ST_PERIOD}, ×{best_mult}) (Last {ZOOM_DAYS}D)",
volume=False, figsize=(14, 8),
addplot=ap_zoom, returnfig=True)
plt.show()
사용법
ST_PERIOD로 ATR 기간을, ST_MULTS로 비교할 배수를 변경할 수 있습니다.
결과 분석
=================================================================
SuperTrend (ATR 14) 백테스트 — ATR 배수 비교
진입: 하락→상승 전환, 청산: 상승→하락 전환
Data: 2018-01-01 ~ 2025-12-31
=================================================================
Mult Capital Return MDD Trades Win Lose WinRate
-----------------------------------------------------------------
×2.0 $ 106,919 +969.19% -53.76% 62 26 36 41.9%
×3.0 $ 59,990 +499.90% -50.57% 38 14 24 36.8%
×4.0 $ 118,515 +1085.15% -56.87% 23 11 12 47.8%
-----------------------------------------------------------------
B&H $ 65,507 +555.07%
=================================================================
앞의 볼린저 밴드, 켈트너 채널과 완전히 다른 결과입니다.
| 배수 | 수익률 | MDD | 거래 | 승률 |
|---|---|---|---|---|
| ×2.0 | +969.19% | -53.76% | 62 | 41.9% |
| ×3.0 | +499.90% | -50.57% | 38 | 36.8% |
| ×4.0 | +1085.15% | -56.87% | 23 | 47.8% |
몇 가지 눈에 띄는 점이 있습니다.
1. 추세추종의 힘. ×2.0과 ×4.0이 모두 B&H(+555%)를 넘었습니다. 앞의 두 글에서 밴드 역추세 전략이 +84% 수준에 머물렀던 것과 대조적입니다. 슈퍼트렌드는 하락장에서 포지션을 빠져나와서 자본을 보존하고, 상승장에서는 추세를 따라가며 수익을 가져갑니다. BTC 같은 추세 시장에 더 적합한 구조입니다.
2. 승률이 낮지만 수익이 높다. ×3.0의 승률은 36.8%로, 3번 중 2번은 집니다. 그런데도 +499%입니다. 이것이 추세추종의 특성입니다 — 지는 횟수가 많지만 이길 때 크게 이깁니다. 볼린저/켈트너의 역추세 전략이 승률 60%대였지만 수익률이 낮았던 것과 정반대입니다.
3. ×4.0이 가장 높은 수익률. 배수가 클수록 전환이 느려지고 거래 횟수가 줄어듭니다(62→38→23회). ×4.0이 +1085%로 가장 높은데, 23회 거래에 승률 47.8%로 가장 균형 잡힌 수치입니다. 둔감한 만큼 휩쏘를 당해서 적지만, 진짜 추세를 잡으면 놓치지 않습니다. 다만 ×3.0이 일반적 기본값으로 쓰이는 데는 이유가 있습니다 — ×4.0의 높은 수익은 BTC의 강한 추세성에 특화된 결과일 수 있고, 다른 시장에서는 너무 둔감해서 전환이 너무 늦을 수 있습니다.
차트에서 초록색 선(상승 추세)이 계단식으로 올라가는 것이 보입니다. 이것이 "한 방향으로만 이동하는 밴드" 원리입니다. 상승 추세에서 지지선은 올라가기만 하고, 가격이 이 선 아래로 떨어지면 빨간색(하락 추세)으로 전환됩니다.
정리
슈퍼트렌드는 ATR을 사용하되, 밴드가 아닌 추세 방향 + 동적 스톱을 제공하는 지표입니다. 볼린저/켈트너가 "가격이 밴드 어디에 있는가"를 보여준다면, 슈퍼트렌드는 "지금 추세가 상승인가 하락인가"를 명확하게 답합니다.
이 섹션의 세 지표를 정리하면 이렇습니다.
| 지표 | 변동성 측정 | 용도 | 단독 성과 |
|---|---|---|---|
| 볼린저 밴드 | 표준편차 | 변동성 판단, 스퀴즈 감지 | +84% (역추세) |
| 켈트너 채널 | ATR | 안정적 밴드 매매 | +83% (역추세) |
| 슈퍼트렌드 | ATR | 추세 방향 + 동적 스톱 | +499~1085% (추세추종) |
같은 ATR을 쓰더라도 어떻게 쓰느냐(밴드 vs 추세 필터)에 따라 결과가 극적으로 달라집니다.
본 포스팅은 지표의 원리와 백테스트 과정을 정리한 기술 블로그이며, 특정 자산의 매매를 권유하지 않습니다.
백테스트 결과는 과거 데이터 기반이며 실제 수익을 보장하지 않습니다. 모든 투자의 책임은 본인에게 있습니다.
'Development > Indicator Lab' 카테고리의 다른 글
| TTM Squeeze (0) | 2026.04.01 |
|---|---|
| 슈퍼트렌드 실전 전략(SuperTrend+트레일링) (0) | 2026.03.31 |
| 켈트너 채널 (Keltner Channel) (0) | 2026.03.29 |
| 볼린저 밴드 (Bollinger Bands) (0) | 2026.03.28 |
| 멀티타임프레임 전략 (0) | 2026.03.26 |