AUTO TRADE/Back test

백테스팅 구축 (13) - 전략 수정하기

지난 게시글까지가 백테스팅 전략을 수립하고 오류를 찾아내는 방법론에 대한 내용을 다루었다면, 이번 게시글부터는 백테스팅 과정에 있어서 어떤 문제점이 있었고 그를 어떻게 수정해야 하는지에 대해 살펴보고자 한다.

 

이동평균선 전략 백테스팅 결과

    여태까지 제작했던 백테스팅 전략을 2020년 1월 1일부터 2020년 12월 31일까지 일년 간의 데이터를 백테스팅해 본 결과, 아래와 같은 결과를 얻었다. (물론 전 종목을 대상으로 하는 것이 아닌, 차트 데이터가 저장되어 있는 데이터만을 대상으로 한 것이다.) 결과값들을 계속해서 돌려보니 6월까지의 누적 손익이 최저값은 약 230만원 정도가 나오고, 어느 정도의 수익을 기록했다가 다시 손실을 기록하고 있는 모습을 보이며 결국에는 23만원의 수익을 얻었음을 확인할 수 있었다.

  • 6월 30일까지의 누적손익 : -2,090,622
  • 12월 31일까지의 누적손익 : 238,108

    하지만 이번 게시글의 목적은 단순한 누적 손익을 확인하고 '아.. 이 전략은 망했구나..'하고 넘어가는 게 아니라, 백테스팅 과정 중에 매수하고 매도했던 종목들의 데이터를 한데 모아 그 종목별·거래별로 수익을 기록했으면 수익을, 손실을 기록했으면 손실을 입력하고 그 외의 부수적인 데이터들도 모아서 어떤 요인을 제거함으로써 보다 우수한 거래 전략을 수립할 수 있는지 확인하기 위해 필요한 그 기반 데이터를 구축하는 것이 목적이다.

 

거래 종목 데이터 구축하기

    거래한 종목의 데이터를 구축하기 위해서는 역시 하나의 데이터프레임이 있어야 하고, 매수한 경우와 매도한 경우에 해당 거래 데이터를 입력하는 방식으로 데이터를 구축해야 한다. 따라서 def __init__ 내부에서 거래 이력 데이터를 입력할 변수를 제작해주도록 하자. 현황 파악을 위한 데이터는 다음과 같다.

