볼린저 밴드란 무엇인가?
볼린저 밴드(Bollinger Bands)는 1980년대 존 볼린저(John Bollinger)가 개발한 변동성 기반 기술적 지표입니다. 이동평균선을 중심으로 상단 밴드와 하단 밴드가 가격의 표준편차에 따라 확장·수축하며, 시장의 변동성 상태와 과매수·과매도 구간을 동시에 파악할 수 있습니다. 퀀트 트레이딩과 알고리즘 자동매매에서 가장 많이 활용되는 지표 중 하나이며, 특히 평균회귀(mean reversion) 전략의 핵심 도구로 널리 알려져 있습니다.
볼린저 밴드는 세 개의 선으로 구성됩니다:
- 중심선(Middle Band): N일 단순이동평균(SMA)
- 상단 밴드(Upper Band): 중심선 + (K × 표준편차)
- 하단 밴드(Lower Band): 중심선 − (K × 표준편차)
기본 설정은 N=20(20일 이동평균), K=2(표준편차 2배)입니다. 통계적으로 가격의 약 95%가 상·하단 밴드 안에 위치하게 되므로, 밴드를 벗어나는 움직임은 극단적인 가격 이탈로 해석할 수 있습니다.
파이썬으로 볼린저 밴드 계산하기
pandas를 사용하면 볼린저 밴드를 간결하게 구현할 수 있습니다. 핵심은 이동평균과 이동표준편차를 동시에 계산하는 것입니다.
import pandas as pd
import numpy as np
def bollinger_bands(prices: pd.Series, window: int = 20,
num_std: float = 2.0) -> pd.DataFrame:
"""볼린저 밴드 계산"""
sma = prices.rolling(window=window).mean()
std = prices.rolling(window=window).std()
upper = sma + (num_std * std)
lower = sma - (num_std * std)
# %B: 현재 가격이 밴드 내 어디에 위치하는지 (0~1)
percent_b = (prices - lower) / (upper - lower)
# 밴드폭(Bandwidth): 변동성 측정
bandwidth = (upper - lower) / sma * 100
return pd.DataFrame({
'upper': upper,
'middle': sma,
'lower': lower,
'percent_b': percent_b,
'bandwidth': bandwidth
})
여기서 두 가지 파생 지표가 특히 중요합니다:
- %B: 현재 가격이 하단 밴드(0)와 상단 밴드(1) 사이 어디에 위치하는지 나타냅니다. 0 미만이면 하단 밴드 이탈, 1 초과면 상단 밴드 이탈입니다.
- 밴드폭(Bandwidth): 상·하단 밴드 사이 거리를 중심선으로 나눈 값으로, 변동성의 크기를 수치화합니다. 밴드폭이 극도로 좁아지는 스퀴즈(Squeeze) 현상은 큰 움직임의 전조로 해석됩니다.
볼린저 밴드 자동매매 전략 3가지
전략 1: 밴드 터치 평균회귀 전략
가장 기본적인 볼린저 밴드 전략입니다. 가격이 하단 밴드에 닿으면 매수하고, 상단 밴드에 닿으면 매도합니다. 횡보장(레인지 마켓)에서 높은 승률을 보이지만, 강한 추세장에서는 손실이 발생할 수 있습니다.
def mean_reversion_signals(df: pd.DataFrame) -> pd.DataFrame:
"""밴드 터치 기반 평균회귀 매매 신호"""
df = df.copy()
df['signal'] = 0
# 하단 밴드 터치 후 반등 → 매수
df.loc[(df['Close'] > df['bb_lower']) &
(df['Close'].shift(1) <= df['bb_lower'].shift(1)), 'signal'] = 1
# 상단 밴드 터치 후 하락 → 매도
df.loc[(df['Close'] < df['bb_upper']) &
(df['Close'].shift(1) >= df['bb_upper'].shift(1)), 'signal'] = -1
return df
전략 2: 볼린저 밴드 스퀴즈 브레이크아웃
밴드폭이 일정 기간 내 최저치로 좁아진 후(스퀴즈) 가격이 밴드를 돌파하면 해당 방향으로 진입합니다. 추세 추종(trend following) 전략으로, 큰 움직임의 시작을 포착합니다.
def squeeze_breakout_signals(df: pd.DataFrame,
squeeze_lookback: int = 120) -> pd.DataFrame:
"""볼린저 밴드 스퀴즈 후 브레이크아웃 신호"""
df = df.copy()
df['signal'] = 0
# 밴드폭이 최근 N일 중 최소 → 스퀴즈 상태
df['bw_min'] = df['bandwidth'].rolling(squeeze_lookback).min()
df['is_squeeze'] = df['bandwidth'] <= df['bw_min'] * 1.05
# 스퀴즈 해제 + 상단 돌파 → 매수
df.loc[(df['is_squeeze'].shift(1)) &
(df['Close'] > df['bb_upper']), 'signal'] = 1
# 스퀴즈 해제 + 하단 돌파 → 매도(공매도)
df.loc[(df['is_squeeze'].shift(1)) &
(df['Close'] < df['bb_lower']), 'signal'] = -1
return df
스퀴즈 브레이크아웃의 핵심은 "변동성은 순환한다"는 원리입니다. 낮은 변동성 기간 후에는 반드시 높은 변동성 기간이 찾아오며, 스퀴즈가 풀리는 순간의 방향을 따라가면 큰 수익 기회를 잡을 수 있습니다.
전략 3: %B + RSI 복합 필터 전략
볼린저 밴드의 %B 지표와 RSI(상대강도지수)를 결합하여 신호의 정확도를 높이는 전략입니다. 두 지표가 동시에 극단값을 보일 때만 거래하므로 거짓 신호를 크게 줄일 수 있습니다.
def composite_signals(df: pd.DataFrame) -> pd.DataFrame:
"""볼린저 %B + RSI 복합 매매 신호"""
df = df.copy()
df['signal'] = 0
# %B < 0.05 AND RSI < 30 → 강력 매수
df.loc[(df['percent_b'] < 0.05) &
(df['rsi'] < 30), 'signal'] = 1
# %B > 0.95 AND RSI > 70 → 강력 매도
df.loc[(df['percent_b'] > 0.95) &
(df['rsi'] > 70), 'signal'] = -1
return df
백테스팅 엔진 구현
세 가지 전략을 공정하게 비교하기 위한 통합 백테스팅 엔진을 구축합니다. 이동평균 크로스오버 백테스팅 가이드의 프레임워크를 확장한 형태입니다.
import yfinance as yf
class BollingerBacktester:
"""볼린저 밴드 전략 백테스팅 엔진"""
def __init__(self, ticker: str, start: str, end: str,
window: int = 20, num_std: float = 2.0):
self.ticker = ticker
self.window = window
self.num_std = num_std
self.df = self._prepare_data(start, end)
def _prepare_data(self, start: str, end: str) -> pd.DataFrame:
df = yf.download(self.ticker, start=start, end=end)
bb = bollinger_bands(df['Close'], self.window, self.num_std)
df = pd.concat([df, bb.rename(columns={
'upper': 'bb_upper', 'lower': 'bb_lower',
'middle': 'bb_middle'
})], axis=1)
return df
def run(self, signal_func, initial_capital: float = 10_000_000,
stop_loss_pct: float = 0.03) -> dict:
"""신호 함수를 받아 백테스팅 실행"""
df = signal_func(self.df)
capital = initial_capital
position = 0
shares = 0
entry_price = 0
trades = []
for i, row in df.iterrows():
# 손절 확인
if position == 1 and entry_price > 0:
loss = (entry_price - row['Close']) / entry_price
if loss >= stop_loss_pct:
capital += shares * row['Close']
trades.append({'date': i, 'type': 'STOP_LOSS',
'price': row['Close']})
shares, position = 0, 0
continue
if row['signal'] == 1 and position == 0:
shares = int(capital // row['Close'])
if shares > 0:
capital -= shares * row['Close']
entry_price = row['Close']
position = 1
trades.append({'date': i, 'type': 'BUY',
'price': row['Close']})
elif row['signal'] == -1 and position == 1:
capital += shares * row['Close']
trades.append({'date': i, 'type': 'SELL',
'price': row['Close']})
shares, position = 0, 0
final_value = capital + (shares * df.iloc[-1]['Close'])
return {
'total_return': round(
(final_value - initial_capital) / initial_capital * 100, 2
),
'final_value': round(final_value),
'num_trades': len(trades),
'win_rate': self._calc_win_rate(trades)
}
@staticmethod
def _calc_win_rate(trades: list) -> float:
"""매매 쌍별 승률 계산"""
wins = 0
total = 0
buy_price = None
for t in trades:
if t['type'] == 'BUY':
buy_price = t['price']
elif t['type'] in ('SELL', 'STOP_LOSS') and buy_price:
total += 1
if t['price'] > buy_price:
wins += 1
buy_price = None
return round(wins / total * 100, 1) if total > 0 else 0
전략별 성과 비교와 최적화
세 전략의 특성은 시장 국면에 따라 크게 달라집니다:
| 전략 | 적합한 시장 | 평균 매매 빈도 | 핵심 리스크 |
|---|---|---|---|
| 밴드 터치 평균회귀 | 횡보장 | 높음 (월 4~8회) | 추세장에서 연속 손실 |
| 스퀴즈 브레이크아웃 | 추세 전환 | 낮음 (분기 2~4회) | 거짓 돌파(fakeout) |
| %B + RSI 복합 | 전 구간 | 중간 (월 1~3회) | 신호 발생 빈도 낮음 |
파라미터 최적화 핵심 포인트
볼린저 밴드 전략에서 튜닝 가능한 주요 파라미터와 그 영향을 정리합니다:
- 이동평균 기간(window): 짧게 하면(10~15) 민감해져 신호가 많아지고, 길게 하면(25~50) 큰 추세만 포착합니다. 일반적으로 단기 트레이딩은 10~15, 스윙 트레이딩은 20~25가 적합합니다.
- 표준편차 배수(num_std): 1.5~2.5 범위에서 조정합니다. 작은 값은 신호 빈도를 높이지만 거짓 신호도 증가하고, 큰 값은 확실한 기회만 포착하지만 매매 기회가 줄어듭니다.
- 손절매 비율: 종목의 일일 변동성(ATR)을 기준으로 설정하면 불필요한 손절을 줄일 수 있습니다. 일반적으로 ATR의 1.5~2배를 권장합니다.
def optimize_bollinger(ticker: str, start: str, end: str,
signal_func) -> dict:
"""그리드 서치로 최적 볼린저 밴드 파라미터 탐색"""
best = {'return': -float('inf')}
for window in [10, 15, 20, 25]:
for num_std in [1.5, 1.75, 2.0, 2.25, 2.5]:
bt = BollingerBacktester(ticker, start, end, window, num_std)
result = bt.run(signal_func)
if result['total_return'] > best['return']:
best = {
'window': window,
'num_std': num_std,
'return': result['total_return'],
'win_rate': result['win_rate'],
'trades': result['num_trades']
}
return best
실전 적용 시 주의사항
볼린저 밴드 자동매매를 실전에 적용할 때 반드시 고려해야 할 사항들입니다:
거래량 확인 필터 추가
밴드 이탈 신호가 발생했을 때 거래량이 평소보다 현저히 높다면 진짜 브레이크아웃일 확률이 높고, 거래량이 낮다면 거짓 신호일 가능성이 큽니다. 거래량 이동평균 대비 1.5배 이상인 경우에만 신호를 따르는 필터를 추가하면 승률이 크게 개선됩니다.
# 거래량 필터: 20일 평균 거래량의 1.5배 이상
vol_ma = df['Volume'].rolling(20).mean()
volume_confirm = df['Volume'] > vol_ma * 1.5
# 기존 신호에 거래량 필터 적용
df.loc[~volume_confirm, 'signal'] = 0
다중 시간프레임 분석
일봉에서 매수 신호가 발생했더라도 주봉에서 하락 추세라면 위험합니다. 상위 시간프레임의 추세 방향과 일치하는 신호만 실행하면 안전성이 높아집니다. 예를 들어 주봉 볼린저 밴드 중심선 위에 있을 때만 일봉 매수 신호를 따르는 방식입니다.
시장 국면 자동 감지
밴드폭(Bandwidth)을 활용하면 현재 시장이 횡보 중인지 추세 중인지 자동으로 판별할 수 있습니다:
- 밴드폭 하위 25% → 횡보장 → 평균회귀 전략 적용
- 밴드폭 상위 25% → 추세장 → 브레이크아웃 전략 적용
- 중간 50% → 복합 전략(%B + RSI) 적용
이렇게 시장 국면에 따라 전략을 자동 전환하는 어댑티브(adaptive) 시스템을 구축하면, 단일 전략의 한계를 극복하고 다양한 시장 환경에서 안정적인 성과를 기대할 수 있습니다.
마무리: 볼린저 밴드 자동매매 실전 체크리스트
볼린저 밴드 기반 자동매매 시스템을 구축하기 전 확인해야 할 항목입니다:
- ✅ 이동평균 기간과 표준편차 배수를 종목 특성에 맞게 최적화했는가?
- ✅ 최소 3년 이상의 데이터로 백테스팅을 수행했는가?
- ✅ 횡보장과 추세장 구간을 분리하여 성과를 검증했는가?
- ✅ 거래량 확인 필터를 추가했는가?
- ✅ 다중 시간프레임 분석으로 추세 방향을 확인하는가?
- ✅ 손절매(ATR 기반)와 포지션 사이징 규칙이 있는가?
- ✅ 거래 비용(수수료 + 슬리피지)을 반영한 수익률인가?
- ✅ Walk-Forward Analysis로 과적합을 점검했는가?
볼린저 밴드는 변동성이라는 시장의 핵심 속성을 직접 활용하는 강력한 도구입니다. 특히 스퀴즈 현상을 통한 브레이크아웃 탐지와, %B를 활용한 정밀한 진입 타이밍 판단은 다른 지표에서 찾기 어려운 고유한 강점입니다. RSI나 MACD와 결합하여 복합 전략을 구성하고, 철저한 백테스팅과 리스크 관리를 통해 견고한 자동매매 시스템을 완성하세요.