프로그래밍/파이썬

1. 파이썬 - 키움API 활용한 가격정보 불러오기

가카리 2025. 6. 3. 21:49
반응형

 

 

키움증권 API서버에 전달하는 요청 단위를 TR이라고 한다.

1. 종목별 가격 정보 요청 함수

def get_price_data(self, code):

 

 

  이 TR을 호출하려면 CommRqData 함수를 사용해야 한다.

 

  - sTrCode에 조회하려는 TR 이름을 전달하면 API를 이용할 수 있다.

  - 변수명을 꼭 sTrCode로 만들어야 하는 것이 아니라 전달되는 매개변수의 순서가 중요하다.

    

  - TR을 호출하는 코드를 살펴보면  

self.dynamicCall("CommRqData(QString, QString, int, QString", "opt10081_req", "opt10081", 0, "0001")

 

  - 첫번째 매개변수인 "CommRqData(QString, QString, int, QString)에서 CommRqData는 API에서 제공하는 함수이며 (QString, QString, int, QString) 는 이 API 함수를 호출할 때 매개변수가 네개 필요함을 의미한다

 

  - 두번째 매개변수("opt10081_req")는 사용자 구분명을 의미한다. 사용자 구분명이란 우리가 호출할 TR의 별명이다.

 

  - 세번째 매개변수 "opt10081"는 우리가 호출할 TR이름이며 이 값에 따라 어느 TR이 호출될지 결정된다.

  

  - 사용할 TR 이름은 다음과 같이 KOA의 TR목록에서 찾을 수 있다.

 

  - 네번째 매개변수 0은 연속 조회 여부를 나타낸다. TR에 대한 응답이 너무 길어 한번에 전달하기 어려울때는 응답을 나누어서 받을지 여부를 선택할 수 있다.(응답의 최대 개수는 600개)

 

  - 마지막 매개변수인 화면 번호는 요청한 TR들의 묶음을 지정한 네자리 숫자 형태의 문자이다.

  (화번 번호는 서버의 결과를 수신할 때 어떤 요청에 의한 수신인지를 구별하기 위한 키 값의 개념)

 

 

  - 위 설명을 보면 종목코드, 기준일자, 수정주가구분 세가지 입력값이 필요한 것을 알 수 있다.

 

  - 따라서 아래와 같이 세팅해줘야된다. 물론 기준일자도 입력해야되지만 기준일자를 입력하지 않으면 최근일자까지 조회해 오기 때문에 사실상 종목코드만 있으면 가격 정보를 가져올 수 있다.

self.dynamicCall("SetInputValue(QString, QString", "종목코드", code)
self.dynamicCall("SetInputValue(QString, QString", "수정주가구분", 1)#1이면 수정주가 적용함
self.dynamicCall("CommRqData(QString, QString, int, QString", "opt10081_req", "opt10081", 0, "0001")

 

  - 여기서 Kiwoom 클래스 생성자에서 만든 tr_event_loop 변수를 이용해서 exec_() 메소드를 실행시킨다.

  - 이부분이 CommRqData를 사용하여 TR요청을 보낸 후 응답 대기 상태로 만드는 코드이다.

self.tr_event_loop.exec_()#TR요청 보낸후 응답 대기상태로 만드는 코드임 이후 코드는 TR에 대한 응답이 도착한 후 실행됨

ohlcv = self.tr_data#응답 slot함수 _on_receive_tr_data에서 수신한 일봉 데이터 저장되어있어서 사용 가능함

 

한번에 호출로 받아 올수있는 데이터가 최대 600개이며 600일치가 ohlcv에 저장된다.

 

get_price_data (tr_event_loop_exec_()함수까지) -> _on_receive_tr_data 함수를 전체 다 돌고 -> 다시 get_price_data의 while문부터 시작됨(ohlcv에 self.tr_data 데이터가 저장됨)

 

 

