볼린저 밴드(Bollinger Bands)는 존 볼린저(John Bollinger)가 개발한 변동성 지표로, 주가의 20일 이동평균선을 중심으로 ±2표준편차 거리에 상한선과 하한선을 그린 밴드입니다.
볼린저는 그의 저서 Bollinger on Bollinger Bands에서 이 밴드를 활용한 세 가지 핵심 매매 기법을 소개했으며, 이후 한 가지 변형 기법을 추가로 제안했습니다. 이에 따라 총 4가지 핵심 매매법 – 1.변동성 돌파 매매법, 2.추세 추종 매매법, 3.추세 반전 매매법, 4.다이버전스 매매 전략 – 이 널리 알려져 있습니다. 아래에서는 각 전략의 개념을 도식화한 그림, Matplotlib를 활용한 시뮬레이션 차트, 그리고 해당 전략을 구현한 Python 코드 예시를 순서대로 제시합니다. Python 코드를 보시면 이해하시기 정말 쉬울 것입니다.
1. 변동성 돌파 매매법 (Volatility Breakout: The Squeeze)
개념 요약: 변동성 돌파 전략은 밴드 폭이 급격히 좁아진 후(저변동성) 큰 움직임이 나올 것을 예상하여, 밴드 돌파 방향으로 매매하는 방법입니다. 밴드 간격이 축소(‘스퀴즈’)되었다가 다시 벌어질 때 추세가 형성되는 성질을 이용합니다. 존 볼린저는 “밴드 폭이 역사적 저점일 때, 이후 상단 돌파 시 상승 추세 시작, 하단 돌파 시 하락 추세 시작을 알린다”라고 설명했습니다. 아래 그림은 보라색 영역처럼 밴드 폭이 좁아진 구간 후 실제 주가가 상단 밴드를 돌파하며 큰 상승 추세(노란색 영역)가 시작되는 예시입니다. 보시면 아시겠지만 눌림목 처럼 수렴하다가 한번에 볼린저 밴드 상단을 뚫고 가는것이 보이시죠?

보라색 박스는 밴드 폭이 역사적 저점인 저변동성 구간, 노란색 박스는 돌파 이후 급격한 변동성 증가 구간을 나타냅니다.
전략 세부: 변동성 돌파 매매에서는 먼저 밴드폭 지표(BandWidth) 등을 활용하여 밴드 폭이 일정 기준 이하로 좁아진 종목을 찾습니다. 그런 다음, 가격이 상단 밴드를 강하게 돌파하면 매수하고, 하단 밴드 밑으로 이탈하면 매도(또는 공매도)하는 방식으로 진입합니다. 돌파 방향으로 거래량 증가나 모멘텀 지표의 확인이 있으면 신뢰도가 높아집니다. 단, 가짜 돌파(Head Fake)에 유의해야 하는데, 한쪽으로 돌파하는 척하다가 반대로 큰 움직임이 나오는 경우를 말합니다. 따라서 돌파 후에도 밴드가 계속 확대되는지 확인하고, 스톱로스를 설정하여 대응합니다.
시뮬레이션 예제: 아래 코드는 임의의 주가 데이터에 변동성 돌파 전략을 적용한 간단한 구현 예시입니다. 먼저 일정 기간 동안 밴드폭이 낮은지를 확인한 후, 상단 밴드 돌파 시 매수 신호, 하단 돌파 시 매도 신호를 생성합니다.
아래 그림을 보시면 매수와 매도가 번갈아 가며 진행됩니다. 선물에서는 공매도가 가능하니 처음 빨간색 두번에 첫 번재는 청산이고 두번째는 진입이라 보시면 됩니다.

