< 6. 실 거래 후 보완된 ETF 변동성 돌파 전략 - 마지막>


1. 개요

2. 데이터 수집

3. 개별 종목 수익율 계산

4. 최적 Scope 계산

5. 노이즈 계산


이번 포스트에서는 지금까지 계산한 데이터를 가지고 백테스팅을 수행해 아래와 같이 수익율 차트, MDD, 연 평균 수익율을 출력할 것이다. 


2011년 7월부터 18년 3월까지 수익율을 계산한다. 슬리피지는 0.1퍼이다. Noise는 0부터 0.5까지 0.1 단위로 모든 범위를 대상으로 한다. 최근 n일 최적 Scope와 Over_night 유무, 최근 m일 Noise 별로 수익율을 구한다. (n=[10,20,40,60,120], m=[1,10,20,40,60,120])  


위 수익율을 계산하기 전에 아래의 sql 문을 이해해야 한다.  

select pft.Date, pft.Profit from 
(select Code, Date, Profit from profit_date_10 where Date>='2011-07-01') as pft 
inner join 
(select Code, Date from noise_20 where Noise>=0.1 and Noise < 0.3) as Noi 
on pft.Code=Noi.Code and pft.Date=Noi.Date order by pft.Date

Noise 값에 따라서 Profit를 구해야 하기 때문에 profit_date_XX 테이블과 noise_XX 테이블을 조인해야 한다.

위 예를 보면 'profit_date_10' 테이블에서 Code, Date, Profit 데이터를 추출한다. 11년 7월부터 백테스팅하기에 Date 조건도 추가한다. 'profit_date_XX' 테이블은 "4. 최적 수익율 계산"에서 설명했다. 0.1에서 0.3 사이의 Noise 값을 가진 데이터를 'noise_20' 테이블에서 추출한다. 'noise_XX' 테이블은 이전 포스트에서 설명했다. 그리고 inner join 결합 기준으로 Code와 Date를 설정해 Noise값과 Profit 값을 결합시킨다.  그 결과로 노이즈 값에 따른 수익율 데이터를 추출한다. 이 데이터를 가지고 수익율 계산만 하면 위 차트를 출력할 수 있다. 


먼저 DB 관련 코드를 보자.  

from sqlalchemy import create_engine
import pymysql
pymysql.install_as_MySQLdb()
import pandas as pd
import logging as log

class StockDB():

    def __init__(self, password):
        log.info("Connecting database.....")

        try:
            self.engine = create_engine("mysql+mysqldb://root:"+password+"@localhost/etf1", encoding='utf-8')
            self.conn = pymysql.connect(host='localhost', user='root', password=password, db='etf1', charset='utf8')
            self.cursor = self.conn.cursor()
        except Exception as e:
            log.warning("Connecting database Error : {}".format(repr(e)))


    def select_test_data(self, scope_during, noise_during, noise_start, noise_end, over_night):
        log.info("Selecting test data - scope_during : {} , noise_during : {}".format(scope_during, noise_during))

        if over_night == True:
            sql = "select pft.Date, pft.Profit from " \
                  "(select Code, Date, Profit from profit_date_" + scope_during + "_over where Date>='2011-07-01') as pft inner join " \
                  "(select Code, Date from noise_" + noise_during + " where Noise>=" + str(noise_start) + " and Noise < " + str(noise_end) + ") " \
                  "as Noi on pft.Code=Noi.Code and pft.Date=Noi.Date order by pft.Date"
        else:
            sql = "select pft.Date, pft.Profit from " \
                  "(select Code, Date, Profit from profit_date_" + scope_during + " where Date>='2011-07-01') as pft inner join " \
                  "(select Code, Date from noise_" + noise_during + " where Noise>=" + str(noise_start) + " and Noise < " + str(noise_end) + ") " \
                  "as Noi on pft.Code=Noi.Code and pft.Date=Noi.Date order by pft.Date"

        print(sql)

        try:
            data = pd.read_sql(sql, self.conn)
        except Exception as e:
            log.warning("Selecting test data - scope_during : {} , noise_during : {}, Error : {}".format(scope_during, noise_during,repr(e)))

        return data