def get_price_data(self, code):
    #종목의 상장일부터 가장 최근 일자까지 일봉정보를 가져오는 함수
    self.dynamicCall("SetInputValue(QString, QString", "종목코드", code)
    self.dynamicCall("SetInputValue(QString, QString", "수정주가구분", 1)#1이면 수정주가 적용함
    self.dynamicCall("CommRqData(QString, QString, int, QString", "opt10081_req", "opt10081", 0, "0001")

    self.tr_event_loop.exec_()#TR요청 보낸후 응답 대기상태로 만드는 코드임 이후 코드는 TR에 대한 응답이 도착한 후 실행됨

    ohlcv = self.tr_data#응답 slot함수 _on_receive_tr_data에서 수신한 일봉 데이터 저장되어있어서 사용 가능함

    while self.has_next_tr_data: #다음 데이터가 있다면 실행
        self.dynamicCall("SetInputValue(QString, QString", "종목코드", code)
        self.dynamicCall("SetInputValue(QString, QString", "수정주가구분", 1)  # 1이면 수정주가 적용함
        self.dynamicCall("CommRqData(QString, QString, int, QString", "opt10081_req", "opt10081", 2, "0001")#2인 이유는 연속조회이므로
        self.tr_event_loop.exec_()#TR요청 보낸후 응답 대기상태로 만드는 코드임 이후 코드는 TR에 대한 응답이 도착한 후 실행됨

        #최초로 얻어온 가격 데이터를 담은 ohlcv에 데이터를 이어 붙이는 작업을 함
        for key, val in self.tr_data.items():
            ohlcv[key] += val


    df = pd.DataFrame(ohlcv, columns=['open', 'high', 'low', 'close', 'volume'], index=ohlcv['date'])

    return df[::-1]

 

  - 여기서 while문에서 tr_event_loop.exec_() 함수 부분은 600일치 이상을 불러와야할 때 한번 더 TR요청을 하고 다시 응답을 기다리는 상태로 진입하기 위해 호출된다.

 

  - 이후 for문을 통해서 ohlcv에 데이터를 이어 붙이는 작업을 한다. 

  - 딕셔너리 for문을 이용해서 이어 붙이는 작업을 한다.

 

  - 반복문(while, for문)이 끝나면 그동안 받아온 데이터를 pandas 패키지를 이용해서 DataFrame을 만든다.(행렬구조로)

  - 여기서 컬럼은 date, open, high, low, close, volume이며 인덱스는 date로 한다는 의미이다.

df = pd.DataFrame(ohlcv, columns=['open', 'high', 'low', 'close', 'volume'], index=ohlcv['date'])

 

 

  - 마지막 df[::-1]은 현재 날짜를 오름차순으로 출력하라는 의미이다.

 

2. 종목별 가격 정보 요청 응답 수신 함수

def _on_receive_tr_data(self, screen_no, rqname, trcode, record_name, next, unused1, unused2, unused3, unused4):

 

KOA > 개발 가이드 > 조회와 실시간 데이터 처리 > 관련 함수

 

1. 첫번째 매개변수 sScrNo: 임의로 정해서 사용하기로 한 화면번호(0001)

 

2. 두번째 매개변수sRQName은 "opt10081_req"로 TR를 호출할 때 사용했던 구분명

 

3. 세번째 매개변수 sTrCode는 호출되는 TR이름 "opt10081"

 

4. 네번째 매개변수 sRecordName은 레코드 이름이지만 실제로 안씀

 

5. sPrevNext는 TR 조회 후 OnReciveTrData를 통해 결과를 수신할 때 동일 TR 조회에 대해 추가적으로 받아 올 데이터가 있는지 의미함(추가로 받아올 데이터가 있다면 2로 전달받고 그렇지 않다면 0을 수신함)

 

6. 나머지 매개변수는 안씀

 

  - opt10081를 보면 OUTPUT 멀티데이터에서 가져오는 값들을 볼 수 있음

  - 멀티데이터를 제공하는 TR은 반복문을 이용해서 값을 가져와야 함

 

  - 이번 요청에서 받아온 데이터 개수를 확인하려면 아래 GetRepeatCnt 함수에 호출한 TR 이름을 전달하면됨

 

tr_data_cnt = self.dynamicCall("GetRepeatCnt(QString, QString", trcode, rqname) #가져온 TR의 응답갯수 즉 600일치면 600이 저장됨

#만약 600일치가 넘은 일본데이터를 필요하면 next를 2로 설정해서 _on_receive_tr_data를 한번더 호출할 수 있게 함
if next == '2':
    self.has_next_tr_data = True
else:
    self.has_next_tr_data = False

 

 

그리고 다음 소스는 TR응답을 수신하는데 거기서 opt10081_req에 대해서만 처리하는 부분임

 

  - 가져올 수있는 데이터들은 위에 멀티데이터 리스트에 언급되있음 그 중 필요한 부분만 가져오려면 GetCommData() 함수를 쓰면됨

 

