다중 종목 벡테스팅
이전 글에서 손절선을 적용한 투자 전략을 알아보았다. 적절한 손절선이 있어야 손해를 입을 확률이 줄어든다는 것을 확인하였다. 하지만 현금이 놀고 있는 기간이 많아진다는 단점이 생겨 투자에 공백이 생겨 전체적인 수익률의 감소 및 비 효율적인 투자 운용이 되어버린다.
2022.11.09 - [개발일지] - 6. 투자지표를 활용한 매매 시점 모니터링 - 손절선
따라서 현금이 놀고 있는 시간을 최대한 줄이기 위해 여러 종목을 동시에 모니터링하고, 매매 시그널이 발생하는 종목을 차례로 매수할 수 있게 코드를 수정해보고자 한다.
단일 종목 벡테스팅 코드의 경우 투자 전략을 세우고 투자 가능성이 있는 종목을 찾는데 초점을 맞춰 사용하면 될 것이다. 개별 종목으로 접근해서 전략을 검증한 후 다중 종목 벡테스팅 코드를 실행해 전략의 최종 검증 및 수익성 확인하는 순서로 활용하면 될 것 같다.
코드의 수정은 단일 종목 벡테스팅으로 작성되었던 기반에서 각각의 변수를 리스트 형태로 바꾸어 일 별 각 종목의 데이터를 각각 저장하도록 하였다.
주요 함수가 있던 set_init 클래스를 확인해보자.
class set_init():
def __init__(self,stock_list,save_path,seed_money,set_base_buy):
self.money = seed_money
self.total_money = seed_money
self.stock_list = stock_list
self.stock_count = [0 for i in range(len(stock_list))]
self.now_index_list = [0 for i in range(len(stock_list))]
self.stock_price = [0 for i in range(len(stock_list))]
self.buy_price = [0 for i in range(len(stock_list))]
self.v1 = [1 for i in range(len(stock_list))]
self.v2 = [1 for i in range(len(stock_list))]
self.save_path = save_path
self.set_stand_price = set_base_buy
def set_data(self,tmp,target_index,i):
self.buy_list = [0 for k in range(len(self.stock_list))]
self.sell_list = [0 for k in range(len(self.stock_list))]
self.stock_price[i] = tmp.tail(1).Close.values[0]
self.now_index_list[i] = tmp[target_index].values[0]
self.time_print = tmp.index.values[0]
self.time_check = tmp.ts.values[0]
self.total_money = self.money + np.round(np.dot(self.stock_count,self.stock_price),6)
def buy(self,i):
money2 = copy.copy(self.money)
trading_fee = 0
if self.money < self.stock_price[i]:
buy_stock = 0
buy_money = 0
elif self.money < self.set_stand_price :
buy_stock = money2 // self.stock_price[i]
buy_money = buy_stock * self.stock_price[i]
self.money = self.money - buy_money
else :
have_money = money2*0.5
if have_money>self.set_stand_price: have_money = self.set_stand_price
buy_stock = have_money // self.stock_price[i]
buy_money = buy_stock * self.stock_price[i]
self.money = self.money - buy_money
self.buy_price[i] = self.buy_price[i] + buy_money
self.stock_count[i] = np.round(self.stock_count[i] + buy_stock,8)
self.total_money = self.money + np.round(np.dot(self.stock_count,self.stock_price),6)
print("buy ",self.time_print,self.stock_list[i],np.round(self.money),self.stock_count[i],buy_stock,
buy_money,np.round(self.total_money))
save_list = [self.stock_list[i],self.time_check,self.money,self.stock_count[i],
self.stock_price[i],self.total_money,"buy",buy_stock,buy_money,trading_fee,
self.now_index_list[i]]
save_txt(self.save_path+".txt",save_list, None)
self.buy_list[i] =1
def sell(self,i,all_tag):
if self.stock_count[i] == 0:
sell_stock = 0
sell_money = 0
trading_fee = 0
elif self.buy_price[i] <= self.set_stand_price or all_tag ==1:
sell_stock = self.stock_count[i]
self.buy_price[i] = 0
sell_money = sell_stock * self.stock_price[i]
trading_fee = sell_money *0.0025
else :
rate_ = np.round(self.set_stand_price/self.buy_price[i],2)
sell_stock = int(self.stock_count[i] * rate_)
self.buy_price[i] = self.buy_price[i] - (self.buy_price[i] * sell_stock/self.stock_count[i])
sell_money = sell_stock * self.stock_price[i]
trading_fee = sell_money *0.0025
self.stock_count[i] = np.round(self.stock_count[i] - np.round(sell_stock,8),8)
sell_money = sell_money - trading_fee
self.money = self.money + np.round(sell_money,2)
self.total_money = self.money + np.round(np.dot(self.stock_count,self.stock_price),6)
if all_tag ==1:
print("all_sell",self.time_print,self.stock_list[i],np.round(self.money),self.stock_count[i],
sell_stock,sell_money,np.round(self.total_money))
save_list = [self.stock_list[i],self.time_check,self.money,self.stock_count[i],
self.stock_price[i],self.total_money,"all_sell",sell_stock,sell_money,trading_fee,
self.now_index_list[i]]
else:
print("sell",self.time_print,self.stock_list[i],np.round(self.money),self.stock_count[i],
sell_stock,sell_money,np.round(self.total_money))
save_list = [self.stock_list[i],self.time_check,self.money,self.stock_count[i],
self.stock_price[i],self.total_money,"sell",sell_stock,sell_money,trading_fee,
self.now_index_list[i]]
save_txt(self.save_path+".txt", save_list, None)
self.sell_list[i] =1
def log_recording(self,i):
log_info = [self.stock_list[i],self.time_check,self.stock_price[i],
self.now_index_list[i],
self.buy_list[i],self.sell_list[i],
self.stock_count[i],
self.v1[i],self.v2[i],
self.total_money]
save_txt(self.save_path+"_"+str(i)+"_log.txt", log_info, None)
log_info2 = [self.time_check,self.money]+self.stock_count+self.stock_price
save_txt(self.save_path+"_total_price_log.txt", log_info2, None)
def yield_table(self,i):
try:
self.v1[i] = self.buy_price[i] / self.stock_count[i]
self.v2[i] = self.stock_price[i] * self.stock_count[i] / self.buy_price[i]
except:
None
모든 변수가 리스트 형식으로 값을 받도록 변경되었다.
여기서 종목의 개수에 따라 생성되도록 하였는데, 이로써 몇 개의 종목을 모니터링하더라도 종목 개수에 맞춰 벡테스팅 코드가 실행될 수 있도록 하였다.
buy 함수와 sell 함수도 마찬가지로 각각 종목에 따라 값을 변경 및 저장하도록 하였다.
save_path = "trading_simulation/trading_simulation"
stock_list = ["KT&G","SK하이닉스","NAVER"]
price_data = None
for kn in stock_list:
ks = df_krx[df_krx["Name"]==kn].Symbol.values[0]
price_data1 = fdr.DataReader(ks,"2012-12-01")
price_data1["stock_name"]= kn
RSI_df = rsi(price_data1, "Close",14)
RMI_df = rmi(price_data1, "Close",14)
price_data1["RSI"] = RSI_df
price_data1["RMI"] = RMI_df
price_data = pd.concat([price_data,price_data1.copy()],axis = 0)
price_data1.head(1)
저장 경로 설정과 모니터링할 종목 이름을 리스트 형식으로 만들어 준다.
벡테스팅 대상으로 KT&G, SK하이닉스, NAVER 세 종목을 선택했다.
세 종목의 데이터를 수집한 후 RSI 지표 값을 생성한다. 해당 코드는 이전 글에 작성되어있으니 참고 바란다.
2022.11.07 - [개발일지] - 2. 투자지표를 활용한 매매 시점 모니터링 - RSI, RMI
마지막에 수집된 데이터의 첫 행을 확인한 이유는 최초 투자 시직 시점을 잡기 위해서이다. 새로 상장하는 경우 수집 시작 일이 아닌 상장일부터 나타나기에 주의 깊게 확인해야 한다.
확인된 첫 행의 일자부터 벡테스팅을 시작한다.
meta_info = ["name","time","보유KRW", "보유주","price","총자산","TAG", "stock","money","trading_fee","target_index"]
save_txt(save_path+".txt", meta_info, None)
for i in range(len(stock_list)):
log_info = ["name","time","price","target_index","buy","sell","current_stock","avg_price","yield","total_money"]
save_txt(save_path+"_"+str(i)+"_log.txt", log_info, None)
log_info = ["time","money"]
for i in range(len(stock_list)):
log_info.append(stock_list[i])
for i in range(len(stock_list)):
log_info.append(stock_list[i]+"_price")
save_txt(save_path+"_total_price_log.txt", log_info, None)
저장할 txt 파일의 헤더를 생성한다.
아래는 벡테스팅 코드이다.
def trading_simulation(bitrade,price_data,target_index,lower_cut,upper_cut,time_index,stop_loss):
plot_value = []
lower_list = [False for i in range(len(stock_list))]
upper_list = [False for i in range(len(stock_list))]
for st in time_index:
dt64 = np.datetime64(st)
ts = (dt64 - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's')
all_tag = [0 for i in range(len(stock_list))]
tmp = price_data[price_data.index == st]
tmp["ts"]=ts
current_price = 0
# bitrade.set_stand_price = int(bitrade.total_money/len(stock_list))*5
for i in range(len(stock_list)):
tmp2 = tmp[tmp["stock_name"]==stock_list[i]]
if tmp2.shape[0]>0:
bitrade.set_data(tmp2,target_index,i)
bitrade.yield_table(i)
if bitrade.v2[i] *100-100 < stop_loss :
all_tag[i] = 1
if bitrade.stock_count[i] >0:
bitrade.sell(i,all_tag[i])
lower_list[i] = False
upper_list[i] = False
else:
if bitrade.now_index_list[i] <=lower_cut:
lower_list[i] = True
elif bitrade.now_index_list[i] > lower_cut and lower_list[i] == True:
if bitrade.money >100000:
bitrade.buy(i)
lower_list[i] = False
elif bitrade.now_index_list[i] >upper_cut:
upper_list[i] = True
elif bitrade.now_index_list[i] <= upper_cut and upper_list[i] == True:
if bitrade.stock_count[i] >0:
bitrade.sell(i,all_tag[i])
upper_list[i] = False
bitrade.log_recording(i)
current_price += bitrade.stock_count[i]*bitrade.stock_price[i]
plot_value.append(int(bitrade.money + current_price))
return plot_value
변경된 점은 역시 변수들이 리스트 형식으로 값을 저장한다. 매 일 별 설정한 종목의 RSI값을 모두 확인하면서 매매 시그널이 발생한 종목을 매수 혹은 매도하도록 설계하였다.
%%time
lower_cut = 30
upper_cut = 70
seed_money = 10000000
set_base_buy = 3000000
stop_loss = -10
time_index = price_data1[20:].index
bitrade = set_init(stock_list,save_path,seed_money,set_base_buy)
plot_value = trading_simulation(bitrade,price_data,"RSI",lower_cut,upper_cut,time_index,stop_loss)
시드 머니 1,000만원은 동일하게 설정하고, RSI 투자 선도 30, 70으로 동일하다.
1회 매매 기준금액은 모니터링 종목이 3개이기 때문에 약 3 분에 1 수준이 300만원으로 설정하였다.
여기에 손절선은 -10%로 설정하고 시작일은 2013년부터 벡테스팅을 진행했다.
file_path = "./trading_simulation/trading_simulation_"
for i in range(len(stock_list)):
log1 = read_log(file_path+str(i)+"_log")
log1["lower"] = lower_cut
log1["upper"] = upper_cut
log_plot(log1,"2013-01-01")
종목 수에 따른 각각의 주가 및 투자 현황 그래프를 나타낸다.
설정한 종목이 세 개이기 때문에 그래프가 3개가 나타난다. 대략 보면 개별적으론 보유를 하지 않은 기간이 있지만, 그 기간에 다른 종목을 보유하고 있는 것을 볼 수 있다.
또한 어느 한 종목을 100% 비중을 가져가는 모습이 없다. 즉, 현금을 덜 놀리는 것이다.
그래프와 관련한 설명은 아래의 글에서 자세하게 설명해놓았다.
2022.11.08 - [개발일지] - 4. 투자지표를 활용한 매매 시점 모니터링 - 시각화
총자산 현황을 보자.
color_list = ["moccasin",'lightcyan',"lightgreen","lightblue","lightsalmon","lightpink","wheat","azure","orchid","lightcoral"]*
log_total = total_price_df(file_path,stock_list,seed_money)
total_plot(log_total,stock_list,color_list,True,"2013-01-01")
회색 부분이 총자산, 각각의 종목을 각기 다른 색으로 보유 금액을 나타낸 것이다. 빨간색 선은 시드머니 대비 손익률을 나타낸다.
2013년~2014년 말까지를 제외하면 현금을 어느 정도 분산하면서 포트폴리오를 잘 가지고 있는 것이 보인다.
그리고 기간 수익률을 계산해보니
np.round((np.power((log_total.tail(1)["total_money"].values[0] / seed_money),1/10)-1)*100,2)
10년간 연평균 수익률 7.18%를 달성했다.
이 정도면 기본 전략으로도 훌륭한 성과를 달성했다고 판단된다. 단순 수익률은 100%로 시드머니만큼 수익금을 확보했다.
총자산 현황도 연도, 월, 일 별로 확인이 가능하다.
total_plot(log_total,stock_list,color_list,True,"2016","2018")
또한, 종목 구성을 빼고 총 자산 현황만 확인할 수도 있다. 네 번째 입력값을 False로 바꿔주면 된다.
total_plot(log_total,stock_list,color_list,False)
# 5,6 번째 입력값은 날짜의 시작과 끝임으로 생략가능함
극단적으로 100개의 종목을 넣고 벡테스팅을 하는 것도 가능하다.
임의로 Kospi 종목을 100개 선택해서 실행해보자. 종목수가 많아졌으니 1회 매매 기준 금액도 수정이 필요하다. 300만원으로 설정하면 3 종목 투자하면 끝나기 때문이다.
trading_simulation 함수의 주석 처리된 부분을 활성화시킨다.
bitrade.set_stand_price = int(bitrade.total_money/len(stock_list))*5
위의 식을 풀어서 설명하면, 총자산을 종목수로 나눈 후 5배만큼 1회 매매 기준 금액으로 설정한다.
총자산이 커지면 1회 매매 기준 금액이 점차 커질 것이고, 반대라면 작아질 것이다. 이렇게 설정함으로써 많은 종목을 분산 투자할 수 있도록 한다.
위의 코드를 실행해보면 이런 결과가 나온다.
종목이 너무 많기 때문에 범례는 생략했다.
총 자산 현황 그래프를 나타낸 것인데, 보유 현금이 대부분 투자금으로 들어가서 언듯 보기엔 효율적으로 운용하는 것처럼 보인다. 세부 보유 내역을 보기는 어렵지만 명확하게 한 가지를 알 수 있다.
- 무작정 많은 종목을 모니터링 목록에 올려두고 분산 투자하는 것이 만능은 아니라는 것.
앞서 위의 세 종목(SK하이닉스, KT&G, NAVER)으로 했을 경우가 수익률뿐만 아니라 안정성도 더 높은 것을 볼 수 있다. 물론 대기업인 3개 기업과 KOSPI 랜덤 100개 기업을 비교하기엔 무리가 있지만, 말하고자 하는 것은 포트폴리오를 구성할 때 종목 분석을 필수로 한 후 모니터링 목록에 올려야 한다는 것이다.
따라서 적절한 전략과 전략에 맞는 포트폴리오 구성 종목 수를 정한 후 손절선을 가미한 반복적인 테스트를 거쳐 검증해야 할 것이다.
또한, 과거 데이터로 결과를 보는 것이기 때문에 벡테스팅의 성과가 좋다고 하더라도 해당 포트폴리오가 수익을 가져다주는 것을 장담하지 못한다. 이 점 때문에 종목 분석이 반드시 따라와야 한다고 강조하는 것이다.
정리 및 앞으로의 개발 계획
여기까지 단일 종목과 다중 종목 벡테스팅 검증에 대한 코드가 정리되었다.
이제 전략을 설계와 카카오톡 API로 매일 보고서를 보내는 것인데, 둘 다 조금씩 진행해 보면서 API 쪽을 먼저 다음 글로 포스팅이 될 것 같다.
API는 종가 혹은 1시간 단위로 매매 시그널이 뜨는 종목을 리포트해주는 형식과 장이 마감된 후 보유 종목에 대한 분석 등으로 활용해 보려한다.
전략 부분은 고민이 많이 필요할 듯싶다.
지금 계획하고 있는 것은 각종 보조 지표들을 사용해보면서 테스트 결과를 정리하고, 지표들을 결합해 새로운 지표를 만들어서도 테스트해보려 한다.
벡테스팅 코드가 0~100 사이 값으로 매매가 이루어지게 되어있지만, trading_simulation 함수의 lower_cut, upper_cut 값을 변경하며 내부 로직을 수정하면 범위가 다른 지표도 적용해 볼 수 있다. 단일 지표값에 한정되지만 알려진 간단한 전략들을 확인해가며 지표의 속성을 이해하는 것에도 도움이 될 것이다.
다양한 지표에 대한 테스트 결과도 후에 정리해서 포스팅하도록 하겠다.
나아가 지표를 결합하는 방식에 대해서고 고민 중이다.
현재 내가 경력을 쌓고 있는 곳이 빅데이터와 관련이 있어 머신러닝, 딥러닝 모델링을 다루고 있기 때문에 각 지표들을 주가와 엮어서 AI 모델링으로 지표를 만들어볼 생각도 하고 있다.
모델이 0~100 사이 값으로 출력할 수 있도록 만들어 위의 코드에 바로 적용해 볼 수 있도록 개발해 볼 예정이다. AI 모델링의 경우 시간이 조금 걸릴 듯하다.
데이터 확보를 위해 주가뿐 아니라 투자 지표, 재무 지표의 수집도 필요하다. 현재 이런 정보를 크롤링하는 코드도 만들어 놨기 때문에 차례로 소개하도록 하겠다.
'개발일지' 카테고리의 다른 글
9. 투자지표를 활용한 매매 시점 모니터링 - RSI 종목 추천 (0) | 2022.11.18 |
---|---|
8. 투자지표를 활용한 매매 시점 모니터링 - 전략 설계 참고 (0) | 2022.11.15 |
6. 투자지표를 활용한 매매 시점 모니터링 - 손절선 (0) | 2022.11.09 |
5. 투자지표를 활용한 매매 시점 모니터링 - Kospi 종목 벡테스팅 (0) | 2022.11.08 |
4. 투자지표를 활용한 매매 시점 모니터링 - 시각화 (0) | 2022.11.08 |
댓글