대신증권 CybosPlus API로 전체 종목 과거 주가 데이터 가져오기

2021. 4. 21. 18:21데이터 분석/주식투자 실험실

728x90

 

알고리즘 트레이딩이나 주가 분석을 위해서 가장 먼저 할 일이 주식 종목 리스트와 주가정보를 가져오는 것인데요. 주식 종목 리스트에 비해서 주가정보는 데이터 양이나 가져올 항목이 훨씬 많아서 좀 더 신경쓸 점이 많습니다.

 

주식 데이터를 가져오는 여러가지 방법 중에 저는 대신증권 CybosPlus API를 활용하였는데요. 다른 API에 비해 아래와 같은 장점이 있습니다.

 

 

1. 수정주가 제공

 

주식은 여러 기업의 이벤트들로 인해 액면가나 기준가가 변경되는 경우들이 있습니다. 가장 대표적으로는 2019년의 삼성전자 액면 분할이 있겠고, 그 밖에도 많은 회사들이 증자, 감자, 배당과 같은 사유로 주가등락과는 별도로 주식 가격 자체가 크게 변화하는 경우들이 꽤 있습니다.

이러한 이벤트들이 발생할 경우 주가변화가 연속성을 잃어버리기 때문에 일반 주가로 계산하면 틀린 결과를 나타내게 됩니다. 따라서 이벤트 전후 주가가 연속성을 가질 수 있도록 보정한 주가를 '수정주가'라고 하며 일반적으로 분석을 위해서는 이 수정주가를 활용해야 합니다. 

 

대신증권 CybosPlus API는 일반주가, 수정주가 여부 파라미터 변경만으로 직접 보정없이도 수정주가를 가져올 수 있기 때문에 데이터 활용 편의성과 신뢰성 측면에서 유리하다고 볼 수 있습니다. 

 

 

2. 시간 당 호출횟수가 많음

 

대신증권의 일일주가 호출횟수 제한은 15초에 60건(종목)으로, 이베스트투자증권 Xing API의 10분당 200건에 비해 같은 시간 동안 10배 정도 많은 데이터를 가져올 수 있습니다. 코스피, 코스닥 전체 종목 숫자가 약 3,000개 인 것을 생각하면 약 13분 정도면 전 종목 데이터를 가져올 수 있는 장점이 있습니다.

 

 

그래서 이번 글에서는 대신증권 CybosPlus API를 활용해서 Python으로 KOSPI, KOSDAQ 전 종목에 대한 과거 일일 주가 정보를 가져오고 CSV 파일로 저장하는 코드를 알아봅니다.

 

 

CybosPlus API의 기본적인 활용 방법은 '파이썬으로 배우는 알고리즘 트레이딩 (개정판)' 또는 같은 책의 인터넷 버전에서 친절하게 설명되어있어서, 저는 이 사이트의 코드를 응용해서 작성하였습니다. 그래서 제가 따로 작성한 부분을 중심으로 설명드리며, 기본적인 API 구조나 명령어는 아래 사이트를 참고하시면 되겠습니다.

 

'파이썬으로 배우는 알고리즘 트레이딩 (개정판)' 인터넷 버전은 아래 위키독스 사이트 메인 화면에서 이 책이 메인에 나와있으니 클릭하면 보실 수 있습니다.

 

wikidocs.net

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

 

그리고 CybosPlus API를 사용할 때 함수나 각종 파라미터 정보 확인이 필요한데, 아래의 도움말 사이트에서 정보를 확인할 수 있으니 API를 활용할 때 수시로 참고하면 됩니다.

 

cybosplus.github.io/

 

CybosPlus Help for Python (비공식)

 

cybosplus.github.io

 

마지막으로 코드 개발환경은 아래와 같이 Anaconda로 파이썬을 설치하여 활용하였습니다. CybosPlus API가 요즘 컴퓨터 환경과 맞지 않게 python 32bit 버전만 지원하기 때문에, 32bit와 64bit 버전을 왔다갔다 하기 편리한 아나콘다를 활용합니다.

 

- OS : Windows 10

- 개발환경 : Anaconda 가상 Python 환경 (Anaconda Navigator 활용) 

- 개발도구 : Jupyter Notebook

- Python 버전 : 3.8 (32비트 버전 / 64비트 버전 두 가지)

 

 

 


 

1> 주가 데이터 가져오는 기본 코드 간단 설명 

 

'파이썬으로 배우는 알고리즘 트레이딩 (개정판)' 책에서 설명하는 과거 데이터 가져오는 코드는 아래와 같습니다. 이 코드의 구조는 크게 네 부분으로 구성됩니다.

 

1. 처음에 차트 데이터를 가져오기 위한 'CpSysDib.StockChart' 클래스를 불러오기

