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

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

needmorecaffeine 2023. 1. 30. 11:03

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

 


 

1. 백테스팅이란?

 과거데이터를 활용해 자신의 알고리즘, 투자전략 원칙을 검증하는 과정하는 것이다.
초기투자금액 설정, 시작일이나 종료일을 설정하거나 매매나 수익의 분석을 제공하는 등의 성과를 분석하여 어느 정도의 수익을 낼 수 있는 지를 확인하는 것이다.
 즉, 과거의 데이터(Back)과 알고리즘을 테스트한다는 의미인 테스팅(testing)이 결합되어 사용된 용어이다.
(Source : https://inshang.tistory.com/19)

 


2.  백테스트 프레임워크 backtrader

 백테스트를 위해 사용하는 프레임워크는 다양한데 그 중에서도 backtrader를 사용해 보겠다. (https://www.backtrader.com/)

주요 기능은 다음과 같다.

  • 방대한 기술 지표와 성능 척도 제공
  • 구축과 새로운 지표 적용이 손쉽다.
  • 다양한 데이터 소스 제공
  • 서로 다른 형태의 주문(시장가, 지정가, 손절), 슬리피지(의도한 주문가와 실제 실행가의 차이), 커미션, 롱, 숏 등 실제 브로커의 여러 측면을 시뮬레이션할 수 있음

 


3. SMA에 기반한 기본 전략

 Signal과 이후 Strategy에서 적용되는 기본 전략은 다음과 같다.

  • 종가가 20일 SMA 보다 높아지면 한 주를 산다.
  • 종가가 20일 SMA 보다 낮아지고 주식을 보유한 상태면 매도한다.
  • 주어진 시간에 오직 한 주만 허용된다.
  • 공매도는 허용되지 않는다.

 


4. Signal

 Signal과 Strategy를 구분하여 작성할 예정인데 Signal을 위 전략에 해당하는 지점을 포착하고 매수, 매도를 실행한다면 Strategy는 Signal의 기능을 수행하고 동일한 결과를 얻지만 실제로 백그라운드에서 발생하는 일에 대한 더 많은 로그기록을 생성한다.

 

 공통적인 환경 세팅은 다음을 통해 이뤄진다. (제공된 실습 코드에서 발생하는 에러에 대한 디버깅 내용추가)

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

# backtrader 설치
!pip install backtrader

# 거래 데이터 
!pip install yfinance

# plot을 위한 버전 맞추기
!pip uninstall matplotlib
!pip install matplotlib==3.1.1

from datetime import datetime
import backtrader as bt
import yfinance as yf
import matplotlib
import matplotlib.pyplot as plt
from IPython import get_ipython

# Plot 생성을 위한 모듈
import cufflinks as cf
from plotly.offline import iplot, init_notebook_mode
import plotly.graph_objs as go


cf.set_config_file(world_readable = True, theme = 'pearl', offline = True)
init_notebook_mode(connected=True)

# iplot 기능이 수행되지 않는 에러 디버깅 함수
def configure_plotly_browser_state():
  import IPython
  display(IPython.core.display.HTML('''
        <script src="/static/components/requirejs/require.js"></script>
        <script>
          requirejs.config({
            paths: {
              base: '/static/base',
              plotly: 'https://cdn.plot.ly/plotly-latest.min.js?noext',
            },
          });
        </script>
        '''))

 

 Signal은 아래의 과정을 통해 수행된다. 상위 주석을 통해 단계를 구분하였다.

# 거래전략을 나타내는 클래스 정의

class SmaSignal(bt.Signal):
  params = (('period', 20), )
  def __init__(self):
  # signal 정의 = 현재 데이터 포인트 - 이동 평균
    self.lines.signal = self.data - bt.ind.SMA(period = self.p.period)
    	## signal > 0 : long(매수)
    	## signal < 0 : short(매도)
    	## signal = 0 : no signal
# 야후 파이낸스에서 데이터 다운로드 (애플)

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

# 아래 코드는 실행 안됨
#data = bt.feeds.PandasData(dataname='AAPL', fromdate=datetime(2018, 1, 1),todate=datetime(2018, 12, 31))
# 백테스트 설정
cerebro = bt.Cerebro(stdstats = False)

# 데이터 추가
cerebro.adddata(data)

# 사용 가능한 금액 설정
cerebro.broker.setcash(1000.0)

# Signal 추가
cerebro.add_signal(bt.SIGNAL_LONG, SmaSignal)

# 관찰자 추가
## BuySell = 매수/매도 결정 (파랑, 빨강 삼각형)
cerebro.addobserver(bt.observers.BuySell)

## Value = 시간에 따른 포트폴리오 가치의 변화 추적
cerebro.addobserver(bt.observers.Value)
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
# 백테스트 실행
cerebro.run()
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')

  • add_signal로 추가할 수 있는 신호 종류
    • LONGSHORT = 롱과 숏 모두 고려
    • LONG : 양 = 롱 포지션을 취함 / 음 = 롱 포지션을 종료
    • SHORT : 음 = 숏 포지션을 취함 / 양 = 숏 포지션을 종료
    • LONGEXIT : 롱 포지션 엑싯을 위해 음의 신호 사용
    • SHORTEXIT : 숏 포지션 엑싯을 위해 양의 신호 사용
    • 포지션 엑시트
      • LONG : LONGEXIT 신호가 있는 경우 위의 기본 동작 대신 롱 위치를 종료 / SHORT 신호가 있고 LONGEXIT 신호가 없는 경우 SHORT 신호는 숏 포지션을 열기 전에 롱 포지션을 종료하는데 사용
      • SHORT : SHORTEXIT 신호가 있는 경우 위의 기본 동작 대신 숏 위치를 종료 / LONG 신호가 있고 SHORTEXIT 신호가 없는 경우 LONG 신호는 롱 포지션을 열기 전에 숏 포지션을 종료하는데 사용
%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)

 

 백테스팅이 완료되었다.

결과적으로 세개의 plot이 나왔다.

  • 포트폴리오 가치 변화 > $1000에서 $1002.61로 가치가 상승하였다.
  • 자산가격 + 매수,매도 지점
  • 선택한 기술 지표 > 여기서는 sma

 


5. Strategy

 로그 기록과 그 기록의 반환을 위해 더 많은 기능이 필요하고 아래를 통해 구현된다.

 

class SmaStrategy(bt.Strategy):

	# SMA 기간 설정
    params = (('ma_period', 20), )

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

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

        # add a simple moving average indicator
        self.sma = bt.ind.SMA(self.datas[0],
                              period=self.params.ma_period)

    # 로그 기록    
    def log(self, txt):
        dt = self.datas[0].datetime.date(0).isoformat()
        print(f'{dt}, {txt}')

    # 주문 상태 (position) 보고 : t일 지표는 종가를 기준으로 포지션을 개설/종료할 것을 제안
    # But 주문은 t+1일에 수행되는데 주문이 취소되거나 현금이 부족할 수 있으므로 주문 실행의 보장이 없음
    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(self):
        # do nothing if an order is pending
        if self.order:
            return

        # check if there is already a position
        if not self.position:
            # buy condition (이동평균가보다 높은지 확인)
            if self.data_close[0] > self.sma[0]:
                self.log(f'BUY CREATED --- Price: {self.data_close[0]:.2f}')
                # 매수량을 선택 > 기본설정 : size = 1
                self.order = self.buy()
        else:
            # sell condition
            if self.data_close[0] < self.sma[0]:            
                self.log(f'SELL CREATED --- Price: {self.data_close[0]:.2f}')
                self.order = self.sell()
cerebro = bt.Cerebro(stdstats = False)

cerebro.adddata(data)
cerebro.broker.setcash(1000.0)
cerebro.addstrategy(SmaStrategy)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)

