PYTHON/Kiwoom Open API

키움증권 Open API - 3분봉 차트 조회

  • -

사실 지난 포스팅들을 바탕으로 대부분의 이벤트 처리가 가능하고 본인 역시 스스로 제작해보길 바라는 마음을 갖고 있긴 하지만 사실 또 제작하다가 오류가 발생했는데 어디서 오류가 발생한 건지, 어떻게 제작하는 것이 올바르게 작동하는 것인지 잘 모를 때에는 누군가의 도움이 필요하기 때문에 이번 포스팅도 제작하게 되었다. 

 

차트 데이터 요청 함수 제작하기

앞전 포스팅에서 제작했던 일봉 차트의 경우에는 def rq_chart_data로 제작했으나 분봉은 일봉과는 다른 값을 전송해야 하기 때문에 분봉 차트 조회는 별도의 함수를 제작함으로써 조회 요청을 할 수 있다. 

KOA Studio 내 일봉 차트 조회와 분봉 차트 조회가 요구하는 입력값

즉 일봉 차트 조회의 경우에는 종목코드, 기준일자, 수정주가구분을 입력했지만 분봉 차트 조회의 경우에는 종목코드, 틱범위, 수정주가구분을 입력해주어야 한다. 

def rq_min_chart_data(self, itemcode, tic, justify):

위의 코드를 보면 rq_min_chart_data에서 분봉에 대한 조회임을 알아볼 수 있게 해두었고, 그 후에 함수에서 받을 변수로 종목코드(itemcode), 틱범위(tic), 수정주가구분(justify)의 세 개를 두었다. 이제 이 세 가지 변수를 SetInputValue 함수를 이용해서 정보를 입력하고, CommRqData를 이용해서 정보를 전송하면 된다. 

def rq_min_chart_data(self, itemcode, tic, justify):
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "틱범위", tic)
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
	self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "rqname_opt10080", "opt10080", 0, "0101")

위의 코드처럼 SetInputValue 함수를 통해 종목코드, 틱범위, 수정주가구분이라는 세 개의 변수를 모두 입력했고 입력한 정보를 바탕으로 CommRqData를 통해 정보 조회를 요청했다. CommRqData 뒤에 있는 부분들에 대해 다시 설명해보자면 rqname_opt10080은 우리가 OnReceiveTrData에서 처리할 rqname에 해당하는 부분으로 아무런 값이나 입력해주어도 된다. 다만 본인이 확인할 수 있는 값을 입력해야 하며, 지난 번 일봉 차트 데이터를 요청하는 코드에서는 'hello'를 rqname으로 설정했었다. 다음으로 opt10080은 분봉 차트 조회의 tr 코드이며 KOA Studio를 통해서 확인할 수 있다. 그 뒤에 있는 0은 연속 조회 여부를 확인하는 것이며 0101은 화면 번호를 의미한다. 

다음으로, 분봉 차트의 경우에는 3분봉을 조회한다면 하루에 기준으로 약 130개 가량의 데이터가 발생하는데, 키움증권에서 조회 요청 횟수당 최대 600개의 데이터를 받아올 수 있는 제한이 있기 때문에, 분봉 차트 조회에 있어서도 while문을 바탕으로 연속조회를 요청하는 부분을 구축해주도록 하자.

def rq_min_chart_data(self, itemcode, tic, justify):
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "틱범위", tic)
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
	self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "rqname_opt10080", "opt10080", 0, "0101")

	while self.remained_data == True:
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "틱범위", tic)
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
		self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "rqname_opt10080", "opt10080", 2, "0101")

이제 마지막으로, 이벤트 처리에 대한 부분이 필요하다. 즉 while문 위에 있는 CommRqData 함수를 통해 데이터를 요청한 후에 OnReceiveTrData가 반응을 하게 되고, 그에 따라 특정한 동작을 수행한 다음에 추가로 조회할 정보가 있을 수밖에 없기 때문에 다시 rq_min_chart_data를 실행하도록 하는 것이다. 이 역시 일봉 차트 데이터 조회 부분과 로그인 처리 부분에서 설명했으니 자세한 설명은 넘어가도록 하겠다.