2. SetInputValue() 함수에 종목코드와 가져올 기간, 가져올 필드 정보(시가, 종가, 거래량 등)을 입력

3. 데이터 요청 및 헤더값 받기

4. 받은 데이터를 반복문을 통해 화면에 표시 

 

import win32com.client

# Create object
instStockChart = win32com.client.Dispatch("CpSysDib.StockChart")

# SetInputValue
instStockChart.SetInputValue(0, "A003540")
instStockChart.SetInputValue(1, ord('2'))
instStockChart.SetInputValue(4, 10)
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8))
instStockChart.SetInputValue(6, ord('D'))
instStockChart.SetInputValue(9, ord('1'))

# BlockRequest
instStockChart.BlockRequest()

# GetHeaderValue
numData = instStockChart.GetHeaderValue(3)
numField = instStockChart.GetHeaderValue(1)

# GetDataValue
for i in range(numData):
    for j in range(numField):
        print(instStockChart.GetDataValue(j, i), end=" ")

 

이 코드는 한 종목의 10영업일 동안의 주가 데이터를 화면에 뿌려주는 기능을 하기 때문에, 전 종목에 대해 수 년치 데이터를 가져오기 위해 위 코드를 수정합니다.

 

 


 

2> CpSysDib.StockChart 제공 항목 확인

 

CpSYsDib.StockChart를 통해 일일, 주간, 월간 및 분 단위, 틱 단위 차트 데이터를 가져올 수 있는데, 가져올 수 있는 항목(필드)이 많습니다. 여기서 어떤 항목을 가져올 지 도움말에서 확인하고, API에서는 가져올 항목만 선택하면 됩니다.

 

이번에 작성한 코드에서 가져오는 정보는 아래와 같습니다.

 

0: 날짜(ulong)

2: 시가(long or float)

3: 고가(long or float)

4: 저가(long or float)

5: 종가(long or float)

8: 거래량(ulong or ulonglong) 주) 정밀도 만원 단위

9: 거래대금(ulonglong)

12: 상장주식수(ulonglong)

13: 시가총액(ulonglong)

17: 외국인현보유비율(float)

20: 기관순매수(long)

21: 기관누적순매수(long)

 

 

가져올 항목을 결정했으면, 데이터프레임 형태로 저장하기 위해 먼저 아래와 같이 컬럼명을 정의한 리스트를 만듭니다.

 

column_dailychart = ['code', 'section', 'date', 'open', 'high', 'low', 'close',
                     'vol', 'value', 'n_stock', 'agg_price', 'foreign_rate','agency_buy', 'agency_netbuy']

 

 

 


 

3> 모든 종목에 대한 차트 데이터 요청하기

 

맨 처음 코드에서는 하나의 종목에 대한 차트 정보를 요청하였습니다. 이것을 전 종목에 대해 요청하기 위해, 이전 글에서 작성한 코드를 활용해 저장한 전 종목 데이터(stockitems.csv)를 불러온 다음 반복문으로 종목코드를 하나씩 바꿔가면서 차트 데이터를 요청합니다.

 

