PYTHON/Kiwoom Open API

키움증권 Open API 차트 데이터 불러오기 (3)

  • -

지난 게시글에서 서버에 데이터를 요청하고 그 결과값을 받아오는 부분까지 코드를 제작했고, 이번 포스팅에서는 추가로 조회할 데이터가 남아 있다면 그 데이터를 어떻게 받아올 것인지에 대한 코드를 제작해보도록 하자.

일단 지난 포스팅에서 결과물이 깔끔하게 나오긴 했지만 사실 글을 업로드한 후에 요청 횟수당 600회를 불러올 수 있다는 점과 OnReceiveTrData 내에도 이벤트 루프를 종료하는 코드를 제작해주어야 연속 조회가 가능하다는 점을 깜빡하고 그 내용을 함께 소개하지 못했다. 그래서 이번에는 한 번의 조회로 총 600개의 데이터를 가져오는 것을 먼저 구축한 후 남아 있는 데이터가 있다면 그 데이터도 불러오도록(이하 연속조회) 하는 구조를 구축하도록 하겠다. 

 

OnReceiveTrData : "나도 이벤트 루프 종료 시켜줘"

지난 포스팅에서 깜빡하고 제작하지 못한 내용부터 제작하도록 하자. 지난 포스팅에서도 설명했듯이 현재 차트 정보 조회 요청 코드의 구조는 rq_chart_data → OnReceiveTrData 이벤트 → OnReceiveTrData 함수의 구조를 갖추고 있다. 다만 맨 앞의 rq_chart_data 함수 내에서 이벤트 루프를 동작시켰는데 마지막에 OnRecieveTrData 함수 내에서는 이벤트 루프를 종료시키지 않은 것이다. 이는 로그인 관련 코드를 구축할 때 서술했던 내용과 같은 내용이므로, 단순하게 OnReceiveTrData 함수의 맨 끝부분에 다음과 같은 코드를 추가해주면 된다.

try:
	self.tr_event_loop.exit()
except AttributeError:
	pass

그 후 코드를 실행시켜보면 연속 조회가 이루어지는 모습을 확인해볼 수 있다. 다만 조회되는 일자가 거래일 기준 600일을 기준으로 껑충껑충 뛰어넘으면서 데이터가 조회되는 모습이 나타나는데, 이 문제는 아래의 GetRepeatCnt()함수를 통해 해결한다.

 

요청 횟수당 600개의 데이터라며 - GetRepeatCnt()

여느 함수와 마찬가지로 키움증권 개발 가이드 내에 서술되어 있는 내용부터 살펴보도록 하자.

원형 GetRepeatCnt(TrCode, RecordName)
설명 레코드 반복횟수를 반환한다.
입력값 TrCode : Trand 명
RecordName : 레코드 명
반환값 레코드의 반복횟수

위의 표를 보면 알 수 있듯이, 이 함수는 레코드의 반복 횟수를 반환하는 값이다. 지난 포스팅을 봤다면 알 수 있겠지만, 우리가 현재 조회하고자 하는 정보의 TR 코드는 opt10081이고 RecordName은 주식일봉차트조회요청이다. 즉, 주식일봉차트조회요청이 몇 번 반복되었는지를 반환하는 함수인데, 이를 써보면 거의 대부분은 600이라는 값이 반환된다. 왜냐하면 일봉차트 조회를 요청했을 때 반환되는 일자의 수가 600개가 넘는 종목이 대부분이기 때문이다. 하지만 이 600이라는 값은 키움증권에서 한 번의 요청이 있을 때 반환하여 정보를 전송해줄 수 있는 최대의 수량이기도 하다. 

물론 for i in range(0, 10000): 등과 같은 형태를 써서 연속으로 조회하면 되지 않냐는 입장이 있을 수도 있다. 하지만 예를 들어 거래일이 1,030일인 종목이 있다고 가정했을 때 이 종목의 첫 번째 GetRepeatCnt 값은 600이고 두 번째 GetRepeatCnt 값은 430이 나올 것이다. 그렇기 때문에 우리는 for i in GetRepeatCnt()와 같은 형태를 사용함으로써 정확하게 필요한 정보가 있는 만큼만 데이터를 요청하고 받아올 수 있다. 그렇다면 이제 opt10081 함수 내에 GetRepeatCnt() 함수를 제작하고 그 값을 확인해보도록 하자.

getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", "opt10081", "주식일봉차트조회요청")
print(getrepeatcnt)

 

바로 위에서도 글로 여러 번 설명했었지만, getrepeatcnt라는 변수의 값은 600이 나온다는 것을 확인해볼 수 있다. 그렇다면 이제 opt10081 함수 내에 getrepeatcnt라는 값을 기준으로 for문을 제작해보자. 아래의 코드는 opt10081 함수의 전체 코드인데, 단순하게 위의 두 줄이 추가되고 GetCommData(trcode, recordname, index, itemname)에서 index 부분이 0에서 i로 바뀌었다. 그럼 이제 잘 작동하는지 확인하기 위해 한 번 코드를 실행해보자.