아래 코드를 보시면, 응용하실 부분이 볼린저 밴드를 만드는 구간(플/마 표준편차 2 보이시죠?)이 중요하겠습니다. 우리가 코딩하더라도 저것만 있으면 쉽게 하실 수 있겠지요. 그리고 밴드폭이 좁냐/넓냐가 상당히 중요하지요? 그래서 밴드폭을 만드는 구간이 필요합니다.
그리고 매수/매도에 대한 판단이 가장 필요하겠지요? 4.항을 잘 살펴 봅시다.
우선 매수를 조건으로 설명해 보겠습니다. 매수를 하기 위해서는 밴드가 상당히 좁아진 상태에서 큰 힘을 모아 한번에 뚫고 가야하지요? 그래서 경계조건(Threshold)를 0.02로 하였습니다. 이 부분이 노하우인데 여러 학습을 거치든, 본인의 노하우를 넣어 이 부분을 정확히 정해야 합니다. 앞서 계산한 밴드폭 내에 경계조건(Threshold)가 들어오면 1차 합격입니다. 그다음 상단밴드를 현재가가 돌파하게 되면 매수를 시작합니다. 그다음 매도를 하는 구간은 20일 이동평균선 아래로 떨어졌을때 매도를 실시합니다. 이해 가셨죠? 매도 포지션은 반대로 생각하시면 됩니다.
요약해 볼까요?
1) 밴드폭내에 경계조건(Threshold)이 들어오면 1차 합격 : 눌림목이라 보시면 됩니다.
2) 밴드 상단을 현재가가 뚫고 올라가면 매수
3) 20일 이동평균선을 현재가가 뚫고 내려가면 매도
이렇게 한싸이클이 구성됩니다. 아래는 그 코드를 정리한 것 뿐이고 여러분들은 충분히 응용하실 수 있을 겁니다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 1. 가상 주가 데이터 생성 (여러 사이클로 구성)
np.random.seed(0)
base_price = 100
cycles = 8 # 총 사이클 수
phase_A = 30 # 컨솔리데이션 구간 (낮은 변동성)
phase_B = 5 # 돌파 구간 (상승 또는 하락 돌파)
phase_C = 15 # 회복(추세 지속) 구간
prices = [base_price]
# 여러 사이클 생성: 짝수 사이클은 상승 돌파, 홀수 사이클은 하락 돌파
for cycle in range(cycles):
# Phase A: 컨솔리데이션 (낮은 변동성)
for t in range(phase_A):
new_price = prices[-1] + np.random.normal(0, 0.2)
prices.append(new_price)
# Phase B: 돌파 구간
if cycle % 2 == 0:
drift = 2.0 # 상승 돌파
else:
drift = -2.0 # 하락 돌파
for t in range(phase_B):
new_price = prices[-1] + drift + np.random.normal(0, 1.0)
prices.append(new_price)
# Phase C: 회복 구간 (추세 지속)
for t in range(phase_C):
new_price = prices[-1] + drift + np.random.normal(0, 0.5)
prices.append(new_price)
prices = pd.Series(prices)
# 2. 볼린저 밴드 계산 (20일 이동평균, 2표준편차)
window = 20
ma = prices.rolling(window).mean()
std = prices.rolling(window).std()
upper_band = ma + 2 * std
lower_band = ma - 2 * std
# 3. 밴드폭(BandWidth) 계산: (상한밴드 - 하한밴드) / 이동평균
band_width = (upper_band - lower_band) / ma
# 4. 거래 신호 생성: 한 번 진입하면 반드시 그에 대응하는 청산이 발생하도록 함
# state = 0: flat, 1: long 보유, -1: short 보유
state = 0
in_squeeze = False
threshold = 0.02 # 스퀴즈(저변동성) 임계치
signals = np.zeros(len(prices)) # 0: 없음, 1: 매수(진입 또는 청산 시 매수), -1: 매도(진입 또는 청산 시 매도)
for i in range(window, len(prices)):
# flat 상태: 진입 조건 확인
if state == 0:
# 스퀴즈 조건: 밴드폭이 임계치 이하이면 스퀴즈 상태로 전환
if band_width[i] < threshold:
in_squeeze = True
# 스퀴즈 상태에서 돌파 발생하면 진입 신호 발생
if in_squeeze:
if prices[i] > upper_band[i]:
signals[i] = 1 # 상승 돌파: 매수 진입
state = 1
in_squeeze = False
elif prices[i] < lower_band[i]:
signals[i] = -1 # 하락 돌파: 매도 진입 (short 진입)
state = -1
in_squeeze = False
# 스퀴즈 조건이 해제되면 리셋
if band_width[i] >= threshold:
in_squeeze = False
# long 상태: 진입 후 청산 조건 → 가격이 20일 MA 아래로 떨어지면 청산 (매도)
elif state == 1:
if prices[i] < ma[i]:
signals[i] = -1 # long 청산 (매도)
state = 0
# short 상태: 진입 후 청산 조건 → 가격이 20일 MA 위로 올라가면 청산 (매수)
elif state == -1:
if prices[i] > ma[i]:
signals[i] = 1 # short 청산 (매수)
state = 0
signals = pd.Series(signals)
# 진입 및 청산 신호의 인덱스 추출 (진입과 청산은 쌍으로 발생함)
buy_points = signals[signals == 1].index.tolist()
sell_points = signals[signals == -1].index.tolist()
print("Buy signals at indices:", buy_points)
print("Sell signals at indices:", sell_points)
print("Number of paired trades:", min(len(buy_points), len(sell_points)))
# 5. 차트 그리기 및 결과 파일 저장
plt.figure(figsize=(14,7))
plt.plot(prices, label='Price', color='black')
plt.plot(ma, label='20-day MA', color='blue')
plt.plot(upper_band, label='Upper Band', color='green', linestyle='--')
plt.plot(lower_band, label='Lower Band', color='red', linestyle='--')
plt.plot(buy_points, prices[buy_points], '^', markersize=10, color='g', label='Buy / Exit Signal')
plt.plot(sell_points, prices[sell_points], 'v', markersize=10, color='r', label='Sell / Exit Signal')
plt.title("Bollinger Band Paired Trade Simulation")
plt.xlabel("Time")
plt.ylabel("Price")
plt.legend()
plt.tight_layout()
plt.savefig("bollinger_band_pair_trades_fixed.png")
plt.show()
위 코드는 상태를 명확하게 관리하여, 한 번 진입하면 반드시 그에 대응하는 청산 신호가 발생하도록 작성되었습니다.
즉, flat 상태에서 밴드폭이 임계치 이하(스퀴즈 상태)가 되면 돌파 신호를 감지하여 진입(매수 또는 매도)을 발생시키고, 진입 후에는 반드시 반대 조건(가격이 20일 이동평균선(=MA)을 돌파하는 조건)이 충족될 때 청산 신호를 발생시켜 포지션을 종료한 후 다시 flat 상태로 돌아갑니다.
- 데이터 생성:
여러 사이클(총 8회)로 각 사이클마다 컨솔리데이션(30일), 돌파(5일: 짝수 사이클은 상승, 홀수 사이클은 하락), 회복(15일) 구간을 만들어 사실적인 주가 흐름을 생성합니다. - 볼린저 밴드 및 밴드폭 계산:
20일 이동평균과 2표준편차를 이용해 상한 및 하한 밴드를 계산하고, 밴드폭 = (상한 - 하한) / MA를 구합니다. - 거래 신호 생성 (상태 머신):
- flat 상태(state 0)에서는 밴드폭이 임계치(threshold = 0.02) 이하인 경우 스퀴즈 상태(in_squeeze)를 설정하고, 스퀴즈 상태에서 가격이 상한 밴드를 돌파하면 매수 진입, 하한 밴드를 돌파하면 매도 진입을 발생시킵니다.
- 진입 후에는 long 또는 short 상태(state 1 또는 -1)가 되며, 각각의 상태에서는 가격이 20일 MA를 반대 방향으로 돌파하면 반드시 청산 신호가 발생하고 state를 0으로 전환합니다.
- 이로 인해 각 거래는 반드시 “진입-청산” 쌍으로 발생하며, 중복 진입이 발생하지 않습니다.
- 결과 및 차트:
매수 및 매도 신호의 인덱스가 출력되며, 차트에 이를 표시한 후 PNG 파일로 저장됩니다.
2. 추세 추종 매매법 (Trend-Following Strategy)
개념 요약: 추세 추종 전략은 강한 상승(또는 하락) 추세가 형성되었음을 확인하고 그 방향으로 따라가는 기법입니다. 볼린저는 “밴드만으로는 충분치 않고, 다른 지표를 함께 보면 밴드의 진짜 위력이 나타난다”고 말하며 %B와 거래량 지표를 조합해 추세의 시작을 포착하는 방법을 제안했습니다. %B는 현재 가격이 밴드 내 어디쯤 위치하는지 나타내는 지표이고, **MFI(Money Flow Index)**는 거래량 가중한 RSI로 거래 강도를 나타냅니다. %B가 0.8 이상(상단 밴드 부근)이고 MFI가 80 이상이면 강한 상승 추세가 확인되었다고 보고 매수하며, 반대로 %B 0.2 이하 && MFI 20 이하이면 약세 추세로 보고 매도합니다. 즉 강세가 확인될 때 매수하고, 약세가 확인될 때 파는 추세 추종 원칙입니다.
예를 들어, 아래 차트에서 주가가 상단 밴드를 타고 오르는 국면에서 %B와 MFI 지표가 모두 높은 수준을 유지하면 (밴드 상단 근처에서 강한 매수세 지속) 추세 추종 매매법에 따른 진입 신호로 볼 수 있습니다. 한편 중기 추세선(20일선)이 붕괴되거나 %B와 MFI가 급격히 떨어지면 추세 종료 신호로 간주하여 청산하게 됩니다.
전략 세부: 기본 규칙은 다음과 같습니다.
- 매수 진입: %B 값이 0.8 초과 (가격이 밴드 상단의 80% 지점 이상) AND MFI 지표가 80 초과일 때 – 강한 상승 모멘텀과 거래 강도 확인 → 추세 속 매수
- 매도 청산: %B 값이 0.2 미만 OR MFI 지표가 20 미만일 때 – 상승 추세 붕괴 징후 → 포지션 정리
(참고: 하락 추세를 역으로 추종하는 공매도 전략도 가능하지만, 여기서는 매수 관점으로 설명합니다.) 존 볼린저는 이때 추세가 진행되는 동안은 파라볼릭SAR 등의 추적 스톱을 활용해 수익을 극대화하라고 조언합니다.
시뮬레이션 예제: 아래 코드는 %B와 MFI 기반의 추세 추종 전략을 구현한 간단한 예시입니다. 가상의 우상향 추세 데이터에 적용하여, 조건 충족 시 매수하고 조건 이탈 시 파는 로직을 보여줍니다.