(전 종목 데이터를 불러오는 방법은 이전 글을 참고 부탁드립니다. https://ellun.tistory.com/326 )

 

import win32com.client
import pandas as pd 
import time
import datetime

column_dailychart = ['code', 'section', 'date', 'open', 'high', 'low', 'close',
                     'vol', 'value', 'n_stock', 'agg_price', 'foreign_rate','agency_buy', 'agency_netbuy']

stockitems = pd.read_csv('stockitems.csv')

instStockChart = win32com.client.Dispatch("CpSysDib.StockChart")

row = list(range(len(column_dailychart)))
rows = list()

instStockChart.SetInputValue(1, ord('1'))
instStockChart.SetInputValue(2, (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"))
instStockChart.SetInputValue(3, '20180101')
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8, 9, 12, 13, 17, 20, 21))
instStockChart.SetInputValue(6, ord('D'))
instStockChart.SetInputValue(9, ord('1'))
    
for idx, stockitem in stockitems.iterrows():
    instStockChart.SetInputValue(0, stockitem['code'])
    
    # BlockRequest
    instStockChart.BlockRequest()

    # GetHeaderValue
    numData = instStockChart.GetHeaderValue(3)
    numField = instStockChart.GetHeaderValue(1)

    # GetDataValue
    for i in range(numData):
        row[0] =  stockitem['code']
        row[1] = stockitem['section']  # 코스피, 코스닥, ETF 여부
        row[2] = instStockChart.GetDataValue(0, i)  # 날짜
        row[3] = instStockChart.GetDataValue(1, i)  # 시가
        row[4] = instStockChart.GetDataValue(2, i)  # 고가
        row[5] = instStockChart.GetDataValue(3, i)  # 저가
        row[6] = instStockChart.GetDataValue(4, i)  # 종가
        row[7] = instStockChart.GetDataValue(5, i)  # 거래량
        row[8] = instStockChart.GetDataValue(6, i)  # 거래대금
        row[9] = instStockChart.GetDataValue(7, i)  # 상장주식수
        row[10] = instStockChart.GetDataValue(8, i)  # 시가총액
        row[11] = instStockChart.GetDataValue(9, i)  # 외국인 보율비율
        row[12] = instStockChart.GetDataValue(10, i)  # 기관순매수
        row[13] = instStockChart.GetDataValue(11, i)  # 기관누적순매수
        rows.append(list(row))

먼저 instStockChart.SetInputValue(0, '종목코드') 부분에서 '종목코드' 값 대신 stockitems에서 종목코드에 해당하는 필드인 stockitem['code']로 변경합니다.

 

그리고 데이터를 요청하고 가져오는 부분을 for 문으로 한 번 더 감쌉니다. 여기서 가져올 기간이나, 타입 등 종목에 따라 변화하지 않는 부분은 for문 바깥으로 빼고, 종목코드가 바뀌는 부분만 for문 안으로 집어넣습니다. 이렇게 해서 stockitems에 저장된 모든 종목에 대해 차트 데이터를 요청할 수 있습니다.

 

차트 데이터를 가져오는 기간을 기존 코드와는 다르게 시작날짜와 마지막날짜를 각각 지정하는 방식으로 변경하였습니다. 예를 들어서 아래 코드의 경우 시작날짜는 '20180101'이 되며, 마지막 날짜는 고정된 날짜가 아닌 현재 시각을 기준으로 전날 날짜로 하기 위해 아래와 같이 datetime 라이브러리를 활용하였습니다.

 

당일이 아닌 전날까지만 데이터를 받는 이유는 당일에는 장 마감(15시) 이후에도 시간외 거래가 계속 업데이트되며, 그 밖의 정보들도 다음날이 되어서야 완전히 업데이트되는 경우가 많기 때문입니다.

 

import datetime

instStockChart.SetInputValue(1, ord('1'))
instStockChart.SetInputValue(2, (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"))
instStockChart.SetInputValue(3, '20180101')

 

두 번째 for문이 시작되는 부분부터는 차트 데이터 요청에 대한 회신 결과를 list타입의 rows 변수에 넣는 과정입니다.

row에 가져온 정보를 위치에 맞게 차례차례 입력시키고 이를 rows에 추가합니다.

 

instStockChart.SetInputValue(5, (0,1,2,3,...)) 에서 요청한 항목(필드)에 따라 저장되는 데이터도 달라지게 됩니다.

 

 


 

4> 최대요청횟수 초과 방지

 

맨 처음 언급했던 내용과 같이 API는 과도한 트래픽 발생 제한을 위해 일정 시간 동안 최대로 요청할 수 있는 횟수 제한이 있습니다. 차트정보 요청의 경우 15초에 최대 60회만 가능하기 때문에, 위의 코드를 그대로 실행시키면 중간중간 횟수제한을 넘겨서 다시 초기화될 때 중간중간 대기를 합니다. 

 

위의 코드에서 남은 요청을 표시하는 코드를 삽입해서 다시 돌려봤을 때 아래와 같이, 남은 요청이 60부터 줄다가 남은 요청이 0이 되면, 10초 정도 멈춘 후에 남은 요청이 다시 60으로 리셋되면 다시 자동으로 코드실행이 재개됩니다.

그런데 이렇게 요청횟수 초과 및 대기가 몇 번 반복되다 보면 아래와 같은 메시지 창이 뜨는데요.

 

'고객님의 계좌등급으로는 많은 시세 데이터를 받는 데는 제한이 있습니다.' 라고 하는군요.

대신증권에 아무 거래실적 없이 모의투자 계정으로 이용했더니 이런 현상이 발생합니다.

 

여기서 확인을 누르면 다시 실행이 재개되긴 합니다만, 앞으로 이런 메시지가 계속 뜰 수도 있고 잘못하면 증권사에서 악성유저로 판단하고 장기간 차단 등의 불이익이 있을 수 있기 때문에, 최대요청횟수를 초과하기 전에 미리 카운트해서 적당히 멈췄다가 다시 실행하도록 코드를 수정할 필요가 있습니다. 

 

 

CybosPlus API에서 남은 요청횟수는 GetLimitRemainCount() 함수로 확인할 수 있습니다.

여기서 파라미터는 0, 1, 2 세 개 중 하나를 선택하는데, 0은 주문관련 요청 남은 횟수 조회이고, 1은 시세조회관련 남은 횟수 조회입니다. 여기서는 주가 데이터를 조회하므로 아래와 같이 파라미터는 1로 하면 됩니다. 

nCpCybos = win32com.client.Dispatch("CpUtil.CpCybos")

nCpCybos.GetLimitRemainCount(1) # 0: 주문관련 요청 / 1: 시세조회관련 요청

 

그리고 for문 안 시작부분에 남은요청횟수(remain_request_count)를 체크하고 이것이 0이 되면, 다시 초기화될 때 까지 대기하는 코드를 while문을 이용해 작성합니다.

import time

for idx, stockitem in stockitems.iterrows():

    remain_request_count =  nCpCybos.GetLimitRemainCount(1)
    print(stockitem['code'], stockitem['name'], '남은 요청 : ', remain_request_count)
    
    if remain_request_count == 0:
        print('남은 요청이 모두 소진되었습니다. 잠시 대기합니다.')

        while True:
            time.sleep(2)
            remain_request_count =  nCpCybos.GetLimitRemainCount(1)
            if  remain_request_count > 0:
                print('작업을 재개합니다. (남은 요청 : {0})'.format(remain_request_count))
                break
            print('대기 중...')
            
	...

 

whlie True: 코드는 무한 루프를 만들기 위해 사용하며, while 문 안에서 특정 조건을 만족했을 때에만 반복문을 강제 이탈하는 용도로 사용합니다. 남은 요청이 0이 된 경우 time.sleep(2)를 통해 2초간 대기한 다음 다시 남은 요청을 조회하고, 남은 요청(remain_request_count)이 0보다 커질 경우 while에서 이탈하여 다음 코드를 진행하며, 그대로 0인 경우 while문을 반복하여 2초간 대기를 반복하는 방식입니다.

 

이렇게 하면 API에서 요청한도초과 되기 이전에 0에서 멈추도록 하여, 잦은 한도초과로 블랙리스트가 되는 불상사를 막을 수 있습니다.

 

위의 코드를 적용하여 실행하면 아래와 같이 한도를 초과하지 않고 조회-대기-조회-대기를 안정적으로 반복할 수 있습니다. 

 

 


 

5> 데이터 확인 및 dataframe으로 저장

 

전체 데이터를 가져오고나면 제대로 가져와졌는지 확인하고 저장하면 됩니다. 특히 장기간 데이터는 누락이 발생할 수 있기 때문에 데이터 확인이 필요합니다.

 

예를 들어서 한 종목의 주가에 대해 12개 필드를 2018-1-1부터 2021-4-15까지 약 3년치 데이터를 조회하면 2018-1-2 부터 정상적으로 가져와지나, 그 두 배가 넘는 2014-1-1부터 2021-4-15까지 약 7년치 데이터를 조회하면 2014-7-8까지만 조회되고 그 이전 기간은 누락되는 것을 알 수 있습니다. 

 

반면 조회할 필드를 12개에서 9개로 줄이면 다시 2014년 1월부터 모두 조회가 되는데요. 이로 미루어보았을 때, CybosPlus API에서 한 종목의 차트 데이터를 조회할 때 최대로 불러올 수 있는 데이터(행(기간)x열(필드 수))에 한계가 있음을 알 수 있습니다. 

instStockChart.SetInputValue(0, 'A000020')
instStockChart.SetInputValue(1, ord('1'))
instStockChart.SetInputValue(2, '20210415')
instStockChart.SetInputValue(3, '20180101')
#instStockChart.SetInputValue(4, 1800)
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8, 9, 12, 13, 17, 20, 21))

> ...
  20180104 10050 10050 9680 9750 161342 1590000000 27931000 272327000000 11.520000457763672 0 -232017 
  20180103 9900 10250 9820 10000 268220 2684000000 27931000 279310000000 11.670000076293945 0 -232017 
  20180102 9750 9900 9700 9870 120676 1181000000 27931000 275678000000 11.680000305175781 8 -232017 

...
instStockChart.SetInputValue(2, '20210415')
instStockChart.SetInputValue(3, '20140101')
  
> ...
  20140709 5460 5530 5410 5530 32329 177000000 27931000 154458000000 5.320000171661377 0 0 
  20140708 5470 5550 5430 5530 33871 185000000 27931000 154458000000 5.320000171661377 0 0 
  
...
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8, 9, 12, 13))

