AUTO TRADE/Back test

백테스팅 구축 (11) - check_list 함수 수정하기

지난 게시글에서 백테스팅 결과를 실제 자료와 대조하면서 결과값을 확인해 본 결과, 매수 함수는 정상적으로 동작하지만 매도 함수는 정상적으로 동작하지 않음을 확인할 수 있었다. 따라서 이번 게시글에서는 오류가 발생한 원인이 무엇인지 확인하고, 그를 해결하고자 한다.

 

왜 매도를 안 해?

지난 게시글에서 004170(신세계) 종목이 2020년 1월 28일에 매도가 이뤄져야 했음에도 불구하고 매도가 이뤄지지 않았음을 확인할 수 있었다. 따라서 문제를 확인하고자 키움증권 데이터를 확인해보니 2020년 1월 28일은 화요일이었기 때문에 우리가 기존에 제작했던 날짜 계산 함수(cal_subday())는 2020년 1월 27일자의 데이터를 불러오고자 했을 것이다. 하지만 실제 데이터에 따르면 2020년 1월 28일 화요일을 기준으로 하루 전의 데이터는 1월 23일로, 목요일이다. 즉, 금요일과 월요일에는 주가 데이터가 없기 때문에 대소 비교가 불가능해졌고, 그에 따라 매도를 진행하지 못한 것이다.

따라서 이번에는 매도 함수의 경우에도 이동평균선 간 대소 비교를 하는 데에 있어서, 매수 함수에서도 작성했듯이 데이터의 개수가 두 개가 아닌 한 개라면 데이터의 개수가 두 개가 될 때까지의 날짜 조합을 찾도록 구축해야 한다. 또한 이를 바탕으로 매수할 당시에도 같은 로직을 거쳐 매수를 하도록 수정해보자. 지금 현재 코드의 문제는 설날 등과 같은 공휴일이 평일일 경우 해당 일자의 데이터를 찾을 수 없다는 것이다. 

 

자료의 개수가 두 개가 되는 일자 찾기 (feat. While)

일단 일자별로 데이터가 들어있는지 확인하는 함수는 is_db함수였다. 즉, 특정 일자의 데이터 개수가 1개 있다면 True를, 1개가 아닌 2개 또는 0개라면 False를 반환하도록 했었다. 따라서 우리는 이를 이용해서 self.today 변수와 self.yesterday가 유효한 일자인지를 판단한 후에 유효한 데이터라면 그대로 코드를 진행하면 된다.

def is_db(self, code, date):

    try:
        chart_data = pd.read_sql("SELECT * FROM s" + code + " WHERE date = " + date, engine_all)
        result = len(chart_data)

        if result == 1:
            return True
        else:
            return False

    except sqlalchemy.exc.ProgrammingError:
        return False

 

이전에 제작했던 is_db() 함수의 코드는 위와 같다. 잘 보면 일자 하나만 받아 오는 것을 확인해볼 수 있는데, 이를 어떻게 활용할 것인가 하면 is_right_day() 함수를 제작한 후 그 안에서 is_db()함수를 이용할 것이다.

is_right_day() 함수 내에서는 가장 처음에 is_db 함수를 이용해 self.today와 self.yesterday 안에 입력된 일자의 데이터가 있는지를 확인하도록 하고, 그 결과값(True or False)을 각각 is_right_today와 is_right_yesterday 안에 입력하도록 한다. 그리고 while문에 진입하도록 함으로써 is_right_today가 True이고 is_right_yesterday가 False라면 cal_subday() 함수에 self.yesterday를 인자로 전달함으로써 하루를 빼도록 한다. 즉, 하루씩 빼면서 해당 일자에 데이터가 존재하는지 그 여부를 판단해주는 is_db 함수를 이용해 그 결과값을 is_right_yesterday로 받고, 다시 while문을 돌면서 두 변수 모두 True인 경우에만 return True를 통해 True를 반환하도록 하는 것이다.