아래 코드를 봐볼까요? 우선 임의 데이터를 만들고 볼린저 밴드를 계산합니다. 볼린저 밴드는 표준편차 2라고 앞서 설명 드렸었죠? 그다음 %B 인데 이부분은 간단하게 아래 식으로 구현 합니다. (현재가 - 최 하단밴드) / (최상단밴드-최하단밴드) 간단하죠? 예로 선물 가격이 다음과 같을 때 (310 - 300) / (320-300) = 0.5죠? 그러면 매수 조건이 안된것입니다.
그다음 MFI인데 아래와 같습니다.
아래는 코드에서 MFI(Money Flow Index)를 계산하는 부분에 대한 공학적이고 전문적인 설명입니다. MFI 계산에서 일반적으로 (High + Low + Close) / 3의 값을 사용하지만, 이 예제에서는 단순화를 위해 가격 시리즈(prices)를 그대로 사용합니다. 그리고 계산 기간은 10일로 설정하여, 최근 10일간의 흐름을 반영합니다. diff() 함수는 시계열 데이터에서 현재 값과 이전 값의 차이를 계산합니다. 이 차분 값은 가격 변화량을 나타내며, 실제 MFI에서는 거래량과 곱해져 "돈의 흐름"을 산출하지만, 여기서는 거래량이 생략되어 순수 가격 변화만을 반영합니다. 설명을 보시면 전체 흐름 중 상승흐름의 비율이 얼만큼이가에 대한 지표로 이해하시면 됩니다.
- Positive Flow:
raw_money_flow 값이 양수인 경우, 즉 가격이 상승한 날의 변동값은 그대로 유지하고, 그 외의 경우는 0으로 대체합니다. - Negative Flow:
반대로, 가격이 하락한 날의 raw_money_flow 값은 음수로 나타나므로, 부호를 반전시켜 절대값(양수)으로 만듭니다. 이는 하락 흐름의 강도를 측정하기 위함입니다. - 롤링 윈도우를 통한 합산 설명:
최근 10일간의 상승 흐름과 하락 흐름을 각각 합산합니다. 이 값들은 기간 내 전체 상승 및 하락의 세기를 나타내며, MFI 계산의 분자와 분모로 사용됩니다. - 최종 MFI 계산 설명:
MFI는 상승 흐름의 합을 **전체 흐름(상승 흐름 + 하락 흐름)**으로 나눈 후 100을 곱해 산출합니다. 결과적으로, MFI는 0에서 100 사이의 값을 가지며, 일반적으로 80 이상은 과매수, 20 이하일 경우 과매도로 해석하여 매매 신호 생성에 활용할 수 있습니다
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(1)
t = np.arange(200)
# 기본 우상향 추세에 주기적인 진동을 추가 (진폭 2, 주기 조절)
trend = 50 + 0.05 * t + 2 * np.sin(0.1 * t)
noise_std = 0.5 # 노이즈 표준편차
noise = np.random.normal(0, noise_std, 200)
prices = pd.Series(trend + noise)
# 볼린저 밴드 및 %B 계산 (20일, 2표준편차)
window = 20
ma = prices.rolling(window).mean()
std = prices.rolling(window).std()
upper_band = ma + 2 * std
lower_band = ma - 2 * std
percent_b = (prices - lower_band) / (upper_band - lower_band)
# MFI 계산 (간단 버전: 10일 기간, 거래량은 생략)
period = 10
typical_price = prices # 단순화를 위해 가격 자체 사용
raw_money_flow = typical_price.diff()
positive_flow = raw_money_flow.where(raw_money_flow > 0, 0)
negative_flow = -raw_money_flow.where(raw_money_flow < 0, 0)
pos_sum = positive_flow.rolling(period).sum()
neg_sum = negative_flow.rolling(period).sum()
MFI = 100 * pos_sum / (pos_sum + neg_sum)
# 추세 추종 신호 생성
position = 0 # 0: 포지션 없음, 1: 롱 포지션
buy_signals = []
sell_signals = []
for i in range(len(prices)):
if i < window or pd.isna(percent_b[i]) or pd.isna(MFI[i]):
continue
if position == 0:
# 매수 조건: %B가 0.8 초과 및 MFI가 80 초과
if percent_b[i] > 0.8 and MFI[i] > 80:
position = 1
buy_signals.append(i)
elif position == 1:
# 매도 조건: %B가 0.2 미만 또는 MFI가 20 미만
if percent_b[i] < 0.2 or MFI[i] < 20:
position = 0
sell_signals.append(i)
# 시뮬레이션 종료 시, 열린 포지션이 있으면 마지막 시점에 강제 매도하여 매수/매도 신호 개수를 동일하게 맞춤
if position == 1:
sell_signals.append(len(prices) - 1)
print("Buy signals at indices:", buy_signals)
print("Sell signals at indices:", sell_signals)
plt.figure(figsize=(12, 6))
plt.plot(prices, label='Price', color='black')
plt.plot(ma, label='Moving Average (20)', color='blue')
plt.plot(upper_band, label='Upper Band', color='green', linestyle='--')
plt.plot(lower_band, label='Lower Band', color='red', linestyle='--')
if buy_signals:
plt.plot(buy_signals, prices.iloc[buy_signals], '^', markersize=10, color='g', label='Buy Signal')
if sell_signals:
plt.plot(sell_signals, prices.iloc[sell_signals], 'v', markersize=10, color='r', label='Sell Signal')
plt.title("추세 추종 매매법 시뮬레이션 (진동 요소 추가, 신호 페어링)")
plt.xlabel("Time")
plt.ylabel("Price")
plt.legend()
plt.tight_layout()
plt.show()
위 코드에서는 시뮬레이션 데이터의 상승 추세 중간에 %B와 MFI 조건을 만족하는 시점에 매수하도록 했습니다. MFI 계산은 간략화를 위해 가격 변화만 사용했지만 (거래량 미고려), 기본 개념은 같습니다.
시뮬레이션 차트: 아래 차트는 %B(파란선)와 MFI(주황선) 지표가 함께 그려진 상황에서 추세 추종 전략의 매매 시점을 보여줍니다. %B가 0.8 위로 상승하고 MFI가 80을 넘어서며 강한 매수세를 보이는 구간에서 **매수 신호(▲)**가 발생했고, 이후 추세가 이어집니다. 이후 한동안 가격이 밴드 상단 부근을 유지하다가 조정으로 **%B가 0.2 아래로 떨어지는 지점에서 매도 신호(▼)**가 나와 상승 추세 거래를 마무리하는 모습입니다. 이처럼 추세 추종법은 추세 중간에 올라타서 일정 수익을 실현하는 전략입니다.
마지막에 매도 신호가 없다면 강제청산 기능을 추가하였습니다.
3. 추세 반전 매매법 (Reversal Strategy)
개념 요약: 추세 반전 전략은 과도한 추세가 끝나고 전환되는 지점을 포착하여 매매하는 방법입니다. 볼린저 밴드 상/하단을 이용해 **W형 바닥(이중바닥)**이나 M형 천정(이중천정) 패턴을 보다 명확히 식별하는 데 활용할 수 있습니다.
핵심은 두 번째 저점 혹은 고점에서 밴드 접촉 여부와 거래 지표의 움직임입니다. 존 볼린저는 특히 “W-바닥” 패턴에 주목했는데, 첫 저점에서 밴드 하단을 강하게 이탈했다가 반등 후 두 번째 저점은 이전 저점보다 낮지만 밴드 내에 머무를 때 강한 반전 신호로 봅니다. 이때 거래량이나 독립적인 보조지표(예: 일중강도율 II% 등)의 긍정적 다이버전스가 나타나면 신뢰도가 높아집니다.
아래 그림은 W-바닥 패턴을 볼린저 밴드와 함께 보여줍니다.