[추신] 제작자가 생각하기에 필요하다고 생각하는 정보를 입력해도 된다.

  • 종목 정보 : code(종목 코드, volume(거래량), tvolume(거래대금)
  • 거래 정보 : buy_date(매수일), buy_price(매수가격), buy_value(총 매수대금), sell_date(매도일), sell_price(매도가격), sell_value(총 매도대금), profit(매도 시점의 수익)
  • 매수 정보 : ma5_buy, ma10_buy, ma20_buy, ma60_buy, ma120_buy(매수 시점의 이동평균선 값)
  • 매도 정보 : ma5_sell, ma10_sell, ma20_sell, ma60_sell, ma120_sell(매도 시점의 이동평균선 값)
self.tracking_data = {'code':[], 'buy_date':[], 'buy_price':[], 'buy_value':[], 'sell_date':[], 'sell_price':[], 'sell_value':[], 'profit':[],
                      'ma5_buy':[], 'ma10_buy':[], 'ma20_buy':[], 'ma60_buy':[], 'ma120_buy':[],
                      'ma5_sell':[], 'ma10_sell':[], 'ma20_sell':[], 'ma60_sell':[], 'ma120_sell':[]}

self.df_tracking_data = pd.DataFrame(self.tracking_data, columns=['code', 'buy_date', 'buy_price', 'buy_value', 'sell_date', 'sell_price', 'sell_value', 'profit',
                                                                  'ma5_buy', 'ma10_buy', 'ma20_buy', 'ma60_buy', 'ma120_buy',
                                                                  'ma5_sell', 'ma10_sell', 'ma20_sell', 'ma60_sell', 'ma120_sell'])

 

이제 buy() 함수와 sell() 함수에서 관련 데이터들을 입력하는 코드를 제작해볼 것인데, 그 전에 우리가 제작한 코드의 대략적인 로직에 대해 한 번 살펴보고 넘어가도록 하자.

백테스팅 코드의 논리구조

    여기서 우리가 self.tracking_data 변수를 입력할 때 보아야 하는 것은 바로 각 함수에서 어떤 처리를 담당하고 있는지이다. 즉, buy_list() 함수 내에서는 매수 대상 종목의 데이터들을 self.buy_list라는 변수로 반환받았고, 우리는 self.buy_list라는 변수를 대상으로 for문을 돌리면서 매수를 진행했다. 그리고 매수를 진행한 종목들은 self.df_account라는 변수에 입력했고, sell() 함수에서는 보유 종목을 대상으로 조건을 충족시키는지 여부를 판단한 후에 매도를 진행했다. 
    따라서 buy() 함수 내에서는 매수 대상 종목의 정보가 담겨 있는 self.buy_list라는 변수를 사용할 수 있고, sell() 함수의 경우에는 별도로 매도 대상이 되는 종목의 리스트를 제작하지 않았기 때문에 특정 종목의 차트 데이터를 조회한 후 그 값들을 self.chart_data에 입력하고, 그를 바탕으로 매도를 진행하도록 구축했다.
    이 이야기를 하는 이유는, 우리가 앞서 제작했던 self.tracking_data라는 변수에서는 매수 시점과 매도 시점의 이동평균선 값들을 이용해야 하기 때문이다. 하지만 데이터가 필요할 때마다 차트 데이터를 SELECT * FROM 문구를 통해 데이터를 불러오고, 거기서 일자가 일치하는 데이터를 불러오는 그 과정에 소요되는 시간이 너무 길기 때문에, 각 함수를 실행하는 그 시점에서 우리가 사용할 수 있는(기존에 불러왔던) 데이터를 활용하는 것이 백테스팅 과정에서 소요되는 시간을 많이 절약해줄 수 있다. 그래서 코드의 로직을 가볍게 살펴본 것이다.

 


728x90

 


 

self.tracking_data 제작, ① buy() 함수 

앞서 살펴봤듯이, buy() 함수 내에서는 self.buy_list 변수를 이용할 수 있었다. 그렇다면 self.buy_list 변수에는 어떤 정보가 입력되어 있는지 check_list() 함수 내에서 확인해보면 된다. 아래에서 볼 수 있듯이, code(종목코드), date(매수 조건 충족 일자), close(매수 조건 충족일의 종가), ma5, ma10, ma20, ma60, ma120(매수 조건 충족 시점의 이동평균선), volume(거래량), tvolume(거래대금) 데이터가 있다. 이 안에 우리가 self.tracking_data 변수에 입력하고자 하는 값들은 모두 입력되어 있다.

def check_list(self):
	self.code_list = self.load_code()
	buy_list = {'code':[], 'date':[], 'close':[], 'ma5':[], 'ma10':[], 'ma20':[], 'ma60':[], 'ma120':[], 'volume':[], 'tvolume':[]}

 

    따라서 이를 바탕으로 해서 buy() 함수 내에서 self.tracking_data 변수에 해당 값들을 입력해주면 된다. 나중에는 self.tracking_data만 출력해서 보더라도 어떤 상황에서 매수했는지, 얼만큼 매수했는지를 확인할 수 있으며 총 거래대금이나 총 거래 횟수 등을 한 번에 확인할 수 있게 된다.
    여기서도 이전에 제작했던 방법과 마찬가지로, 자료형을 튜플 형태로 제작한 후에 그를 데이터프레임(DataFrame) 자료형으로 변환시켜주고, 변환된 데이터를 self.df_tracking_data 변수 안에 입력해주도록 하자. (이전에 현재 보유 종목을 처리할 때에도 self.account 변수를 바탕으로 self.df_account 변수를 제작했었다.) 

    def buy(self):

        for row in self.buy_list.itertuples():
            quantity = int(self.money_by_unit / int(row[3]))

            if self.init_money >= int(row[3]) * int(quantity):
                buy_data = [(row[2], row[1], row[3], int(quantity))]
                tempt_df_account = pd.DataFrame(buy_data, columns = ['date', 'code', 'buy_price', 'quantity'])
                self.df_account = self.df_account.append(tempt_df_account).reset_index(drop=True)
                self.init_money = self.init_money - (int(row[3]) * int(quantity))
                buy_value = int(row[3]) * int(quantity)
                
                ## 추가된 부분 ##
                ## row[1] : code, row[2] : buy_date, row[3] : buy_price
                ## row[4] : ma5_buy, row[5] : ma10_buy, row[6] : ma20_buy
                ## row[7] : ma60_buy, row[8] : ma120_buy
                ## buy_value : buy_value
                
                item_data = [(row[1], row[2], row[3], buy_value, row[4], row[5], row[6], row[7], row[8])]
                tempt_item_data = pd.DataFrame(item_data, columns = ['code', 'buy_date', 'buy_price', 'buy_value',
                                                                     'ma5_buy', 'ma10_buy', 'ma20_buy', 'ma60_buy', 'ma120_buy'])
                self.df_tracking_data = self.df_tracking_data.append(tempt_item_data).reset_index(drop=True)

 

 

self.tracking_data 제작, ② sell() 함수

    매도 함수의 경우에는 조금 주의해야 할 부분이 있다. 이전에 사용했던 drop의 경우에는 특정 값이 위치해 있는 행의 데이터를 삭제하는 것이었기 때문에 큰 문제가 안 됐지만, 지금은 self.tracking_data 변수 내에 데이터를 입력하고자 하는 것이기 때문에 문제가 발생하는 부분이 있다.
    바로, 000020이라는 종목을 먼저 매수하고 그 후에 000040이라는 종목을 매수했다고 가정해보자. 그렇다면 현재 보유 종목 변수 안에는 000020이라는 종목이 인덱스 0번, 000040이라는 종목이 인덱스 1번을 차지하고 있을 것이다. 하지만 여기서 만약에 000020 종목이 아닌 000040 종목이 먼저 매도 조건이 충족되어 매도한 후에 우리가 입력하고자 하는 데이터들을 튜플 형태의 자료로 제작해서 self.tracking_data에 입력하게 되면, 000040 종목을 매도했음에도 000020 종목이 매도된 것처럼 입력될 수 있다. 따라서 어느 위치에 입력할 것인지를 확인한 후에 데이터를 입력해야 한다.
    이 부분은 이전에 drop 메서드를 실행할 때처럼 우리가 매도한 종목의 데이터가 위치해 있는 인덱스 값을 구한 후에, 해당 인덱스 값이 위치한 행에서 입력하고자 하는 칼럼의 이름을 입력한 후 입력하고자 하는 데이터를 전달해줌으로써 데이터프레임의 데이터를 변경할 수 있다. 이 때 사용하는 메서드는 at()이다. set_value()도 사용 가능하다.

def sell(self):

    for row in self.df_account.itertuples():
        self.chart_data = self.load_chart_indate(self.yesterday, self.today, row[2])

        try:
            yester_ma5 = self.chart_data['MA5'].iloc[0]
            yester_ma20 = self.chart_data['MA20'].iloc[0]
            today_ma5 = self.chart_data['MA5'].iloc[1]
            today_ma20 = self.chart_data['MA20'].iloc[1]

            if float(yester_ma5) > float(yester_ma20) and float(today_ma5) < float(today_ma20):
                today_close = self.chart_data['close'].iloc[1]  ## 매도 가격(sell_price)
                buy_value = int(row[3]) * int(row[4])
                sell_value = int(today_close) * int(row[4])
                code_profit = int(int(sell_value) - int(buy_value))   ## 매도 가격 빼기 매수 가격, 주당 손익

                print("######### SELL #########")
                print("매도 진행 종목:", row[2])
                print("매수 가격:", buy_value)
                print("매도 가격:", sell_value)
                print("종목별 매도 수익금:", code_profit)

                self.init_money = self.init_money + int(sell_value)  ## 전체 매도 금액을 더하고
                self.today_profit = self.today_profit + int(code_profit)  ## 수익을 더하고
                
                index1 = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
                self.df_account.drop(self.df_account.index[index1], inplace=True)
                
                #################
                ## 수정된 부분 ##
                #################
                index2 = self.df_tracking_data.index[(self.df_tracking_data['code'] == row[2])].to_list()[0]
                self.df_tracking_data.set_value(index2, 'sell_date', self.today)
                self.df_tracking_data.set_value(index2, 'sell_price', today_close)
                self.df_tracking_data.set_value(index2, 'sell_value', sell_value)
                self.df_tracking_data.set_value(index2, 'profit', code_profit)
                self.df_tracking_data.set_value(index2, 'ma5_sell', self.chart_data['MA5'].iloc[1])
                self.df_tracking_data.set_value(index2, 'ma10_sell', self.chart_data['MA10'].iloc[1])
                self.df_tracking_data.set_value(index2, 'ma20_sell', self.chart_data['MA20'].iloc[1])
                self.df_tracking_data.set_value(index2, 'ma60_sell', self.chart_data['MA60'].iloc[1])
                self.df_tracking_data.set_value(index2, 'ma120_sell', self.chart_data['MA120'].iloc[1])

            else:
                pass

        except IndexError:
            pass

 

이제 def __init__에서 print("누적 손익:", self.all_profit) 아래에 print("거래 이력:")를 추가하고 나서 코드를 실행해보도록 하자. 

class algorithm1():
    def __init__(self, start_date, end_date, all_range):
        self.all_range = all_range
        self.today = start_date

        self.account = {'date':[], 'code':[], 'buy_price':[], 'quantity':[], 'profit':[]}
        self.df_account = pd.DataFrame(self.account, columns=['date', 'code', 'buy_price', 'quantity', 'profit'])
        self.tracking_data = {'code':[], 'buy_date':[], 'buy_price':[], 'buy_value':[], 'sell_date':[], 'sell_price':[], 'sell_value':[], 'profit':[],
                              'ma5_buy':[], 'ma10_buy':[], 'ma20_buy':[], 'ma60_buy':[], 'ma120_buy':[],
                              'ma5_sell':[], 'ma10_sell':[], 'ma20_sell':[], 'ma60_sell':[], 'ma120_sell':[]}
        self.df_tracking_data = pd.DataFrame(self.tracking_data, columns=['code', 'buy_date', 'buy_price', 'buy_value', 'sell_date', 'sell_price', 'sell_value', 'profit',
                                                                          'ma5_buy', 'ma10_buy', 'ma20_buy', 'ma60_buy', 'ma120_buy',
                                                                          'ma5_sell', 'ma10_sell', 'ma20_sell', 'ma60_sell', 'ma120_sell'])
        self.init_money = 10000000
        self.money_by_unit = 1000000
        self.all_profit = 0

        while self.today != end_date:
            self.today_profit = 0
            self.yesterday = self.cal_subday(self.today, 1)

            print("현재 조회일자:", self.today, "(self.today값), ", self.yesterday, "(self.yesterday값)")
            self.buy_list = self.check_list()
            self.buy()

            print("현재 보유 종목")
            print(self.df_account)

            self.sell()
            print("현재 잔고:", self.init_money)
            print("일별 손익:", self.today_profit)
            self.all_profit = self.all_profit + self.today_profit
            print("누적 손익:", self.all_profit)
            print("거래 이력:")
            print(self.df_tracking_data)

            self.today = self.cal_addday(self.today, 1)

## 출력 결과 ##

#### 첫 매수 시점 #####
현재 조회일자: 20200103 (self.today값),  20200102 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
0  20200103  004170    292500       3.0     NaN
현재 잔고: 9122500
일별 손익: 0
누적 손익: 0
거래 이력:
     code  buy_date buy_price  ...  ma20_sell  ma60_sell  ma120_sell
0  004170  20200103    292500  ...        NaN        NaN         NaN


#### 첫 매도 시점 ####
현재 조회일자: 20200120 (self.today값),  20200117 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
0  20200103  004170    292500       3.0     NaN
1  20200113  000100     44692      22.0     NaN
2  20200113  002450      1980     505.0     NaN
3  20200114  003580      8400     119.0     NaN
4  20200115  000080     31650      31.0     NaN
5  20200115  001630    113000       8.0     NaN
6  20200115  002320     30426      32.0     NaN
7  20200115  003690      9310     107.0     NaN
8  20200117  000720     42050      23.0     NaN
9  20200117  001390     12800      78.0     NaN
현재 잔고: 1291960
일별 손익: -10538
누적 손익: -10538
거래 이력:
     code  buy_date buy_price  ...  ma60_sell  ma120_sell   sell_date
0  004170  20200103    292500  ...        NaN         NaN         NaN
1  000100  20200113     44692  ...   41765.95   41312.825    20200120
2  002450  20200113      1980  ...        NaN         NaN         NaN
3  003580  20200114      8400  ...        NaN         NaN         NaN
4  000080  20200115     31650  ...        NaN         NaN         NaN
5  001630  20200115    113000  ...        NaN         NaN         NaN
6  002320  20200115     30426  ...        NaN         NaN         NaN
7  003690  20200115      9310  ...        NaN         NaN         NaN
8  000720  20200117     42050  ...        NaN         NaN         NaN
9  001390  20200117     12800  ...        NaN         NaN         NaN


현재 조회일자: 20200204 (self.today값),  20200203 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
1  20200113  002450      1980     505.0     NaN
4  20200115  001630    113000       8.0     NaN
5  20200115  002320     30426      32.0     NaN
6  20200115  003690      9310     107.0     NaN
7  20200117  000720     42050      23.0     NaN
9  20200122  000520     10625      94.0     NaN
현재 잔고: 4784525
일별 손익: 0
누적 손익: -359543
거래 이력:
      code  buy_date buy_price  ...      ma60_sell     ma120_sell   sell_date
0   004170  20200103    292500  ...  281616.666667  261787.500000    20200128
1   000100  20200113     44692  ...   41765.950000   41312.825000    20200120
2   002450  20200113      1980  ...    1842.666667    1822.458333    20200131
3   003580  20200114      8400  ...    8153.833333    6213.291667    20200128
4   000080  20200115     31650  ...            NaN            NaN         NaN
5   001630  20200115    113000  ...            NaN            NaN         NaN
6   002320  20200115     30426  ...            NaN            NaN         NaN
7   003690  20200115      9310  ...    8631.166667    8274.833333    20200128
8   000720  20200117     42050  ...            NaN            NaN         NaN
9   001390  20200117     12800  ...            NaN            NaN         NaN
10  000520  20200122     10625  ...            NaN            NaN         NaN

 

출력 결과를 보니 우리가 입력했던 데이터가 너무 길고 많기 때문에 모든 데이터가 출력되지는 않고 있지만, 보이는 자료만 본다면 적어도 매도가 이뤄졌을 때 데이터(ma20_sell, ma60_sell, ma120_sell)들을 잘 입력하고 있으며 데이터프레임 자료형으로 깔끔하게 만들어주고 있다. 그리고 거래 이력 데이터가 담겨 있는 self.df_tracking_data 변수는 계속해서 누적되기 때문에, 최종적으로는 해당 변수에 입력되어 있는 자료의 개수가 우리가 거래한 횟수와 동일한 값을 갖게 된다.

이제 다음 게시글에서는 백테스팅을 완료한 후에 거래 이력이 입력된 self.df_tracking_data 변수를 기반으로 결과값을 어떻게 분석하는지 확인해보도록 하겠다.

 

 


728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.