논문이랑 다르게 구현할 점
- 중국 주식 데이터 -> 한국 주식 데이터 (익숙하기 때문에)
- 분 단위 데이터 -> 일 단위 데이터 (한국 분단위 데이터를 구하기가 쉽지 않더라구요.)
- 프로그램에서 지표 출력 -> 코드로 지표 계산 (이왕이면 프로그램 사용 안하고 코딩으로 다 해결하면 재밌겠죠..?)
이 세가지가 가장 크게 다를 부분이구요. 혹시 논문과 다른 점이 있다면 이후에 표시하도록 하겠습니다.
1. Data
무엇을 하든 모델링에는 데이터가 필요합니다.
논문에서는 Dataset에 대해 다음과 같이 말했습니다.
4.1 Dataset
기간은 2016년 1월 1일 ~ 12월 31일, 244 거래일, 각 거래일은 242분. 그래서 총 59048분의 거래 데이터.
각 주식은 다음과 같은 기준으로 선정.
1. CSI 300에 속함. (CSI 300은 상하이와 심천 증권 거래소에서 거래되는 상위 300 개 주식의 실적을 재현)
2. Not suspended during the period (주가와 주가 예측값에 significant한 영향을 주는 경우를 제외했다)
3. High-frequency exchange의 변동성(volatility)를 보장하기 위해서 첫 시작 날짜 (1월 1일)의 종가가 30 이상
저도 위의 조건과 비슷하게 데이터를 수집해보겠습니다.
먼저 크롤링을 통해서 분단위 주가 데이터를 얻으려고 했습니다. 보통 1주일 이상의 데이터 까지 저장하는 경우가 별로 없다고 하더라구요. 그래서 불가피하게 일단위 주가 데이터를 사용하게 되었습니다. 위의 논문에서는 총 59048분의 거래 데이터 인데, 일단위 데이터를 수집하게 되면 그보다 훨씬 적은 데이터를 사용합니다.
1년에 거래일이 250일 전후, 9년의 데이터를 수집해서 2250일의 데이터 입니다. [2009년은 금융위기 이후에 코스피가 회복되는 기간이라 제외했습니다.]
1. KOSPI 200에 속해있는 데이터 사용
2. 주가나 주가 예측값에 심한 영향을 주는 경우는 제외
논문의 3번 조건의 경우 high-frequency data의 변동성을 보장하기 위해 종가가 30이상(현재 환율 기준으로 5000원 정도)인 주식만 포함시켰습니다. 그러나 이번 구현에서는 일 단위 데이터 이기 때문에 종가의 범위를 좀더 낮춰보겠습니다. 대신에 거래의 유용성을 위해 저 나름의 규칙을 추가해보겠습니다.
3. 일 평균 거래량이 x 이상
x의 값은 아직 정하질 않았습니다. 거래량이 있어야 실제 수익을 낼 수 있으며, 거래량이 있어야 기술적 지표를 보고 투자하는 사람들이 있을 거라고 생각했습니다. 실제로 기술적 지표 대신 재무적 지표를 보는 가치 투자자들의 경우에는 거래량이 극히 적은 가치주에 투자하시는 분들이 있습니다. 기술적 지표를 보시는 분은 거래량이 적을 경우 접근을 잘 하지 않을거라고 생각했습니다. 거래의 난이도도 어려울 것이구요. 어떻게 보면 논문의 내용처럼 변동성을 보장한다고도 생각할 수 있습니다.
주식 데이터 수집엔 여러가지 방법이 있고, 위의 조건에 해당 하는 주식만 추려내는 방법도 여러가지가 있습니다.
참고링크 : 파이썬으로 코스피200 종목코드 회사명 가져오기
위의 글에서 참고하여 코드를 작성했습니다.
import os
import re
import requests
from bs4 import BeautifulSoup
import FinanceDataReader as fdr
BaseUrl = "http://finance.naver.com/sise/entryJongmok.nhn?&page="
date_from = '2010'
date_to = '2019'
ksp200_codes = []
for i in range(1, 22, 1) :
try :
url = BaseUrl + str(i)
r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')
items = soup.find_all('td', {'class': 'ctg'})
for item in items :
txt = item.a.get('href')
k = re.search('[\d]+', txt)
if k :
code = k.group()
ksp200_codes.append(code)
except :
pass
if not os.path.exists("./data") :
os.makedirs("./data")
for i, symbol in enumerate(ksp200_codes) :
df = fdr.DataReader(symbol, date_from, date_to)
df.to_csv(path_or_buf="./data/{}_from_{}.csv".format(symbol, date_from),index=False, header=False)
개인적으로 jupyter notebook은 자주 사용하지 않아서, 한번에 실행 시킬 수 있는 형식으로 작성했습니다. [효율적인 코드가 아닐 수도 있고 영어가 이상할 수도 있습니다 ㅎㅎ;;]
위의 코드를 실행시키면 data 폴더에 2010년 부터 2018년 마지막 날짜까지 10년 간의 일자 데이터가 각 파일에 저장될 겁니다.
이제 위의 data 폴더에 들어가서 원하는 조건에 맞춰서 data를 선별해도 되고, 조건을 코드로 만들어서 작동시켜도 됩니다. 그리고 위의 코드를 변형시켜서 조건에 맞는 data만 저장하는 방법도 있습니다.
다음과 같은 조건들을 적용시켜보겠습니다.
1. 2010년 부터 데이터가 존재해야함.
총 거래일이 2220일 (2010년 1월 2일 ~ 2018년 12월 28일까지)
2. 2010년 첫 거래일의 종가가 4,000원 이상.
(논문에서는 첫 거래일의 종가가 30위안이어야 한다는 조건. 당시 5,000원 정도. 2010년의 원화 가치로 따지면 대략 4,100원 이어서 4,000원 이라는 기준을 만듬. (한국은행 기준금리로 계산))
3. 일평균 거래량이 40,000이상
많이 거래되는 날도 있고 적게 거래되는 경우도 있을텐데, 평균적으로 4만번도 거래가 일어나지 않을 경우... 유동성이 적다고 볼 수 있음. (거래가 많이 될때 x번 이상, 그 외에 평균적으로 x번 이상의 거래량 발생 등으로 설정 가능)
제가 생각하기에 손이 제일 많이 갈 수 있는 부분이 논문에서 말한 두번째 조건 입니다.
2. Not suspended during the period (주가와 주가 예측값에 significant한 영향을 주는 경우를 제외했다)
먼저 설정한 3가지의 조건을 거치면, KOSPI 200의 종목 갯수가 130개로 줄어듭니다. 130개의 종목 차트를 하나하나 보면서 예측이 불가능 한 사건들에 의해 주가가 영향을 받는 경우를 제외하는 방법이 정석이라고 생각합니다. 차트 뿐만이 아니라 주식이 상한가를 간다던가, 하한가를 갔을때 어떤 일이 있어나 하는 뉴스를 다 살펴봐야겠죠.
1차적으로 적용할 방법은 기간동안 상한가나 하한가 근처에 몇번이나 접근했는가를 확인하고, 그 횟수가 일정 횟수 이상이면 제외시키는 방법입니다.
if (len(df.iloc[:1349,5][df.iloc[:1349,5]>0.135]) +
len(df.iloc[:1349,5][df.iloc[:1349,5]<-0.135]) +
len(df.iloc[1349:,5][df.iloc[1349:,5]>0.27]) +
len(df.iloc[1349:,5][df.iloc[1349:,5]<-0.27])) < 7 :
2015년 6월 15일 이전에는 코스피의 상한가와 하한가가 15% 까지였지만, 이후에는 30%로 바뀌었습니다. 주가라는게 상한가나 하한가로 정확히 끝나는 경우는 드물기 때문에, 상한가와 하한가에 근접했다의 기준을 각각 기준의 90%로 잡았습니다.
위의 코드에서 1349는 2015년 6월 15일을 의미하고, 5번째 칼럼은 해당 거래일의 주가 변동(퍼센트) 입니다. 상한가와 하한가 근접한 횟수를 6회 이하로 기준을 잡았는데, 대략 1~2년에 한 두번을 의미합니다.
위의 내용들을 한번에 정리해둔 코드는 다음과 같습니다.
import os
import re
import requests
from bs4 import BeautifulSoup
import FinanceDataReader as fdr
BaseUrl = "http://finance.naver.com/sise/entryJongmok.nhn?&page="
date_from = '2010'
date_to = '2019'
ksp200_codes = []
for i in range(1, 22, 1) :
try :
url = BaseUrl + str(i)
r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')
items = soup.find_all('td', {'class': 'ctg'})
for item in items :
txt = item.a.get('href')
k = re.search('[\d]+', txt)
if k :
code = k.group()
ksp200_codes.append(code)
except :
pass
if not os.path.exists("./data") :
os.makedirs("./data")
n_saved_code = 0
for i, symbol in enumerate(ksp200_codes) :
df = fdr.DataReader(symbol, date_from, date_to)
if len(df) == 2220 :
if df["Close"][0] > 4000 :
if df["Volume"].mean() > 40000 :
if (len(df.iloc[:1349,5][df.iloc[:1349,5]>0.135]) +
len(df.iloc[:1349,5][df.iloc[:1349,5]<-0.135]) +
len(df.iloc[1349:,5][df.iloc[1349:,5]>0.27]) +
len(df.iloc[1349:,5][df.iloc[1349:,5]<-0.27])) < 7 : # 1349 : 2015-06-15 이후로 상한제 30%
df.to_csv(path_or_buf="./data/{}_from_{}.csv".format(symbol, date_from),index=False, header=False)
n_saved_code += 1
else :
pass
print("{} stock data saved.".format(n_saved_code))
다음 글에서는 수집한 데이터들을 어떻게 전처리하고 어떻게 batch를 만들지에 대해 작성해보도록 하겠습니다.