def is_right_day(self, code):
    is_right_today = self.is_db(code, self.today)
    is_right_yesterday = self.is_db(code, self.yesterday)

    while is_right_today == True and is_right_yesterday != True:
        self.yesterday = self.cal_subday(self.yesterday, 1)
        is_right_today = self.is_db(code, self.today)
        is_right_yesterday = self.is_db(code, self.yesterday)

    return True

 

앞에서도 이야기했듯이 특정 일자의 데이터가 있는지 없는지를 확인하기 위해서는 종목 코드가 필요하기 때문에, 위에서 제작한 함수 역시 종목 코드(변수 명 : code)를 하나의 인자로 받아서 is_db함수에 다시 전달해줌으로써 그 결과값을 확인한다. 이제 이 is_right_day 함수를 check_list 함수 내에서 제작하면 된다. 그리고 나서 시작 일자 변수에 20200128을 입력한 후에 코드를 돌려보면, self.yesterday 변수 안에 20200123이 잘 입력되어 있는 것을 확인할 수 있다.

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

    for code in self.code_list['code']:
        result = self.is_right_day(code)
        self.chart_data = self.load_chart_indate(self.yesterday, self.today, code)
        
        # 기존 코드
        # if self.is_db(code, self.today) == True and self.is_db(code, self.yesterday) == True:
        
        # 신규 코드
        # is_right_day가 True라면
        if result == True:
            print("code:", code, "  today:", self.today, " yester:", self.yesterday)
            result = self.justify_ma()
            
            ## justify_ma가 True라면
            if result == True:

                if int(self.chart_data['trade_volume'].iloc[1]) > 5000:
                    buy_list['code'].append(code)
                    buy_list['date'].append(self.today)
                    buy_list['close'].append(self.chart_data['close'].iloc[1])
                    buy_list['ma5'].append(self.chart_data['MA5'].iloc[1])
                    buy_list['ma10'].append(self.chart_data['MA10'].iloc[1])
                    buy_list['ma20'].append(self.chart_data['MA20'].iloc[1])
                    buy_list['ma60'].append(self.chart_data['MA60'].iloc[1])
                    buy_list['ma120'].append(self.chart_data['MA120'].iloc[1])
                    buy_list['volume'].append(self.chart_data['volume'].iloc[1])
                    buy_list['tvolume'].append(self.chart_data['trade_volume'].iloc[1])


            elif result == False:
                pass

        else:
            pass

    self.buy_list = pd.DataFrame(buy_list, columns=['code', 'date', 'close', 'ma5', 'ma10', 'ma20', 'ma60', 'ma120', 'volume', 'tvolume'])
    return self.buy_list

현재 조회일자: 20200128 (self.today값),  20200127 (self.yesterday값)
code: 000020   today: 20200128  yester: 20200123
code: 000040   today: 20200128  yester: 20200123
code: 000050   today: 20200128  yester: 20200123
code: 000060   today: 20200128  yester: 20200123
code: 000070   today: 20200128  yester: 20200123
code: 000080   today: 20200128  yester: 20200123
code: 000100   today: 20200128  yester: 20200123
code: 000120   today: 20200128  yester: 20200123
code: 000140   today: 20200128  yester: 20200123
code: 000150   today: 20200128  yester: 20200123

 

 


728x90

 

 

코드 수정 후 오류 재발생

code: 002630   today: 20200128  yester: 20200123
None
Traceback (most recent call last):
    if len(self.chart_data) == 2:
TypeError: object of type 'NoneType' has no len()

 

현재 조회하고 있는 종목이 데이터베이스에 존재하지 않는 경우에 발생하는 오류로, justify_ma() 함수 내에서 발생했다고 알려주고 있다. 이 역시 try문과 except문을 통해 오류를 제거해주도록 하자. 다만 여기서 주의해야 할 점은 except에 해당할 경우 except 아래 부분에서 pass를 실행하는 것이 아닌 return False를 통해 반환값으로 False를 반환해줌으로써 check_list 아래에 작성해둔 if result == True: 문에 들어가지 않고 result == False:에 들어가도록 구축함으로써 pass가 실행되도록 한다. 그 후에 코드를 다시 실행해보자.

