AUTO TRADE/Back test

백테스팅 구축 (12) - buy 함수 수정하기

    지난 게시글에서 관련 오류를 모두 제거한 줄 알았는데, 매도 후 drop() 함수를 통해 특정 라인의 데이터를 삭제하는 것이 정상적으로 동작하지 않는 오류가 있었다. 왜 그런가 하고 살펴봤더니 buy() 함수에서 매수한 종목들은 self.account에 입력되었고, self.df_account는 self.account를 기반으로 제작되는데, drop()을 self.df_account를 대상으로 해버리니 암만 지워도 데이터는 정작 self.account에 남아 있기 때문에 매도한 종목이 현재 보유 종목에 계속해서 출력되는 것이다. 따라서 매수할 종목 데이터를 self.account 변수가 아닌 self.df_account 변수 내에 입력을 하게 되면 drop()으로 인해 데이터를 올바르게 삭제할 수 있다. 그러기 위해서는 일단 buy() 함수 내에 있는 코드를 수정해야 한다.

 

오류 발생 원인

  기존에 제작했던 매수 함수는 아래와 같다. 코드만 봐도 알 수 있듯이, 매수한 종목들의 일자, 매수가 등의 값을 self.account 변수를 대상으로 append()를 하고 있음을 확인할 수 있다. 

def buy(self):

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

		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 __init__ 함수에서도 확인할 수 있듯이, self.df_account 변수는 self.account 변수를 기반으로 제작되는 로직을 갖고 있기 때문에 self.df_account 내에 있는 데이터를 삭제했음에도 불구하고 self.df_account(데이터프레임 변수)는 self.account 변수(딕셔너리 형 변수)를 기반으로 제작되기 때문에 삭제한 종목이 다시 출력되었던 것이다. 출력되지 않도록 하기 위해서는 self.account(딕셔너리 형 변수)에 입력되어 있는 데이터를 삭제했어야 했다. 

self.account = {'date':[], 'code':[], 'buy_price':[], 'quantity':[], 'profit':[]}
self.df_account = pd.DataFrame(self.account, columns=['date', 'code', 'buy_price', 'quantity'])

 

 


728x90

 


 

buy() 함수 내 코드 수정

    위에서 설명했듯이, 지금 해결 방법은 크게 두 가지로 나뉜다. 첫째는 self.account에 입력되어 있는 값을 삭제함으로써 데이터프레임 자료형에 나타나지 않도록 하는 방법. 둘째는 매수한 종목의 데이터를 self.account가 아닌 self.df_account에 추가하는 방법이다. 딕셔너리형 자료는 수정이 어렵고 복잡하다 보니 후자의 방법을 선택했다.
    즉, buy_data라는 일시적인 변수를 생성해서 튜플 형태로 네 개의 값을 입력해주고, 그를 하나의 데이터프레임으로 만들어서 그 값을 tempt_df_account라는 변수에 입력해주었다. 그리고 데이터프레임 간 결합을 지원하는 append() 메소드(함수)를 이용해서 기존의 self.df_account 변수와 새롭게 만든 tempt_df_account 변수를 합쳐주었다. 즉, 데이터를 입력하는 변수가 기존에는 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], quantity)]
            tempt_df_account = pd.DataFrame(buy_data, columns = ['date', 'code', 'buy_price', 'quantity'])
            self.df_account = self.df_account.append(tempt_df_account)

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

 

하지만 결과값을 보니 아래와 같이, 모든 데이터들의 인덱스가 0으로 입력되고 있다.

현재 보유 종목
       date    code buy_price  quantity  profit
0  20200103  004170    292500       3.0     NaN
0  20200113  000100     44692      22.0     NaN
0  20200113  002450      1980     505.0     NaN
0  20200114  003580      8400     119.0     NaN
0  20200115  000080     31650      31.0     NaN
0  20200115  001630    113000       8.0     NaN
0  20200115  002320     30426      32.0     NaN
0  20200115  003690      9310     107.0     NaN

 

    우리는 sell() 함수에서 특정 종목 코드가 데이터프레임(DataFrame) 내에서 가지고 있는 인덱스 값을 반환한 후에 그 인덱스 값에 해당하는 행의 데이터를 제거하도록 구축하였는데, 위처럼 출력되면 가장 먼저 매도하게 되는 000100 종목의 인덱스 값이 0이 반환되고, 그 0을 바탕으로 데이터를 제거하기 때문에 모든 데이터가 삭제된다. 따라서 buy() 부분에서 각각의 데이터프레임(DataFrame)을 합친(append) 후에 인덱스를 새롭게 설정해주어야 한다. 데이터프레임의 인덱스를 새롭게 설정해주는 메서드는 reset_index()이다. 따라서 아래의 코드를 수정 후 코드와 같이 수정해주도록 하자.

## buy() 함수 내 수정 전 코드 ##
self.df_account = self.df_account.append(tempt_df_account)

## buy() 함수 내 수정 후 코드 ##
self.df_account = self.df_account.append(tempt_df_account).reset_index()

현재 조회일자: 20200103 (self.today값),  20200102 (self.yesterday값)
현재 보유 종목
   index      date    code buy_price  quantity  profit
0      0  20200103  004170    292500       3.0     NaN

Traceback (most recent call last):
    yester_ma5 = self.chart_data['MA5'].iloc[0]