> ...
  20140103 4420 4575 4420 4540 107190 484000000 27931000 126806000000 
  20140102 4440 4510 4410 4440 99452 444000000 27931000 124013000000 

 

따라서 6-7년 이상의 장기간 데이터를 가져올 때는 해당 호출이 데이터 한계를 초과하지 않도록 해야합니다. 만약 한 번에 불러올 수 없는 범위의 데이터를 가져와야 할 경우에는 전체 코드를 기간을 나눠서 두 번 실행한 다음 따로 만들어진 파일을 다시 하나로 합치는 방식으로 구현하면 됩니다.

 

 

모든 차트 정보가 저장된 리스트(rows)를 csv파일로 저장하는 방법은 여러가지가 있으나, 저는 pandas 라이브러리의 dataframe으로 변환 후 csv파일로 저장하였습니다.

pd.DataFrame() 안에 data=rows로, columns으로 맨 처음 정의했던 column_dailychart를 입력합니다.

import pandas as pd

dailychart= pd.DataFrame(data = rows, columns= column_dailychart)
dailychart[['value' ,'agg_price']] = (dailychart[['value' ,'agg_price']]/1000000).astype(int)
dailychart = dailychart.sort_values(by=['code','date'])
dailychart.to_csv('dailychart.csv', index=False)

 

