PYTHON/Back test

백테스팅 구축 (4) - 일자별 차트 데이터 불러오기

  • -

지난 게시글에서 while문을 통해 백테스팅을 진행하는 일자가 우리가 입력했던 종료일자(end_date)와 같아지는 순간 진행을 멈추도록 하는 코드를 구축했다. 이번 게시글에서는 while문 아래에서 하루 하루 돌면서 그 날의 종목의 차트 데이터를 불러오고 그를 바탕으로 매수 조건에 충족하는지를 살펴보는 코드를 구축할 예정이다.

 

실제 거래와 백테스팅 간의 괴리

사실 우리가 구현하고자 하는 거래 전략을 검증(백테스팅) 해보는 방법은 크게 두 가지가 있다. 머리로 가볍게 생각해낼 수 있는 전략들을 직접 차트를 보면서 어렵지 않게 분석할 수 있지만 그 수치들을 일일이 직접 계산하고 엑셀 등과 같은 데이터 관리 프로그램에 저장해야 한다는 단점이 있는 방법과 머리로 생각해낸 전략을 오랜 시간 정상적으로 동작하는 코드로 만들어서 손쉽게 결과를 확인하는 방법이다. 어느 방법을 사용하든지 간에 본인의 선택이고, 본인이 편리하다고 생각하는 방법을 사용하면 된다.

이 얘기를 여기서 하는 이유는, 바로 우리가 생각한 전략을 코드로 만드는 것은 상상을 초월할 정도로 많은 시행착오를 거쳐야 하기 때문이다. 당장 오늘 제작할 코드만 봐도 그렇다. 우리가 이전 게시글에서 이야기했던 거래 전략은 5일 이동평균선과 20일 이동평균선 간의 골든크로스 및 데드크로스를 기준으로 매수와 매도를 진행하는 것인데, 이를 코드로 제작하려다 보면 많은 오류가 발생한다. 예를 들어, 아래와 같은 코드를 만들었다고 가정해보자. 

if 5일 이동평균선 > 20일 이동평균선:
	매수
elif 5일 이동평균선 < 20일 이동평균선:
	매도

 

위의 코드는 상상할 수 없을 정도로 많은 오류를 범할 것이다. 왜냐하면, 저 코드가 골든 크로스와 데드 크로스를 의미하는 것이 아니기 때문이다. 즉, 5일 이동평균선이 20일 이동평균선을 상향 돌파했을 경우를 포함하는 코드인 것은 분명하지만, 상향 돌파한 이후에도 5일 이동평균선은 20일 이동평균선보다 높은 값에 위치해 있기 때문에 5일 이동평균선이 20일 이동평균선을 하향 돌파할 때까지 매일 매일 매일 매일 매수에 가담하다가, 이탈했을 때 매도할 것이다. 뿐만 아니라 하향 돌파를 했을 경우에도 동일한 문제점이 발생하게 된다. 

그렇다면 가장 오류 없는 코드는 무엇인가? 바로 전날의 데이터와 비교해보는 것이다. 즉, 어제는 5일 이동평균선이 20일 이동평균선보다 아래에 있었는데 오늘은 5일 이동평균선이 위에 있으면 매수하고, 어제는 5일 이동평균선이 20일 이동평균선보다 위에 있었는데 오늘은 5일 이동평균선이 아래에 있으면 매도하면 되는 것이다.

여기서 우리는 한 가지 아주 좋은 포인트를 체크할 수 있다. 우리는 백테스팅을 할 때, 우리가 조회하는 일자의 데이터와 조회하는 일자의 전 날의 데이터. 딱 두 개만 있으면 매수와 매도 신호의 발생 여부를 판단할 수 있다는 것이다. 이는 어찌 보면 실제 거래를 진행할 때와의 공통점에 해당하는 부분이기도 하다. 그렇다면 우리는 이전 게시글에서 제작했던 cal_addday와 똑같은 형태를 가진 cal_subday 함수를 제작해서, 오늘 일자를 기준으로 하루를 뺀 날의 일자를 구한 후에 그 두 개의 값을 함께 이용해서 데이터를 구해보도록 하자.(cal_addday와의 차이점은 end = str() 코드 부분에, +가 아니라 -라는 점이다. 그 외에는 모든 코드가 동일하다.)

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]

 

 


