종목 추천 프로세스#

완성된 모델을 이용하여 종목 추천을 받는 프로세스를 순서대로 만들어보겠습니다. 오늘이 2022년 4월 1일라고 가정하고 어떤 종목들이 추천되는 지 보겠습니다. 4월1일 장 마감 후 프로그램을 돌려 추천 종목을 받고, 익일(4월 2일) 날 4월 1일의 종가에 매수를 하는 전략입니다.

import FinanceDataReader as fdr
import matplotlib.pyplot as plt
%matplotlib inline

import pandas as pd
import numpy as np
import requests
import bs4

pd.options.display.float_format = '{:,.3f}'.format


오늘이 2022년 4월 1일라고 가정하고 어떤 종목들이 추천되는 지 보겠습니다. 먼저 오늘 기준으로 100 일전 날짜를 timedelta 를 이용해 찾습니다.

import datetime
today_dt = '2022-04-01'
today = datetime.datetime.strptime(today_dt, '%Y-%m-%d')
start_dt = today - datetime.timedelta(days=100) # 100 일전 데이터 부터 시작 - 피쳐 엔지니어링은 최소 60 개의 일봉이 필요함
print(start_dt, today_dt)
2021-12-22 00:00:00 2022-04-01


위 코드에서 찾은 시작일부터 오늘까지 종목별로 일봉을 가져와서 데이터셋을 구성합니다. 총 67 개의 일봉이 있습니다. 입력 피처를 생성하기 위해서는 최소한 60일의 데이터가 필요합니다.

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() # 컬럼 이름 소문자로 변경

print(price_data.index.nunique())
67


주가지수 데이터를 가져오고, 일봉데이터에 추가합니다. 그리고 결과물을 merge 라는 이름으로 저장합니다. FinanceDataReader 에서 지수데이터가 수집이 안 될 경우, 야후 파이낸스를 이용할 수 도 있습니다.

kosdaq_index = fdr.DataReader('KQ11', start = start_dt) # 데이터 호출
kosdaq_index.columns = ['close','open','high','low','volume','change'] # 컬럼명 변경
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')
merged.to_pickle('merged.pkl')

야후 파이낸스에서 지수 데이터 수집은 아래와 같이 할 수 있습니다.

import yfinance as yf

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')
merged.to_pickle('merged.pkl')
[*********************100%***********************]  1 of 1 completed


주가 지수 수익률과 종목별 수익율을 비교한 결과를 win_market 이라는 변수에 담습니다.

merged = pd.read_pickle('merged.pkl')

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)    


데이터가 잘 생성되었는 지 확인해 봅니다.

return_all.head().style.set_table_attributes('style="font-size: 12px"').format(precision=3)
  open high low close volume change code name kosdaq_return return win_market
date                      
2021-12-23 00:00:00 3195 3260 3195 3220 104180 -0.002 060310 3S 1.003 0.998 0
2021-12-24 00:00:00 3230 3355 3220 3290 238933 0.022 060310 3S 1.004 1.022 0
2021-12-27 00:00:00 3290 3380 3275 3305 130826 0.005 060310 3S 1.004 1.005 0
2021-12-28 00:00:00 3355 3355 3180 3190 267316 -0.035 060310 3S 1.016 0.965 0
2021-12-29 00:00:00 3200 3350 3200 3330 115094 0.044 060310 3S 1.001 1.044 0


모델에 입력할 변수를 생성합니다.

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() # 지난 60 일 동안 장대양봉의 갯 수
    
    
     # 거래량이 종좀 터지며 매집의 흔적을 보인다   
    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 값 있는 행 모두 제거

model_inputs.to_pickle('model_inputs.pkl')


모델에 입력할 변수를 생성하고 X 에 담습니다.

# 최종 피처만으로 구성
model_inputs = pd.read_pickle('model_inputs.pkl')
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'] + feature_list].set_index('code') # 오늘 날짜 2022년 4월 1일 데이터만
X.head().style.set_table_attributes('style="font-size: 12px"').format(precision=3)
  name return price_z volume_z num_high/close num_win_market pct_win_market return over sector
code                
238490 힘스 0.997 -1.290 -0.510 0.000 6.000 1.000 1.002
037440 희림 0.981 0.144 -0.839 0.000 12.000 1.009 0.970
189980 흥국에프엔비 1.004 0.304 -0.555 0.000 14.000 1.002 1.000
010240 흥국 0.996 0.962 1.153 0.000 8.000 1.000 1.001
024060 흥구석유 1.011 -0.838 -0.591 0.000 15.000 1.006 1.012


저장한 GAM 모델을 불러 읽고, 입력변수를 넣어 예측값을 생성합니다. 입력변수의 순서는 모델에 사용한 입력변수와 동일해야 합니다. X 라는 데이터 프레임에 예측값 yhat 이 추가되었습니다.

import pickle
with open("gam.pkl", "rb") as file:
    gam = pickle.load(file)     
    
yhat = gam.predict_proba(X[feature_list])
X['yhat'] = yhat
X.head().style.set_table_attributes('style="font-size: 12px"').format(precision=3)
  name return price_z volume_z num_high/close num_win_market pct_win_market return over sector yhat
code                  
238490 힘스 0.997 -1.290 -0.510 0.000 6.000 1.000 1.002 0.205
037440 희림 0.981 0.144 -0.839 0.000 12.000 1.009 0.970 0.297
189980 흥국에프엔비 1.004 0.304 -0.555 0.000 14.000 1.002 1.000 0.218
010240 흥국 0.996 0.962 1.153 0.000 8.000 1.000 1.001 0.237
024060 흥구석유 1.011 -0.838 -0.591 0.000 15.000 1.006 1.012 0.290