print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
cerebro.run()
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')

 위 이미지와 같이 Strategy는 포트폴리오 매수, 매도의 로그와 해당 지점에서의 정보를 제공한다.

 

%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)

 

 


4. 매개변수 최적화

 backtrader는 백테스팅 시 사용하는 매개변수를 최적화하는 기능도 제공한다.

이전까지는 SMA period = 20으로 고정하여 사용했는데 아래의 최적화 기능을 통해 최적의 SMA period를 확인할 수 있다.

다른 내용은 동일하나 클래스의 stop 함수 추가와 cerebro.add_strategy가 아닌 cerebro.optstrategy를 사용하였다.

 

# Create a Stratey
class SmaStrategy(bt.Strategy):
    params = (('ma_period', 20),)

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

        # keep track of pending orders
        self.order = None

        # add a simple moving average indicator
        self.sma = bt.ind.SMA(self.datas[0], period=self.params.ma_period)

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

    def notify_order(self, order):
        # set no pending order
        self.order = None

    def next(self):
        # do nothing if an order is pending
        if self.order:
            return

        # check if there is already a position
        if not self.position:
            # buy condition
            if self.data_close[0] > self.sma[0]:
                self.order = self.buy()
        else:
            # sell condition
            if self.data_close[0] < self.sma[0]:
                self.order = self.sell()

    # SMA 일수 최적화 : 각 매개변수의 최종 terminal 포트폴리오 가치 반환
    def stop(self):
        self.log(f'(ma_period = {self.params.ma_period:2d}) --- Terminal Value: {self.broker.getvalue():.2f}')
data = bt.feeds.PandasData(dataname=yf.download('AAPL', '2018-01-01', '2018-12-31'))

cerebro = bt.Cerebro(stdstats = False)

cerebro.adddata(data)

# 이전의 add strategy가 아닌 아래 기능에 전략이름과 최적화할 매개 변수 값을 제공
cerebro.optstrategy(SmaStrategy, ma_period=range(10, 31))
cerebro.broker.setcash(1000.0)
cerebro.run(maxcpus=4)

terminal value를 봤을 때 sma_period = 22일이 최적의 전략임을 알 수 있다.

 


 

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

 

Ref

  • 금융 파이썬 쿡북, 에릭 르윈슨.