728x90

 

일자 기준을 충족시키는 차트 데이터 불러오기

앞에서 살펴봤듯이, 백테스팅을 하기 위해서는 전날의 데이터가 필요하다. 그렇기 때문에 class 하단에서 cal_addday()를 통해 계산한 값을 self.today에 입력했던 것처럼, self.today를 cal_subday()에 입력한 후 반환된 값을 self.yesterday에 입력해주도록 하자.

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

        while self.today != end_date:
            print("조회일자:", self.today)
            self.today = self.cal_addday(self.today, 1)
            self.yesterday = self.cal_subday(self.today, 1)

 

이제 우리가 사용하고자 하는 두 개의 일자(self.today와 self.yesterday)를 모두 계산했으니, 이를 바탕으로 우리가 사용하고자 하는 데이터들을 MySQL에서 가져와야 한다. 그렇기 때문에 새로운 함수(check_list 함수)를 제작한 후에 그 함수에서 두 개의 일자 변수를 받아오고, 그 일자의 데이터를 출력하도록 하는 함수를 구축해보자.

check_list에서는 가장 먼저, 우리가 이전에 제작했던 load_code()라는 함수를 통해 현재 데이터베이스에 저장되어 있는 종목 코드를 불러오고, 그를 대상으로 하여 for문을 돌면서 특정 종목의 일별 주가 데이터를 조회하는 과정으로 코드를 구축해야 한다. 이제 for문 아래에서 print(code)를 제작해보면 데이터베이스에 저장되어 있는 종목 코드를 하나 하나씩 출력하게 된다.  

def check_list(self):
	self.code_list = self.load_code()
    
	for code in self.code_list['code']:

 

 

그렇다면 이제 code라는 변수에 종목 코드가 입력되어 있으니, 그 code 변수와 두 개의 일자 변수, 총 세 개의 변수를 가지고 차트 데이터를 조회하면 된다. (load_chart_indate 함수 제작)

맨 첫 줄에서 pd.read_sql을 통해 code라는 변수를 전달해주고, 그 값을 chart_data에 저장하도록 한다. 그 다음에 둘째 줄에서는 chart_data 변수 안에서 ['date']라는 칼럼의 값이 start_date(self.yesterday)보다 크고 end_date(self.today)보다 작은 구간을 chart_data 변수에 다시 입력한 후에, 그 값을 반환하도록 하는 코드이다.

[추신] 이전에 숫자만으로 이루어진 종목코드를 이용해서 테이블을 만들면 안 되니 종목 코드 앞에 알파벳 하나쯤은 입력해야 한다고 설명했던 적이 있다. 아래의 코드에도 볼 수 있다시피 SELECT * FROM s와 같이 "s"가 포함되어 있으니, 본인이 s가 아닌 다른 문구를 사용했다면 그 문구를 그대로 입력해주어야 한다.

def load_chart_indate(self, start_date, end_date, code):
	chart_data = pd.read_sql("SELECT * FROM s" + code, engine_all)
	chart_data = chart_data[(chart_data['date'] >= start_date) & (chart_data['date'] <= end_date)]
	return chart_data

 

 

이제 차트 데이터를 조회하는 코드는 구축했으니, 다시 for문으로 돌아와서 load_chart_indate 함수를 이용해 차트 데이터를 조회하고, 출력하는 코드를 구축한 후 결과 데이터를 확인해보도록 하자.

def check_list(self):
	self.code_list = self.load_code()
    
	for code in self.code_list['code']:
		self.chart_data = self.load_chart_indate(self.yesterday, self.today, code)
		print("################")
		print("self.yesterday와 self.today:", self.yesterday, self.today)
		print(self.chart_data)

 