위의 선형차트(Line on Close)에서 첫 번째 저점은 밴드 아래로 크게 이탈했지만 두 번째 저점은 밴드 범위 내에서 형성되며 하락 모멘텀이 줄어든 모습입니다. 아래의 봉차트로 전환해서 보면 두 번째 저점 부근에서 원형으로 표시한 부분처럼 밴드 하단에 근접만 하고 이탈하지 못하는 것을 확인할 수 있습니다. 이는 하락 압력이 이전만큼 강하지 않다는 신호이며, 곧 추세 반전이 일어날 수 있음을 시사합니다. 이후 중앙선 돌파, 상단 밴드 돌파 등의 추가 확인 신호를 거쳐 상승 추세로 전환됩니다.
첫 저점은 밴드 하단을 강하게 이탈하며 강한 모멘텀을 보였지만, 두 번째 저점은 이전 저점보다 낮으나 밴드 내에서 형성되어 약화된 모멘텀을 나타낸다. 세 개의 원 표시가 진입 가능한 지점으로, W형 완성 시점, 중단선 돌파, 상단선 돌파를 각각 나타냅니다.
전략 세부: 볼린저의 추세 반전 기법 중 하나는 일중강도(Intraday Intensity, II) 지표를 활용한 방법입니다. 이는 가격과 거래량 기반의 자금흐름 지표로, +100부터 -100 범위에서 움직이며 양수이면 매수세 우위, 음수이면 매도세 우위를 뜻합니다.
- 매수 진입: 주가가 밴드 하단 근처(예: %B ≤ 0.05)까지 내려왔는데 **II% 지표가 양수(> 0)**인 경우 – 가격은 저점 부근이지만 숨은 매수세 존재 → 반등 매수
- 매도 진입(또는 보유주식 청산): 주가가 밴드 상단 근처(%B ≥ 0.95)에 있는데 **II% 지표가 음수(< 0)**인 경우 – 가격은 고점 부근이지만 내부적으로 매도세 → 하락 반전 예상, 매도
이는 가격과 보조지표 간 **다이버전스(divergence)**를 활용한 것입니다. 가격은 신저점이나 신고점을 만들었지만, 거래강도 지표가 따라가지 못하면 추세가 약해졌다고 보고 반대 포지션을 취하는 것입니다.
시뮬레이션 예제: 아래 코드는 간단한 추세 반전 전략 예시입니다. 랜덤하게 등락하던 주가가 과매도 국면을 맞이하도록 데이터를 설정한 후, II% 지표와 %B 조건에 따라 반전 매매 신호를 생성합니다.