def rq_min_chart_data(self, itemcode, tic, justify):
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "틱범위", tic)
	self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
	self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "rqname_opt10080", "opt10080", 0, "0101")
	self.tr_event_loop = QEventLoop()
	self.tr_event_loop.exec_()

	while self.remained_data == True:
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", itemcode)
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "틱범위", tic)
		self.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", justify)
		self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "rqname_opt10080", "opt10080", 2, "0101")
		self.tr_event_loop = QEventLoop()
		self.tr_event_loop.exec_()

 

OnReceiveTrData 이벤트 처리하기

이 이벤트에 대해서도 여러 번 설명했으니 이번 포스팅에서는 간략하게 설명하고 넘어가도록 하겠다. 이전에 일봉 차트 데이터에서도 'rq_chart_data 함수 → OnReceiveTrData 이벤트 발생 → OnReceiveTrData 함수로 연결'과 같은 절차에 대해 설명했었다. 이번 분봉 차트 조회에서도 마찬가지로 'rq_min_chart_data 함수 → OnReceiveTrData 이벤트 발생 → OnReceiveTrData 함수로 연결'과 같은 절차를 거쳐 진행된다. 따라서 def OnReceiveTrData 아래에 다음과 같은 내용을 추가해주도록 하자.

if rqname == 'rqname_opt10080':
	self.opt10080()

 

opt10080 함수 제작하기

기억할지 모르겠지만 지난 번 일봉 차트 데이터 조회에서는 아래와 같이 다소 복잡하게 이루어져 있는 코드를 제작했었다. 

def opt10081(self):
	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()

다만 우리는 opt10081이라는 부분이 trcode라는 부분을 알고 있고, 주식일봉차트조회요청이라는 부분이 recordname이라는 것도 알고 있다. 근데 opt10081 함수는 OnReceiveTrData 함수 내에서 연결해주었고 OnReceiveTrData 내에서는 scrno, rqname, trcode, recordname, prenext라는 다섯 개의 변수를 사용할 수 있으니 opt10081을 실행할 때에도 그 값을 전달해줄 수 있고, 그를 바탕으로 우리는 코드를 조금 단순하게 제작할 수 있게 된다. 이 내용은 내가 봐도 처음 들을 땐 무슨 소린지 전혀 이해하지 못할 것 같기도 한데, 아래의 내용을 통해 이해해보도록 하자. 어렵지 않다.

지금 이야기할 내용은 사실 OnReceiveTrData 이벤트 처리와 관련된 내용이다. 아래의 도식을 확인해보도록 하자.

위의 도식에서는 분봉차트조회 요청 함수인 opt10080을 기준으로 제작하였다. 다시 말해, opt10080 함수 내에서는 opt10081에서 제작했던 것과 같이 GetCommData(trcode, recordname, index, item)의 형태로 제작하게 되는데 여기서 사용되는 trcode와 recordname은 OnReceiveTrData에서 값을 전송해줄 수 있다는 것이다. 그렇다면 이제 OnReceiveTrData 함수 내에 있는 코드를 아래와 같이 수정해주도록 하자.

# 수정 전
if rqname == 'rqname_opt10080':
	self.opt10080()
    
# 수정 후
if rqname == 'rqname_opt10080':
	self.opt10080(trcode, recordname)

 

이에 따라서 def opt10080 함수에서도 trcode와 recordname을 받아오면 된다.

def opt10080(self, trcode, recordname):

이제 opt10081에서 제작했던 것처럼 반복 횟수를 조회(GetRepeatCnt 함수)하고, for문 제작해주도록 하자. 아래의 코드를 보면 알 수 있겠지만, GetRepeatCnt 함수 역시 trcode와 recordname이라는 변수를 사용하기 때문에 opt10080 함수에서 받아온 두 개의 변수인 trcode와 recordname을 그대로 사용해주도록 하자.