그 다음 거래대금을 의미하는 'value' 컬럼과 시가총액을 뜻하는 'agg_price'가 숫자 크기가 너무 커서 1백만으로 나눠주는 변환처리를 하였습니다.

그리고 CybosPlus API는 최근날짜부터 오래된 날짜순서로 데이터를 주기 때문에, 저는 과거에서 최근 순으로 다시 정렬하기 위해 종목코드(code)와 날짜(date)를 기준으로 sort_vales() 함수를 통해 재정렬하였습니다. 그 다음 to_csv()를 거치면 전체 종목에 대한 일일 주가 데이터가 csv로 생성됩니다.

 

 

이상의 수정한 내용을 반영하여 종합한 코드는 아래와 같습니다.

 

##주가정보 가져오기  (최초 생성)

import win32com.client
import pandas as pd 
import time
import datetime

column_dailychart = ['code', 'section', 'date', 'open', 'high', 'low', 'close',
                     'vol', 'value', 'n_stock', 'agg_price', 'foreign_rate','agency_buy', 'agency_netbuy']

stockitems = pd.read_csv('stockitems.csv')

instStockChart = win32com.client.Dispatch("CpSysDib.StockChart")
nCpCybos = win32com.client.Dispatch("CpUtil.CpCybos")

row = list(range(len(column_dailychart)))
rows = list()

instStockChart.SetInputValue(1, ord('1'))
instStockChart.SetInputValue(2, (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"))
instStockChart.SetInputValue(3, '20180101')
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8, 9, 12, 13, 17, 20, 21))
instStockChart.SetInputValue(6, ord('D'))
instStockChart.SetInputValue(9, ord('1'))
    
for idx, stockitem in stockitems.iterrows():

    remain_request_count =  nCpCybos.GetLimitRemainCount(1)
    print(stockitem['code'], stockitem['name'], '남은 요청 : ', remain_request_count)
    
    if remain_request_count == 0:
        print('남은 요청이 모두 소진되었습니다. 잠시 대기합니다.')

        while True:
            time.sleep(2)
            remain_request_count =  nCpCybos.GetLimitRemainCount(1)
            if  remain_request_count > 0:
                print('작업을 재개합니다. (남은 요청 : {0})'.format(remain_request_count))
                break
            print('대기 중...')
            
    instStockChart.SetInputValue(0, stockitem['code'])

    # BlockRequest
    instStockChart.BlockRequest()

    # GetHeaderValue
    numData = instStockChart.GetHeaderValue(3)
    numField = instStockChart.GetHeaderValue(1)

    # GetDataValue
    for i in range(numData):
        row[0] =  stockitem['code']
        row[1] = stockitem['section']  # 코스피, 코스닥, ETF 여부
        row[2] = instStockChart.GetDataValue(0, i)  # 날짜
        row[3] = instStockChart.GetDataValue(1, i)  # 시가
        row[4] = instStockChart.GetDataValue(2, i)  # 고가
        row[5] = instStockChart.GetDataValue(3, i)  # 저가
        row[6] = instStockChart.GetDataValue(4, i)  # 종가
        row[7] = instStockChart.GetDataValue(5, i)  # 거래량
        row[8] = instStockChart.GetDataValue(6, i)  # 거래대금
        row[9] = instStockChart.GetDataValue(7, i)  # 상장주식수
        row[10] = instStockChart.GetDataValue(8, i)  # 시가총액
        row[11] = instStockChart.GetDataValue(9, i)  # 외국인 보율비율
        row[12] = instStockChart.GetDataValue(10, i)  # 기관순매수
        row[13] = instStockChart.GetDataValue(11, i)  # 기관누적순매수
        rows.append(list(row))  
        
print('데이터를 모두 불러왔습니다.')

dailychart= pd.DataFrame(data = rows, columns= column_dailychart)
dailychart[['value' ,'agg_price']] = (dailychart[['value' ,'agg_price']]/1000000).astype(int)
dailychart = dailychart.sort_values(by=['code','date'])
dailychart.to_csv('dailychart.csv', index=False)