TypeError: 'NoneType' object is not subscriptable

 

    코드를 실행해보니 오류가 또 발생했다. yester_ma5 변수를 계산하는데 self.chart_data의 자료형이 NoneType이라는 것이다. 즉, 특정 변수를 바탕으로 데이터를 조회하는데 데이터를 받아오지 못했다는 것이다. 이런 경우 우리는 해당 코드를 수정해주지 않았음에도 불구하고 발생한 오류이기 때문에 앞전에 우리가 수정한 부분의 영향으로 인해 발생하는 오류라는 것을 눈치챌 수 있다. 이 오류가 발생하게 된 원인은 바로 현재 보유 종목(self.df_account 변수) 데이터 안에 인덱스가 두 개가 존재하기 때문이다. 즉, 우리가 이전에는 차트 데이터를 조회할 때에는 종목 코드를 인자로 전달해주었는데, 인덱스 데이터가 한 개가 더 존재하게 되면서 row[2]에 위치하고 있던 004170이 row[3]으로 자리가 이동한 것이다. 즉, 우리는 지금 종목의 차트 데이터를 조회할 때 code 부분에 전달해야 하는 인자로 종목 코드가 아니라, date에 위치해 있는 20200103을 종목 코드로 전달했기 때문에 발생한 오류이다.
    따라서 앞서 수정했던 reset_index() 안에 drop의 인자로 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):
            buy_data = [(row[2], row[1], row[3], 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))

현재 조회일자: 20200103 (self.today값),  20200102 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
0  20200103  004170    292500       3.0     NaN

(중략)

현재 조회일자: 20200115 (self.today값),  20200114 (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

 

    이제 마지막으로 코드를 돌려보면서 특정 종목의 매도 조건이 충족되었는지, 그리고 충족된 종목이 매도된 후에 누적 수익금은 유지되는지, 현재 보유 종목 변수 내에서 매도한 종목이 제거되는지를 확인해보자. 

[추신] self.df_account 변수를 보면 quantity 칼럼에 3.0과 같이 자료형이 float 형태로 입력되어 있는데, 이는 buy() 함수 내에서 buy_data = [()] 안에 있는 quantity를 int(quantity)로 수정해주면 현재 보유 종목의 quantity 란에 3이 입력된다.

#################################################
## 매도 후 현재 보유 종목 데이터에서 삭제되는지 ##
#################################################
현재 조회일자: 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
######### SELL #########
매도 진행 종목: 000100
매수 가격: 983224
매도 가격: 972686
종목별 매도 수익금: -47900
현재 잔고: 4740574
일별 손익: -47900
누적 손익: -47900

현재 조회일자: 20200121 (self.today값),  20200120 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
0  20200103  004170    292500       3.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
현재 잔고: 4740574
일별 손익: 0
누적 손익: -47900

############################################################
## 첫째, 1월 28일에 종목코드 004170(신세계)을 매도하는지?       
## 둘째, 매도 후 현재 보유 종목 데이터에서 잘 삭제되는지?
############################################################
현재 조회일자: 20200128 (self.today값),  20200127 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
0  20200103  004170    292500       3.0     NaN
1  20200113  002450      1980     505.0     NaN
2  20200114  003580      8400     119.0     NaN
3  20200115  000080     31650      31.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
8  20200117  001390     12800      78.0     NaN
9  20200122  000520     10625      94.0     NaN
######### SELL #########
매도 진행 종목: 004170
매수 가격: 877500
매도 가격: 808500
종목별 매도 수익금: -69000
######### SELL #########
매도 진행 종목: 003580
매수 가격: 999600
매도 가격: 924630
종목별 매도 수익금: -74970
######### SELL #########
매도 진행 종목: 003690
매수 가격: 996170
매도 가격: 944810
종목별 매도 수익금: -51360
현재 잔고: 2971150
일별 손익: -195330
누적 손익: -205868


현재 조회일자: 20200129 (self.today값),  20200128 (self.yesterday값)
현재 보유 종목
       date    code buy_price  quantity  profit
1  20200113  002450      1980     505.0     NaN
2  20200114  003580      8400     119.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
######### SELL #########
매도 진행 종목: 000720
매수 가격: 967150
매도 가격: 911950
종목별 매도 수익금: -55200
현재 잔고: 3883100
일별 손익: -55200
누적 손익: -261068
더보기
from sqlalchemy import create_engine
import pandas as pd
import datetime
import Modules
import sqlalchemy

engine_all = create_engine('mysql+mysqldb://root:a9985623@127.0.0.1:3306/day_data', echo=False)
connection_all = engine_all.connect()

engine_item = create_engine('mysql+mysqldb://root:a9985623@127.0.0.1:3306/item_savepoint', 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', 'profit'])
        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:
                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(sell_value - 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)  ## 수익을 더하고
                    
                    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)

                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):
                buy_data = [(row[2], row[1], row[3], 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))

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

 

    이전 게시글에서는 매도 함수를 수정함으로써 004170(신세계) 종목이 1월 28일에 매도가 진행되지 않는 부분을 수정했고, 이번 게시글에서는 매도가 진행된 후에도 현재 보유 종목 데이터프레임(DataFrame)에서 데이터가 삭제되지 않는 부분을 수정했다. 여태 게시글들을 보면서 알 수 있듯이, 코드가 돌아가는 과정 상에서는 문제가 없더라도 데이터 처리 과정에서의 문제가 발생할 수도 있다. 이 부분이 오류 발생 여부를 판단하기가 가장 어렵다. 따라서 그 결과값들을 실제 데이터와 대조해보면서 본인이 제작하고 있는 코드가 올바르게 동작하는 코드인지를 확인해보아야 한다. 사실 이쯤 됐으면 프로그래머들이 우스갯소리로 하는 말들이 이해가 갈 것이다.(물론 본인은 문과다.)

  • 오류가 없을 때 : "오류가 왜 없어?"
  • 오류가 있을 때 : "오류가 왜 있어?"

 

 


728x90
반응형
Contents

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

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