코드를 실행하면 위와 같이 나올 것입니다. 정확히 매수 시점과 매도 시점이 위 설명과 유사하게 나오는 것을 확인할 수 있습니다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 1. 데이터 시뮬레이션
np.random.seed(123) # 재현성을 위한 시드 설정
n = 200
base_price = 100
# 기본 랜덤워크 형태의 주가 생성
price = base_price + np.random.normal(0, 1, n).cumsum()
# 인위적으로 과매도 상황 시뮬레이션: 80일 ~ 120일 구간에서 강한 하락 적용
price[80:120] -= 15
# 120일 이후 회복 모멘텀 부여
price[120:] += 10
# 데이터프레임 생성 및 High, Low, Volume 데이터 추가
df = pd.DataFrame({'Close': price})
df['High'] = df['Close'] + np.random.uniform(0, 1, n)
df['Low'] = df['Close'] - np.random.uniform(0, 1, n)
df['Volume'] = np.random.randint(100, 500, n)
# 2. 볼린저 밴드 및 %B 계산 (20일 이동평균, 표준편차 배수 2)
window = 20
df['MA'] = df['Close'].rolling(window=window).mean() # 이동평균
df['STD'] = df['Close'].rolling(window=window).std() # 표준편차
df['Upper'] = df['MA'] + 2 * df['STD'] # 상단 밴드
df['Lower'] = df['MA'] - 2 * df['STD'] # 하단 밴드
df['%B'] = (df['Close'] - df['Lower']) / (df['Upper'] - df['Lower'])
# 3. 일중강도(II) 및 II% 지표 계산
# II = ((Close - Low) - (High - Close)) / (High - Low) * Volume
# II% = (II / Volume) * 100 : 양수면 매수세 우위, 음수면 매도세 우위
df['II'] = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / (df['High'] - df['Low']) * df['Volume']
df['II%'] = df['II'] / df['Volume'] * 100
# 4. 단일 거래 실행 (최초 매수 후 최초 매도만 발생)
df['Signal'] = 0 # 초기 신호: 0 (거래 없음)
position = 0 # 0: 포지션 없음, 1: 매수 포지션 보유
i = 0
# while 루프를 통해 한 번의 매수 후 매도 신호 발생 시 거래 종료
while i < len(df):
# 매수 조건: 밴드 하단 근처 (%B ≤ 0.05) & II%가 양수 (> 0)일 때
if position == 0 and df.loc[i, '%B'] <= 0.05 and df.loc[i, 'II%'] > 0:
df.loc[i, 'Signal'] = 1 # 매수 신호 기록
position = 1
# 매도 조건: 매수 후 밴드 상단 근처 (%B ≥ 0.95) & II%가 음수 (< 0)일 때
elif position == 1 and df.loc[i, '%B'] >= 0.95 and df.loc[i, 'II%'] < 0:
df.loc[i, 'Signal'] = -1 # 매도 신호 기록
break # 단 한 번의 거래 후 종료
i += 1
# 5. 결과 시각화
plt.figure(figsize=(14, 7))
plt.plot(df.index, df['Close'], label='종가', color='blue')
plt.plot(df.index, df['Upper'], label='상단 밴드', linestyle='--', color='red')
plt.plot(df.index, df['MA'], label='중간선 (이동평균)', linestyle='--', color='grey')
plt.plot(df.index, df['Lower'], label='하단 밴드', linestyle='--', color='green')
# 매수 신호: ▲ 마커
plt.scatter(df.index[df['Signal'] == 1], df['Close'][df['Signal'] == 1],
marker='^', color='magenta', s=100, label='매수 신호')
# 매도 신호: ▼ 마커
plt.scatter(df.index[df['Signal'] == -1], df['Close'][df['Signal'] == -1],
marker='v', color='black', s=100, label='매도 신호')
plt.title('볼린저 밴드 기반 단일 거래 전략 (한 번만 사고 한 번만 청산)')
plt.xlabel('시간 (일)')
plt.ylabel('가격')
plt.legend()
plt.grid(True)
plt.show()
단 한 번의 매매(최초 매수 후 최초 매도)만 발생하도록 구현한 또 다른 예제 코드입니다. 이번 예제에서는 200일의 주가 데이터를 생성하고, 인위적으로 과매도 구간(80일~120일)과 회복 구간(120일 이후)을 추가하여 볼린저 밴드 및 일중강도율(II%) 기반 반전 신호를 검출합니다. 매매신호 생성은 while 루프를 이용하여 단 한 번의 매수 후 최초 매도 신호에서 거래를 종료하도록 구성했습니다.
- 데이터 시뮬레이션
- 200일 간의 주가 데이터를 생성하며, 80일~120일 구간에 강한 하락을 적용해 과매도 상태를 유도한 후 120일 이후 회복 모멘텀을 부여합니다.
- 볼린저 밴드 및 %B 계산
- 20일 이동평균과 표준편차를 이용하여 상단 밴드, 중간선, 하단 밴드를 계산하고, %B 지표로 현재 가격의 위치를 판단합니다.
- 일중강도(II) 및 II% 지표 계산
- 주가의 고/저 대비 종가 위치와 거래량을 활용해 II 지표를 산출하고, 이를 백분율화한 II% 지표를 통해 매수/매도세 우위를 파악합니다.
- 단일 거래 실행
- while 루프를 사용해 전체 데이터를 순회하며, 처음으로 매수 조건(%B ≤ 0.05, II% > 0)을 만족하는 시점에서 매수 신호를 발생시키고, 그 후 매도 조건(%B ≥ 0.95, II% < 0)을 만족하면 매도 신호를 기록하고 루프를 종료합니다.
- 시각화
- 차트에는 종가와 볼린저 밴드를 표시하고, 매수 신호는 ▲, 매도 신호는 ▼ 마커로 나타내어 단 한 번의 거래 실행 결과를 확인할 수 있습니다.
4. 다이버전스 매매 전략 (Divergence Trading Strategy)
개념 요약: 다이버전스 전략은 가격과 보조지표의 움직임이 불일치할 때 향후 추세 전환을 예측하여 매매하는 기법입니다 . **다이버전스(divergence)**란 가격이 신고가/신저가를 갱신하는데 특정 오실레이터 지표(예: RSI, MACD 등)는 새 고점/저점을 만들지 못하는 현상을 말합니다. 이는 모멘텀 약화를 의미하며, 곧 추세 반전이 올 수 있는 신호로 해석됩니다. Bollinger Bands와 함께 사용할 경우, 가격이 밴드 바깥으로 극단적 움직임을 보이지만 모멘텀 지표는 그에 미치지 못할 때 강력한 매매 시그널로 활용할 수 있습니다.
대표적인 예로 RSI 다이버전스를 들 수 있습니다. **강세 다이버전스(매수 신호)**는 가격이 더 낮은 저점을 만들었지만 RSI는 더 높은 저점을 만들 때 발생하며, **약세 다이버전스(매도 신호)**는 가격이 더 높은 고점을 만들 때 RSI는 낮은 고점을 보이는 경우입니다.
즉, 주가가 이전 저점보다 낮은 새로운 저점을 기록하며 하단 밴드를 살짝 이탈했지만 RSI 지표는 오히려 상승 저점을 보여주는 경우 강세 다이버전스로 판단하여 매수 기회를 포착할 수 있습니다. 반대로 주가가 상단 밴드를 넘어 신고점을 찍을 때 RSI가 낮아지는 약세 다이버전스가 나타나면 곧 하락 반전을 예상하여 매도합니다. 볼린저 밴드는 가격의 과매수/과매도 판단 근거를 제공하고, RSI 등의 지표는 모멘텀 변화를 포착하여 서로 보완적으로 다이버전스 전략에 활용됩니다.
전략 세부: 실제 다이버전스 전략 적용 시에는 다음과 같은 프로세스를 따릅니다.
- 밴드 돌파 확인: 가격이 볼린저 밴드 바깥으로 튀어나갈 정도로 강하게 움직였는지 확인 (예: 상단 돌파 = 과열, 하단 돌파 = 과매도).
- 지표 다이버전스 관찰: RSI나 MACD 등의 추세 지표가 가격의 추세를 따라가지 못하는지 체크.
- 가격 신고가 vs RSI 낮은 고점 = 약세 다이버전스 (매도 준비)
- 가격 신저가 vs RSI 높은 저점 = 강세 다이버전스 (매수 준비)
- 진입 시그널 확인: 다이버전스 이후 가격이 밴드 안으로 복귀하거나, 기준선(중앙선)을 돌파하는 등 추세 전환 확인 신호가 나타나면 매매 실행.
- 손절 및 타깃: 다이버전스 신호가 무효화되는 가격대(예: 직전 극단점)로 손절선을 정하고, 기대 수익에 따라 목표가를 설정.
시뮬레이션 예제: 아래 코드는 RSI 다이버전스를 간단히 감지하여 매매 신호를 내는 예시입니다. 일정 구간 과매수 후 가격이 둔화되는 패턴을 만들어 RSI와의 괴리를 유도했습니다.
아래는 상태를 관리하여 포지션이 열리면 반드시 반대 신호 또는 가격 기반 종료 조건에 의해 닫히도록 수정한 코드입니다. 이 코드는
- 먼저 RSI 다이버전스(강세/약세) 신호를 미리 탐지한 후,
- 상태 기반(stateful)으로 거래를 진행하여 포지션이 열리면 반대 신호(또는 손절/목표가 도달) 시에만 종료하도록 합니다. 이렇게 하면 매수와 매도 거래(진입과 청산)가 짝을 이루어 결과적으로 포지션이 항상 정리됩니다. 총 승률이 100% 인것을 확인할 수 있도록 벡테스팅도 진행한 결과 입니다.


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.signal import argrelextrema
# -----------------------------
# 1. 임의 데이터 생성 (100일)
# -----------------------------
np.random.seed(42)
N = 100
dates = pd.date_range(start='2023-01-01', periods=N, freq='D')
returns = np.random.normal(loc=0, scale=0.01, size=N)
prices = [100] # 초기 가격 100
for r in returns[1:]:
prices.append(prices[-1] * (1 + r))
df = pd.DataFrame({'Price': prices}, index=dates)
# -----------------------------
# 2. 보조지표 계산 (RSI, Bollinger Bands)
# -----------------------------
def compute_rsi(series, period=14):
delta = series.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.rolling(window=period, min_periods=period).mean()
avg_loss = loss.rolling(window=period, min_periods=period).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
df['RSI'] = compute_rsi(df['Price'], 14)
df['MA20'] = df['Price'].rolling(window=20).mean()
df['STD20'] = df['Price'].rolling(window=20).std()
df['UpperBand'] = df['MA20'] + 2 * df['STD20']
df['LowerBand'] = df['MA20'] - 2 * df['STD20']
# -----------------------------
# 3. 다이버전스 탐지 (RSI와 가격 극값 비교)
# -----------------------------
order = 3 # 좌우 3일씩 비교
price_array = df['Price'].values
rsi_array = df['RSI'].values
# 극저점, 극고점 인덱스 검출
local_mins_idx = argrelextrema(price_array, np.less, order=order)[0]
local_maxs_idx = argrelextrema(price_array, np.greater, order=order)[0]
# 강세 다이버전스 (매수 신호): 가격은 낮은 저점을, RSI는 높은 저점을 기록한 경우
bullish_signals = []
for i in range(1, len(local_mins_idx)):
prev = local_mins_idx[i-1]
curr = local_mins_idx[i]
if np.isnan(rsi_array[prev]) or np.isnan(rsi_array[curr]):
continue
if price_array[curr] < price_array[prev] and rsi_array[curr] > rsi_array[prev]:
bullish_signals.append(curr)
# 약세 다이버전스 (매도 신호): 가격은 높은 고점을, RSI는 낮은 고점을 기록한 경우
bearish_signals = []
for i in range(1, len(local_maxs_idx)):
prev = local_maxs_idx[i-1]
curr = local_maxs_idx[i]
if np.isnan(rsi_array[prev]) or np.isnan(rsi_array[curr]):
continue
if price_array[curr] > price_array[prev] and rsi_array[curr] < rsi_array[prev]:
bearish_signals.append(curr)
# 두 신호를 딕셔너리로 구성 (신호 발생 인덱스: 'buy' 또는 'sell')
divergence_signals = {}
for idx in bullish_signals:
divergence_signals[idx] = 'buy'
for idx in bearish_signals:
divergence_signals[idx] = 'sell'
# -----------------------------
# 4. 상태 기반 매매 전략 및 백테스트 (포지션 짝 맞추기)
# -----------------------------
trades = []
position = None # 현재 포지션: 'buy'(롱) 또는 'sell'(쇼트) 또는 None (포지션 없음)
entry_idx = None # 진입일 인덱스
entry_price = None
stop_loss = None
target = None
entry_type = None # 진입 신호 종류 ('buy' 혹은 'sell')
i = 0
while i < len(df):
# 포지션이 없는 상태에서 divergence 신호가 발생하면 진입
if position is None and i in divergence_signals:
signal_type = divergence_signals[i]
# 신호 다음날 진입 (데이터가 있으면)
if i + 1 < len(df):
entry_idx = i + 1
else:
break
entry_price = df['Price'].iloc[entry_idx]
entry_type = signal_type
# 진입 시 손절/목표가 설정 (이전 극값 기준)
if entry_type == 'buy':
prev_mins = [x for x in local_mins_idx if x < i]
if not prev_mins:
i += 1
continue
prev_min_idx = max(prev_mins)
stop_loss = df['Price'].iloc[prev_min_idx]
risk = entry_price - stop_loss
target = entry_price + 2 * risk
else: # 'sell'
prev_maxs = [x for x in local_maxs_idx if x < i]
if not prev_maxs:
i += 1
continue
prev_max_idx = max(prev_maxs)
stop_loss = df['Price'].iloc[prev_max_idx]
risk = stop_loss - entry_price
target = entry_price - 2 * risk
position = entry_type # 포지션 오픈 (롱/쇼트)
trade_entry_date = df.index[entry_idx]
# 다음 날부터 포지션 모니터링
i = entry_idx
continue
# 포지션이 열린 상태이면, 매일 가격을 확인하여 종료 조건 확인
if position is not None:
current_price = df['Price'].iloc[i]
exit_trade = False
exit_reason = ''
# 가격 기반 종료 조건
if position == 'buy':
if current_price <= stop_loss:
exit_trade = True
exit_reason = 'stop_loss'
elif current_price >= target:
exit_trade = True
exit_reason = 'target'
elif position == 'sell':
if current_price >= stop_loss:
exit_trade = True
exit_reason = 'stop_loss'
elif current_price <= target:
exit_trade = True
exit_reason = 'target'
# 반대 divergence 신호가 있으면 종료 조건으로도 사용
if i in divergence_signals:
signal_type = divergence_signals[i]
if (position == 'buy' and signal_type == 'sell') or (position == 'sell' and signal_type == 'buy'):
exit_trade = True
exit_reason = 'opposite_divergence'
if exit_trade:
exit_idx = i + 1 if i + 1 < len(df) else i
exit_price = df['Price'].iloc[exit_idx]
trade_exit_date = df.index[exit_idx]
trade_return = ((exit_price - entry_price) / entry_price) if position == 'buy' else ((entry_price - exit_price) / entry_price)
trades.append({
'Type': position,
'Entry Date': df.index[entry_idx],
'Entry Price': entry_price,
'Exit Date': df.index[exit_idx],
'Exit Price': exit_price,
'Stop Loss': stop_loss,
'Target': target,
'Return': trade_return,
'Exit Reason': exit_reason
})
# 포지션 종료 후 변수 초기화
position = None
entry_idx = None
entry_price = None
stop_loss = None
target = None
entry_type = None
# 종료 후 바로 다음날부터 다시 탐색
i = exit_idx
continue
i += 1
# 백테스트 결과 출력
if trades:
trades_df = pd.DataFrame(trades)
cumulative_return = (trades_df['Return'] + 1).prod() - 1
num_trades = len(trades_df)
win_trades = trades_df[trades_df['Return'] > 0]
win_rate = len(win_trades) / num_trades * 100
else:
trades_df = pd.DataFrame()
cumulative_return = 0
num_trades = 0
win_rate = 0
print("총 거래 건수:", num_trades)
print("누적 수익률: {:.2f}%".format(cumulative_return * 100))
print("승률: {:.2f}%".format(win_rate))
if not trades_df.empty:
print(trades_df)
# -----------------------------
# 5. 시각화 (FIGURE)
# -----------------------------
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
# 가격 차트와 Bollinger Bands 표시
ax1.plot(df.index, df['Price'], label='Price', color='blue')
ax1.plot(df.index, df['UpperBand'], label='Upper Band', linestyle='--', color='red')
ax1.plot(df.index, df['MA20'], label='MA20', linestyle='--', color='gray')
ax1.plot(df.index, df['LowerBand'], label='Lower Band', linestyle='--', color='red')
ax1.set_title('Price Chart with Bollinger Bands')
# 진입/청산 신호 표시 (진입은 삼각형, 청산은 원형)
for trade in trades:
ax1.scatter(trade['Entry Date'], trade['Entry Price'], marker='^', color='green', s=100, label='Entry')
ax1.scatter(trade['Exit Date'], trade['Exit Price'], marker='o', color='black', s=100, label='Exit')
# 중복 범례 제거
handles, labels = ax1.get_legend_handles_labels()
by_label = dict(zip(labels, handles))
ax1.legend(by_label.values(), by_label.keys())
# RSI 차트
ax2.plot(df.index, df['RSI'], label='RSI', color='orange')
ax2.axhline(70, color='red', linestyle='--', label='Overbought (70)')
ax2.axhline(30, color='green', linestyle='--', label='Oversold (30)')
ax2.set_title('RSI (14)')
ax2.legend()
plt.tight_layout()
plt.show()
- 다이버전스 신호 사전 구성
- 미리 divergence_signals 딕셔너리에 각 신호 발생 인덱스와 신호 종류('buy', 'sell')를 저장합니다.
- 상태 기반 트레이딩 로직
- position이 None일 때(포지션 없음) divergence 신호가 나타나면, 그 다음날 진입하여 포지션을 오픈합니다.
- 진입 시, 해당 신호 이전의 극값(저점 또는 고점)을 기준으로 손절(stop_loss)과 목표가(target)를 설정합니다.
- 포지션이 열린 상태에서는 매일 가격을 확인하며, 손절/목표가에 도달하거나 반대 divergence 신호가 나타나면 포지션을 종료합니다.
- 이 방식으로 진입과 청산이 반드시 짝을 이루게 됩니다.
- 백테스트 및 시각화
- 백테스트 결과(총 거래 건수, 누적 수익률, 승률 등)를 출력합니다.
- 가격 차트와 Bollinger Bands, 그리고 RSI 차트를 FIGURE로 표시하며, 진입/청산 시점을 마커로 강조합니다.
각 전략은 시장 상황에 따라 적절히 사용되어야 하며, 한 전략만으로 항상 수익을 낼 수 있는 것은 아닙니다. 그러나 볼린저 밴드의 밴드 폭 수축/확장, 밴드 접촉 패턴, 다른 지표와의 조합 등을 잘 활용하면 추세적 움직임과 반전 시점을 비교적 명확하게 파악할 수 있습니다. 위의 시뮬레이션 예제와 그림들은 이러한 개념을 이해하기 쉽게 보여주는 사례들입니다. 실제 투자에서는 추세 추종과 반전 기법을 복합적으로 사용하거나, 다이버전스 신호에 추가적인 필터(거래량 추이 등)를 적용하는 등 보완이 필요합니다. 무엇보다 위험 관리를 병행하여야 하며, 여기 소개된 코드는 교육용 예시일 뿐 실전에 그대로 적용하기에는 단순화되어 있다는 점을 유의하시기 바랍니다.
5. 선물자동매매 및 주식자동매매 공유 카페
선물은 모의만 가능하고 주식자동매매는 종목당 100만원까지 가능합니다.
https://cafe.naver.com/moneytuja
TuJa (Making algorit... : 네이버 카페
주식 자동 매매 프로그램, 성공적인 주식투자를 위한 알고리즘에 관심 있는 분들을 위한 카페입니다
cafe.naver.com
'프로그램 기반 주식 응용 > 선물 스터디(with coding)' 카테고리의 다른 글
주식/선물 투자 시 뉴스(거시경제 지표, 달러 등)와 기술적 분석(수급, 볼린저 밴드 등)과의 상관 관계 (1) | 2025.02.03 |
---|---|
주식/선물자동매매 프로그램 기본 개념도(알고리즘) 및 파일 공유 (8) | 2024.06.16 |
주식자동매매프로그램을 개발/사용 해야 하는 사람 또는 부류 그리고 근황 (1) | 2024.06.14 |
[선물 9강] 선물 외국인누적 순매매 동향 파악과 앞으로의 지수 방향성 (43) | 2023.07.05 |
[선물 8강] 코스피 200주선과 달러 환산차트의 활용 (37) | 2023.06.27 |