볼린저 밴드를 이용한 트레이딩¶
볼린저 밴드에 대한 간략한 설명은 이전 글을 참고: [python] 볼린저 밴드(Bollinger bands) - (1) 볼린저밴드 그리기
이번 글에서는 볼린저 밴드를 이용한 트레이딩 전략을 구현해보려고 한다. 전략은 단순하다. 시가가 하단밴드 밑에 있으면 매수하고, 상단밴드 위에 있으면 매도한다.
In [75]:
from pykrx import stock
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 한글폰트 설정, 그래프 마이너스 표시 설정
import matplotlib
from matplotlib import font_manager, rc
import platform
if platform.system() == 'Windows':
# 윈도우인 경우
font_name = font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
rc('font', family=font_name)
else:
# Mac 인 경우
rc('font', family='AppleGothic')
matplotlib.rcParams['axes.unicode_minus'] = False
In [62]:
# 종목코드와 종목명 가져오기
stock_list = pd.DataFrame({'종목코드':stock.get_market_ticker_list(market="ALL")})
stock_list['종목명'] = stock_list['종목코드'].map(lambda x: stock.get_market_ticker_name(x))
stock_list.head()
Out[62]:
종목코드 | 종목명 | |
---|---|---|
0 | 060310 | 3S |
1 | 095570 | AJ네트웍스 |
2 | 006840 | AK홀딩스 |
3 | 054620 | APS홀딩스 |
4 | 265520 | AP시스템 |
In [63]:
# 와이지엔터테인먼트의 20200101~20210528 데이터 가져오기
ticker = stock_list.loc[stock_list['종목명']=='와이지엔터테인먼트', '종목코드']
df = stock.get_market_ohlcv_by_date(fromdate="20200101", todate="20210528", ticker=ticker)
df
Out[63]:
시가 | 고가 | 저가 | 종가 | 거래량 | |
---|---|---|---|---|---|
날짜 | |||||
2020-01-02 | 27250 | 27700 | 26500 | 27500 | 224040 |
2020-01-03 | 27600 | 28800 | 26600 | 28400 | 563162 |
2020-01-06 | 28400 | 30500 | 28000 | 30000 | 1141917 |
2020-01-07 | 30500 | 31200 | 29500 | 30750 | 634303 |
2020-01-08 | 30750 | 30950 | 29400 | 30550 | 457119 |
... | ... | ... | ... | ... | ... |
2021-05-24 | 48000 | 49450 | 47600 | 49050 | 506747 |
2021-05-25 | 49100 | 50800 | 48600 | 50000 | 538883 |
2021-05-26 | 50100 | 50600 | 49100 | 50000 | 319239 |
2021-05-27 | 50400 | 50500 | 48600 | 50500 | 350836 |
2021-05-28 | 50100 | 50300 | 48750 | 49400 | 369453 |
348 rows × 5 columns
In [64]:
# 칼럼명 영문명으로 변경
# 시가(Open), 고가(High), 저가(Low), 종가(Close), 거래량(Volume)
df = df.rename(columns={'시가':'Open', '고가':'High', '저가':'Low', '종가':'Close', '거래량':'Volume'})
In [65]:
# 중심선, 상단밴드, 하단밴드 계산
df['ma20'] = df['Close'].rolling(window=20).mean() # 20일 이동평균
df['stddev'] = df['Close'].rolling(window=20).std() # 20일 이동표준편차
df['upper'] = df['ma20'] + 2*df['stddev'] # 상단밴드
df['lower'] = df['ma20'] - 2*df['stddev'] # 하단밴드
df = df[19:]
df
Out[65]:
Open | High | Low | Close | Volume | ma20 | stddev | upper | lower | |
---|---|---|---|---|---|---|---|---|---|
날짜 | |||||||||
2020-01-31 | 33000 | 33350 | 31600 | 31600 | 242343 | 32490.0 | 2296.370133 | 37082.740267 | 27897.259733 |
2020-02-03 | 30400 | 34250 | 30400 | 33950 | 454352 | 32812.5 | 1991.354670 | 36795.209340 | 28829.790660 |
2020-02-04 | 33650 | 35050 | 33250 | 34200 | 261373 | 33102.5 | 1718.588390 | 36539.676779 | 29665.323221 |
2020-02-05 | 34250 | 34700 | 33400 | 33500 | 175969 | 33277.5 | 1556.605196 | 36390.710392 | 30164.289608 |
2020-02-06 | 33950 | 34350 | 33000 | 33650 | 193934 | 33422.5 | 1439.432911 | 36301.365822 | 30543.634178 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
2021-05-24 | 48000 | 49450 | 47600 | 49050 | 506747 | 44007.5 | 2637.396793 | 49282.293585 | 38732.706415 |
2021-05-25 | 49100 | 50800 | 48600 | 50000 | 538883 | 44315.0 | 2957.199066 | 50229.398132 | 38400.601868 |
2021-05-26 | 50100 | 50600 | 49100 | 50000 | 319239 | 44590.0 | 3219.414788 | 51028.829577 | 38151.170423 |
2021-05-27 | 50400 | 50500 | 48600 | 50500 | 350836 | 44922.5 | 3472.428052 | 51867.356105 | 37977.643895 |
2021-05-28 | 50100 | 50300 | 48750 | 49400 | 369453 | 45245.0 | 3577.521194 | 52400.042388 | 38089.957612 |
329 rows × 9 columns
In [66]:
trading_book = df.copy().assign(action='', state='').reset_index().rename(columns={'날짜':'Date'})
trading_book.head()
Out[66]:
Date | Open | High | Low | Close | Volume | ma20 | stddev | upper | lower | action | state | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2020-01-31 | 33000 | 33350 | 31600 | 31600 | 242343 | 32490.0 | 2296.370133 | 37082.740267 | 27897.259733 | ||
1 | 2020-02-03 | 30400 | 34250 | 30400 | 33950 | 454352 | 32812.5 | 1991.354670 | 36795.209340 | 28829.790660 | ||
2 | 2020-02-04 | 33650 | 35050 | 33250 | 34200 | 261373 | 33102.5 | 1718.588390 | 36539.676779 | 29665.323221 | ||
3 | 2020-02-05 | 34250 | 34700 | 33400 | 33500 | 175969 | 33277.5 | 1556.605196 | 36390.710392 | 30164.289608 | ||
4 | 2020-02-06 | 33950 | 34350 | 33000 | 33650 | 193934 | 33422.5 | 1439.432911 | 36301.365822 | 30543.634178 |
In [67]:
for i in range(len(trading_book)):
# 초기값 설정
if i==0:
# 종가가 하단밴드 이하이면 매수
if trading_book.loc[i, 'Close'] <= trading_book.loc[i, 'lower']:
trading_book.loc[i, 'action'] = 'buy'
trading_book.loc[i, 'state'] = 'holding'
# 이외에는 아무것도 안하기
else:
trading_book.loc[i, 'action'] = ''
trading_book.loc[i, 'state'] = ''
else:
# 종가가 하단밴드 이하인데
if trading_book.loc[i, 'Close'] <= trading_book.loc[i, 'lower']:
# 이전상태가 보유중이 아니면 매수
if trading_book.loc[i-1, 'state'] not in ['holding']:
trading_book.loc[i, 'action'] = 'buy'
trading_book.loc[i, 'state'] = 'holding'
# 이전상태가 보유중이면 그대로 보유
elif trading_book.loc[i-1, 'state'] in ['holding']:
trading_book.loc[i, 'action'] = 'hold'
trading_book.loc[i, 'state'] = 'holding'
# 종가가 상단밴드 이상인데
elif trading_book.loc[i, 'Close'] >= trading_book.loc[i, 'upper']:
# 이전상태가 보유중이 아니면 그대로 있기
if trading_book.loc[i-1, 'state'] not in ['holding']:
trading_book.loc[i, 'action'] = 'hold'
# 이전상태가 보유중이면 매도
elif trading_book.loc[i-1, 'state'] in ['holding']:
trading_book.loc[i, 'action'] = 'sell'
trading_book.loc[i, 'state'] = ''
# 그 외에는 아무 행동도 하지 않고 상태는 이전상태 그대로 유지
else:
trading_book.loc[i, 'action'] = ''
trading_book.loc[i, 'state'] = trading_book.loc[i-1]['state']
In [77]:
report = trading_book.loc[trading_book['action'].isin(['buy', 'sell']), ['Date', 'Close', 'upper', 'lower', 'action', 'state']].reset_index(drop=True)
report
Out[77]:
Date | Close | upper | lower | action | state | |
---|---|---|---|---|---|---|
0 | 2020-02-24 | 30050 | 35237.078689 | 30477.921311 | buy | holding |
1 | 2020-04-10 | 28900 | 28310.107999 | 18864.892001 | sell | |
2 | 2020-06-15 | 28550 | 33818.567595 | 28616.432405 | buy | holding |
3 | 2020-06-26 | 33850 | 33308.382553 | 28296.617447 | sell | |
4 | 2020-10-12 | 47600 | 61820.796383 | 48199.203617 | buy | holding |
5 | 2020-12-03 | 49050 | 47665.088345 | 41539.911655 | sell | |
6 | 2020-12-22 | 42400 | 48655.042883 | 42479.957117 | buy | holding |
7 | 2021-01-20 | 51000 | 48861.167998 | 40658.832002 | sell | |
8 | 2021-04-23 | 43850 | 46397.196696 | 43912.803304 | buy | holding |
9 | 2021-05-18 | 47250 | 47239.802192 | 39665.197808 | sell |
In [78]:
report = report.assign(buy_price=report['Close'].shift(1))
report
Out[78]:
Date | Close | upper | lower | action | state | buy_price | |
---|---|---|---|---|---|---|---|
0 | 2020-02-24 | 30050 | 35237.078689 | 30477.921311 | buy | holding | NaN |
1 | 2020-04-10 | 28900 | 28310.107999 | 18864.892001 | sell | 30050.0 | |
2 | 2020-06-15 | 28550 | 33818.567595 | 28616.432405 | buy | holding | 28900.0 |
3 | 2020-06-26 | 33850 | 33308.382553 | 28296.617447 | sell | 28550.0 | |
4 | 2020-10-12 | 47600 | 61820.796383 | 48199.203617 | buy | holding | 33850.0 |
5 | 2020-12-03 | 49050 | 47665.088345 | 41539.911655 | sell | 47600.0 | |
6 | 2020-12-22 | 42400 | 48655.042883 | 42479.957117 | buy | holding | 49050.0 |
7 | 2021-01-20 | 51000 | 48861.167998 | 40658.832002 | sell | 42400.0 | |
8 | 2021-04-23 | 43850 | 46397.196696 | 43912.803304 | buy | holding | 51000.0 |
9 | 2021-05-18 | 47250 | 47239.802192 | 39665.197808 | sell | 43850.0 |
In [79]:
report.loc[~report['action'].isin(['sell']), 'buy_price'] = np.nan
report
Out[79]:
Date | Close | upper | lower | action | state | buy_price | |
---|---|---|---|---|---|---|---|
0 | 2020-02-24 | 30050 | 35237.078689 | 30477.921311 | buy | holding | NaN |
1 | 2020-04-10 | 28900 | 28310.107999 | 18864.892001 | sell | 30050.0 | |
2 | 2020-06-15 | 28550 | 33818.567595 | 28616.432405 | buy | holding | NaN |
3 | 2020-06-26 | 33850 | 33308.382553 | 28296.617447 | sell | 28550.0 | |
4 | 2020-10-12 | 47600 | 61820.796383 | 48199.203617 | buy | holding | NaN |
5 | 2020-12-03 | 49050 | 47665.088345 | 41539.911655 | sell | 47600.0 | |
6 | 2020-12-22 | 42400 | 48655.042883 | 42479.957117 | buy | holding | NaN |
7 | 2021-01-20 | 51000 | 48861.167998 | 40658.832002 | sell | 42400.0 | |
8 | 2021-04-23 | 43850 | 46397.196696 | 43912.803304 | buy | holding | NaN |
9 | 2021-05-18 | 47250 | 47239.802192 | 39665.197808 | sell | 43850.0 |
In [80]:
report.loc[report['action'].isin(['sell']), 'yield(%)'] = round((report['Close']/report['buy_price']-1)*100, 2)
report
Out[80]:
Date | Close | upper | lower | action | state | buy_price | yield(%) | |
---|---|---|---|---|---|---|---|---|
0 | 2020-02-24 | 30050 | 35237.078689 | 30477.921311 | buy | holding | NaN | NaN |
1 | 2020-04-10 | 28900 | 28310.107999 | 18864.892001 | sell | 30050.0 | -3.83 | |
2 | 2020-06-15 | 28550 | 33818.567595 | 28616.432405 | buy | holding | NaN | NaN |
3 | 2020-06-26 | 33850 | 33308.382553 | 28296.617447 | sell | 28550.0 | 18.56 | |
4 | 2020-10-12 | 47600 | 61820.796383 | 48199.203617 | buy | holding | NaN | NaN |
5 | 2020-12-03 | 49050 | 47665.088345 | 41539.911655 | sell | 47600.0 | 3.05 | |
6 | 2020-12-22 | 42400 | 48655.042883 | 42479.957117 | buy | holding | NaN | NaN |
7 | 2021-01-20 | 51000 | 48861.167998 | 40658.832002 | sell | 42400.0 | 20.28 | |
8 | 2021-04-23 | 43850 | 46397.196696 | 43912.803304 | buy | holding | NaN | NaN |
9 | 2021-05-18 | 47250 | 47239.802192 | 39665.197808 | sell | 43850.0 | 7.75 |
In [76]:
plt.figure(figsize=(9, 5))
plt.plot(df.index, df['Close'], label='Close')
plt.plot(df.index, df['upper'], linestyle='dashed', label='Upper band')
plt.plot(df.index, df['ma20'], linestyle='dashed', label='Moving Average 20')
plt.plot(df.index, df['lower'], linestyle='dashed', label='Lower band')
plt.title(f'{"와이지엔터테인먼트"}({int(ticker.values)})의 볼린저 밴드(20일, 2 표준편차)')
plt.legend(loc='best');
위에서 구현한 방법은 정말 나이브한 방법이다. 하단밴드 아래라고 해서 저점이라고 볼 수도 없고(언제나 지하실은 있다), 상단밴드 위라고 해서 고점이라고 볼 수도 없다(항상 내가 팔면 날아감).
또한, 종목에 따라서는 하단밴드나 상단밴드를 번갈아가며 태그하는 일이 거의 없을수도 있다.
그래서 추세를 확증할 수 있는 다른 보조지표가 함께 사용되어야 한다. 이 부분도 추후에 업데이트 할 예정.