def opt10080(self, trcode, recordname):
	getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", trcode, recordname)

	for i in range(getrepeatcnt):

 

그 다음, opt10080 분봉차트조회에서 반환해주는 결과값들은 아래의 왼쪽 사진에 있는 빨간색 영역과 같다. 또한 KOA Studio 프로그램 내에서 해당 정보를 요청해보니 아래의 오른쪽에 있는 사진과 같은 결과값을 반환받았다. 데이터를 조회한 시간이 아침 10시 18분이라 오른쪽과 같이 나왔다. 오늘 시간이 4시 이전인가 이후인가를 기준으로 요청할 일자를 선택하는 코드는 추후에 제작할 예정이다.

본인은 여기서 현재가, 거래량, 체결시간, 시가, 고가, 저가에 대해 불러와보도록 하겠다. 그리고 거래대금에 대한 부분이 없기 때문에, 거래량과 종가를 기준으로 계산해주도록 하자.

def opt10080(self, trcode, recordname):
	getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", trcode, recordname)

	for i in range(getrepeatcnt):	
		item_code = self.GetCommData(trcode, recordname, 0, "종목코드").strip()
		close = self.GetCommData(trcode, recordname, i, "현재가").strip()
		volume = self.GetCommData(trcode, recordname, i, "거래량").strip()
		date = self.GetCommData(trcode, recordname, i, "체결시간").strip()
		open = self.GetCommData(trcode, recordname, i, "시가").strip()
		high = self.GetCommData(trcode, recordname, i, "고가").strip()
		low = self.GetCommData(trcode, recordname, i, "저가").strip()
		trade_volume = int(volume) * int(close)

어떤가? 앞전에 살펴봤던 opt10081과 비교해보면 상당히 깔끔해졌다는 것을 확인할 수 있다.

그렇다면 분봉 차트도 하나의 dataframe으로 만들어주기 위해 class 아래에 다음과 같은 코드를 추가해주도록 하자.

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

# 일봉 차트 제작 시 제작했던 코드
self.day_data = {'date':[], 'open':[], 'high':[], 'low':[], 'close':[], 'volume':[], 'trade_volume':[]}

그 후에 opt10080 함수 아래 부분에 다음과 같은 부분을 추가해주도록 하자.

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

 

테스트 해보기

if문 아래에 밑에 있는 코드를 대입한 후 실행시켜보면, 그 아래에 있는 결과물이 출력되는 모습을 확인할 수 있다. 만약 15분봉 또는 5분봉 데이터를 조회하고 싶다면, trade.rq_min_chart_data("005930", 3, 1) 부분에서 3을 15 또는 5로 변경해주면 알아서 해당 분봉 데이터를 요청하게 된다.

trade.rq_min_chart_data("005930", 3, 1)
df_min_data = pandas.DataFrame(trade.min_data, columns=['date', 'open', 'high', 'low', 'volume', 'trade_volume'])
print(df_min_data)

 

                 date    open    high     low   volume  trade_volume
0      20210607104800  -81900  -81900  -81800    15905   -1301029000
1      20210607104500  -81900  -81900  -81800    36753   -3006395400
2      20210607104200  -81900  -81900  -81800    30258   -2478130200
3      20210607103900  -81900  -82000  -81800    86866   -7114325400
4      20210607103600  -81900  -82000  -81800   128429  -10518335100
...               ...     ...     ...     ...      ...           ...
32520  20200601091200  +50900  +50900  +50800   110314    5614982600
32521  20200601090900   50700  +50900   50700   312095   15885635500
32522  20200601090600  +50800  +50800  -50600   319380   16192566000
32523  20200601090300  -50600  +50800  -50600   296482   15061285600
32524  20200601090000  +50800  +50800  -50600  1115575   56559652500

[32525 rows x 6 columns]

그런데, 결과값을 보니 이상하다. open 또는 high 등등에 -나 +와 같은 부호가 포함되어 있어 trade_volume을 계산할 때에도 -값이 붙어버린다는 것이다. 왜 그런지 검색해보았더니, 이전 가격 대비 하락했다면 -가, 상승했다면 +라는 부호가 붙는다고는 하는데 필요하다면 그냥 내버려둬도 되지만, 만약 필요 없다면 replace()함수를 사용해서 제거해주도록 하자. replace()함수는 아래와 같이 사용한다.