print('모든 데이터를 저장하였습니다.')

 

 


 

6> 32-bit 버전 메모리 에러 해결

 

위의 코드를 실행했을 때 rows 생성까지는 잘 되다가 데이터프레임으로 변환할 때 다음과 같은 메모리 에러가 발생하는 경우가 있습니다.

 

'MemoryError: Unable to allocate 000. MiB for an array with shape (0000, 00) and data type object'

 

이 에러는 rows 데이터 크기가 너무 커서 메모리가 초과되어 발생하는 문제인데, 64-bit 버전에서는 어지간히 큰 데이터가 아니면 발생하지 않지만, 32-bit버전은 컴퓨터 사양이 좋더라도 최대 할당할 수 있는 메모리 제한이 낮기 때문에 이런 에러가 심심치 않게 발생합니다.

 

따라서 전체 종목의 차트 데이터 1년 치 정도는 에러가 발생하지 않지만 3~4년 이상 가져오는 경우 메모리 에러가 발생할 가능성이 매우 높습니다. (dataframe으로 저장하든 list를 바로 저장하든 동일하게 메모리 에러가 발생합니다.)

 

이때 해결할 수 있는 방법으로는 전체 데이터가 저장된 rows를 여러개로 분할해서 저장한 다음, 64비트 버전으로 바꿔서 python을 실행해서 하나로 합쳐서 저장하는 방법이 있습니다.

 

list를 파일로 저장하는 방법은 몇 가지가 있지만 이번에는 pickle이라는 라이브러리를 활용해봅니다. 아래의 코드는 차트 데이터를 unit(아래 코드 기준으로는 50만개) 단위로 쪼개서 각각 dailychart0.dat, dailychart1.dat ... 로 저장하는 기능을 합니다. 예를 들어 전체 데이터가 320만개인 경우, 6개 파일은 50개씩 저장되고, 마지막 7번째 파일에는 남은 20만개 파일이 저장되는 것입니다. dailychart 뒤의 숫자는 자동으로 0부터 1,2,3,4 순서로 붙도록 하였습니다. unit 숫자는 메모리 에러가 발생하지 않는 선에서 적당히 큰 숫자로 조절하면 됩니다. (대략 1백만이 넘어가면 쪼개는 크기가 커져서 또 메모리 에러가 발생할 수 있습니다.)

 

import pickle

unit = 500000

i = 0
for i in range(0, int(len(rows)/unit)):
    with open('dailychart{0}.dat'.format(i), 'wb') as f:
        pickle.dump(rows[i*unit:(i+1)*unit], f)

if i > 0:
    i += 1
with open('dailychart{0}.dat'.format(i), 'wb') as f:
    pickle.dump(rows[(i)*unit:len(rows)], f)

 

분할된 파일이 생성되면, 현재 작업 중인 Juypter Notebook을 종료 후, Anaconda에서 64비트 python을 실행시키고, 아래와 같이 분할된 파일을 전부 불러온 다음 하나의 파일로 합쳐서 저장하는 코드를 실행시킵니다.

 

## 분할저장한 주가정보파일 합치기

import pickle
import pandas as pd

n_file = 10  #파일 개수
data = list()
column_dailychart = ['code', 'section', 'date', 'open', 'high', 'low', 'close',
                     'vol', 'value', 'n_stock', 'agg_price', 'foreign_rate','agency_buy', 'agency_netbuy']

for i in range(0, n_file): 
    with open('dailychart{0}.dat'.format(i), 'rb') as f:
        tmp_data = pickle.load(f)
    data = data + tmp_data

dailychart = pd.DataFrame(data = data, columns= column_dailychart)
dailychart[['value' ,'agg_price']] = (dailychart[['value' ,'agg_price']]/1000000).astype(int)

dailychart = dailychart.sort_values(by=['code','date'])
dailychart.to_csv('dailychart.csv', index=False)

 

위의 코드에서 n_file은 분할 저장된 파일의 총 개수이며, 0번 파일부터 최대 숫자 파일까지 for을 통해 열어서 list 타입의 data 변수에 담습니다. 그 다음에는 위에서 dataframe로 변환 후 csv로 저장한 것과 동일한 코드를 복사합니다.

 

이렇게 하면 파이썬 32bit 버전에서 메모리 에러로 인해 파일로 저장하지 못한 것을 해결할 수 있습니다.

 

메모리 에러 발생을 예방할 수 있는 코드를 종합하면 아래와 같습니다.

 

##주가정보 가져오기  (최초 생성)

import win32com.client
import pandas as pd 
import time
import datetime

