Advance Deep Learning/[Quant] 금융 파이썬 쿡북

볼린저 밴드 계산과 매수/매도 전략 테스트

needmorecaffeine 2023. 1. 30. 14:27

"금융 파이썬 쿡북 Ch2. 파이썬에서의 기술적 분석 "의 내용을 기반으로 작성하였습니다. (실습 깃헙)

 


 

1. 볼린저 밴드 (Bollinger Band)란?

 볼린저 밴드란 시간에 따른 특정 자산 가격과 변동성의 정보를 도출하는데 사용하는 통계적 방법이다.

볼린저 밴드를 구하려면 지정된 윈도우(일반적으로 20일)를 사용해 시계열 가격의 이동 평균과 표준편차를 계산해야 한다.

이후 상, 하한 밴드를 이동 평균 상, 하의 표준 편차의 K배(일반적으로 2배)로 설정한다. 

이 때 밴드는 변동성이 증가하면 넓어지고 변동성이 감소하면 축소한다.

더욱 자세한 설명은 이 곳에서 확인 가능하다.

 

https://wikidocs.net/87171

 

01) 볼린저밴드 (Bollinger Bands) 개념

[TOC] # 볼린저밴드 (Bollinger Bands) 1980년대 초반 미국의 재무분석가인 **존 볼린저**가 개발하고 상표권을 취득한 주가 기술적 분석 도구 볼린저밴드…

wikidocs.net

 

 


 

2. 기본 전략

 

 이 글에서 구현할 단순 거래 전략은 다음과 같다.

  • 가격이 아래 볼린저 밴드를 상향으로 교차할 때 매수하라
  • 가격이 위 볼린저 밴들를 하향 교차할 때 매도하라
  • 올인 전략 - 매수 전략을 수립할 때 최대한 많은 주식을 매수하라
  • 공매도는 허용되지 않는다.

 가장 기본적인 전략으로, 개발자 존 볼린저 또한 밴드 자체의 폭이 축소되고 밀집되는 구간을 거차고 난 후 상단 밴드를 돌파할 때 주식을 매입하고 하단 밴드를 벗어날 때 주식을 공매도 하는 것을 추천하였다.


 볼린저 밴드에서 폭이 좁아지는 것 = 주가 안정기 이고 그 후 추세를 결정하는데 상단 밴드를 건드리면 상단 돌파, 하단 밴드를 건드리면 하향 추세로 간다는 가정이 위 방식의 배경이다.

 


 

3. 구현

 기본적인 내용은 이전 backtrader 백테스트에서 설명하였고 이번 글에서는 추가, 변경된 부분만 확인한다. (환경 세팅도 동일)

https://needmorecaffeine.tistory.com/21

 

단순이동평균(SMA)를 기반으로 전략 백테스팅

"금융 파이썬 쿡북 Ch2. 파이썬에서의 기술적 분석 "의 내용을 기반으로 작성하였습니다. (실습 깃헙) 1. 백테스팅이란? 과거데이터를 활용해 자신의 알고리즘, 투자전략 원칙을 검증하는 과정하

needmorecaffeine.tistory.com

 

class BBand_Strategy(bt.Strategy):
    params = (('period', 20),
              ('devfactor', 2.0),)

    def __init__(self):
        # keep track of close price in the series
        self.data_close = self.datas[0].close
        self.data_open = self.datas[0].open

        # keep track of pending orders/buy price/buy commission
        self.order = None
        self.price = None
        self.comm = None

        # add Bollinger Bands indicator and track the buy/sell signals
        self.b_band = bt.ind.BollingerBands(self.datas[0], 
                                            period=self.p.period, 
                                            devfactor=self.p.devfactor)
        # CrossOver
        ## 1 : 첫 데이터가 두번째 데이터를 상향으로 가로지르면
        ## -1 : 하향으로 가르지르면
        self.buy_signal = bt.ind.CrossOver(self.datas[0], 
                                           self.b_band.lines.bot)
        self.sell_signal = bt.ind.CrossOver(self.datas[0], 
                                            self.b_band.lines.top)

    def log(self, txt):
        '''Logging function'''
        dt = self.datas[0].datetime.date(0).isoformat()
        print(f'{dt}, {txt}')

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # order already submitted/accepted - no action required
            return

        # report executed order
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    f'BUY EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}'
                )
                self.price = order.executed.price
                self.comm = order.executed.comm
            else:
                self.log(
                    f'SELL EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}'
                )

        # report failed order
        elif order.status in [order.Canceled, order.Margin, 
                              order.Rejected]:
            self.log('Order Failed')

        # set no pending order
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log(f'OPERATION RESULT --- Gross: {trade.pnl:.2f}, Net: {trade.pnlcomm:.2f}')

    def next_open(self):
        if not self.position:
            if self.buy_signal > 0:
                # calculate the max number of buyable shares ('all-in') 
                size = int(self.broker.getcash() / self.datas[0].open)
                # buy order
                self.log(f'BUY CREATED --- Size: {size}, Cash: {self.broker.getcash():.2f}, Open: {self.data_open[0]}, Close: {self.data_close[0]}')
                self.buy(size=size)
        else:
            if self.sell_signal < 0:
                # sell order
                self.log(f'SELL CREATED --- Size: {self.position.size}')
                self.sell(size=self.position.size)

 

data = bt.feeds.PandasData(dataname=yf.download('MSFT', '2018-01-01', '2018-12-31'))

cerebro = bt.Cerebro(stdstats = False, cheat_on_open=True) 
# cheat_on_open = t일의 종가에 신호를 계산했지만 t+1일의 시가를 기준으로 사고 싶은 주식을 매수했음을 의미

cerebro.addstrategy(BBand_Strategy)
cerebro.adddata(data)
cerebro.broker.setcash(10000.0)
# 수수료 추가
cerebro.broker.setcommission(commission=0.001)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)
# Return = 전체 기간에 걸친 서로 다른 로그 수익률 : 전체 복리 수익률, 전 기간 동안의 평균 수익률과 연간 수익률
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
# TimeReturn = 시간에 따른 수익률의 모음 (여기서는 일일)
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='time_return')

print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
backtest_result = cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

 

%matplotlib inline

configure_plotly_browser_state()
init_notebook_mode(connected=True)

# 진짜 해결하기 드럽게 힘드네,,, java 코드 지원 안되는 오류 아래 두 라인으로 해결
plt.rcParams['figure.figsize'] = [15, 12]
plt.rcParams.update({'font.size': 12}) 

# 결과 도면 표시
cerebro.plot(iplot = False, volume = False)

 전략에서 설명하였듯 밴드와 이동평균선의 교차 지점(corss over)과 그 값 (-1 or 1)을 아래 두 plot에서 확인할 수 있다.

 

 다른 return 측도도 확인해보자.

# 다른 return 측도
print(backtest_result[0].analyzers.returns.get_analysis())

 

 일별 포트폴리오 수익도 확인해보자.

 

# 일별 포트폴리오 수익률 도식화
returns_dict = backtest_result[0].analyzers.time_return.get_analysis()
returns_df = pd.DataFrame(list(returns_dict.items()), 
                          columns = ['report_date', 'return']).set_index('report_date')
returns_df.plot(title='Portfolio returns')

 

 위 수익률 plot에서 편평한 부분은 오픈 포지션이 없는 구간(매수, 매도를 하지 않음)이다.

 


(해당 게시물 학습을 위한 임의적 설정이므로, 수익을 대변하지 않습니다.)

 

Ref