백테스팅 구축 (7) - 매수 조건 설정 및 계좌 현황 제작
지난 포스팅에서 길고 긴 오류 수정 과정을 거치고 넘어왔다. 이제 오류가 없었으면 하지만 사실 오류는 언제 발생할지도 모르고 어디서 발생할지도 모른다. 기껏 게시글을 보면서 "오... 잘 따라하기만 하면 나도 할 수 있을 것 같아."라는 생각에 따라하기 시작했다가 "아 뭔데... 왜 제대로 안 만드는데.."라는 생각이 들었다면 변명의 여지가 없지만, 너그러운 마음으로 이해해주십사 하는 마음으로 이번 게시글을 작성한다.
디테일한 매수 조건 설정
일전에 시작일자를 20200103으로 설정하고 백테스팅을 진행해본 결과, 2020년 1월 3일에 매수할 종목이 총 10개가 나왔다. 아래에 나와 있는 종목의 종가 중 가장 작은 값이 30만원이니 종목당 30만원씩만 매수한다고 가정하면 하루에만 300만원 어치를 매수하게 되고, 이게 하루 이틀이 아니라 일주일 정도 종목을 보유한 후에 매도하게 된다면 일주일 동안 이 종목에 자금이 묶여 있는 문제가 발생하게 된다. 즉, 매도를 못하고 보유하고만 있다면 일주일 동안 하루에 10종목을 매수한다면, 일주일 간 총 매수 금액은 2,100만원에 달한다. 종목당 30만원을 매수했으니 일주일 간 매수한 종목의 수는 70종목이나 된다. (개인적으로, 하루 매수 종목은 한 종목이면 족하다고 보는 입장이다.) 따라서 종목 선정에 있어서 보다 세밀한 조건을 추가함으로써 매수 대상 종목을 조금 더 줄여보도록 하자.
########################
## self.buy_list 변수 ##
########################
code date close ... ma20 ma60 ma120
0 001130 20200103 149500 ... 147125.00 153250.000000 166541.666667
1 001550 20200103 15450 ... 15222.50 16610.000000 18031.666667
2 002030 20200103 108000 ... 105050.00 109358.333333 110891.666667
3 002140 20200103 3530 ... 3374.25 3493.500000 3500.750000
4 002240 20200103 18193 ... 17786.45 18738.200000 19729.500000
5 002780 20200103 2350 ... 2294.75 2247.083333 2146.375000
6 003960 20200103 16300 ... 16070.00 16600.833333 17045.000000
7 004170 20200103 292500 ... 286325.00 264433.333333 257000.000000
8 004310 20200103 5410 ... 5083.25 5059.083333 4847.833333
9 004710 20200103 9060 ... 8898.00 7852.000000 7324.583333
[10 rows x 8 columns]
일단 우리가 기존에 제작했던 매수 조건 충족을 확인하는 지점은 justify_ma() 함수였고, 이 함수를 check_list() 함수 내에서 is_db() 함수를 통해 데이터의 존재 여부를 확인한 후 for문 안에서 self.today와 self.yesterday 두 값 모두 True인 경우에만 justify_ma() 함수를 실행하고 buy_list 변수(매수 리스트)에 데이터들을 입력하도록 했다. 즉, if result == True: 아래에서 또 하나의 조건을 만듦으로써 해당 조건이 충족되는 경우에만 buy_list[].append()를 실행하도록 구축하면 되는 것이다.
그렇다면 우리는 어떤 기준을 두어야 할지 잘 모를 수 있는데, buy_list[] 부분에 추가적인 데이터를 입력할 수 있는 칼럼을 만들어주고 그 안에 데이터를 입력함으로써 그 값을 바탕으로 조건을 추가할 수 있다. 따라서 buy_list 변수를 다음과 같이 수정해주도록 하자.(거래량(volume)과 거래대금(tvolume) 칼럼을 추가했다.)
# 수정 전
buy_list = {'code':[], 'date':[], 'close':[], 'ma5':[], 'ma10':[], 'ma20':[], 'ma60':[], 'ma120':[]}
# 수정 후
buy_list = {'code':[], 'date':[], 'close':[], 'ma5':[], 'ma10':[], 'ma20':[], 'ma60':[], 'ma120':[], 'volume':[], 'tvolume':[]}
그 후에는 if result == True: 아래에 있는 buy_list[].append()부분에서 volume과 tvolume을 추가해준 후에 코드를 실행해보면, 매수 대상 종목들의 데이터를 확인할 수 있다.
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])
## 아래의 두 줄을 추가했음 ##
buy_list['volume'].append(self.chart_data['volume'].iloc[1])
buy_list['tvolume'].append(self.chart_data['trade_volume'].iloc[1])
마지막으로 buy_list 변수에 입력된 여러 가지 값들을 데이터프레임으로 만들어주는 코드의 맨 뒷 부분에도 'volume'과 'tvolume'이라는 두 개의 칼럼을 추가해줌으로써 결과값을 확인해보도록 하자.
# 수정 전
self.buy_list = pd.DataFrame(buy_list, columns=['code', 'date', 'close', 'ma5', 'ma10', 'ma20', 'ma60', 'ma120'])
# 수정 후
self.buy_list = pd.DataFrame(buy_list, columns=['code', 'date', 'close', 'ma5', 'ma10', 'ma20', 'ma60', 'ma120', 'volume', 'tvolume'])
code date close ... ma120 volume tvolume
0 001130 20200103 149500 ... 166541.666667 1077 160
1 001550 20200103 15450 ... 18031.666667 12465 193
2 002030 20200103 108000 ... 110891.666667 412 44
3 002140 20200103 3530 ... 3500.750000 468449 1659
4 002240 20200103 18193 ... 19729.500000 19759 356
5 002780 20200103 2350 ... 2146.375000 370875 881
6 003960 20200103 16300 ... 17045.000000 9184 150
7 004170 20200103 292500 ... 257000.000000 40984 12004
8 004310 20200103 5410 ... 4847.833333 885942 4767
9 004710 20200103 9060 ... 7324.583333 321390 2930
위의 결과값을 보면 거래대금(column name : tvolume) 값이 상당히 작은 모습을 보여주고 있는데, 이는 키움증권 Open API에서 사용하는 자료의 단위를 그대로 반환해주고, 우리는 그 반환값을 그대로 MySQL에 입력했기 때문에 그렇다. 키움증권에서 사용하는 데이터들은 가격의 경우에는 원 단위를 사용하고 거래량은 말 그대로 거래된 수량, 거래대금은 백만단위를 기준으로 한다. 즉, 위의 결과 데이터 값 안에 있는 tvolume의 경우에는 160은 1억 6천만원, 1659는 16억 5900만원, 12004는 약 120억에 해당하는 거래대금이 발생했다는 것이다.
그렇다면 우리는 5일 이동평균선이 20일 이동평균선을 돌파하는 봉이 완성된 시점에 거래대금과 거래량이 크면 클수록 좋다는 것을 이미 알고 있다. 하지만 거래대금이 얼마 정도가 되어야 크게 발생했는지는 종목마다 그 기준이 다르다. 예를 들어 삼성전자의 경우에는 하루에 발생하는 거래대금이 약 6700억인데 반해 SK하이닉스는 약 2000억이다. 즉, 각 종목별로 이동평균선의 골든 크로스가 발생했을 때 더 높은 신뢰도를 가지고 있다고 파악하기 위해 사용하고자 한 큰 거래대금 기준이 종목마다 다르다는 것이다. 다만 이는 종목별 데이터를 또 고려해야 하는 복잡성이 있으므로 여기서는 제외하고, 골든 크로스가 발생했을 때 거래대금이 50억 이상 발생한 종목만 매수 대상으로 하도록 하자. 그 후에 코드를 실행해보면, 2020년 1월 3일자를 기준으로 한 종목만 매수 대상이 된다는 것을 어렵지 않게 확인할 수 있다.
[주의] 백테스팅 시 MySQL을 통해 불러온 데이터들을 특정 값과 비교하기 위해서는 int() 또는 float()을 통해 자료형을 변환시켜주어야 한다. 왜냐하면 우리는 해당 데이터값을 MySQL에 to_sql()을 통해 입력할 때에는 문자 자료형인 str()형태로 입력했기 때문이다. 즉, 문자열과 숫자 간에는 대소 관계의 비교가 불가능하기 때문에 반드시 자료형을 변환시켜줘야 한다.
if result == True:
## 거래대금 기준 설정
if int(self.chart_data['trade_volume'].iloc[1]) > 5000:
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])
buy_list['volume'].append(self.chart_data['volume'].iloc[1])
buy_list['tvolume'].append(self.chart_data['trade_volume'].iloc[1])
## self.buy_list 변수 출력값 ##
## 조회일자:20200103 ##
code date close ma5 ... ma60 ma120 volume tvolume
0 004170 20200103 292500 286500.0 ... 264433.333333 257000.0 40984 12004
[1 rows x 10 columns]
## 조회일자:20200106 ##
Empty DataFrame
Columns: [code, date, close, ma5, ma10, ma20, ma60, ma120, volume, tvolume]
Index: []
## 조회일자:20200107 ##
Empty DataFrame
Columns: [code, date, close, ma5, ma10, ma20, ma60, ma120, volume, tvolume]
Index: []
## 조회일자:20200108 ##
Empty DataFrame
Columns: [code, date, close, ma5, ma10, ma20, ma60, ma120, volume, tvolume]
Index: []
## 조회일자:20200109 ##
Empty DataFrame
Columns: [code, date, close, ma5, ma10, ma20, ma60, ma120, volume, tvolume]
Index: []
## 조회일자:20200110 ##
Empty DataFrame
Columns: [code, date, close, ma5, ma10, ma20, ma60, ma120, volume, tvolume]
Index: []
## 조회일자:20200113 ##
code date close ... ma120 volume tvolume
0 000100 20200113 44692 ... 41220.108333 143020 6374
1 002450 20200113 1980 ... 1801.041667 3473077 6745
[2 rows x 10 columns]
## 조회일자:20200114 ##
code date close ma5 ... ma60 ma120 volume tvolume
0 003580 20200114 8400 7936.0 ... 7900.333333 5924.083333 1501684 12663
[1 rows x 10 columns]
계좌 현황 제작
일단 지금 본인의 경우에는 아직 모든 차트 데이터가 MySQL에 입력된 것이 아니기 때문에 일부의 종목만을 대상으로 백테스팅을 진행한다는 점을 인지하고 읽어주셨으면 하는 마음이 있다. 종목의 개수가 다르다는 것은 백테스팅 결과에 엄청난 영향을 미치게 되기 마련이다. 왜냐하면 같은 기준으로 매수했다 하더라도 각 종목이 위치해 있는 상황이나 가격적인 위치 등의 요인으로 인해 각기 다른 결과가 도출될 수 있기 때문이다.
매수 코드를 제작하기 전에 앞서, 우리의 계좌에서는 어떤 정보들을 담고 있어야 하는지 한 번 생각하고 넘어갈 필요가 있다. 일단 가장 기본적으로는 매수한 종목의 코드, 매수 가격과 매수 수량, 현재 수익률(또는 수익금액) 및 최종 수익률(또는 수익금액) 등등이다. 만약 이외에도 결과값을 확인하는 데 있어 본인이 이 부분은 알아야 겠다 싶은 부분이 있다면 그 값을 추가로 입력해주고 나중에 개인적으로 다시 확인해보면 된다. 일단 본인의 경우에는 아래와 같은 요건에 대해서만 확인해보도록 하겠다.
- 계좌 칼럼 : 일자(date), 종목코드(code), 매수가격(buy_price), 매수수량(qunatity), 수익금(profit)
- 초기 금액 : 10,000,000
- 매수 금액 : 1,000,000(종목 당)
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':[], 'qunatity':[], 'profit':[]}
self.init_money = 10000000
self.money_by_unit = 1000000
while self.today != end_date:
self.yesterday = self.cal_subday(self.today, 1)
print("현재 조회일자:", self.today, "(self.today값), ", self.yesterday, "(self.yesterday값)")
self.buy_list = self.check_list()
self.today = self.cal_addday(self.today, 1) ## 항상 맨 밑에 두어야 함
매수 함수 제작
이제 계좌 현황은 제작했으니, 매수 리스트를 바탕으로 매수를 진행할 매수 코드를 제작해보도록 하자. 현재 이동평균선 간 골든 크로스 조건이 충족되어 매수할 예정인 종목의 리스트는 self.buy_list 변수에 모두 입력되어 있다. 또한 백테스팅 결과의 확률을 높여줄 요인으로 골든 크로스나 데드 크로스가 완성되겠다 싶은 것을 미리 예측할 수 있는 몇 가지 요건들을 추가하는 방법이 있긴 하지만 그렇게 되면 너무 복잡해지니 이 부분은 논외로 하고, 단순하게 골든 크로스 또는 데드 크로스가 발생한 날의 종가에 매수를 한다고 가정하고 매수 함수를 제작해보자.
일단 매수를 하기 위해서는 별도의 함수를 제작하고, 그 함수내에서 self.buy_list 변수를 오밀조밀 잘 이용해서 우리가 이전에 제작했던 계좌 변수인 self.account에 관련 정보들을 입력해주면 된다. 일단 buy() 함수를 새롭게 정의한 후에, def __init__ 함수 내에 self.buy()를 입력하고 buy() 함수 내에서는 self.buy_list 변수를 제대로 받아오는지 확인해보도록 하자.
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
while self.today != end_date:
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()
self.today = self.cal_addday(self.today, 1) ## 항상 맨 밑에 두어야 함
def buy(self):
print(self.buy_list)
현재 조회일자: 20200103 (self.today값), 20200102 (self.yesterday값)
code date close ma5 ... ma60 ma120 volume tvolume
0 004170 20200103 292500 286500.0 ... 264433.333333 257000.0 40984 12004
buy() 함수 내에서 입력한 print(self.buy_list) 코드가 제대로 동작하고 있음을 확인했다. 그렇다면 이제 self.buy_list를 대상으로 for문을 돌리면서 값들을 하나하나 참조하여 self.account 변수에 입력해주자. 여기서 for문을 돌릴 때에는 해당 데이터프레임 안에 있는 모든 값들을 사용해야 하기 때문에, 데이터프레임이 입력되어 있는 변수 뒤에 .itertuples()라는 메서드를 활용해서 데이터프레임 안에 있는 데이터들을 한줄 한줄 읽어올 수 있다. 그리고 그 한줄 한줄 안에 있는 데이터에 접근하기 위해서는 인덱스를 통해 접근해야 한다. 따라서 만약에 본인이 self.account = {} 안에 들어갈 칼럼명(키값)을 따로 입력했다면, 그 칼럼명(키값)이 위치해 있는 순번을 기준으로 인덱스 번호를 입력해주어야 해당 값을 이용할 수 있다는 것이다.
def buy(self):
print(self.buy_list)
for row in self.buy_list.itertuples():
print(row[0])
print(row[1])
code date close ma5 ... ma60 ma120 volume tvolume
0 004170 20200103 292500 286500.0 ... 264433.333333 257000.0 40984 12004
[1 rows x 10 columns]
0
004170
이제 매수 대상 종목이 담긴 데이터(self.buy_list 변수)를 불러왔고 for문 역시 제작했으며 그 안에 있는 데이터 하나하나에 대해서도 개별적으로 접근이 가능하다는 것을 확인했으니 하나하나의 값들을 계산해서 self.account 변수에 입력해주도록 하자.
- 매수할 수량(quantity) : 종목 당 매수 금액(self.money_by_unit) 나누기(/) 종목의 종가(row[3])로 계산. 다만 계산 후 int()로 묶어줌으로써 나눗셈을 계산할 때 발생할 수 있는 소수점을 정수로 바꿔주어야 한다.
- 매수 가능 금액(self.init_money) : 초기 매수 설정 금액(self.init_money)에서 당일 매수 금액을 빼준다.
[주의] self.account['quantity'].append 부분에서 append 뒤에 있는 quantity의 경우에는 for문의 바로 아래에서 계산한 결과값이 int, 즉 정수형이기 떄문에 append()를 사용할 때에는 str()로 묶어서 입력해주어야 한다.
def buy(self):
for row in self.buy_list.itertuples():
quantity = int(self.money_by_unit / int(row[3]))
self.account['code'].append(row[1])
self.account['date'].append(row[2])
self.account['buy_price'].append(row[3])
self.account['quantity'].append(quantity)
self.init_money = self.init_money - (int(row[3]) * int(quantity))
이후에 def __init__ 함수 내에서, 위의 def buy()를 지나면서 입력된 값들을 확인할 수 있도록 하는 코드를 아래와 같이 제작한 후에 코드를 실행하여 그 결과값을 확인해보도록 하자.
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':[], 'qunatity':[], 'profit':[]}
self.init_money = 10000000
self.money_by_unit = 1000000
while self.today != end_date:
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("현재 보유 종목")
print(self.account)
print("현재 잔고:", self.init_money)
self.today = self.cal_addday(self.today, 1)
현재 조회일자: 20200103 (self.today값), 20200102 (self.yesterday값)
현재 보유 종목
{'date': ['20200103'], 'code': ['004170'], 'buy_price': ['292500'], 'quantity': [3], 'profit': []}
현재 잔고: 9122500
현재 조회일자: 20200106 (self.today값), 20200103 (self.yesterday값)
현재 보유 종목
{'date': ['20200103'], 'code': ['004170'], 'buy_price': ['292500'], 'quantity': [3], 'profit': []}
현재 잔고: 9122500
현재 조회일자: 20200107 (self.today값), 20200106 (self.yesterday값)
현재 보유 종목
{'date': ['20200103'], 'code': ['004170'], 'buy_price': ['292500'], 'quantity': [3], 'profit': []}
현재 잔고: 9122500
현재 조회일자: 20200108 (self.today값), 20200107 (self.yesterday값)
현재 보유 종목
{'date': ['20200103'], 'code': ['004170'], 'buy_price': ['292500'], 'quantity': [3], 'profit': []}
현재 잔고: 9122500
현재 조회일자: 20200109 (self.today값), 20200108 (self.yesterday값)
현재 보유 종목
{'date': ['20200103'], 'code': ['004170'], 'buy_price': ['292500'], 'quantity': [3], 'profit': []}
현재 잔고: 9122500
현재 조회일자: 20200110 (self.today값), 20200109 (self.yesterday값)
현재 보유 종목
{'date': ['20200103'], 'code': ['004170'], 'buy_price': ['292500'], 'quantity': [3], 'profit': []}
현재 잔고: 9122500
현재 조회일자: 20200113 (self.today값), 20200110 (self.yesterday값)
현재 보유 종목
{'date': ['20200103', '20200113', '20200113'], 'code': ['004170', '000100', '002450'], 'buy_price': ['292500', '44692', '1980'], 'quantity': [3, 22, 505], 'profit': []}
현재 잔고: 7139376
현재 조회일자: 20200114 (self.today값), 20200113 (self.yesterday값)
현재 보유 종목
{'date': ['20200103', '20200113', '20200113', '20200114'], 'code': ['004170', '000100', '002450', '003580'], 'buy_price': ['292500', '44692', '1980', '8400'], 'quantity': [3, 22, 505, 119], 'profit': []}
현재 잔고: 6139776
현재 조회일자: 20200115 (self.today값), 20200114 (self.yesterday값)
현재 보유 종목
{'date': ['20200103', '20200113', '20200113', '20200114', '20200115', '20200115', '20200115', '20200115'], 'code': ['004170', '000100', '002450', '003580', '000080', '001630', '002320', '003690'], 'buy_price': ['292500', '44692', '1980', '8400', '31650', '113000', '30426', '9310'], 'quantity': [3, 22, 505, 119, 31, 8, 32, 107], 'profit': []}
현재 잔고: 2284824
매수가 잘 되고 있음을 확인할 수 있다. 아직 현재 보유 종목인 self.account를 데이터프레임화 시키지 않았기 때문에 데이터가 다소 복잡하게 보일 수도 있지만, 어찌됐든 간에 20200113의 현재 보유 종목에는 종목 코드개 세 개가 있고 20200114의 경우에는 종목 코드가 네 개, 20200115의 경우에는 종목 코드가 8개가 있다는 것을 대략적으로나마 확인할 수 있다.
즉, 20200106의 시점에서는 추가적으로 매수할 종목이 없었기 때문에 현재 보유 종목 아래에 출력되는 self.account 변수에는 변함이 없고, 현재 잔고를 나타내는 self.init_money 변수에도 변함이 없다. 그 후에는 아무런 매수 종목이 없다가 20200113이 되어서야 매수 종목이 출력되고 그 때가 되면 또 다른 종목 데이터들을 self.account 변수 안에 입력하는 모습을 확인할 수 있다. (매수 조건을 추가할 때 출력된 결과값을 보면 20200103과 20200113, 20200114에 매수 대상 종목이 발생한다는 것을 확인할 수 있다.)
여기까지가 종목을 매수하도록 하는 코드이고, 다음 게시글에서는 일단 self.account를 데이터프레임화 시킨 다음에 현재 보유 중인 종목을 대상으로 매도 조건이 충족되었는지, 그리고 매도 조건이 충족되었다면 해당 종목을 매도한 후에 해당 거래로부터 발생한 수익은 얼마인지, 마지막으로 백테스팅하는 일자까지 발생한 총 수익은 얼마인지를 확인할 수 있도록 하는 코드를 구축해보도록 하자.
'AUTO TRADE > Back test' 카테고리의 다른 글
백테스팅 구축 (9) - 실시간 잔고 업데이트 (0) | 2021.07.06 |
---|---|
백테스팅 구축 (8) - 매도 함수 구축 (0) | 2021.07.06 |
백테스팅 구축 (6) - 오류 수정 (0) | 2021.07.05 |
백테스팅 구축 (5) - 매수 조건 제작 (0) | 2021.07.04 |
백테스팅 구축 (4) - 일자별 차트 데이터 불러오기 (0) | 2021.07.04 |
소중한 공감 감사합니다