백테스팅 구축 (9) - 실시간 잔고 업데이트
매도 대상 종목, 보유 종목에서 제거하기
데이터프레임 내에서 특정 행을 삭제하기 위해서는 인덱스를 입력해서 해당 인덱스를 제거해야 한다. 데이터프레임 자료형(변수 이름은 df라고 가정)에서 특정 데이터가 입력되어 있는 위치의 인덱스 값을 얻어오는 방식은 아래와 같다.
- df.index(df['column_name'] == data)
우리가 제작한 코드 내에서 df에 해당하는 변수는 바로 self.df_account이다. 현재 보유 종목을 데이터프레임 형태로 만들어서 출력했고 sell() 함수 내에서도 그를 바탕으로 for문을 돌리고 있으니, 매도 후에는 조회하고 있는 종목 코드가 위치한 행의 인덱스 값을 반환받아서 self.df_account 변수 내에서 제거해야 하는 것이다. 아래는 특정 자료가 위치해 있는 행의 인덱스 값을 불러오는 사용 예시이다.(아래의 변수 a를 코드 내에서는 변수 이름을 index로 했다.)
a = self.df_account.index[(self.df_account['code'] == row[2])]
print(a)
## 출력 결과 ##
Int64Index([0], dtype='int64')
#######################################################################
a = int(self.df_account.index[(self.df_account['code'] == row[2])]).to_list()
print(a)
## 출력 결과 ##
[0]
#######################################################################
a = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
print(a)
## 출력 결과 ##
0
이제 for문 내에서 돌고 있는 종목 코드가 위치한 행의 인덱스 값을 얻었으니, 이제 이 인덱스 값을 이용해서 데이터프레임 내에서의 자료를 제거해야 한다. 자료 제거는 drop() 함수를 통해 이용할 수 있으며 사용 예시는 아래와 같다.
- df.drop(df.index['value'], inplace=True)
여기서도 마찬가지로 df에 해당하는 값은 self.df_account이기 때문에, 그에 따라 코드를 제작해주도록 하자. 이전 게시글에서도 살펴봤지만 처음으로 매도 신호가 발생하는 일자가 2020년 1월 20일이었기 때문에 시간이 좀 걸릴 수도 있다. 하지만 이는 백테스팅 시에만 발생하는 문제이다. 왜냐하면 실제로는 키움증권의 조건검색식을 이용해서 종목을 찾아낼 수 있기 때문에 시간이 오래 걸린다는 가장 큰 문제점은 곧바로 해결되기 때문이다. 조건검색식이 찾아준 종목을 대상으로 알아서 차트 데이터를 불러오고 저장한 후에 백테스팅을 진행함으로써 거래를 진행할 수 있다. 사실 그럴 만도 한 게, 종목이 총 2천 종목 가까이 되며 모든 차트 데이터를 저장했을 경우에는 약 40GB에 달하는 데이터베이스가 구축된다는 점을 생각하면 충분히 이해가 된다. 40GB에 달하는 데이터를 몇초만에 좌라라락 읽어낸다는 것은 컴퓨터가 아니고선 못할 짓이고 내가 직접 검증을 한다고 해도 컴퓨터보다 못하니 그냥 기다려줄 만하다.
아래의 코드에서는 if문 아래의 코드를 수정했고, 하단에 위치한 네 줄의 코드를 통해서 drop() 함수를 이용해 데이터를 제거하기 전의 self.df_account와 제거한 후의 self.df_account를 출력하도록 했으며 if문 바로 아래에서는 매도 조건이 충족된 종목의 코드를 출력해도록 했다. 이 데이터들을 바탕으로 자료가 어떻게 바뀌는지 확인해보도록 하자.
def sell(self):
for row in self.df_account.itertuples():
self.chart_data = self.load_chart_indate(self.yesterday, self.today, row[2])
print(self.chart_data)
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):
print("매도 진행 종목:", row[2])
index = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
print(self.df_account)
self.df_account.drop(self.df_account.index[index], inplace=True)
print("#################")
print(self.df_account)
else:
pass
매도 진행 종목: 000100
date code buy_price quantity
0 20200103 004170 292500 3
1 20200113 000100 44692 22
2 20200113 002450 1980 505
3 20200114 003580 8400 119
4 20200115 000080 31650 31
5 20200115 001630 113000 8
6 20200115 002320 30426 32
7 20200115 003690 9310 107
8 20200117 000720 42050 23
9 20200117 001390 12800 78
#################
date code buy_price quantity
0 20200103 004170 292500 3
2 20200113 002450 1980 505
3 20200114 003580 8400 119
4 20200115 000080 31650 31
5 20200115 001630 113000 8
6 20200115 002320 30426 32
7 20200115 003690 9310 107
8 20200117 000720 42050 23
9 20200117 001390 12800 78
매도 진행 종목: 000100이라는 결과값이 출력되고, 가운데에 있는 ########를 기준으로 위의 데이터는 종목 코드가 000100인 행을 삭제하기 전의 데이터이고, 아래의 데이터는 삭제한 후의 데이터이다. 인덱스 번호를 보면 위의 데이터는 0부터 9까지 잘 있는데 반해 아래의 데이터는 1만 빠지고 나머지 인덱스가 출력되는 모습이다. 그러면 이제 매도 조건이 충족된 종목을 보유 종목에서 삭제했으니, 해당 종목을 매도할 때 얼만큼의 수익이 발생했는지 확인해보도록 하자.
[추신] buy 함수의 경우에는 이동평균선 간 데이터를 비교하는 코드가 없기 때문에 인덱스 에러가 발생하지 않고, 매수 대상 종목 데이터가 담겨 있는 buy_list 변수를 제작할 때 사용하는 justify_ma() 함수에서는 데이터를 비교하다보니 인덱스 에러가 발생했었다. 우리는 그를 해결하기 위해 try: 문과 except IndexError를 통해 오류를 해결해주었다. 다만 sell의 경우에도 역시 데이터 간 비교를 통해 매도 조건이 충족되는지를 확인하므로, justify_ma() 함수에서 사용했던 방법과 동일하게 try:와 except를 사용해주어야 한다.
def sell(self):
for row in self.df_account.itertuples():
self.chart_data = self.load_chart_indate(self.yesterday, self.today, row[2])
print(self.chart_data)
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):
print("매도 진행 종목:", row[2])
index = self.df_account.index[(self.df_account['code'] == row[2])].to_list()[0]
print(self.df_account)
self.df_account.drop(self.df_account.index[index], inplace=True)
print("#################")
print(self.df_account)
except IndexError:
pass
수익 현황 코드 구축
수익 현황은 보유 계좌와는 다른 변수를 통해 일자별로 실현 손익을 기록할 예정이다. 또한 백테스팅을 진행할 때 "나는 복리로 하고 싶은데.."라는 분이 계신다면 종목을 매도한 금액을 self.init_money(초기 시작 금액) 변수에 더해주면 된다. 어차피 종목 당 최대 매수 금액으로 설정해 둔 self.money_by_unit 변수는 고정되어 있고, 현재의 buy() 함수는 매수 가능 잔액이 부족하면 매수를 멈추기 때문에 self.init_money 변수에만 매도 금액을 더해주면 된다. 수수료 역시 마찬가지로, 매도 금액에서 증권사 별 수수료를 계산해주면 결과값을 얻을 수 있다. (추가적으로 현재 보유 종목 안에서 평가 손익을 계산하고 싶은 분들이 있을 수도 있으니 이에 대해서는 다음 게시글에서 다뤄보도록 하겠다. 필요한 사람만 보고 해당 코드를 보면서 추가하면 된다.)
일단 일자별 실현 손익의 경우에는 하루하루 백테스팅을 하면서 그날 매도했던 종목이 있는 경우 그 때 발생하는 수익 금액을 특정 변수에 입력시킨 후, 일자가 변경될 때 그 변수를 다른 곳에 저장한 다음 초기화시켜서 다시 누적시키는 방법이 있다. 이외에도 백테스팅 전반에 걸친 누적 수익을 확인할 수도 있다. 일별 실현 손익의 경우에는 self.today_profit, 백테스팅 전반의 누적 손익의 경우에는 self.all_profit 변수에 값을 입력하도록 하겠다. 아래의 코드를 보면 일별 실현 손익과 누적 손익은 서로 다른 위치에 놓여 있다. 왜냐하면 일별 실현 손익은 self.today 값을 하나 하나씩 늘릴 때마다 초기화해야 하기 때문이며, self.all_profit은 일자가 변하더라도 초기화하지 않아도 되기 때문이다.
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.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("현재 보유 종목")
self.df_account = pd.DataFrame(self.account, columns=['date', 'code', 'buy_price', 'quantity'])
print(self.df_account)
print("현재 잔고:", self.init_money)
self.sell()
self.today = self.cal_addday(self.today, 1)
이제 위의 매도 함수에서는 sell() 함수의 최하단에 drop() 함수를 통해 매도한 종목의 데이터를 self.df_account에서 삭제했다. 그렇다면 이제 매도한 종목의 세부적인 사항들을 계산한 후에 우리의 실현손익이나 self.init_money에 더해주어야 계속해서 거래를 할 것이니, 그 부분을 추가로 제작해주자.
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):
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)
today_close = self.chart_data['close'].iloc[1] ## 매도 가격(sell_price)
quantity = row[4] ## 매수 수량
dif = int(today_close) - int(row[3]) ## 매도 가격 빼기 매수 가격, 주당 손익
print("매도 진행 종목:", row[2])
print("매수 가격:", int(row[3]) * int(row[4]))
print("매도 가격:", int(today_close) * int(row[4]))
print("종목별 매도 수익:", int(dif) * int(quantity))
self.init_money = self.init_money + (int(today_close) * int(quantity)) ## 전체 매도 금액을 더하고
self.today_profit = self.today_profit + (int(dif) * int(quantity)) ## 수익을 더하고
else:
pass
except IndexError:
pass
마지막으로 def __init__ 부분에서 코드의 위치를 조금만 수정해주도록 하자. 아래의 코드를 보면 self.buy() 밑에서는 현재 보유 종목만 출력하는 것으로 하고, self.sell()을 실행한 후에 현재 매수 가능 잔고와 당일의 수익을 출력하도록 수정했다. 여기서 self.init_money는 self.sell() 밑에 있어도 되고 위에 있어도 된다. 위치에 따른 차이가 있을 뿐이다. 이제 코드를 실행해보자.
- self.sell() 위에 있는 경우 : 매도 시의 매도 금액을 self.init_money에 추가하기 전의 매수 가능 금액
- self.sell() 아래 있는 경우 : 매도 시의 매도 금액을 self.init_money에 추가한 후의 매수 가능 금액
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.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("현재 보유 종목")
self.df_account = pd.DataFrame(self.account, columns=['date', 'code', 'buy_price', 'quantity'])
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)
## 출력 결과 ##
## 앞 부분 생략 ##
현재 조회일자: 20200117 (self.today값), 20200116 (self.yesterday값)
현재 보유 종목
date code buy_price quantity
0 20200103 004170 292500 3
1 20200113 000100 44692 22
2 20200113 002450 1980 505
3 20200114 003580 8400 119
4 20200115 000080 31650 31
5 20200115 001630 113000 8
6 20200115 002320 30426 32
7 20200115 003690 9310 107
8 20200117 000720 42050 23
9 20200117 001390 12800 78
현재 잔고: 319274
일별 손익: 0
누적 손익: 0
현재 조회일자: 20200120 (self.today값), 20200117 (self.yesterday값)
현재 보유 종목
date code buy_price quantity
0 20200103 004170 292500 3
1 20200113 000100 44692 22
2 20200113 002450 1980 505
3 20200114 003580 8400 119
4 20200115 000080 31650 31
5 20200115 001630 113000 8
6 20200115 002320 30426 32
7 20200115 003690 9310 107
8 20200117 000720 42050 23
9 20200117 001390 12800 78
매도 진행 종목: 000100
매수 가격: 983224
매도 가격: 972686
종목별 매도 수익: -10538
현재 잔고: 1291960
일별 손익: -10538
누적 손익: -10538
현재 조회일자: 20200121 (self.today값), 20200120 (self.yesterday값)
현재 보유 종목
date code buy_price quantity
0 20200103 004170 292500 3
1 20200113 000100 44692 22
2 20200113 002450 1980 505
3 20200114 003580 8400 119
4 20200115 000080 31650 31
5 20200115 001630 113000 8
6 20200115 002320 30426 32
7 20200115 003690 9310 107
8 20200117 000720 42050 23
9 20200117 001390 12800 78
현재 잔고: 1291960
일별 손익: 0
누적 손익: -10538
출력 결과를 보면 000100 종목을 20200120을 기준으로 종가 가격으로 매도를 진행했고, 총 98만원어치를 매수했다가 97만원에 매도를 했다. 그 과정에서 만원의 손실이 발생했고, 20200119을 기준으로 약 32만원의 잔고에 97만원을 더해 129만원의 잔고가 있음을 보여주고 있다. 그리고 20200120 시점에 일별 손익에 만원의 손실이 집계됐고, 이것이 누적 손익에도 집계되어 그 다음 날인 20200121에서도 누적 손익에는 -10,000이 출력되고 있다. 즉, 문제 없이 잘 돌아가고 있는 것이다.
이번 게시글까지가 백테스팅을 진행하는 방법에 대한 내용이고, 다음 게시글부터는 제작한 백테스팅 전략을 보수하는 방법에 대해 살펴보도록 하겠다. 따라서 다음부터는 우리가 설계한
백테스팅 전략이 올바르게 돌아가고 있는지 예를 들면 매수는 했는데 매도는 안 하고 있는 것은 아닌지 등에 대해 진짜 차트를 기반으로 대조해보고 오류가 있다면 수정하는 방향으로 게시글을 작성하도록 하겠다. 본인은 다 만들어진 코드를 보여주며 이대로 작성하면 된다고 보기 보다는, 직접 같이 작성해가면서 오류가 있다면 오류를 수정하는 방법과 오류가 왜 발생했는지 등에 대해 설명하면서 게시글을 써내려가고 싶다.
'AUTO TRADE > Back test' 카테고리의 다른 글
백테스팅 구축 (11) - check_list 함수 수정하기 (0) | 2021.07.06 |
---|---|
백테스팅 구축 (10) - 차트와 대조하기 (0) | 2021.07.06 |
백테스팅 구축 (8) - 매도 함수 구축 (0) | 2021.07.06 |
백테스팅 구축 (7) - 매수 조건 설정 및 계좌 현황 제작 (0) | 2021.07.05 |
백테스팅 구축 (6) - 오류 수정 (0) | 2021.07.05 |
소중한 공감 감사합니다