def justify_ma(self):

    try:
        if len(self.chart_data) == 2:
            today_ma5 = self.chart_data['MA5'].iloc[1]
            today_ma20 = self.chart_data['MA20'].iloc[1]
            yester_ma5 = self.chart_data['MA5'].iloc[0]
            yester_ma20 = self.chart_data['MA20'].iloc[0]

            if float(yester_ma5) < float(yester_ma20) and float(today_ma5) > float(today_ma20):
                return True
            else:
                return False

        elif len(self.chart_data) != 2:
            return False

    except TypeError:
        return False

## 출력 결과 ##
현재 조회일자: 20200127 (self.today값),  20200124 (self.yesterday값)
현재 보유 종목
        date    code buy_price  quantity
0   20200103  004170    292500         3
1   20200113  000100     44692        22
2   20200113  002450      1980       505
3   20200114  003580      8400       119
4   20200115  000080     31650        31
5   20200115  001630    113000         8
6   20200115  002320     30426        32
7   20200115  003690      9310       107
8   20200117  000720     42050        23
9   20200117  001390     12800        78
10  20200122  000520     10625        94
현재 잔고: 293210
일별 손익: 0
누적 손익: -10538

현재 조회일자: 20200128 (self.today값),  20200127 (self.yesterday값)
현재 보유 종목
        date    code buy_price  quantity
0   20200103  004170    292500         3
1   20200113  000100     44692        22
2   20200113  002450      1980       505
3   20200114  003580      8400       119
4   20200115  000080     31650        31
5   20200115  001630    113000         8
6   20200115  002320     30426        32
7   20200115  003690      9310       107
8   20200117  000720     42050        23
9   20200117  001390     12800        78
10  20200122  000520     10625        94
매도 진행 종목: 004170
매수 가격: 877500
매도 가격: 808500
종목별 매도 수익: -69000
매도 진행 종목: 003580
매수 가격: 999600
매도 가격: 924630
종목별 매도 수익: -74970
매도 진행 종목: 003690
매수 가격: 996170
매도 가격: 944810
종목별 매도 수익: -51360
현재 잔고: 2971150
일별 손익: -195330
누적 손익: -205868

 

이제 다시 전체 코드를 실행해보면, 2020년 1월 28일에 004170(신세계)의 매도 뿐만 아니라 기존에 오류가 있어서 매도가 이뤄지지 않았던 003580(넥스트사이언스) 종목과 003690(코리안리) 종목도 매도가 진행됐음을 확인할 수 있다. 

추가적으로, 이번에 수정한 코드로 인해 특정 일자의 데이터를 조회할 때 하루를 뺀 날짜로 저장된 데이터가 없는 경우에는 하루를 더 빼면서 데이터가 존재하는 시점까지 일자를 계속해서 계산하게 되었다. 그렇기 때문에 self.today에 입력된 일자만 유효한 데이터가 저장되어 있는 날이라면 알아서 그 전에 존재하는 일자의 데이터를 불러오게 된다. 이 말인 즉슨 2020년 1월 2일자를 self.today 변수에 입력하게 되면 이전의 코드에서는 2020년 1월 1일자의 데이터가 없기 때문에 백테스팅이 불가능했지만 오늘 수정한 코드로 인해 2020년 1월 2일을 self.today에 입력하더라도 데이터가 존재하는 2019년 12월 30일의 데이터를 가지고 와서 이동평균선 간의 대소 비교를 진행하게 되는 것이다. 

WorkBench 내 저장된 차트 데이터 화면

