백테스팅 구축 (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'])
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)에서 데이터가 삭제되지 않는 부분을 수정했다. 여태 게시글들을 보면서 알 수 있듯이, 코드가 돌아가는 과정 상에서는 문제가 없더라도 데이터 처리 과정에서의 문제가 발생할 수도 있다. 이 부분이 오류 발생 여부를 판단하기가 가장 어렵다. 따라서 그 결과값들을 실제 데이터와 대조해보면서 본인이 제작하고 있는 코드가 올바르게 동작하는 코드인지를 확인해보아야 한다. 사실 이쯤 됐으면 프로그래머들이 우스갯소리로 하는 말들이 이해가 갈 것이다.(물론 본인은 문과다.)
- 오류가 없을 때 : "오류가 왜 없어?"
- 오류가 있을 때 : "오류가 왜 있어?"
'AUTO TRADE > Back test' 카테고리의 다른 글
백테스팅 구축 (14) - 결과값 엑셀에 입력하기 (0) | 2021.07.08 |
---|---|
백테스팅 구축 (13) - 전략 수정하기 (0) | 2021.07.07 |
백테스팅 구축 (11) - check_list 함수 수정하기 (0) | 2021.07.06 |
백테스팅 구축 (10) - 차트와 대조하기 (0) | 2021.07.06 |
백테스팅 구축 (9) - 실시간 잔고 업데이트 (0) | 2021.07.06 |
소중한 공감 감사합니다