PYTHON/Back test

백테스팅 구축 (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:  과 같은 방식으로 작성해주어야 한다. 이래야만 데이터가 다른 곳에 입력될 가능성을 배제할 수 있기 때문이다.

 


728x90

 

 

매수 함수 수정하기

일단 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  체크를 풀어주면 된다.

왼쪽에 빨간 점이 있는 곳이 breakpoint 지점이다. 디버그를 하면서 해당 지점에서 실행을 멈추게 된다.
self 버튼을 눌러 df_tracking_data의 맨 오른쪽에 있는 View as DataFrame 버튼을 누르면 아래의 화면이 나온다.
너무 길어서 보이지는 않지만, 칼럼들이 하나하나 다 포함되어 있음을 확인할 수 있다.

 

 

매도 함수 수정하기

그럼 이제   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("세 번째 수익률 발생")

 

 


728x90
반응형
Contents

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

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