def opt10081(self):
	getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", "opt10081", "주식일봉차트조회요청")
        
	for i in range(getrepeatcnt):
		item_code = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "종목코드").strip()
		date = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "일자").strip()
		open = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "시가").strip()
		high = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "고가").strip()
		low = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "저가").strip()
		close = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "현재가").strip()
		yester_close = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "전일종가").strip()
		volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래량").strip()
		trade_volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래대금").strip()
    
		print("종목코드:", item_code)
		print("조회일자:", date)
		print("전일종가:", yester_close)
		print("시가:", open)
		print("고가:", high)
		print("저가:", low)
		print("종가:", close)
		print("거래량:", volume)
		print("거래대금:", trade_volume)

 

종목코드: 
조회일자: 19850104
전일종가: 
시가: 130
고가: 130
저가: 129
종가: 129
거래량: 111765
거래대금: 0

이제 코드를 실행해보면, 맨 밑 줄에는 위와 같은 결과물이 나타나는 것을 확인해볼 수 있다. 그리고 이 위에는 1985년 1월 4일의 바로 다음 날 값인 1985년 1월 5일의 데이터가 출력되었다. 하지만 잘 보면 종목코드란과 전일 종가가 비어있다는 점을 확인할 수 있다. 뭐가 문젠지 KOA Studio를 통해 확인해보도록 하자.

KOA Studio를 통해 005930 종목의 일봉 차트 데이터를 요청해보면 위와 같은 결과가 반환되었음을 확인해볼 수 있다. 가장 먼저 전일 종가의 경우에는 정보를 제공해주지 않고 있기 때문에 나타나지 않는 것이었다. 다음으로 종목코드를 보면 [0][opt10081] 구간에는 005930이라고 적혀 있지만 [1][0][opt10081] 구간에는 아무것도 적혀 있지 않다는 것을 확인할 수 있다. 저기서 각 칸 별로 [0], [1], [2] 이런 식으로 나와 있는데 그게 바로 우리의 for문 안에서 i의 값이다. 오른쪽의 사진을 보면 [599] 밑으로는 데이터가 없다는 것을 확인할 수 있다. 저 [599]라는 값이 바로 우리가 이전 글에서 살펴봤던 getrepeatcnt와 연관되어 있는 것이다. 

그렇다면 이제 전일 종가가 출력되지 않는다는 점은 확인했고, 종목 코드가 출력되지 않는 부분은 어떻게 해결해야 할까? 그 답은 단순하다. item_code = ~~ 부분의 index 자리에 있는 i를 0으로 바꿔주기만 하면 된다. (전일종가는 제공하지 않으니 지우자.) 지금까지 작성한 코드는 아래와 같다. 이제 이를 실행해보면 종목코드가 005930으로 잘 나타내지는 것을 확인할 수 있다.

def opt10081(self):
	getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", "opt10081", "주식일봉차트조회요청")

	for i in range(getrepeatcnt):
		item_code = self.GetCommData("opt10081", "주식일봉차트조회요청", 0, "종목코드").strip()
		date = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "일자").strip()
		open = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "시가").strip()
		high = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "고가").strip()
		low = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "저가").strip()
		close = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "현재가").strip()
		volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래량").strip()
		trade_volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래대금").strip()

		print("종목코드:", item_code)
		print("조회일자:", date)
		print("시가:", open)
		print("고가:", high)
		print("저가:", low)
		print("종가:", close)
		print("거래량:", volume)
		print("거래대금:", trade_volume)

 

종목코드: 005930
조회일자: 19850104
시가: 130
고가: 130
저가: 129
종가: 129
거래량: 111765
거래대금: 0

 

 


728x90

 

아니 근데 이거 좀 너무 지저분한데... 좀 예쁘게 좀 어떻게 안 되나...?

결과값이 위 아래로 너무 길어서 원하는 일자의 데이터를 찾아 보는 것은 사실상 불가능해 보인다. 그래서 이 데이터를 데이터프레임이라는 모듈을 이용해서 데이터를 깔끔하게 나타내도록 하자. 이 데이터프레임이라는 동작을 수행할 수 있는 모듈은 바로 Pandas라는 모듈이다. 일단 모듈을 사용하는 방법에 대해서는 다른 포스팅에서 이야기했듯이 import를 통해 사용할 수 있다.

 

모듈이란 무엇인가?

모듈(module)이란? 파이썬 내에서는 모듈(module)을 잘 사용하게 된다. 흔히 게임을 다운로드 한다던지, 아니면 프로그램을 다운로드 한다던지 하는 경우에 설치 폴더 내에 수많은 파일들이 설치되

