백테스팅 구축 (18) - 매도 전략 수정하기 ②
지난 게시글에서 sell() 함수 내에 있는 코드 중 수정해야 할 코드는 어떤 부분들인지, 그리고 수정하는 과정에서 발생할 수 있는 문제는 무엇이 있는지에 대해 살펴봤다. 따라서 지난 게시글에서 살펴봤던 내용들을 바탕으로 이번 게시글에서는 코드를 제작하고, 그 결과값들을 확인해보도록 하겠다.
fillna() 입력하기
데이터프레임(DataFrame) 내에서는 데이터가 입력되지 않은 부분을 NaN으로 표시된다. 즉, 아직 매도했던 이력이 없다면 매도 일자( f_sell_date, s_sell_date, t_sell_date, n_sell_date ) 자리에는 데이터가 입력되어 있지 않으므로 NaN 값이 입력되어 있다. 하지만 이는 하나의 데이터가 아니기 때문에 데이터가 저장되어 있는지 아닌지의 여부를 판단하는, 즉 if문을 통해 NaN 값을 잡아낼 수가 없다. 따라서 이 NaN 값을 다른 값으로 대체해주는 fillna() 메서드를 통해 NaN 자리에 특정한 값을 입력해주는 것이다. fillna() 를 적용할 변수는 self.df_tracking_data 변수이다. 따라서 이 코드를 sell() 함수 내에서 수정해주도록 하자.
def sell(self):
for row in self.df_account.itertuples():
self.chart_data = self.load_chart_indate(self.yesterday, self.today, row[2])
df_account_index = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
df_tracking_index = self.df_tracking_data.index[(self.df_tracking_data['code'] == row[2])].to_list()[0]
self.df_tracking_data = self.df_tracking_data.fillna(0)
그 후에, 특정 셀의 값이 우리가 입력한 값과 동일하다면 아직 매도 데이터가 입력되지 않은 것이기 때문에 그 자리에 데이터를 입력하면 된다. 두 번째 자리의 경우에는 첫 번째 자리에는 우리가 입력한 값과 같지 않고, 두 번째 자리에는 우리가 입력한 값과 같아야 한다. 위의 코드를 보면 self.df_tracking_data 변수의 맨 뒤에 fillna(0) 가 입력되어 있는데, 이는 NaN 자리에 0이라는 데이터를 입력한다는 것을 의미한다. 이 내용은 아래의 예시를 참고해보도록 하자.
종목코드 | 매수일자 | 매도1 | 매도2 | 매도3 | 손절 | 수익 |
예시 ① | 20200101 | 0 | 0 | 0 | 0 | XXX,XXX |
예시 ② | 20200101 | 20200103 | 20200104 | 0 | 0 | XXX,XXX |
위처럼, 예시 ①의 경우에는 매수만 했도 매도는 한 번도 이루어지지 않은 상태이다. 따라서, 조건문을 제작할 때에는 매도1 == 0: 과 같은 방식으로 작성해주면 된다. 다만 예시 ②의 경우에는 벌써 첫 번째 매도와 두 번째 매도가 이미 이루어진 상황이기 때문에, 매도1 != 0 and 매도2 != 0 and 매도3 == 0: 과 같은 방식으로 작성해주어야 한다. 이래야만 데이터가 다른 곳에 입력될 가능성을 배제할 수 있기 때문이다.
매수 함수 수정하기
일단 self.df_tracking_data 변수가 가지는 칼럼 값들이 변경되었으니, 매수 함수 내에서도 입력하는 값들을 변경해주어야 하지 않냐는 의문이 들 수 있다. 왜냐하면 매수 함수 내에서의 코드는 아래와 같기 때문이다.
즉, self.df_tracking_data 내에 'code', 'buy_date', 'buy_price', 'buy_value' 네 개의 변수 외에도 'ma5_buy', 'ma10_buy', 'ma20_buy', 'ma60_buy', 'ma120_buy'를 입력하고 있기 때문이다. 하지만 굳이 이 코드를 수정할 필요는 없다. 맨 밑에 있는 코드에서 사용된 append() 는 말 그대로 데이터프레임을 합치는 기능을 수행하기 때문에, 우리가 기존에 지정해주지 않았던 칼럼명이 있다고 하더라도 오류가 발생하지 않고 새롭게 입력해준다. self.df_tracking_data 변수를 출력해보면, 맨 뒤에는 ma5_buy, ma10_buy, ma20_buy 등의 변수들이 붙어서 출력되는 모습을 확인할 수 있다.
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], int(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))
buy_value = int(row[3]) * int(quantity)
item_data = [(row[1], row[2], row[3], buy_value, row[4], row[5], row[6], row[7], row[8])]
tempt_item_data = pd.DataFrame(item_data, columns = ['code', 'buy_date', 'buy_price', 'buy_value',
'ma5_buy', 'ma10_buy', 'ma20_buy', 'ma60_buy', 'ma120_buy'])
self.df_tracking_data = self.df_tracking_data.append(tempt_item_data).reset_index(drop=True)
## 출력 결과 ##
code buy_date buy_price ... ma20_buy ma60_buy ma120_buy
0 004170 20200103 292500 ... 286325.0 264433.333333 257000.0
하지만 상관은 없다. 저 ... 세 개 안에 우리가 사전에 입력했던 칼럼들이 모두 들어가 있기 때문이다. 만약 정확하게 입력됐는지 궁금하다면 보고 싶은 지점에 breakpoint 를 걸어놓고 디버그(debug)를 해보면 변수 안에 어떤 값이 포함되어 있는지 확인할 수 있다. 확인을 했다면 breakpoint 체크를 풀어주면 된다.
매도 함수 수정하기
그럼 이제 self.df_tracking_data 변수를 대상으로 조건문을 만들어보자. 일단 이전 게시글에서 제작했듯이, f_sell_date 나 s_sell_date , 또는 t_sell_date 등의 변수는 self.df_tracking_data 변수 안에 입력되어 있다. 여기서 우리는 예전에 조회 중인 종목 코드가 self.df_tracking_data 변수 내에서 갖고 있는 인덱스 번호를 얻어오는 방법에 대해 작성했었다. 아래의 코드가 바로 그 인덱스 번호이고, 우리는 이 인덱스 번호를 통해 특정 인덱스의 자료열을 수정하거나 삭제하거나 데이터를 입력할 수 있었다.
df_account_index = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
df_tracking_index = self.df_tracking_data.index[(self.df_tracking_data['code'] == row[2])].to_list()[0]
또한 지난 게시글에서도 살펴봤듯이, 변수 이름이 dataframe 이고 우리가 접근하고자 하는 인덱스 번호가 0번이라면 dataframe.iloc[0] 을 통해 인덱스가 0번인 위치에 입력되어 있는 데이터들에 접근할 수 있었고, 그 데이터들을 하나의 리스트 형태의 자료형으로 보아 또 다시 인덱싱(인덱스 번호를 통한 접근 방법)이 가능했다. 만약 dataframe.iloc[0]에 있는 데이터가 ['20200101', '000020', '10300']의 세 개라면 dataframe.iloc[0][0] 은 20200101을, dataframe.iloc[0][1] 은 000020을, dataframe.iloc[0][2] 는 10300을 반환하게 된다.
따라서 우리는 이 방법을 통해서 self.df_tracking_data 변수에 입력되어 있는 여러 값들에 접근할 수 있다. 그렇다면 f_sell_data 칼럼은 과연 몇 번째 자리에 위치해 있을까? 우리가 맨 처음에 설정했던 변수를 바탕으로 생각해보자. 아래의 코드를 보면 'code', 'buy_date' 등등 여러 가지 값들이 포함되어 있는데, 각각의 값이 [0]부터 순서대로 자리를 차지하게 된다. 하지만 실제로 self.df_tracking_data 변수 안에는 index가 생력되어 있기 때문에, 사실상 'code'는 [1]부터 자리를 차지하게 되고, 그에 따라 f_sell_data 는 [6]에, s_sell_date는 [11]에, t_sell_date는 [16]에, n_sell_date는 [21]에 위치해 있다.
self.df_tracking_data = pd.DataFrame(self.tracking_data, columns=['code', 'buy_date', 'buy_price', 'buy_quantity', 'buy_value',
'f_sell_date', 'f_sell_price', 'f_sell_quantity', 'f_sell_value', 'f_profit',
's_sell_date', 's_sell_price', 's_sell_quantity', 's_sell_value', 's_profit',
't_sell_date', 't_sell_price', 't_sell_quantity', 't_sell_value', 't_profit',
'n_sell_date', 'n_sell_price', 'n_sell_quantity', 'n_sell_value', 'n_profit',])
그렇다면 이제 매도 함수 내에서는 f_sell_data 의 값에 0이 입력되어 있고(fillna() 메서드로 인해) 그와 동시에 조회 일자를 기준으로 보유하고 있는 종목의 수익률이 5% 이상이면 매도하도록 코드를 구축하면 된다. 이는 지난 게시글에서 수정했던 sell() 코드를 기반으로 해서 추가적으로 수정하도록 하겠다.(지난 게시글에서는 if문을 통해 수익률 조건을 설정했었다.)
아래의 코드를 보면 맨 처음에는 self.chart_data['high'].iloc[1] 값을 얻어오고 있다. sell() 함수 내에서의 self.chart_data 변수는 self.yesterday와 self.today 값을 인자로 전달해서 차트 데이터를 얻어온 것이기 때문에 .iloc[1]을 사용하게 되면 self.today의 차트 데이터를 얻어오는 것과 동일하다. 즉, self.chart_data['high'].iloc[1]은 조회 일자( self.today )의 고가 데이터를 의미한다.
이전에는 close를 가지고 왔었는데 여기서는 high를 사용하는 이유는 바로 우리의 매도 전략을 변경했기 때문이다. 그 다음으로 row[3]는 현재 for문을 for row in self.df_account를 대상으로 돌리고 있기 때문에, 보유 종목의 데이터 중 하나이며 self.df_account 변수는 index, date, code, buy_price, quantity, profit으로 총 6개의 변수가 입력되어 있다. 따라서 row[3]은 buy_price 변수에 해당한다. 즉, row[3] * self.first_sellprofit은 곧 매수 가격에 첫 번째 수익률을 곱한 금액을 의미한다. 다시 말해, 현재 조회하고 있는 시점의 고가가 매수 가격에 수익률을 곱한 금액보다 크다면 아래의 코드를 실행하라는 의미이다.
그 후에는 if문을 통해 앞서 살펴봤던 self.df_tracking_data.iloc[df_tracking_index][6] 에 입력된 값이 0일 경우에 아래의 코드를 실행하도록 하고 있는데, 두 개의 if 문을 통해 우리의 목표 수익률 중 첫 번째 목표 수익률을 달성했으며 첫 번째로 매도했던 이력이 없는 경우를 찾아냈다. 이제 이 아래 부분에서 종목들을 매도하는 코드를 구축하면 된다.
## 첫 번쨰 수익률 조건
## 조회 시점의 종가가 매수 가격 * 수익률보다 클 경우
if float(self.chart_data['high'].iloc[1]) >= (int(row[3]) * int(self.first_sellprofit)):
if self.df_tracking_data.iloc[df_tracking_index][6] == 0:
print("첫 번째 수익률 발생")
그렇다면 첫 번째 수익률 조건이 아닌 두 번째와 세 번째 수익률 조건에서는 조건문을 어떻게 설정해야 할까? 같은 방식으로 작성하되, 몇 가지 조건들이 추가로 입력돼야 한다. 즉, 두 번째 수익률 조건에서는 위에서 설명했듯이 첫 번째 매도 일자의 값에 0이 입력되어 있으면 아직 첫 번째 매도 조건이 충족되지 않은 것이기 때문에 반드시 0이 아닌 값이 입력되어 있는지 확인해야 하며, 세 번째 수익률 조건에서는 첫 번째 매도 조건과 두 번째 매도 조건이 모두 충족되었어야 하기 때문에 둘 다 0이 아닌 값이 입력되어 있어야 하고, 세 번째 매도 조건은 처음으로 충족된 것이기 떄문에 0이 입력되어 있어야 한다.
## 첫 번쨰 수익률 조건
## 조회 시점의 종가가 매수 가격 * 수익률보다 클 경우
if float(self.chart_data['high'].iloc[1]) >= (int(row[3]) * int(self.first_sellprofit)):
if self.df_tracking_data.iloc[df_tracking_index][6] == 0:
print("첫 번째 수익률 발생")
## 두 번째 수익률 조건
if self.chart_data['high'].iloc[1] >= float(row[3] * self.sec_sellprofit):
if self.df_tracking_data.iloc[df_tracking_index][6] != 0 and self.df_tracking_data.iloc[df_tracking_index][11] == 0:
print("두 번째 수익률 발생")
## 세 번째 수익률 조건
if self.chart_data['high'].iloc[1] >= (row[3] * self.third_sellprofit):
if self.df_tracking_data.iloc[df_tracking_index][6] != 0 and self.df_tracking_data.iloc[df_tracking_index][11] != 0 and self.df_tracking_data.iloc[df_tracking_index][16] == 0:
print("세 번째 수익률 발생")
'PYTHON > Back test' 카테고리의 다른 글
백테스팅 구축 (20) - 매도 전략 수정하기 ④ (0) | 2021.07.10 |
---|---|
백테스팅 구축 (19) - 매도 전략 수정하기 ③ (0) | 2021.07.10 |
백테스팅 구축 (17) - 매도 전략 수정하기 ① (0) | 2021.07.09 |
백테스팅 구축 (16) - 매도 전략 수정 로드맵 수립 (0) | 2021.07.09 |
백테스팅 구축 (15) - 엑셀로 분석할 데이터 제작하기 (0) | 2021.07.09 |
소중한 공감 감사합니다