DataFrame.replace("이전 문자열", "바꿀 문자열")

이에 대해서 - 부호와 + 부호가 있는데, 분명 검색을 해서 적용해봤음에도 불구하고 뭘 잘못한 건지, 예전에 제작했던 코드에서는 정확하게 제거가 됐었는데 제거가 이상하게도 + 또는 - 한쪽만 제거가 되고 완벽하게 제거되지 않는 문제가 발생하는 바람에 약간의 꼼수를 부려서 만들었다. 즉, 특정 값에 int()를 씌우면 + 부호는 없어지나 -부호는 남게 되니 - 부호만 replace("-", "")를 통해 제거해주었다. 코드가 많이 지저분해지기는 했지만, 이 상태로 실행을 하게 되면 아래와 같은 결과값을 얻을 수 있게 된다. 

[추신] DataFrame에 대해 일괄적으로 replace를 적용하는 방법이 있으니 그렇게 사용하고자 한다면 찾아보시길 권합니다. 저는 도대체 모르겠습니다. 특정 파일에서는 잘 제거된 상태로 출력되는데, 특정 파일에서는 제거되지 않고 있습니다.

def opt10080(self, trcode, recordname):
	getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", trcode, recordname)

	for i in range(getrepeatcnt):
		item_code = self.GetCommData(trcode, recordname, 0, "종목코드").strip()
		close = int(self.GetCommData(trcode, recordname, i, "현재가").strip().replace("-", ""))
		volume = int(self.GetCommData(trcode, recordname, i, "거래량").strip().replace("-", ""))
		date = self.GetCommData(trcode, recordname, i, "체결시간").strip()
		open = int(self.GetCommData(trcode, recordname, i, "시가").strip().replace("-", ""))
		high = int(self.GetCommData(trcode, recordname, i, "고가").strip().replace("-", ""))
		low = int(self.GetCommData(trcode, recordname, i, "저가").strip().replace("-", ""))
		trade_volume = volume * close

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

 

                 date   open   high    low   volume  trade_volume
0      20210607132100  82000  82100  81900    83506    6839141400
1      20210607131800  81900  82000  81900   196902   16145964000
2      20210607131500  81900  82000  81800   100360    8229520000
3      20210607131200  81900  82000  81800   233564   19105535200
4      20210607130900  81800  81900  81700    86752    7104988800
...               ...    ...    ...    ...      ...           ...
32571  20200601091200  50900  50900  50800   110314    5614982600
32572  20200601090900  50700  50900  50700   312095   15885635500
32573  20200601090600  50800  50800  50600   319380   16192566000
32574  20200601090300  50600  50800  50600   296482   15061285600
32575  20200601090000  50800  50800  50600  1115575   56559652500

[32576 rows x 6 columns]

이를 데이터베이스에 저장하고 싶다면 앞서 만들었던 day_data 데이터베이스가 아닌 min_data, 또는 15분봉 데이터도 필요하다면 min3_data 데이터베이스와 min15_data 데이터베이스를 별도로 만들어두고 거기에 저장하도록 하자. 여기서도 주의해야 하는 점은, 데이터베이스의 이름이던 테이블의 이름이던 간에 숫자가 맨 앞에 와서는 안 된다는 것이다.

그리고 위의 결과물을 보면 알겠지만, 조회 시점이 장마감 전인 경우에는 당일 오전 10시 데이터 포함되어버리는 문제가 발생하게 되는데, 다음 포스팅에서는 조회를 요청하는 시점이 주말인지 또는 평일이라 하더라도 현재 시간이 장마감 기준 시간인 4시 이전인지 이후인지에 따라 현재 조회를 요청할 거래일자를 다르게 설정하는 방법에 대해 다룰 예정이다.

 

 


 

728x90
반응형
Contents

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

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