어떤 종목이 높은 스코어를 받았는지 궁금합니다. 스코어의 내림차순 정렬한 후 종목을 확인해 봅니다.

X.sort_values(by='yhat', ascending=False).head(5).style.set_table_attributes('style="font-size: 12px"').format(precision=3)
  name return price_z volume_z num_high/close num_win_market pct_win_market return over sector yhat
code                  
056090 에디슨INNO 1.080 -1.621 1.764 3.000 16.000 1.026 1.064 0.540
145020 휴젤 0.868 -3.379 7.109 0.000 6.000 0.997 0.868 0.497
185490 아이진 0.908 -2.249 2.269 0.000 6.000 0.991 0.920 0.490
069920 아이에스이커머스 1.300 2.396 3.518 2.000 15.000 1.019 1.224 0.469
010280 쌍용정보통신 1.062 2.993 7.584 1.000 11.000 1.004 1.066 0.455


그리고 필터링을 적용해서 최종 종목을 선정합니다. 최종적으로 5 개의 종목이 선정되었습니다. 우리는 4월 1일 이후에 주가 흐름을 알고 있습니다. 4월 2일이후 데이터를 추가하여 선택된 종목들이 유의미한지 점검해 보겠습니다.

tops = X[X['yhat'] >= 0.3].copy() # 스코어 0.3 이상 종목만 
print(len(tops))
select_tops = tops[(tops['return'] > 1.03) & (tops['price_z'] < 0)][['name','return','price_z','yhat','return']]          
select_tops.style.set_table_attributes('style="font-size: 12px"').format(precision=3)
203
  name return price_z yhat return
code          
024740 한일단조 1.062 -0.823 0.355 1.062
174880 장원테크 1.090 -0.366 0.305 1.090
056090 에디슨INNO 1.080 -1.621 0.540 1.080
122690 서진오토모티브 1.058 -0.114 0.314 1.058
083660 CSA 코스믹 1.035 -0.094 0.351 1.035
outcome_data = pd.DataFrame()

today_dt = '2022-04-01'
end_dt = '2022-04-08'

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']   # 4월 1일 종가 매수한 후, 4월 4일 수익율
    daily_price['close_r2'] = daily_price['Close'].shift(-2)/daily_price['Close']   # 4월 1일 종가 매수한 후, 4월 5일 수익율
    daily_price['close_r3'] = daily_price['Close'].shift(-3)/daily_price['Close']   # 4월 1일 종가 매수한 후, 4월 6일 수익율
    daily_price['close_r4'] = daily_price['Close'].shift(-4)/daily_price['Close']   # 4월 1일 종가 매수한 후, 4월 7일 수익율
    daily_price['close_r5'] = daily_price['Close'].shift(-5)/daily_price['Close']   # 4월 1일 종가 매수한 후, 4월 8일 수익율

    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) # 4월 2일 매수일, 4월 1일 종가에 살 수 있는 지 여부
    daily_price['target'] = np.where(daily_price['max_close']>=1.05, 1, 0)    
    
    outcome_data = pd.concat([outcome_data, daily_price], axis=0)  


최종 선정된 종목들의 결과가 궁금합니다. 선정된 종목 데이터에 결과 데이터를 병합합니다. 두 데이터셋의 인덱스는 종목이어야 병합이 가능합니다. 5% 익절할 확률은 83.3% 로 높게 나왔습니다. 최저 수익률의 평균은 .98 로 리스크도 비교적 낮은 것으로 보입니다. 2022년 4월 1일 매수한 종목은 수익권으로 예상이 됩니다. 물론 모든 날짜에 대하여 동일한 결과가 나오지는 않습니다.

outcome = outcome_data.loc[today_dt][['code','buy','buy_price','buy_low','buy_high','max_close','mean_close','min_close','target']].set_index('code')
select_outcome = tops.merge(outcome, left_index=True, right_index=True, how='inner')
select_outcome[['yhat','buy','max_close','mean_close','min_close']].mean()
yhat         0.375
buy          0.800
max_close    1.127
mean_close   1.055
min_close    0.983
dtype: float64


buy 는 4월 1일 종가에 4월 2일 매수할 수 있는 기회가 있는 지를 알려주는 Flag 입니다. CSA 코스믹은 4월 2일 갭상승으로 시작했습니다. 4월 1일 종가에 살 수 있는 기회가 없습니다.

select_outcome[['name','buy','buy_price', 'buy_low','buy_high','yhat','max_close','mean_close','min_close']].style.set_table_attributes('style="font-size: 12px"').format(precision=3)
  name buy buy_price buy_low buy_high yhat max_close mean_close min_close
code                  
024740 한일단조 1 3185 3185.000 3300.000 0.357 1.057 1.025 0.983
174880 장원테크 1 1990 1985.000 2225.000 0.302 1.116 0.988 0.925
056090 에디슨INNO 1 12800 11350.000 13350.000 0.542 1.273 1.127 0.930
122690 서진오토모티브 1 3350 3330.000 3680.000 0.321 1.103 1.070 1.037
083660 CSA 코스믹 0 2080 2100.000 2270.000 0.353 1.087 1.067 1.041

2022년 4월 1일 추천받은 종목들의 일봉 차트를 보겠습니다. CSA 코스믹은 전일 종가로 당일 매수가 불가능합니다. 2022년 4월 2일 갭상승으로 시작을 했습니다. 에디슨 INNO 는 4월 2일 매수 후 익절할 기회를 제공하고 있습니다.


한일단조

GET_IMAGE


장원테크

GET_IMAGE


에디슨INNO

GET_IMAGE


서진오토모티브

GET_IMAGE


CSA 코스믹

GET_IMAGE