백테스팅 구축 (6) - 오류 수정
지난 게시글에서 이동평균선 간 대소 비교를 통해 매수 가능 종목을 튜플 형태로 자료를 정리한 후에 마지막에는 데이터프레임으로 만들어서 print()문을 적용해서 출력했었다. 하지만 언뜻 봐도 매수 대상 종목이 많았고, 매일 매일 매수 종목들이 10개씩 생긴다면 단순한 모의 백테스팅에서 뿐만 아니라 실전 투자에 있어서도 문제가 발생하게 된다. 하지만 이 문제는 나중에 해결하는 거로 하고, 이번 게시글부터는 buy_list 변수에 입력된 모든 종목을 매수한다고 가정하고 각각 얼만큼의 금액만큼 매수할 것인지를 설정하고 그 결과를 살펴볼 예정이다.
코드 오류 수정 (1) - def __init__ 부분
빨간 글씨로 오류가 발생하지는 않았지만 일자 계산에 있어서 오류가 발생했다. 입력한 일자가 20200101인 경우, 일단 하루를 더하고 나서 20200102를 기준으로 백테스팅을 진행하게 되는 문제가 발생했는데, 이는 def __init__ 안에 있는 while문 하단부에 위치한 self.today = self.cal_addday(self.today, 1)의 순서를 잘못 뒀기 때문이다. 따라서 아래와 같이 수정해주면 된다. 이 코드가 아래에 위치해야 하는 이유는 바로 self.today에 입력된 값을 바탕으로 백테스팅을 진행한 후, 매수와 매도가 모두 이루어진 후에야 self.today라는 변수값을 하루만큼 더 늘려주고 나서 그 일자를 기준으로 다시 백테스팅을 진행해야 하기 때문이다.
class algorithm1():
def __init__(self, start_date, end_date, all_range):
print("시작일자:", start_date)
print("종료일자:", end_date)
print("전체범위:", all_range)
self.all_range = all_range
self.today = start_date
while self.today != end_date:
print("현재 조회일자:", self.today, "(self.today값)")
self.yesterday = self.cal_subday(self.today, 1)
self.check_list()
## 아래의 코드, 위치 수정! ##
self.today = self.cal_addday(self.today, 1)
코드 오류 수정 (2) - def check_list 부분
Traceback (most recent call last):
today_ma5 = self.chart_data['MA5'].iloc[1]
raise IndexError("single positional indexer is out-of-bounds")
IndexError: single positional indexer is out-of-bounds
def __init__부분을 수정한 후에 코드를 돌려보면 또 오류가 발생한다. 그 이유는 바로 self.today의 위치를 조금 전에 수정했기 때문이다. 이 문제는 왜 발생하냐면, self.today의 위치를 수정하게 됨에 따라 데이터가 없는 경우가 발생하기 때문이다. 다시 말해, 20200101을 입력했을 경우에는 20200101과 20191231 모두 차트 데이터가 없기 때문에 오류가 발생하고 20200102를 입력했을 경우에는 그 날에는 데이터가 있을지 언정 20200101에는 데이터가 없기 때문에 오류가 발생한다. 즉, 20200103부터는 오류가 발생하지 않게 된다는 것이다.
그러면 그냥 20200103을 입력하면 되는거 아닌가 싶을 수도 있는데, 가만 생각해보면 20200815의 경우에도 데이터가 없다. 이 오류가 발생한 위치는 justify_ma() 함수이니 justify_ma() 함수 내에서 오밀조밀 잘 만져서 오류를 수정해주도록 하자. 해결 과정이 다소 복잡해질 수 있으니 한숨 크게 내쉬고 읽도록 하자.
일단 가장 근본적인 문제점은 특정 일자의 데이터가 없기 떄문에 인덱스 에러가 발생하는 것이다. 흔히 발생하는 인덱스 에러는 주로(거의 99%) [i]와 같이 특정 값에 접근하고자 하지만 i에 해당하는 값이 없는 경우에 주로 발생한다. 따라서 위의 오류 코드를 보면 오류가 발생한 코드는 today_ma5 = self.chart_data['MA5'].iloc[1]라고 하는데, 바로 .iloc[1] 부분에서 인덱스 에러가 발생한다는 것이다. 즉, .iloc[1]로 접근하고자 했는데 데이터가 없다는 것이다. (아니 없으면 알아서 전 날 데이터를 찾아오면 안 되나..? 뭐 어려운 일이라고..;;) 그러면 데이터가 있는지 없는지를 판단할 수 있는 방법은 간단하다. 바로 데이터의 개수를 세어주는 len() 함수를 이용하면 되기 때문이다. 즉, 우리가 인덱싱(인덱스를 통한 자료 접근 방법)을 시도했으나 .iloc[1]에서 오류가 발생하지 않도록 하기 위해서는 자료의 개수가 2개면 된다. 다시 말해 .iloc[0]과 .iloc[1]을 사용하려면, 자료가 두 개가 있어야 하니 self.chart_data의 자료의 개수를 확인해보자는 것이다. justify_ma() 함수를 아래와 같이 수정한 후 실행해보았다.
def justify_ma(self):
print(self.chart_data)
print("자료 개수:", len(self.chart_data))
# 이틀치의 데이터가 있는 경우
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 yester_ma5 < yester_ma20 and today_ma5 > today_ma20:
return True
else:
return False
# 하루의 데이터만 있거나 데이터가 없는 경우
elif len(self.chart_data) == 1 or len(self.chart_data) == 0:
self.yesterday = self.cal_subday(self.yesterday, 1)
pass
조회중인 종목: 004690
index date open ... MA20 MA60 MA120
1230 6557 20200107 82700 ... 84875.0 85006.666667 85553.333333
1231 6558 20200108 82700 ... 84690.0 84951.666667 85459.166667
[2 rows x 15 columns]
자료 개수: 2
조회중인 종목: 004700
index date open ... MA20 MA60 MA120
1230 9287 20200107 37550 ... 37925.0 38265.0 37482.916667
1231 9288 20200108 37400 ... 37902.5 38250.0 37490.416667
[2 rows x 15 columns]
자료 개수: 2
그렇다면 이제 check_list()를 수정해주도록 하자. 일단 기본적으로 len(self.chart_data)의 값이 2가 아닐 수 있는 경우로는 어떤 경우가 있는지 생각해 볼 필요가 있는데, 대략적으로 아래의 두 가지 경우로 나눌 수 있다.
- 개수가 0개인 경우 : self.today와 self.yesterday 모두 잘못된 경우
- 개수가 1개인 경우 : sefl.today 또는 self.yesterday 둘 중 하나가 잘못된 경우
하지만 정확히 어떤 일자의 데이터가 없는지 확인할 수 없기 때문에, self.today와 self.yesterday 중 어떤 데이터가 없는 것인지를 확인하는 함수(is_db() 함수)를 만들어 줄 필요가 있다. 특정 일자를 받아와서 해당 일자의 데이터를 조회하고, 그 결과값의 자료 개수가 1개라면 정상적인 자료이기 때문에 True를 반환하나 그 외의 경우에는 False를 반환하는 것이다.
def is_db(self, code, date):
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
이제 이 is_db()함수를 check_list() 함수 내에서 활용함으로써 데이터의 존재 유무를 확인하고 데이터가 있는 경우에만 백테스팅을 진행하도록 하는 코드를 구축할 수 있다. 아래의 코드를 보면 for문 안에서 self.is_db() 함수에 조회하고 있는 code와 self.today, 그리고 code와 self.yesterday라는 두 개의 인자를 전달하고 그 결과값을 출력하도록 하고 있으며, 그 바로 아래의 조건문(if 문) 안에서는 두 개의 반환값이 모두 True인 경우에만 이전에 제작했던 코드를 실행하면서 이동평균선의 대소비교를 진행하도록 구축했다. 따라서 else:문의 경우에는 self.today의 값이 False이거나 self.yesterday의 값이 False인 경우, 또는 둘 다 False인 경우에는 코드를 실행하지 말라는 의미가 된다. 마지막으로 for문이 끝난 후에는 결과값을 출력한 후에 반환하도록 하였다.
def check_list(self):
self.code_list = self.load_code()
buy_list = {'code':[], 'date':[], 'close':[], 'ma5':[], 'ma10':[], 'ma20':[], 'ma60':[], 'ma120':[]}
for code in self.code_list['code']:
print("조회중인 종목:", code)
self.chart_data = self.load_chart_indate(self.yesterday, self.today, code)
print("self.is_db(code, self.today)", self.is_db(code, self.today))
print("self.is_db(code, self.yesterday)", self.is_db(code, self.yesterday))
# 데이터가 모두 있는 경우
if self.is_db(code, self.today) == True and self.is_db(code, self.yesterday) == True:
result = self.justify_ma()
if result == True:
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])
elif result == False:
pass
# 한 쪽의 데이터가 없거나 모두 없는 경우
else:
pass
self.buy_list = pd.DataFrame(buy_list, columns=['code', 'date', 'close', 'ma5', 'ma10', 'ma20', 'ma60', 'ma120'])
print("self.buy_list")
print(self.buy_list)
print("################")
return self.buy_list
▶ 출력결과
########################################################
## 시작일자가 2020.01.02인 경우의 결과값
########################################################
현재 조회일자: 20200102 (self.today값), 20200101 (self.yesterday값)
조회중인 종목: 000020
self.is_db(code, self.today) True
self.is_db(code, self.yesterday) False
조회중인 종목: 000040
self.is_db(code, self.today) True
self.is_db(code, self.yesterday) False
조회중인 종목: 000050
self.is_db(code, self.today) True
self.is_db(code, self.yesterday) False
########################################################
## 시작일자가 2020.01.04인 경우의 결과값
########################################################
현재 조회일자: 20200104 (self.today값), 20200103 (self.yesterday값)
조회중인 종목: 000020
self.is_db(code, self.today) False
self.is_db(code, self.yesterday) True
조회중인 종목: 000040
self.is_db(code, self.today) False
self.is_db(code, self.yesterday) True
조회중인 종목: 000050
self.is_db(code, self.today) False
self.is_db(code, self.yesterday) True
########################################################
## 시작일자가 2020.01.10인 경우의 결과값
########################################################
현재 조회일자: 20200110 (self.today값), 20200109 (self.yesterday값)
조회중인 종목: 000020
self.is_db(code, self.today) True and: 20200110
self.is_db(code, self.yesterday) True and: 20200109
date open high low ... MA15 MA20 MA60 MA120
1232 20200109 8020 8060 7900 ... 8144.666667 8057.0 8052.166667 8307.916667
1233 20200110 7970 8140 7880 ... 8140.000000 8082.0 8057.166667 8294.083333
[2 rows x 14 columns]
자료 개수: 2
조회중인 종목: 000040
self.is_db(code, self.today) True and: 20200110
self.is_db(code, self.yesterday) True and: 20200109
date open high ... MA20 MA60 MA120
1232 20200109 1068 1100 ... 1126.45 1600.250000 1826.775000
1233 20200110 1104 1153 ... 1128.30 1585.033333 1819.333333
[2 rows x 14 columns]
자료 개수: 2
조회중인 종목: 000050
self.is_db(code, self.today) True and: 20200110
self.is_db(code, self.yesterday) True and: 20200109
date open high low ... MA15 MA20 MA60 MA120
1232 20200109 9350 9410 9340 ... 9558.666667 9613.5 9844.166667 9863.666667
1233 20200110 9320 9400 9320 ... 9517.333333 9592.0 9833.666667 9856.333333
[2 rows x 14 columns]
자료 개수: 2
위의 출력 결과에서 확인할 수 있듯이, 시작일자가 20200102인 경우에는 20200101의 데이터와 대소 비교를 통해 결과값을 얻어와야 하지만 20200101의 데이터가 없기 때문에(is_db의 반환값이 False 이므로) 백테스팅을 진행하지 않는다. 반대로 시작일자가 20200104인 경우에는 20200103의 데이터와 대소 비교를 진행하게 되는데, 20200104의 데이터가 없기 때문에(마찬가지로 is_db의 반환값이 False이므로) 백테스팅을 진행하지 않게 된다. 다만 시작일자가 20200110인 경우에는 20200109의 데이터와 대소 비교를 진행함으로써 백테스팅을 진행하는 것을 확인할 수 있으며 자료의 개수 역시 2개로 잘 출력되는 모습을 확인할 수 있다.
[추신] 시작일자가 20200106인 경우에는 self.yesterday가 20200105로 잡히게 되니 백테스팅을 진행할 수 없는 거 아니냐는 의문이 생길 수도 있는데 그렇지 않다. 우리의 cal_subday()함수나 cal_addday()함수는 이미 주말을 거르고 나서 계산해주는 기능을 넣어주었기 때문에 cal_subday() 함수의 일자 변수로 20200106을 입력하면 알아서 주말을 거르고 난 후의 일자 값인 20200103을 반환해주며, 이를 self.yesterday로 보아 백테스팅을 진행하게 된다. 아래는 결과값이다.
###############################
## 시작일자가 20200106인 경우
###############################
현재 조회일자: 20200106 (self.today값), 20200103 (self.yesterday값)
조회중인 종목: 000020
self.is_db(code, self.today) True and: 20200106
self.is_db(code, self.yesterday) True and: 20200103
date open high low ... MA15 MA20 MA60 MA120
1228 20200103 8400 8450 8290 ... 8091.333333 8006.5 8039.666667 8358.25
1229 20200106 8290 8330 8120 ... 8123.333333 8015.0 8045.500000 8348.25
[2 rows x 14 columns]
자료 개수: 2
조회중인 종목: 000040
self.is_db(code, self.today) True and: 20200106
self.is_db(code, self.yesterday) True and: 20200103
date open high low ... MA15 MA20 MA60 MA120
1228 20200103 1145 1161 1129 ... 1140.066667 1137.9 1664.250000 1858.141667
1229 20200106 1141 1145 1100 ... 1139.800000 1133.8 1648.616667 1850.458333
[2 rows x 14 columns]
자료 개수: 2
조회중인 종목: 000050
self.is_db(code, self.today) True and: 20200106
self.is_db(code, self.yesterday) True and: 20200103
date open high low ... MA15 MA20 MA60 MA120
1228 20200103 9580 9690 9440 ... 9650.666667 9675.5 9883.333333 9895.666667
1229 20200106 9580 9580 9450 ... 9634.000000 9665.0 9876.000000 9889.333333
[2 rows x 14 columns]
자료 개수: 2
이제 길고 긴 오류 수정이 끝났으니 다음 게시글부터는 다시 매수 기준을 설정하고 계좌 현황 등을 제작하는 등의 코드를 구축함으로써 백테스팅 결과를 확인하는 방법에 대해 살펴보도록 하겠다.
'AUTO TRADE > Back test' 카테고리의 다른 글
백테스팅 구축 (8) - 매도 함수 구축 (0) | 2021.07.06 |
---|---|
백테스팅 구축 (7) - 매수 조건 설정 및 계좌 현황 제작 (0) | 2021.07.05 |
백테스팅 구축 (5) - 매수 조건 제작 (0) | 2021.07.04 |
백테스팅 구축 (4) - 일자별 차트 데이터 불러오기 (0) | 2021.07.04 |
백테스팅 구축 (3) - 일자 계산하기 (0) | 2021.07.04 |
소중한 공감 감사합니다