더 나아가 종목마다 거래 정지 등과 같은 사유로 인해 주가 데이터가 존재하지 않는 경우가 있는데, 그런 경우에는 주가 데이터가 입력되어 있긴 하지만 모든 값들이 0이기 때문에 암만 계산해봐도 0으로 집계되며 결국 어떠한 기준에도 들어맞지 않기 때문에 거래를 진행하지 않게 된다.

이제 백테스팅은 어떤 과정과 로직을 거쳐 이루어지는지 어느 정도 아는 수준이 되었을 것이다. 백테스팅 전략을 수정하는 등의 코드는 이제 스스로 찾아보기도 하고 생각해내기도 하고 하면서 제작할 수 없다 하더라도 방법론에 대해서는 상당 부분 다루었기 때문에 스스로 생각했던 전략을 적용해보고, 매수 과정에 있어서 추가적인 조건들을 제작함으로써 매수 조건을 더 까다롭게 만든다거나 또는 지수 데이터를 다운로드 받아 지수의 가격적인 위치에 따라 비중을 조절한다거나 하는 등의 디테일한 거래 전략을 수립하면 된다. 다음 게시글에서는 백테스팅 과정에서 거래 자료들을 어떻게 처리해야 결과를 분석하거나 전략을 수정할 때 보다 편리하고 직관적으로 확인할 수 있는지 등에 대해 작성할 게획이다.

 

더보기
from sqlalchemy import create_engine
import pandas as pd
import datetime
import Modules
import sqlalchemy

engine_all = create_engine('mysql+mysqldb://root:비밀번호@127.0.0.1:3306/일봉 DB 이름', echo=False)
connection_all = engine_all.connect()