trustyou.tistory.com

import pandas

이제 class 안에 있는 def __init__() 아래에 다음과 같은 코드를 추가해주도록 하자. 

self.day_data = {'date':[], 'open':[], 'high':[], 'low':[], 'close':[], 'volume':[], 'trade_volume':[]}

 

지금까지의 def__init__() 안에 있는 코드 현황은 아래의 더보기를 클릭해서 확인할 수 있다.

더보기
def __init__(self):
	self.kiwoom = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
	print("연결되었습니다..")

	self.kiwoom.OnEventConnect.connect(self.OnEventConnect)
	self.kiwoom.dynamicCall("CommConnect()")
	self.login_event_loop = QEventLoop()
	self.login_event_loop.exec_()
        
	self.kiwoom.OnReceiveTrData.connect(self.OnReceiveTrData)

	self.day_data = {'date':[], 'open':[], 'high':[], 'low':[], 'close':[], 'volume':[], 'trade_volume':[]}

 

그렇다면 이제 차트 데이터를 요청하고 그 결과값을 프린트했던 opt10081 함수에서 print문을 지우고 아래와 같이 수정해주면 된다. 즉, self.day_data의 ['date']라는 부분에는 date라는 값을 입력(append)하고 self.day_data의 ['open']이라는 부분에는 open이라는 값을 입력(append)하도록 하는 코드이다. 

def opt10081(self):
	getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", "opt10081", "주식일봉차트조회요청")

	for i in range(getrepeatcnt):
		item_code = self.GetCommData("opt10081", "주식일봉차트조회요청", 0, "종목코드").strip()
		date = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "일자").strip()
		open = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "시가").strip()
		high = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "고가").strip()
		low = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "저가").strip()
		close = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "현재가").strip()
		volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래량").strip()
		trade_volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래대금").strip()

		self.day_data['date'].append(date)
		self.day_data['open'].append(open)
		self.day_data['high'].append(high)
		self.day_data['low'].append(low)
		self.day_data['close'].append(close)
		self.day_data['volume'].append(volume)
		self.day_data['trade_volume'].append(trade_volume)

 

그 후에 이제 self.day_data라는 변수 안에 입력되어 있는 값들을 바탕으로 데이터프레임을 제작하면 되는데, 해당 값들을 데이터프레임이라는 틀 안에 넣어 결과물을 만드는 코드는 아래와 같은 형식을 지니고 있다.

result = pandas.DataFrame(data, columns=[])

이를 바탕으로 응용해보자면 우리는 현재 if문 밖에서 trade.day_data라는 코드를 통해 차트 데이터가 저장된 self.day_data 변수를 불러왔으니, 이제는 trade.day_data라는 변수 안에 입력되어 있는 자료들을 바탕으로 DataFrame을 만들면 된다. 이를 코드로 구축하자면 아래와 같이 제작할 수 있다. 이 코드는 당연히 코드 최하단에 위치한 if문 아래에 두어야 정상적으로 동작한다.

df_day_data = pandas.DataFrame(trade.day_data, columns=['date', 'open', 'high', 'low', 'close', 'volume', 'trade_volume'])
print(df_day_data)

이제 코드를 실행해보면 아래와 같이, 전에 봤던 결과와는 달리 상당히 예쁜 결과값이 출력되는 것을 확인할 수 있다. 

          date   open   high    low  close    volume trade_volume
0     20210528  79800  80400  79400  80100  12360199       988666
1     20210527  80000  80000  79100  79600  23198510      1845463
2     20210526  80400  80500  79700  79800  11984359       959016
3     20210525  80000  80400  79800  79900  13628942      1091585
4     20210524  80100  80400  79500  79700  13398666      1069031
...        ...    ...    ...    ...    ...       ...          ...
9627  19850109    126    126    122    123    324837            1
9628  19850108    129    129    127    127    845098            4
9629  19850107    129    130    128    129    771895            3
9630  19850105    129    129    128    128    108497            0
9631  19850104    130    130    129    129    111765            0

 

추가적으로, pandas.DataFrame(~~~~~~~)을 사용할 때 columns= 외에도 index= 를 사용할 수도 있는데, 저장되어 있는 값에서는 어떤 문제가 있는 건지 index=['date']의 형태로 입력해도 인덱스 값이 설정되지 않는다. 이 때 발생하는 오류는 ValueError: Shape of passed values is (9632, 6), indices imply (1, 6)인데, 사실 이렇게 하기보다도 위에서 제작했던 코드처럼 제작하되 .set_index()를 통해 date를 지정해주면 인덱스가 포함된 결과값을 얻을 수 있다. 아래의 코드 중 두 번째 줄 맨 오른쪽에 보면 set_index('date')라는 부분이 추가되었는데, 이를 바탕으로 코드를 실행해보면 아래와 같은 결과가 출력된다.