if rqname == "opt10081_req":
    ohlcv = {'date': [], 'open': [], 'high': [], 'low': [], 'close': [], 'volume': []}#ohlcv는 지역변수임

    for i in range(tr_data_cnt):
        date = self.dynamicCall("GetCommData(QString, QString, int, QString", trcode, rqname, i, "일자")
        open = self.dynamicCall("GetCommData(QString, QString, int, QString", trcode, rqname, i, "시가")
        high = self.dynamicCall("GetCommData(QString, QString, int, QString", trcode, rqname, i, "고가")
        low = self.dynamicCall("GetCommData(QString, QString, int, QString", trcode, rqname, i, "저가")
        close = self.dynamicCall("GetCommData(QString, QString, int, QString", trcode, rqname, i, "현재가")
        volume = self.dynamicCall("GetCommData(QString, QString, int, QString", trcode, rqname, i, "거래량")

        ohlcv['date'].append(date.strip())
        ohlcv['open'].append(int(open))
        ohlcv['high'].append(int(high))
        ohlcv['low'].append(int(low))
        ohlcv['close'].append(int(close))
        ohlcv['volume'].append(int(volume))

    self.tr_data = ohlcv#글로벌 변수로 편입시킴

 

KOA에 보면 GetCommData 함수에 대한 설명은 다음과 같다.

 

 

 1. 첫번째 매개변수:         BSTR strTrCode,   // TR 이름 ( opt10081)
 2. 두번째 매개변수        BSTR strRecordName,   // 레코드이름 
 3. 세번째 매개변수       long nIndex,      // nIndex번째
 4. 네번째 매개변수         BSTR strItemName) // TR에서 얻어오려는 출력항목이름
         

여기서 두번째 매개변수는  sRecordName은 레코드 이름이지만 실제로 안쓰지만 편의상 rqname을 넘겨준다.

 

그리고 아래 부분에서 ohlcv를 딕셔너리 형태로 저장해뒀는데 글로벌 변수로 편입시키는 부분이다.

self.tr_data = ohlcv#글로벌 변수로 편입시킴

 

time.sleep(0.5)는 0.5초만큼 쉰다는 의미인데 키움 API 이용 시 1초에 최대 5회의 요청만 허용하는 정책 때문이다.

self.tr_event_loop.exit()#tr 요청을 보내고 응답을 대기시키는데 사용하는 self.tr_event_loop 를 종료 시킴
time.sleep(0.5)#0.5초만듬 쉼

 

self.tr_event_loop.exit()는 TR요청을 보내고 응답을 대기시키는데 사용하는 self.tr_event_loop를 종료하는 역할을 한다.

(응답 대기상태를 해제함)

 

아래를 보면 Kiwoom 클래스 생성자에서 tr_event_loop = QEventLoop()부분이 실제 응답 요청하고 대기를 위한 변수를 생성하는 부분이다.

 

class Kiwoom(QAxWidget):
    def __init__(self):
        super().__init__()
        self._make_kiwoom_instance()
        self._set_signal_slots()
        self._comm_connect()#로그인 요청하는 메소드 시작

        self.account_number = self.get_account_number()#계좌번호 가져옴

        self.tr_event_loop = QEventLoop()#TR 요청에 대한 응답 대기를 위한 변수 데이터가 올 때까지 기다렸다가 처리해!" 하는 용도로 사용하는 이벤트 루프

 

 

3. 실행 화면

  - 삼성전자의 6월 2일 시가는 56300원 종가는 56800원으로 잘 맞다.

 

  - 실제 영웅문 데이터는 아래와 같다. 거래량 데이터가 약간 다른데? 곰곰히 생각해보니까

  - NXT거래소가 생기면서 종목코드만 보내면 안되고 KRX데이터인지, NXT 데이터인지 통합데이터인지를 구분해서 보내야된다.

 

  - 즉 아래와 같이 호출할때 종목코드_AL  을 붙여줘야 전체 KRX+NXT 데이터 거래량이 나온다.

from api.Kiwoom import *
import sys

app = QApplication(sys.argv)
kiwoom = Kiwoom()
#kiwoom.get_account_number()


df = kiwoom.get_price_data("005930_AL")
print(df)

 

 

  -거래량 16416858이 제대로 나온것을 알 수 있다.

 

참고. 소스코드

 

https://github.com/GaKaRi/python_trading/tree/main/SystemTrading

 

python_trading/SystemTrading at main · GaKaRi/python_trading

Contribute to GaKaRi/python_trading development by creating an account on GitHub.

github.com