Abstract

많은이들이 개미들은 주식시장에서 성공할 수 없다고 말한다. 나또한 이에 어느정도 동의하는데, 이는 정보력, 자본금의 차이에서 기인한다고 생각한다.
그래서 예전부터 기관이나 외인들의 매매를 유심히 관찰해왔는데 생각보다 손해를 보는경우도 많았다. 예를들어 특정종목의 과거 외인, 기관의 순매수량을 관찰했을 때
이들의 순매수량이 높은 시기이후에 주가가 떨어지거나, 반대로 순매수량이 음수를 기록한 다음날 오르는 등의 case를 많이 관찰할 수 있었다. 이를통해 느낀 바는
단기적으로 이들의 매매형태를 트래킹하기보다는 좀더 거시적으로 접근해볼 필요가 있다는 생각이 들었다.

KRX 데이터 사용 이유

네이버에도 종목별 기관, 외인의 순매수량이 나와있지만, 각 페이지에 나와있는 정보가 한정적이라 많은 페이지를 탐색해야하기 때문에 KRX에서 excel파일로 제공하는 데이터를 이용하기로 했다. (통계-주식-거래실적-기관외국인 순매수 추이 탭)

Page scraping

이렇게 excel이나 csv 파일을 누르면 해당종목, 기간의 기관, 외인 거래량 엑셀파일을 받을 수 있다. 하지만 이를 하나하나 다운받는데는 시간이 너무 오래걸리기때문에 Python을 이용해 자동으로 데이터를 저장하고자 한다.
KRX와같은 사이트의 경우 버튼의 클릭 등으로 화면의 변화가생겨도 URL은 변화가 없기때문에 일반적인 GET방식이 아닌 POST방식을 이용해 데이터를 수집해야한다. 실제로 python의 라이브러리인 ‘finance datareader’의 경우 이와같은 POST형태로 KRX에서 데이터를 수집한다. github site-fdr
그래서 해당 깃에 공개된 코드를 이용해 데이터 수집에 활용했다.

import requests
import pandas as pd
#from urllib import parse
def getFullCode(code):
    checkNum = 0
    for idx, i in enumerate(code):
        if(idx%2==0):
            checkNum+=int(i)
        else:
            temp = int(i)*2
            if(temp>=10):
                temp = 1+temp%10
            checkNum +=temp
#    checkNum += 20#KR
    checkNum = checkNum%10
    if(checkNum!=0):
        checkNum = 10-checkNum
    code = f'KR7{code}00{checkNum}' #보통주(끝이0으로끝나면)면 00x
    return code

먼저 종목코드(ex 005930)를 유가증권표준코드(ex kr7005930003)로 바꿔주는 과정이 필요했다.(이유는 뒤에 설명) 그리고 종목코드의 맨 뒷자리가 0이 아닌 주식(우선주 등)은 유가증권표준코드로의 변환이 까다로워 제외했다. 코드 변환법
이제 본격적으로 데이터를 수집해보자.

code = '005930'
fromDate = '20200101' #시작일자

def tradeWho(code, fromDate):
    if(code[-1] !='0'):
        print('not valid code form, pleas enter xxxxx0')
        return -1
    fullCode = getFullCode(code)
    url = 'http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx?'\
    'name=fileDown&filetype=xls&url=MKD/13/1302/13020304/mkd13020304_01'\
    '&isu_cd='+fullCode+\
    '&isu_nm=%EC%A0%84%EC%B2%B4&'\
    'type=D&period_selector=day'\
    '&fromdate='+fromDate+'&todate=20201214&pagePath=%2Fcontents%2FMKD%2F13%2F1302%2F13020304%2FMKD13020304.jsp'
 
    header_data = {
    'User-Agent': 'Chrome/78.0.3904.87 Safari/537.36',
    }
    r = requests.get(url, headers=header_data)
		form_data = {'code': r.text}

이부분은 POST에 요청할 data부분을 추출하는 것이다. 여기서 핵심은 url인데 excel데이터를 요청하는 과정을 검사해서 url에 어떤 값들을 넣어야 하는지를 알 수 있다. 먼저 ctrl+shift+i를 통해 검사창-network에 들어간 다음 화면의 excel버튼을 누르면 network의 변화를 알 수 있다. 위의 경우 GenerateOTP~와 download.jspx 두개의 파일이 요청된것을 알 수 있고, download.jspx의 url은 http://file.krx.co.kr/download.jspx으로 획일화된 것으로 보아 GenerateOTP를 download.jspx에 넣으면 해당하는 데이터를 다운받는 과정인 것으로 예상할 수 있다. 그래서 GenerateOTP에서 요청하는 정보들을 확인하면 name, filetype, isu_cd, isu_nm 등의 정보를 입력해야함을 알 수 있다. 그래서 나온것이 위 코드의 url이다.
또한 isu_cd는 KR70~형태의 유가증권표준코드이기때문에 위에서 종목코드를 변환하는 과정을 거쳤다. isu_nm은 종목이름으로 필수적으로 들어가야되는 값인데 parse.quote(“전체”)=%EC%A0%84%EC%B2%B4 값을 넣어도 작동이 잘되기에 그냥 이값을 사용했다.

    url = 'http://file.krx.co.kr/download.jspx'

    header_data = {
            'Referer': 'http://marketdata.krx.co.kr/contents/MKD/13/1302/13020304/MKD13020304.jsp',
            'User-Agent': 'Chrome/78.0.3904.87 Safari/537.36',
        }
    r = requests.post(url, data=form_data, headers=header_data)
    df = pd.read_excel(io.BytesIO(r.content))
    df = df[:-1]
    df.columns = ['Date','Close','Change','Volume','inst_buy',
                     'inst_sell','inst_sum','foreign_buy','foreign_sell','foreign_sum']
    df.iloc[:,1] = df.iloc[:,1].str.replace(',','').astype(int)
    if(type(df['Change'][0]) == str):
        nc = 2
    else:
        nc=3
    df.iloc[:,nc:] = df.iloc[:,nc:].apply(lambda x:x.str.replace(',','').astype(int),axis=1)
    return df 

이제 GenerateOTP에서 제공한 값을 download.jspx에 POST방식을 통해 요청하면 최종 데이터프레임을 얻을 수 있다.
한개의 종목코드를 요청하는데 수초이상이 걸리므로 데이터를 저장해놓은것이 좋을것같아 추가적으로 pymysql을 통해 mysql database에 저장하는 과정까지 진행했다.

import tqdm
from sqlalchemy import create_engine
from pandas.io import sql

PASSWORD='{your password}'
engine = create_engine("mysql+pymysql://{user}:{pw}@localhost/{db}"
                       .format(user="root",
                               pw=PASSWORD,
                               db="stock"))
codesFiltered=['095570', '006840','027410','282330', '138930', '001460', '001040'] #원하는 종목코드들(끝자리가 0인것)
														 
fromDate = '20200101'
for code in tqdm(codesFiltered):
    fullCode = getFullCode(code)
    df = tradeWho(code,fromDate)
    df.to_sql('c'+str(code), con=engine, if_exists="append",chunksize=1000, 
         index=False)