trade.rq_chart_data("005930", "20210530", 1)
df_day_data = pandas.DataFrame(trade.day_data, columns=['date', 'open', 'high', 'low', 'close', 'volume', 'trade_volume']).set_index('date')
print(df_day_data)
           open   high    low  close    volume trade_volume
date                                                       
20210528  79800  80400  79400  80100  12360199       988666
20210527  80000  80000  79100  79600  23198510      1845463
20210526  80400  80500  79700  79800  11984359       959016
20210525  80000  80400  79800  79900  13628942      1091585
20210524  80100  80400  79500  79700  13398666      1069031
...         ...    ...    ...    ...       ...          ...
19850109    126    126    122    123    324837            1
19850108    129    129    127    127    845098            4
19850107    129    130    128    129    771895            3
19850105    129    129    128    128    108497            0
19850104    130    130    129    129    111765            0

사실은 이렇게 하나 저렇게 하나 사용하는 데에는 별반 다를 것 없지만, 개인적으로는 인덱스를 별도로 설정하지 않는 것을 권하는 입장을 취하고 있다. 왜냐하면 추후에 백테스팅을 진행할 때 index 값에 날짜가 들어가 있게 되면 거래량이 발생했던 일자를 찾는다거나 하는 데에서 어려움을 겪었기 때문이다. 

다음 포스팅에서는 (살짝 순서가 뒤집힌 것 같지만) 상장되어 있는 모든 종목의 종목코드를 불러온 후에 그 전체 코드 내에서 불필요한 우선주나 스팩주 등을 골라낸 후에 남은 종목들만 따로 골라내는 방법에 대해 포스팅할 예정이다. 

 

이번 포스팅까지 제작한 코드의 전체 현황

아래의 더보기를 클릭하면 볼 수 있습니다.

더보기
import sys
from PyQt5.QAxContainer import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import pandas


class system_trading():
	def __init__(self):
		self.kiwoom = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
		print("연결되었습니다..")

		self.kiwoom.OnEventConnect.connect(self.OnEventConnect)
        
		self.kiwoom.dynamicCall("CommConnect()")
		self.login_event_loop = QEventLoop()
		self.login_event_loop.exec_()
        
		self.kiwoom.OnReceiveTrData.connect(self.OnReceiveTrData)

		self.day_data = {'date':[], 'open':[], 'high':[], 'low':[], 'close':[], 'volume':[], 'trade_volume':[]}

	def OnEventConnect(self, err_code):
		if err_code == 0:
			print("로그인에 성공하였습니다.")
		else:
			print("로그인에 실패하였습니다.")
		self.login_event_loop.exit()

	def OnReceiveTrData(self, scrno, rqname, trcode, recordname, prenext, unused1, unused2, unused3, unused4):
		if prenext == '2':
			self.remained_data = True
		elif prenext != '2':
			self.remained_data = False

		if rqname == 'hello':
			self.opt10081()

		try:
			self.tr_event_loop.exit()
		except AttributeError:
			pass

	def opt10081(self):
		getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", "opt10081", "주식일봉차트조회요청")

		for i in range(getrepeatcnt):
			item_code = self.GetCommData("opt10081", "주식일봉차트조회요청", 0, "종목코드").strip()
			date = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "일자").strip()
			open = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "시가").strip()
			high = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "고가").strip()
			low = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "저가").strip()
			close = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "현재가").strip()
			volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래량").strip()
			trade_volume = self.GetCommData("opt10081", "주식일봉차트조회요청", i, "거래대금").strip()

			self.day_data['date'].append(date)
			self.day_data['open'].append(open)
			self.day_data['high'].append(high)
			self.day_data['low'].append(low)
			self.day_data['close'].append(close)
			self.day_data['volume'].append(volume)
			self.day_data['trade_volume'].append(trade_volume)

	def rq_chart_data(self, itemcode, date, justify):
		print("차트를 조회합니다.")
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "기준일자", date)
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
		self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "hello", "opt10081", 0, "0101")
		self.tr_event_loop = QEventLoop()
		self.tr_event_loop.exec_()

		while self.remained_data == True:
			time.sleep(0.3)
			self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
			self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "기준일자", date)
			self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
			self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "hello", "opt10081", 2, "0101")
			self.tr_event_loop = QEventLoop()
			self.tr_event_loop.exec_()

	def GetCommData(self, trcode, recordname, index, itemname):
		result = self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", trcode, recordname, index, itemname)
		return result

if __name__ == "__main__":
	app = QApplication(sys.argv)
	trade = system_trading()

	trade.rq_chart_data("005930", "20210530", 1)
	df_day_data = pandas.DataFrame(trade.day_data, columns=['date', 'open', 'high', 'low', 'close', 'volume', 'trade_volume'])
	print(df_day_data)
728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.