################
self.yesterday와 self.today: 20200101 20200102
           date   open   high  ...     MA20          MA60         MA120
27852  20200102  15600  15850  ...  17030.0  16795.833333  16865.833333

################
self.yesterday와 self.today: 20200101 20200102
           date    open    high  ...     MA20          MA60         MA120
37136  20200102   20150   20350  ...  49105.0  49282.500000  50170.833333

################
self.yesterday와 self.today: 20200101 20200102
          date  open  high   low  ...         MA15    MA20         MA60        MA120
7844  20200102  2450  2500  2430  ...  2819.333333  2838.5  3391.083333  4573.708333

################
self.yesterday와 self.today: 20200101 20200102
           date  open  high  ...    MA20         MA60        MA120
15808  20200102  2270  2290  ...  2495.0  2582.083333  2782.750000


한 줄만 나오는 것을 보고 오류라는 생각이 들 수도 있겠지만, 사실 20200101은 휴일이라 주가 데이터가 없어서 출력되지 않는 것이다. 20191231과 20200101은 휴일이고 20200815 역시 휴일이고 수능의 경우에는 장이 늦게 열리기 때문에 주가 데이터는 없거나 다르지만, datetime 모듈은 우리나라가 연말과 연초, 광복절에는 쉬고 수능일에는 늦게 열리고 늦게 마감한다는 것을 모른다. 그냥 토요일과 일요일만 제거해주는 것이다. 이 부분에 대해서는 이후에, 우리가 조회한 데이터의 개수가 몇 개인지를 판단함으로써 휴일인지 아닌지를 판단할 수 있도록 코드를 구축할 예정이다.

그리고  조금 더 진행하다보면 아래와 같은 오류가 발생할 수 있는데, 이 오류는 해당 종목 코드에 대한 주가 데이터가 없음을 의미하는 오류이다. 따라서 주가 데이터를 모두 저장한 경우에는 발생하지 않는 오류다.

sqlalchemy.exc.ProgrammingError: (_mysql_exceptions.ProgrammingError) (1146, "Table 'day_data.s001230' doesn't exist")
[SQL: SELECT * FROM s001230]

 

사실 모든 종목에 대해 백테스팅을 실시해보는 것이 당연지사 훨씬 좋겠지만, 차트 데이터를 저장하는 것은 차치하고 일단 백테스팅부터 구축하고 싶은 경우에는 이런 오류가 발생했을 경우 넘어가라는 코드를 구축해줄 수 있다.

 

오류 처리하기

오류를 처리하는 방법은 try:문과 except문을 통해 해결할 수 있다. 일단 위에서 발생한 오류의 경우에는 SQL: SELECT * FROM s001230과 같은 형태로 발생하였는데, 우리는 해당 코드를 load_chart_indate함수에서 사용했다. 그렇기 때문에 해당 함수 아래 부분을 다음과 같이 수정해주면 된다. 그러면 결과값이 다음과 같이 오류가 발생하지 않고 데이터가 없다는 문구가 출력되는 것을 확인할 수 있다.

def load_chart_indate(self, start_date, end_date, code):
	try:
		chart_data = pd.read_sql("SELECT * FROM s" + code, engine_all)
		chart_data = chart_data[(chart_data['date'] >= start_date) & (chart_data['date'] <= end_date)]
		return chart_data
	except sqlalchemy.exc.ProgrammingError:
		print("[데이터 없음] 종목 코드:%s", code)

 

[데이터 없음] 종목 코드:%s 001230
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001250
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001260
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001270
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001290
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001340
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001360
################
self.yesterday와 self.today: 20200101 20200102
None
[데이터 없음] 종목 코드:%s 001380
################
self.yesterday와 self.today: 20200101 20200102
None

 

다음 게시글에서는 이번 글에서 제작했던 데이터를 불러오는 코드들을 바탕으로 하여 그 결과값을 분석하고 거래 대상이 되는 종목을 찾아서 데이터프레임화 시키는 방법에 대해 살펴보도록 하겠다.

 

 


728x90
반응형
Contents

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

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