engine_item = create_engine('mysql+mysqldb://root:비밀번호@127.0.0.1:3306/종목 정보 DB 이름', echo=False)
connection_item = engine_all.connect()


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'])
        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)

            self.today = self.cal_addday(self.today, 1)  ## 항상 맨 밑에 두어야 함

    def sell(self):

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

            try:
                code = row[2]
                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):
                    index = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
                    self.df_account.drop(self.df_account.index[index], inplace=True)

                    today_close = self.chart_data['close'].iloc[1]  ## 매도 가격(sell_price)
                    quantity = row[4]   ## 매수 수량
                    dif = int(today_close) - int(row[3])    ## 매도 가격 빼기 매수 가격, 주당 손익

                    print("######### SELL #########")
                    print("매도 진행 종목:", row[2])
                    print("매수 가격:", int(row[3]) * int(row[4]))
                    print("매도 가격:", int(today_close) * int(row[4]))
                    print("종목별 매도 수익금:", int(dif) * int(quantity))

                    self.init_money = self.init_money + (int(today_close) * int(quantity))  ## 전체 매도 금액을 더하고
                    self.today_profit = self.today_profit + (int(dif) * int(quantity))  ## 수익을 더하고

                else:
                    pass

            except IndexError:
                pass

    def is_right_day(self, code):
        is_right_today = self.is_db(code, self.today)
        is_right_yesterday = self.is_db(code, self.yesterday)

        while is_right_today == True and is_right_yesterday != True:
            self.yesterday = self.cal_subday(self.yesterday, 1)
            is_right_today = self.is_db(code, self.today)
            is_right_yesterday = self.is_db(code, self.yesterday)

        return True

    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):
                self.account['code'].append(row[1])
                self.account['date'].append(row[2])
                self.account['buy_price'].append(row[3])
                self.account['quantity'].append(quantity)

                self.init_money = self.init_money - (int(row[3]) * int(quantity))

    def is_db(self, code, date):

        try:
            chart_data = pd.read_sql("SELECT * FROM s" + code + " WHERE date = " + date, engine_all)
            result = len(chart_data)

            if result == 1:
                return True
            else:
                return False

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

        for code in self.code_list['code']:
            result = self.is_right_day(code)
            self.chart_data = self.load_chart_indate(self.yesterday, self.today, code)

            if result == True:

            # if self.is_db(code, self.today) == True and self.is_db(code, self.yesterday) == True:
                result = self.justify_ma()

                if result == True:

                    if int(self.chart_data['trade_volume'].iloc[1]) > 5000:
                        buy_list['code'].append(code)
                        buy_list['date'].append(self.today)
                        buy_list['close'].append(self.chart_data['close'].iloc[1])
                        buy_list['ma5'].append(self.chart_data['MA5'].iloc[1])
                        buy_list['ma10'].append(self.chart_data['MA10'].iloc[1])
                        buy_list['ma20'].append(self.chart_data['MA20'].iloc[1])
                        buy_list['ma60'].append(self.chart_data['MA60'].iloc[1])
                        buy_list['ma120'].append(self.chart_data['MA120'].iloc[1])
                        buy_list['volume'].append(self.chart_data['volume'].iloc[1])
                        buy_list['tvolume'].append(self.chart_data['trade_volume'].iloc[1])


                elif result == False:
                    pass

            else:
               pass

        self.buy_list = pd.DataFrame(buy_list, columns=['code', 'date', 'close', 'ma5', 'ma10', 'ma20', 'ma60', 'ma120', 'volume', 'tvolume'])
        return self.buy_list

    def justify_ma(self):

        try:
            if len(self.chart_data) == 2:
                today_ma5 = self.chart_data['MA5'].iloc[1]
                today_ma20 = self.chart_data['MA20'].iloc[1]
                yester_ma5 = self.chart_data['MA5'].iloc[0]
                yester_ma20 = self.chart_data['MA20'].iloc[0]

                if float(yester_ma5) < float(yester_ma20) and float(today_ma5) > float(today_ma20):
                    return True
                else:
                    return False

            elif len(self.chart_data) != 2:
                return False

        except TypeError:
            return False

    def load_chart_indate(self, start_date, end_date, code):
        try:
            chart_data = pd.read_sql("SELECT * FROM s" + code + " WHERE date > " + self.all_range, engine_all)
            chart_data = chart_data[(chart_data['date'] >= start_date) & (chart_data['date'] <= end_date)]
            return chart_data
        except sqlalchemy.exc.ProgrammingError:
            pass

    def load_code(self):
        code_data = pd.read_sql("SELECT code FROM item_savepoint", engine_item).drop_duplicates()
        return code_data

    def load_chart(self, all_range, code):
        cd = pd.read_sql("SELECT * FROM s" + code + " WHERE date > " + all_range, engine_all)
        return cd

    def cal_addday(self, standard_day, howmany):
        day_list = []
        day_list.append(standard_day)

        for i in range(1, 15):
            current_day = datetime.date(int(standard_day[0:4]), int(standard_day[4:6]), int(standard_day[6:9]))
            end = str(current_day + datetime.timedelta(days=i))
            tempt_var_date = str(end).split("-")
            var_date = tempt_var_date[0] + tempt_var_date[1] + tempt_var_date[2]
            var_week = datetime.date(int(var_date[0:4]), int(var_date[4:6]), int(var_date[6:8])).weekday()

            if var_week < 5:
                day_list.append(var_date)
            else:
                pass

        return day_list[howmany]

    def cal_subday(self, standard_day, howmany):
        day_list = []
        day_list.append(standard_day)

        for i in range(1, 15):
            current_day = datetime.date(int(standard_day[0:4]), int(standard_day[4:6]), int(standard_day[6:9]))
            end = str(current_day - datetime.timedelta(days=i))
            tempt_var_date = str(end).split("-")
            var_date = tempt_var_date[0] + tempt_var_date[1] + tempt_var_date[2]
            var_week = datetime.date(int(var_date[0:4]), int(var_date[4:6]), int(var_date[6:8])).weekday()

            if var_week < 5:
                day_list.append(var_date)
            else:
                pass

        return day_list[howmany]

if __name__ == "__main__":
    start_date = '20200103'
    end_date = '20210101'
    all_range = '20150101'
    algorithm1(start_date, end_date, all_range)

 

 


728x90
반응형
Contents

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

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