column_dailychart = ['code', 'section', 'date', 'open', 'high', 'low', 'close',
                     'vol', 'value', 'n_stock', 'agg_price', 'foreign_rate','agency_buy', 'agency_netbuy']

stockitems = pd.read_csv('stockitems.csv')

instStockChart = win32com.client.Dispatch("CpSysDib.StockChart")
nCpCybos = win32com.client.Dispatch("CpUtil.CpCybos")

row = list(range(len(column_dailychart)))
rows = list()

instStockChart.SetInputValue(1, ord('1'))
instStockChart.SetInputValue(2, (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"))
instStockChart.SetInputValue(3, '20180101')
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8, 9, 12, 13, 17, 20, 21))
instStockChart.SetInputValue(6, ord('D'))
instStockChart.SetInputValue(9, ord('1'))
    
for idx, stockitem in stockitems.iterrows():

    remain_request_count =  nCpCybos.GetLimitRemainCount(1)
    print(stockitem['code'], stockitem['name'], '남은 요청 : ', remain_request_count)
    
    if remain_request_count == 0:
        print('남은 요청이 모두 소진되었습니다. 잠시 대기합니다.')

        while True:
            time.sleep(2)
            remain_request_count =  nCpCybos.GetLimitRemainCount(1)
            if  remain_request_count > 0:
                print('작업을 재개합니다. (남은 요청 : {0})'.format(remain_request_count))
                break
            print('대기 중...')
            
    instStockChart.SetInputValue(0, stockitem['code'])

    # BlockRequest
    instStockChart.BlockRequest()

    # GetHeaderValue
    numData = instStockChart.GetHeaderValue(3)
    numField = instStockChart.GetHeaderValue(1)

    # GetDataValue
    for i in range(numData):
        row[0] =  stockitem['code']
        row[1] = stockitem['section']  # 코스피, 코스닥, ETF 여부
        row[2] = instStockChart.GetDataValue(0, i)  # 날짜
        row[3] = instStockChart.GetDataValue(1, i)  # 시가
        row[4] = instStockChart.GetDataValue(2, i)  # 고가
        row[5] = instStockChart.GetDataValue(3, i)  # 저가
        row[6] = instStockChart.GetDataValue(4, i)  # 종가
        row[7] = instStockChart.GetDataValue(5, i)  # 거래량
        row[8] = instStockChart.GetDataValue(6, i)  # 거래대금
        row[9] = instStockChart.GetDataValue(7, i)  # 상장주식수
        row[10] = instStockChart.GetDataValue(8, i)  # 시가총액
        row[11] = instStockChart.GetDataValue(9, i)  # 외국인 보율비율
        row[12] = instStockChart.GetDataValue(10, i)  # 기관순매수
        row[13] = instStockChart.GetDataValue(11, i)  # 기관누적순매수
        rows.append(list(row))  
        
print('데이터를 모두 불러왔습니다.')

import pickle

unit = 500000
for i in range(0, int(len(rows)/unit)):
    with open('dailychart{0}.txt'.format(i), 'wb') as f:
        pickle.dump(rows[i*unit:(i+1)*unit], f)

i+=1
with open('dailychart{0}.txt'.format(i), 'wb') as f:
    pickle.dump(rows[(i)*unit:len(rows)], f)

print('모든 데이터를 저장하였습니다.')

 

 


 

7> 차트 데이터 업데이트

 

위의 코드로 차트 데이터를 최초로 생성하고 나면 한 번 만들고 끝이 아니라, 시간이 지나면서 새로운 데이터를 계속 업데이트하게 될 수 있습니다. 새로운 데이터가 생길때마다 예전 데이터까지 다시 새로 불러와서 덮어씌워도 되긴 하지만 비효율적이기 때문에, 기존에 만들어져 있는 데이터에서 새로 추가되는 날짜에 해당하는 차트정보만 새롭게 입력하도록 하는 코드를 하나 더 만들어 봅니다.

 

큰 구조는 위의 최초 차트 데이터 가져오는 코드와 동일하며, 약간의 수정만 있으면 됩니다.

 

 

최초 생성 시에는 stockitems만 불러오면 되었으나, 업데이트 시에는 기존에 만들었던 차트 데이터를 불러와야 하므로 dailychart.csv를 불러오는 코드를 한 줄 추가합니다.

stockitems = pd.read_csv('stockitems.csv')
dailychart_prev = pd.read_csv('dailychart.csv')

 