"select_test_data()" 메서드가 위에서 설명한 sql 문을 실행하여 결과값을 반환한다.

다음은 데이터의 수익율을 계산해 차트로 나타내고 이를 png 파일로 저장하는 코드이다.
import pandas as pd
import pandas
import DB as db
from mpldatacursor import datacursor
from matplotlib.dates import DateFormatter
import matplotlib.pyplot as plt
import logging as log
import sys

log.basicConfig(stream=sys.stdout, level=log.DEBUG)


dB = db.StockDB("qhdks12#$")

def backtesting(scope_during, noise_during, noise_start, noise_end, over_night):
    
    # 수익율 데이터 추출
    backtest_data = dB.select_test_data(scope_during, noise_during, noise_start, noise_end, over_night)

    if len(backtest_data)==0:
        return

    # 일별로 그룹핑해 수익율의 평균을 계산하여 일별 수익율 계산
    profit = backtest_data.groupby('Date')['Profit'].mean()
    # 누적 수익율 계산
    result = pd.DataFrame({'Profit': (profit).cumprod()})
    # Drawdown 계산
    result['Drawdown'] = result['Profit'] / (result['Profit'].cummax())
    result['Return'] = profit

    # 연평균 수익율 계산
    cagr = (result['Profit'][-1] / result['Profit'][0]) ** (1 / 6.75) - 1

    result = result.subtract(1)
    
    # MDD 추출
    mdd = result['Drawdown'].min()

    # cul_profit.plot(grid=True, logy=True)
    axes = result.plot(grid=True)
    lines = axes.get_lines()
    fmt = DateFormatter('%Y-%m-%d')
    datacursor(lines,
               formatter=lambda **kwargs: 'Return : {y:.4f}'.format(**kwargs) + '\ndate: ' + fmt(kwargs.get('x')))

    L = plt.legend()
    L.get_texts()[0].set_text('Cul Return, CAGR : %0.2f' % (cagr))
    L.get_texts()[1].set_text('Drawdawn, MDD : %0.2f' % (mdd))

    title = "11.07 ~ 18.03 Return - Scope : {}, OverNight : {}, Noise : {} ({}~{})".format(scope_during, over_night, noise_during, noise_start, noise_end)
    plt.title(title)
    #plt.show()

    # 차트를 파일로 저장
    file_title = "{},{},{},{}~{}.png".format(scope_during, over_night, noise_during, noise_start, noise_end)
    plt.savefig(file_title)


scope_durings = ['10','20','40','60','120']
noise_durings = ['1','5','10','20','40','60','120']
noise_range = [0,0.1,0.2,0.3,0.4,0.5]
over_nights = [True, False]

for over_night in over_nights:
    for scope_during in scope_durings:
        for noise_during in noise_durings:
            for i, start_noise in enumerate(noise_range):
                for end_noise in noise_range[i+1:]:
                    backtesting(scope_during,noise_during,start_noise, end_noise, over_night)
                    log.info("finish {} {} {} {} {}".format(scope_during,noise_during,start_noise, end_noise, over_night))

주석을 보면 코드가 이해 될 것이다. 위와 같이 결과로 나올 수 있는 모든 경우를 계산해 파일로 저장한다. 


위 코드를 실행하면 천 개가 넘는 차트를 얻을 수 있다. 



그 결과 가장 좋은 수익율을 기록한 백테스팅은 아래와 같다. 연평균 수익율이 28%에 MDD가 -6% 이다. 




참고로 필자는 컴퓨터 공학과를 재학 중인 대학생입니다. 따라서 코드가 완벽할 수 없습니다. 알고리즘이나 코드가 비효율적이거나 오류가 있다면 댓글 달아주세요..

+ Recent posts