AUTO TRADE/[키움증권] Kiwoom Open API

013. 키움증권 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")

 

위의 코드처럼 1️⃣ SetInputValue 함수를 통해 종목코드, 틱범위, 수정주가구분이라는 세 개의 변수를 모두 입력했고 입력한 정보를 바탕으로 2️⃣ CommRqData 함수를 통해 서버에 정보 조회를 요청했다.

CommRqData 뒤에 있는 부분들에 대해 다시 설명해보자면, rqname_opt10080은 우리가 OnReceiveTrData에서 처리할 `rqname`에 해당하는 부분으로 사실 아무런 값이나 입력해주어도 된다(지난 번 일봉 차트 데이터를 요청하는 코드에서는 'hello'를 rqname으로 설정했었음). 다음으로 opt10080은 분봉 차트 조회의 `trcode`에 해당하는 부분이며 사용하고 있는 함수의 `trcod`가 무엇인지는 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):

 


728x90

 

그렇다면 이제 opt10080 분봉차트조회에서 반환해주는 결과값에는 무엇이 있을까 ? 아래 사진의 빨간색 영역을 참고하자. 

opt10080 반환값 목록

일단은 이 중에서 현재가, 거래량, 체결시간, 시가, 고가, 저가에 대해 불러와보자. 그리고 거래대금에 대한 부분도 추가할 예정인데, 거래대금은 따로 결과값을 제공하지 않기 때문에, 거래량과 종가(해당 분봉의 종가)를 기준으로 계산해주도록 하자.

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)

 

어떤가? `trcode`와 `recordname`을 하나의 파라미터로 전달받자, 앞전에 살펴봤던 opt10081과 비교해보면 상당히 깔끔해졌다는 것을 확인할 수 있다. 사실상 'opt10081'을 'trcode'로 대체했고 '주식일봉차트조회요청'을 'recordname'으로 대체했을 뿐이지만, 우리는 `trcode`와 `recordname`을 하나의 파라미터로 전달받고 있으며 ✅ 데이터를 요청한 함수에 따라 각기 다른 결과값을 적절하게 받아올 수 있기 때문에 코드 작성이 한결 수월해진다. 

그렇다면 분봉 차트도 하나의 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

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

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