그 다음 업데이트할 기간을 설정하는데, 데이터를 불러올 시작 날짜를 기존 차트 데이터에 저장된 마지막 날의 그 다음날로 설정합니다. (예를 들어 기존 데이터가 2021-04-21까지 저장되어있으면, 새로 업데이트할 때는 2021-04-22 날짜부터 불러오게 됩니다.) 

    instStockChart.SetInputValue(1, ord('1'))
    instStockChart.SetInputValue(2, (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"))
    instStockChart.SetInputValue(3, str(np.max(dailychart['date'])+1))

 

마지막으로 새로 불러온 데이터(dailychart)를 기존 데이터(dailychart_prev)와 합치기 위해 append()함수 코드를 한 줄 추가합니다. 

dailychart = pd.DataFrame(data = rows, columns= column_dailychart)

dailychart = dailychart.append(dailychart_prev)
dailychart = dailychart.sort_values(by=['code','date'])
dailychart.to_csv('dailychart.csv', index=False)

 

이상의 수정사항을 반영한 차트 데이터 업데이트 코드는 아래와 같습니다.

주가를 최초로 가져올 때만 위의 '최초 생성' 코드를 실행하고, 그 이후부터는 필요할 때 아래의 업데이트 코드를 실행하면, 안정적으로 전 종목 주가 데이터를 가져올 수 있게 됩니다.

 

(시간이 지나면서 종목 리스트도 바뀌니, 주가정보 업데이트 하기 전에 종목 리스트 먼저 가져오는 것을 잊지 마세요!) 

##주가정보 가져오기  (업데이트)

import win32com.client
import pandas as pd 
import time
import datetime

stockitems = pd.read_csv('stockitems.csv')
dailychart_prev = pd.read_csv('dailychart.csv')

instStockChart = win32com.client.Dispatch("CpSysDib.StockChart")
nCpCybos = win32com.client.Dispatch("CpUtil.CpCybos")

column_dailychart = ['code', 'section', 'date', 'open', 'high', 'low', 'close',
                     'vol', 'value', 'n_stock', 'agg_price', 'foreign_rate','agency_buy', 'agency_netbuy']
row = list(range(len(column_dailychart)))
rows = list()

instStockChart.SetInputValue(1, ord('1'))
instStockChart.SetInputValue(2, (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"))
instStockChart.SetInputValue(3, str(np.max(dailychart['date'])+1))
instStockChart.SetInputValue(5, (0, 2, 3, 4, 5, 8, 9, 12, 13, 17, 20, 21))
instStockChart.SetInputValue(6, ord('D'))
instStockChart.SetInputValue(9, ord('1'))
    
for idx, stockitem in stockitems.iterrows():
    remain_request_count =  nCpCybos.GetLimitRemainCount(1)
    print(stockitem['code'], stockitem['name'], '남은 요청 : ', remain_request_count)
    
    if remain_request_count == 0:
        while True:
            print('남은 요청이 모두 소진되었습니다. 잠시 대기합니다.')
            time.sleep(2)
            remain_request_count =  nCpCybos.GetLimitRemainCount(1)
            if  remain_request_count > 0:
                print('작업을 재개합니다. (남은 요청 : {0})'.format(remain_request_count))
                break
        
    instStockChart.SetInputValue(0, stockitem['code'])

    # BlockRequest
    instStockChart.BlockRequest()

    # GetHeaderValue
    numData = instStockChart.GetHeaderValue(3)
    numField = instStockChart.GetHeaderValue(1)

    # GetDataValue
    for i in range(numData):
        row[0] =  stockitem['code']
        row[1] = stockitem['section']  # 코스피, 코스닥, ETF 여부
        row[2] = instStockChart.GetDataValue(0, i)  # 날짜
        row[3] = instStockChart.GetDataValue(1, i)  # 시가
        row[4] = instStockChart.GetDataValue(2, i)  # 고가
        row[5] = instStockChart.GetDataValue(3, i)  # 저가
        row[6] = instStockChart.GetDataValue(4, i)  # 종가
        row[7] = instStockChart.GetDataValue(5, i)  # 거래량
        row[8] = instStockChart.GetDataValue(6, i)  # 거래대금
        row[9] = instStockChart.GetDataValue(7, i)  # 상장주식수
        row[10] = instStockChart.GetDataValue(8, i)  # 시가총액
        row[11] = instStockChart.GetDataValue(9, i)  # 외국인 보율비율
        row[12] = instStockChart.GetDataValue(10, i)  # 기관순매수
        row[13] = instStockChart.GetDataValue(11, i)  # 기관누적순매수
        rows.append(list(row))
    
print('데이터를 모두 불러왔습니다.')

dailychart = pd.DataFrame(data = rows, columns= column_dailychart)

dailychart[['value' ,'agg_price']] /= 1000000
dailychart[['value' ,'agg_price']] = dailychart[['value' ,'agg_price']].astype(int)
dailychart = dailychart.append(dailychart_prev)
dailychart = dailychart.sort_values(by=['code','date'])

dailychart.to_csv('dailychart.csv', index=False)
print('데이터를 모두 불러왔습니다.')