해결책 테스트
해결책 테스트#
오늘 날짜만 입력하면 내일 매수할 종목이 추천되도록 각 프로세스를 통합하여 함수를 구현합니다. 임의 날짜를 넣어서 테스트 해 봅니다. 이 책에서는 2022년 4월 1일, 2022년 4월 18일, 2022년 5월 2일, 2022년 5월 9일, 2022년 5월 25일, 2022년 6월 2일, 6월 16일에 대하여 종목 선정 및 결과 수익률을 테스트 해 보았습니다. 모델을 개발하는데 사용한 날짜는 모델 검증 용도로 적절하지 않습니다. 왜냐하면 개발에서 사용한 데이터는 모델이 좋은 성과가 나오도록 최적화되어 있기 때문입니다. 참고로 모델 개발은 2021년 1월 4일부터 2022년 3월 24일까지 데이터가 사용되었습니다.
이제 종목을 추천하는 프로세스를 완성했습니다. 장 마감 후 종목 추천을 받아 익일 증권사 API 를 이용해서 자동매매를 구현하고 한 달 동안의 수익이 어떤지 검증해 보겠습니다. 홈트레이딩 시스템에도 자동매매가 가능합니다. 책에서 구현할 자동매매는 홈트레이딩 감시 매매 설정으로도 충분히 가능합니다.
실전에서는 HTS 에서 제공하는 예약 매수기능과 매도 감시기능을 이용하는 것리 편리합니다. HTS 를 활용하여 자동으로 매수 매도가 가능합니다.
import FinanceDataReader as fdr
import yfinance as yf
import matplotlib.pyplot as plt
%matplotlib inline
import pandas as pd
import numpy as np
import datetime
import pickle
import glob
import datetime
추전 종목을 만드는 여러 개의 프로세스를 하나의 함수로 만들었습니다.
def select_stocks(today_dt):
today = datetime.datetime.strptime(today_dt, '%Y-%m-%d')
start_dt = today - datetime.timedelta(days=100) # 100 일전 데이터 부터 시작 - 피쳐 엔지니어링은 최소 60 개의 일봉이 필요함
print(start_dt, today_dt)
kosdaq_list = pd.read_pickle('kosdaq_list.pkl')
price_data = pd.DataFrame()
for code, name in zip(kosdaq_list['code'], kosdaq_list['name']): # 코스닥 모든 종목에서 대하여 반복
daily_price = fdr.DataReader(code, start = start_dt, end = today_dt) # 종목, 일봉, 데이터 갯수
daily_price['code'] = code
daily_price['name'] = name
price_data = pd.concat([price_data, daily_price], axis=0)
price_data.index.name = 'date'
price_data.columns= price_data.columns.str.lower() # 컬럼 이름 소문자로 변경
# DataReder 코스닥 인덱스 조회 실패시, 야후파이낸스로 추출
# kosdaq_index = fdr.DataReader('KQ11', start = start_dt, end = today_dt) # 데이터 호출
# kosdaq_index.columns = ['close','open','high','low','volume','change'] # 컬럼명 변경
kosdaq_index = yf.download('^KQ11', start = start_dt)
kosdaq_index.columns = ['open','high','low','close','adj_close','volume'] # 컬럼명 변경
kosdaq_index.index.name='date' # 인덱스 이름 생성
kosdaq_index.sort_index(inplace=True) # 인덱스(날짜) 로 정렬
kosdaq_index['kosdaq_return'] = kosdaq_index['close']/kosdaq_index['close'].shift(1) # 수익율 : 전 날 종가대비 당일 종가
merged = price_data.merge(kosdaq_index['kosdaq_return'], left_index=True, right_index=True, how='left')
return_all = pd.DataFrame()
for code in kosdaq_list['code']:
stock_return = merged[merged['code']==code].sort_index()
stock_return['return'] = stock_return['close']/stock_return['close'].shift(1) # 종목별 전일 종가 대비 당일 종가 수익율
c1 = (stock_return['kosdaq_return'] < 1) # 수익율 1 보다 작음. 당일 종가가 전일 종가보다 낮음 (코스닥 지표)
c2 = (stock_return['return'] > 1) # 수익율 1 보다 큼. 당일 종가가 전일 종가보다 큼 (개별 종목)
stock_return['win_market'] = np.where((c1&c2), 1, 0) # C1 과 C2 조건을 동시에 만족하면 1, 아니면 0
return_all = pd.concat([return_all, stock_return], axis=0)
return_all.dropna(inplace=True)
model_inputs = pd.DataFrame()
for code, name, sector in zip(kosdaq_list['code'], kosdaq_list['name'], kosdaq_list['sector']):
data = return_all[return_all['code']==code].sort_index().copy()
# 가격변동성이 크고, 거래량이 몰린 종목이 주가가 상승한다
data['price_mean'] = data['close'].rolling(20).mean()
data['price_std'] = data['close'].rolling(20).std(ddof=0)
data['price_z'] = (data['close'] - data['price_mean'])/data['price_std']
data['volume_mean'] = data['volume'].rolling(20).mean()
data['volume_std'] = data['volume'].rolling(20).std(ddof=0)
data['volume_z'] = (data['volume'] - data['volume_mean'])/data['volume_std']
# 위꼬리가 긴 양봉이 자주발생한다.
data['positive_candle'] = (data['close'] > data['open']).astype(int) # 양봉
data['high/close'] = (data['positive_candle']==1)*(data['high']/data['close'] > 1.1).astype(int) # 양봉이면서 고가가 종가보다 높게 위치
data['num_high/close'] = data['high/close'].rolling(20).sum()
data['long_candle'] = (data['positive_candle']==1)*(data['high']==data['close'])*\
(data['low']==data['open'])*(data['close']/data['open'] > 1.2).astype(int) # 장대 양봉을 데이터로 표현
data['num_long'] = data['long_candle'].rolling(60).sum() # 지난 20 일 동안 장대양봉의 갯 수
# 거래량이 종좀 터지며 매집의 흔적을 보인다
data['volume_mean'] = data['volume'].rolling(60).mean()
data['volume_std'] = data['volume'].rolling(60).std()
data['volume_z'] = (data['volume'] - data['volume_mean'])/data['volume_std'] # 거래량은 종목과 주가에 따라 다르기 떄문에 표준화한 값이 필요함
data['z>1.96'] = (data['close'] > data['open'])*(data['volume_z'] > 1.65).astype(int) # 양봉이면서 거래량이 90%신뢰구간을 벗어난 날
data['num_z>1.96'] = data['z>1.96'].rolling(60).sum() # 양봉이면서 거래량이 90% 신뢰구간을 벗어난 날을 카운트
# 주가지수보다 더 좋은 수익율을 보여준다
data['num_win_market'] = data['win_market'].rolling(60).sum() # 주가지수 수익율이 1 보다 작을 때, 종목 수익율이 1 보다 큰 날 수
data['pct_win_market'] = (data['return']/data['kosdaq_return']).rolling(60).mean() # 주가지수 수익율 대비 종목 수익율
# 동종업체 수익률보다 더 좋은 수익율을 보여준다.
data['return_mean'] = data['return'].rolling(60).mean() # 종목별 최근 60 일 수익율의 평균
data['sector'] = sector
data['name'] = name
data = data[(data['price_std']!=0) & (data['volume_std']!=0)]
model_inputs = pd.concat([data, model_inputs], axis=0)
model_inputs['sector_return'] = model_inputs.groupby(['sector', model_inputs.index])['return'].transform(lambda x: x.mean()) # 섹터의 평균 수익율 계산
model_inputs['return over sector'] = (model_inputs['return']/model_inputs['sector_return']) # 섹터 평균 수익률 대비 종목 수익률 계산
model_inputs.dropna(inplace=True) # Missing 값 있는 행 모두 제거
feature_list = ['price_z','volume_z','num_high/close','num_win_market','pct_win_market','return over sector']
X = model_inputs.loc[today_dt][['code','name','return','kosdaq_return','close'] + feature_list].set_index('code')
with open("gam.pkl", "rb") as file:
gam = pickle.load(file)
yhat = gam.predict_proba(X[feature_list])
X['yhat'] = yhat
tops = X[X['yhat'] >= 0.3].sort_values(by='yhat', ascending=False) # 스코어 0.3 이상 종목만
print(len(tops))
select_tops = tops[(tops['return'] > 1.03) & (tops['price_z'] < 0)][['name','return','price_z','yhat', 'kosdaq_return','close']] # 기본 필터링 조건
if len(select_tops) > 1: # 최소한 2개 종목 - 추천 리스크 분산
return select_tops
else:
return None
수익률 검정하는 프로세스도 하나의 함수로 구현합니다.
def outcome_tops(select_tops, today_dt, end_dt):
outcome_data = pd.DataFrame()
for code in list(select_tops.index): # 스코어가 생성된 모든 종목에서 대하여 반복
daily_price = fdr.DataReader(code, start = today_dt, end = end_dt) # 종목, 일봉, 데이터 갯수
daily_price['code'] = code
daily_price['close_r1'] = daily_price['Close'].shift(-1)/daily_price['Close']
daily_price['close_r2'] = daily_price['Close'].shift(-2)/daily_price['Close']
daily_price['close_r3'] = daily_price['Close'].shift(-3)/daily_price['Close']
daily_price['close_r4'] = daily_price['Close'].shift(-4)/daily_price['Close']
daily_price['close_r5'] = daily_price['Close'].shift(-5)/daily_price['Close']
daily_price['max_close'] = daily_price[['close_r1','close_r2','close_r3','close_r4','close_r5']].max(axis=1)
daily_price['mean_close'] = daily_price[['close_r1','close_r2','close_r3','close_r4','close_r5']].mean(axis=1)
daily_price['min_close'] = daily_price[['close_r1','close_r2','close_r3','close_r4','close_r5']].min(axis=1)
daily_price['buy_price'] = daily_price['Close']
daily_price['buy_low'] = daily_price['Low'].shift(-1) # 익일 저가
daily_price['buy_high'] = daily_price['High'].shift(-1) # 익일 고가
daily_price['buy'] = np.where((daily_price['buy_price'].between(daily_price['buy_low'], daily_price['buy_high'])), 1, 0) # 당일 종가로 익일 매수 가능한지 여부
outcome_data = pd.concat([outcome_data, daily_price], axis=0)
outcome = outcome_data.loc[today_dt][['code','buy','buy_price','buy_low','buy_high','max_close','mean_close','min_close']].set_index('code')
select_outcome = select_tops.merge(outcome, left_index=True, right_index=True, how='inner')
return select_outcome[['name','buy','buy_price', 'buy_low','buy_high','yhat','max_close','mean_close','min_close']]
2022년 4월 1일 - 종목 선정 및 수익률 테스트
상당이 고무적입니다. 모든 종목이 익절이 가능합니다. 단 CSA 코스믹은 전일 종가로 당일 매수가 불가능합니다. 2022년 4월 2일 갭상승으로 시작을 했습니다.
select_tops = select_stocks('2022-04-01')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-04-01', '2022-04-08') # 5 영업일
results.sort_values(by='buy').style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2021-12-22 00:00:00 2022-04-01
[*********************100%***********************] 1 of 1 completed
203
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
083660 | CSA 코스믹 | 0 | 2080 | 2100.000 | 2270.000 | 0.351 | 1.087 | 1.067 | 1.041 |
056090 | 에디슨INNO | 1 | 2560 | 2270.000 | 2670.000 | 0.540 | 1.273 | 1.127 | 0.930 |
024740 | 한일단조 | 1 | 3185 | 3185.000 | 3300.000 | 0.355 | 1.057 | 1.025 | 0.983 |
122690 | 서진오토모티브 | 1 | 3350 | 3330.000 | 3680.000 | 0.314 | 1.103 | 1.070 | 1.037 |
174880 | 장원테크 | 1 | 1990 | 1985.000 | 2225.000 | 0.305 | 1.116 | 0.988 | 0.925 |
2022년 4월 18일 - 종목 선정 및 수익률 테스트
4 월 18일은 인성정보는 수익권, 웨이버스는 손절로 대응이 필요합니다.
select_tops = select_stocks('2022-04-18')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-04-18', '2022-04-25') # 5 영업일
results.sort_values(by='buy').style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2022-01-08 00:00:00 2022-04-18
180
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
109820 | 진매트릭스 | 0 | 6690 | 6450.000 | 6670.000 | 0.311 | 0.978 | 0.943 | 0.903 |
089530 | 에이티세미콘 | 0 | 1940 | 1810.000 | 1915.000 | 0.310 | 1.134 | 1.037 | 0.951 |
033230 | 인성정보 | 1 | 2960 | 2930.000 | 3045.000 | 0.313 | 1.030 | 0.994 | 0.922 |
336060 | 웨이버스 | 1 | 2630 | 2535.000 | 2665.000 | 0.305 | 0.970 | 0.867 | 0.759 |
2022년 5월 2일 - 종목 선정 및 수익률 테스트
미래생명자원은 매수 후, 주가가 하락하는 것으로 나왔습니다. 다행이 급락 종목은 아니여서 손절로 대응하는 것이 좋을 것으로 판단됩니다.
select_tops = select_stocks('2022-05-02')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-05-02', '2022-05-10') # 5 영업일 (5월 5일 어린이날)
results.sort_values(by='buy').style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2022-01-22 00:00:00 2022-05-02
169
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
218150 | 미래생명자원 | 1 | 9690 | 9360.000 | 9870.000 | 0.430 | 0.991 | 0.918 | 0.863 |
014200 | 광림 | 1 | 2515 | 2445.000 | 2950.000 | 0.370 | 1.151 | 1.083 | 1.000 |
258610 | 케일럼 | 1 | 4575 | 4445.000 | 5100.000 | 0.327 | 1.045 | 0.999 | 0.954 |
2022년 5월 9일 - 종목 선정 및 수익률 테스트
5월 9일은 추천종목이 없습니다.
select_tops = select_stocks('2022-05-09')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-05-09', '2022-05-16') # 5 영업일 (5월 5일 어린이날)
results.sort_values(by='buy').style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2022-01-29 00:00:00 2022-05-09
348
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
218150 | 미래생명자원 | 1 | 9690 | 9360.000 | 9870.000 | 0.430 | 0.991 | 0.918 | 0.863 |
014200 | 광림 | 1 | 2515 | 2445.000 | 2950.000 | 0.370 | 1.151 | 1.083 | 1.000 |
258610 | 케일럼 | 1 | 4575 | 4445.000 | 5100.000 | 0.327 | 1.045 | 0.999 | 0.954 |
2022년 5월 25일 - 종목 선정 및 수익률 테스트
지더블유바이텍과 아이에스이커머스는 5영업일이내 익절이 가능할 것으로 보입니다. 조이시티와 상지카일룸은 대응이 필요합니다.
select_tops = select_stocks('2022-05-25')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-05-25', '2022-06-02') # 5 영업일 (6월 1일 지방선거)
results.sort_values(by='buy').style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2022-02-14 00:00:00 2022-05-25
144
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
069920 | 아이에스이커머스 | 1 | 6970 | 6960.000 | 7690.000 | 0.554 | 1.070 | 1.013 | 0.950 |
036180 | 지더블유바이텍 | 1 | 887 | 883.000 | 1045.000 | 0.413 | 1.125 | 1.074 | 1.025 |
005860 | 한일사료 | 1 | 8000 | 7670.000 | 8150.000 | 0.390 | 1.211 | 1.127 | 0.966 |
067000 | 조이시티 | 1 | 5990 | 5800.000 | 6180.000 | 0.335 | 0.990 | 0.977 | 0.967 |
227100 | 에이치앤비디자인 | 1 | 4430 | 4430.000 | 5640.000 | 0.335 | 1.411 | 1.341 | 1.221 |
104540 | 코렌텍 | 1 | 11550 | 11350.000 | 14800.000 | 0.327 | 1.139 | 1.099 | 1.065 |
042940 | 상지카일룸 | 1 | 1235 | 1170.000 | 1235.000 | 0.302 | 0.992 | 0.959 | 0.923 |
2022년 6월 2일 - 종목 선정 및 수익률 테스트
토탈소프트를 제외한 모든 종목이 익절이 가능할 것으로 보입니다.
select_tops = select_stocks('2022-06-02')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-06-02', '2022-06-10') # 5 영업일 (6월 6일 현충일)
results.sort_values(by='buy').style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2022-02-22 00:00:00 2022-06-02
125
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
069920 | 아이에스이커머스 | 1 | 7410 | 7270.000 | 7720.000 | 0.419 | 1.076 | 1.006 | 0.961 |
021880 | 메이슨캐피탈 | 1 | 668 | 652.000 | 685.000 | 0.323 | 1.157 | 1.087 | 1.016 |
014200 | 광림 | 1 | 2100 | 2040.000 | 2185.000 | 0.308 | 1.221 | 1.035 | 0.938 |
045340 | 토탈소프트 | 1 | 6540 | 6450.000 | 6740.000 | 0.305 | 1.031 | 0.981 | 0.945 |
2022년 6월 16일 - 종목 선정 및 수익률 테스트
2022년 6월 16일 추천종목은 20 종목이 넘습니다. 종목은 모델 스코어가 높은 5 종목만 선택하도록 하겠습니다. 한탑, 에이에프더블류, 베셀이 매수가 가능했습니다. 익절 가능할 것으로 예상됩니다.
select_tops = select_stocks('2022-06-16')
if select_tops is not None:
results = outcome_tops(select_tops, '2022-06-16', '2022-06-23') # 5 영업일 (6월 6일 현충일)
results.sort_values(by=['buy','yhat'], ascending=False).head(5).style.set_table_attributes('style="font-size: 12px"').format(precision=3)
2022-03-08 00:00:00 2022-06-16
[*********************100%***********************] 1 of 1 completed
395
name | buy | buy_price | buy_low | buy_high | yhat | max_close | mean_close | min_close | |
---|---|---|---|---|---|---|---|---|---|
code | |||||||||
002680 | 한탑 | 1 | 3155 | 3050.000 | 3595.000 | 0.457 | 1.132 | 1.029 | 0.805 |
177350 | 베셀 | 1 | 7630 | 7300.000 | 8280.000 | 0.413 | 1.054 | 1.006 | 0.920 |
312610 | 에이에프더블류 | 1 | 3505 | 3315.000 | 4555.000 | 0.397 | 1.185 | 1.054 | 0.874 |
317850 | 대모 | 1 | 10950 | 10250.000 | 11700.000 | 0.373 | 1.032 | 0.986 | 0.904 |
067010 | 이씨에스 | 1 | 3980 | 3800.000 | 4395.000 | 0.362 | 1.062 | 0.954 | 0.812 |