ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [키움API]파이썬 주식 종목별 종가정보 불러오기5: UI파일 화면구성(Qt Designer) 및 프로그램module과 CLASS구성
    금융퀀트/(퀀트)증권사API활용(키움) 2023. 12. 19. 08:07
    반응형

    프로그램 UI 재구성

    코스피의 전체종목 정보를 불러와서([키움API]파이썬 주식 종가정보 불러오기3: 전체종목 기간별 종가조회)DB에 저장하는([키움API]파이썬 주식 종가정보 불러오기4: 데이터 DB저장(mysql)) 기능을 [키움API]python 메인 화면 만들기(Qt Designer 사용)에서 만든 메인 화면버튼에 추가할 필요가 있다.

    그림1: Qt Designer로 구성한 화면 위젯의 클래스명과 아이디

    위 그림 1 처럼 먼저 화면 내에 다양한 모양의 위젯을 추가해 준다. 각 위젯에는 클래스명과 아이디가 있고 위 그림 1에서는 "클래스명":"아이디"로 표시했다. 위젯에서 데이터를 가져오거나 클릭 이벤트를 감지할 때 클래스명과 아이디를 통해서 기능을 구현한다. 

    프로그램 구조도

    "주식 종목별 종가정보를 불러오는" 단순한 작업은 "메인화면에서 전체 기능을 조율", "키움 API 호출", "키움 API로 받은 데이터를 정제", "MySQL을 이용해서 DB에 데이터 저장", "UI파일의 화면에 기능 부여"라는 복잡한 과정을 거쳐서 완료할 수 있다. 따라서 프로그램의 기능을 세분화하여 모듈화하고(기능별로 파이썬 파일을 만들고), 모듈별로 클래스를 만들어서 프로그램을 구조화하는 것이 필요하다. 따라서 아래 그림 2와 같이 main.py(메인화면에서 전체 기능을 조율), kiwoomapi.py(키움API 호출), apihandle.py(키움 API로 받은 데이터를 정제), sqlhandle.py(MySQL을 이용해서 DB에 데이터 저장), uihandle.py(UI파일의 화면에 기능 부여)라는 5개의 파일을 만들고(각각의 파일을 모듈이라고 한다.) 모듈에 각각 클래스를 만들어서 프로그램을 구성한다.

    그림2: 주식 종가정보 불러오기 프로그램 구조

    프로그램 기능별 코드

    main.py(KiwoomAPIForm)

    메인에서는 main UI를 불러오고, QApplication 실행, 키움 API 객체를 불러와서 ocx라는 변수에 저장, 초기화면 구성, 버튼별 함수 부여 기능을 한다. 여기서 로그인 버튼의 함수는 uihandle 모듈의 UIhandle 클래스 내의 apilogin 함수를 사용하고, 기간별 종가데이터 입수 버튼의 함수는 uihandle 모듈의 UIhandle 클래스 내의 timeseriesdataget 함수를 사용한다.

    import sys
    from PyQt5.QtWidgets import *
    from PyQt5.QAxContainer import *
    from PyQt5.QtCore import QDate
    from PyQt5 import uic
    from uihandle import UIhandle
    
    form_class = uic.loadUiType("UI 파일의 경로 입력")[0]
    
    class KiwoomAPIForm(QMainWindow, form_class):
        def __init__(self):
            super().__init__()
            self.setupUi(self)
            self.setWindowTitle("KiwoomTest")
            self.ocx = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
            # UIhandle 클래스 불러오고 현재 QApplication 을 변수로 넘김(두 번째 self 변수)
            self.ui = UIhandle(self.ocx, self)
            # UI의 버튼들을 그룹화
            self.buttongroup = QButtonGroup()
            # 하단의 초기 세팅 함수 작동
            self.initsetting()
            # 버튼 클릭시 하단의 butoonfunction 함수 작동 
            self.buttongroup.buttonClicked.connect(self.buttonfunction)
        
        def initsetting(self):
        	# 직전일 가져오기
            yesterday = QDate.currentDate().addDays(-1)
            # 화면 내부에서 그림 1의 date_base 부분 찾아서 직전일로 세팅
            self.findChild(QDateEdit, "date_base").setDate(yesterday)
            # 버튼그룹에 btn_login, btn_dataget 두가지 버튼 넣어주기(인덱스 1, 2 부여)
            self.buttongroup.addButton(self.btn_login, 1)
            self.buttongroup.addButton(self.btn_dataget, 2)
        
        def buttonfunction(self, button):
            buttonid = self.buttongroup.id(button)
            # 인덱스 1 일 때 UIhandle 클래스의 apilogin 함수 불러오기
            if buttonid == 1:
                self.ui.apilogin()
            # 인덱스 2 일 때 UIhandle 클래스의 timeseriesdataget 함수 불러오기
            elif buttonid == 2:
                self.ui.timeseriesdataget()
                
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        window = KiwoomAPIForm()
        window.show()
        sys.exit(app.exec_())

     

    kiwoomapi.py(KIWOOMAPI)

    키움API에서는 키움 API에서 제공하는 함수를 API개발가이드에 있는 그대로 사용하는 부분이다. 키움 API의 ActiveX를 변수로 받아서 변수로 받은 ActiveX를 사용해서 키움API 함수를 호출하는 것이 핵심이다.

    #KIWOOMapi
    class KIWOOMapi:
        def __init__(self, ocx: object):
            self.ocx = ocx
        
        def CommConnect(self):
            self.ocx.dynamicCall("CommConnect()")
            
        def GetCodeListByMarket(self, mkcode: str):
            return self.ocx.dynamicCall("GetCodeListByMarket(Qstring)", mkcode)
    
        def SetInputValue(self, valuename: str, value: str):
            self.ocx.dynamicCall("SetInputValue(QString, QString)", valuename, value)
            
        def CommRqData(self, rqname: str, trcode: str, trname: str):
            self.ocx.dynamicCall("CommRqData(QString, QString, int, QString)", rqname, trcode, 0, trname)
        
        def GetCommData(self, trcode: str, trname: str, resultidx: int, item: str):
            return self.ocx.dynamicCall("GetCommData(QString, QString, int, QString)", trcode, trname, resultidx, item)

     

    apihandle.py(APIHANDLE)

    API핸들은 키움API함수를 사용해서 실제 KOAStudio에 구현된 것처럼 데이터를 실제로 주고받고, 처리할 수 있게 구현한 부분이다. rqstockpriceinfo 부분은 주식일봉차트 데이터를 요청하는 부분을 키움 API의 함수를 조합해서 만들고, getstockpricedf는 입수된 주식일봉차트 데이터를 dataframe으로 만들어서 return 하는 부분 insertstockpricedf는 받은 데이터를 아래에 나오는 SQL핸들을 이용해서 DB에 저장하는 부분이다.(자세한 설명은 [키움API]파이썬 주식 종가정보 불러오기3: 전체종목 기간별 종가조회 참조)

    #APIhandle
    from kiwoomapi import KIWOOMapi
    from sqlhandle import SQLhandle
    import pandas as pd
    
    class APIhandle:
        def __init__(self, ocx: object):
            self.api = KIWOOMapi(ocx)
            self.sql = SQLhandle()
            
        def connectevent(self, err_code):
            if err_code == 0:
                return "Login Success!!"
            else:
                return "Login Failed"
        
        def rqstockpriceinfo(self, rqname: str, trcode: str, trname: str, stockcode: str, date: str):
            self.api.SetInputValue("종목코드", stockcode)
            self.api.SetInputValue("기준일자", date)
            self.api.SetInputValue("수정주가구분", "")
            self.api.CommRqData(rqname, trcode, trname)
        
        def getstockpricedf(self, trcode: str, trname: str):
            data = []
            columns = ["종목코드", "일자", "현재가", "거래량", "시가", "고가", "저가"]
            for resultidx in range(600):
                items = []
                for column in columns:                    
                    item = self.api.GetCommData(trcode, trname, resultidx, column)
                    if resultidx == 0 and column == "종목코드":
                        stockcode = item.strip()
                        items.append(item.strip())
                    elif resultidx > 0 and column == "종목코드":
                        items.append(stockcode)
                    else:
                        items.append(item.strip())
                data.append(items)
            data = pd.DataFrame(columns=columns, data=data)
            return data
      
        def insertstockpricedf(self, trcode: str, trname: str):
            df = self.getstockpricedf(trcode, trname)
            self.sql.timeseriesdatasave("stockdailychart", df)

     

    sqlhandle.py(SQLHANDLE)

    SQL핸들은 주식일봉데이터 저장을 위해서 존재한다. 그 사전작업으로 createengine으로 MySQL 연결을 만들고 checktableexists로 테이블이 존재하는지 확인하고, checkdataexists로 데이터가 존재하는지 확인하고, getcolumns로 데이터의 컬럼을 확인하는 작은 함수들이 존재한다.(자세한 코드 설명은 [키움API]파이썬 주식 종가정보 불러오기4: 데이터 DB저장(mysql) 참조)

    #SQLhandle
    import pymysql
    import sqlalchemy as db
    import pandas as pd
    pymysql.install_as_MySQLdb()
    
    class SQLhandle:
        def __init__(self):
            self.user = "MySQL에 설정한 사용자명"
            self.passwd = "서버 비밀번호"
            self.host = "127.0.0.1" # 내 컴퓨터의 localhost 접속이므로 기본 ip인 127.0.0.1 입력
            self.db_port = "포트번호" # 로컬이면 3306 
            self.db_name = "kiwoom"
            self.engine = self.createengine()
        
        def createengine(self):
            db_connection_str = "mysql+mysqldb://" + self.user + ":" +  \
                self.passwd + "@" + self.host + ":" + self.db_port +"/"+ \
                self.db_name
            db_connection = db.create_engine(db_connection_str)
            return db_connection
        
        def checktableexists(self, engine, tablename: str):
            inspector = db.inspect(engine)
            return inspector.has_table(tablename)
    
        def checkdataexists(self, tablename: str, condition: list):
            with self.engine.connect() as connection:
                query = db.text(f"SELECT * FROM {tablename} WHERE 종목코드 = '{condition[0]}' AND 일자 = '{condition[1]}'")
                result = connection.execute(query)
                return result.fetchall()
        
        def getcolumns(self, tablename: str):
            with self.engine.connect() as connection:
                columns = db.inspect(connection).get_columns(tablename)
                columns = [column['name'] for column in columns]
                return columns
    
        def timeseriesdatasave(self, tablename: str, df: pd.DataFrame):
            if len(df)>0:
                conn = self.engine.connect()
                if self.checktableexists(self.engine, tablename):
                    columns = self.getcolumns(tablename)
                    columnnames = ", ".join(columns)
                    valueslist = []
                    for row in df.itertuples():
                        conditions = [row[1], row[2]]
                        if self.checkdataexists(tablename, conditions):
                            pass
                        else:
                            values = [f"'{value}'" for value in row[1:]]
                            values = ", ".join(values)
                            valueslist.append(f"({values})")
                    if len(valueslist) > 0:
                        valueslist = ", ".join(valueslist)
                        query = db.text(f"INSERT INTO {tablename} ({columnnames}) VALUES {valueslist}")
                        try:
                            conn.execute(query)
                            conn.commit()
                        except Exception as e:
                            print(f"Error executing query: {e}")
                    else:
                        pass   
                    conn.close()     
                else:
                    df.to_sql(name=tablename, con=conn, index=False)
                    conn.close()
            else:
                pass

     

    uihandle.py(UIHANDLE)

    UI핸들은 프로그램 UI에서 나타나는 이벤트를 처리하는 부분이다. 그림 1의 QTextEdit 클래스의 txt_result에 결과값을 넣어주는 postresult 함수, QDateEdit 클래스에서 날짜를 받아오는 getdate 함수, QSpinBox에서 값을 받아 오는 getspinboxvalue 함수 등이 기본적으로 존재한다. 그리고 화면에서 로그인 버튼을 눌렀을 때 작동하는 apilogin, 데이터 입수 버튼을 눌렀을 때 작동하는 timeseriesdataget 함수가 존재한다.

    #UIhandle
    from PyQt5.QtWidgets import *
    import time
    from kiwoomapi import KIWOOMapi
    from apihandle import APIhandle
    from sqlhandle import SQLhandle
    
    class UIhandle(QWidget):
        def __init__(self, ocx: object, mainui):
            super().__init__()
            self.mainui = mainui
            self.ocx = ocx
            self.api = KIWOOMapi(ocx)
            self.handle = APIhandle(ocx)
            self.sql = SQLhandle()
            self.timer = time
        
        # data 받아서 txt_result 에 값 입력(set)
        def postresult(self, data: object):
            txt_result = self.mainui.findChild(QTextEdit, "txt_result")
            txt_result.setPlainText(str(data))
            
        # 날짜 나타내는 위젯 아이디 받아서 날짜값 return
        def getdate(self, dateeditid: str):
            date = self.mainui.findChild(QDateEdit, dateeditid)
            return date
    	
        # spinbox 아이디 받아서 spinbox 값 return
        def getspinboxvalue(self, spinboxeditid: str):
            spinbox = self.mainui.findChild(QSpinBox, spinboxeditid)
            return spinbox.value()
    
    	# api 로그인 함수
        def apilogin(self):
        	# KIWOOMapi의 객체인 self.api 의 CommConnect 함수 사용
            self.api.CommConnect()
            # 콜백함수 정의
            def login_callback(err_code):
            	# APIhandle의 객체인 self.handle 의 connectevent 함수 사용
                result = self.handle.connectevent(err_code)
                # data 받아서 txt_result 에 값 입력(set)
                self.postresult(result)
            self.ocx.OnEventConnect.connect(login_callback)
    
        def timeseriesdataget(self):
        	# KIWOOMapi의 객체인 self.api 의 GetCodeListByMarket 함수 사용
            data = self.api.GetCodeListByMarket("0")
            stockcodes = data.split(";")
            stockcodes = stockcodes[:-1] if stockcodes[-1] == "" else stockcodes
            # 이 클래스의 위에 정의된 getdate 함수 사용해서 날짜 받기
            basedate = self.getdate("date_base").date().toString("yyyyMMdd")
            # 이 클래스의 위에 정의된 getspinboxvalue 함수 사용해서 작업범위 받기 
            fromno = int(self.getspinboxvalue("spinBox_from"))
            tono = int(self.getspinboxvalue("spinBox_to"))
            # 콜백 함수 정의
            # APIhandle의 객체인 self.handle 의 insertstockpricedf 함수 사용
            callback = lambda: self.handle.insertstockpricedf("opt10081", "주식일봉차트")
            self.ocx.OnReceiveTrData.connect(callback)
            for stockcode in stockcodes[fromno:tono+1]:
            	# APIhandle의 객체인 self.handle 의 rqstockpriceinfo 함수 사용
                self.handle.rqstockpriceinfo(stockcode, "opt10081", "주식일봉차트", stockcode, basedate)
                self.timer.sleep(0.5)
                print("{} processed!".format(stockcode))
            self.ocx.OnReceiveTrData.disconnect(callback)
            # 이 클래스의 위에 정의된 postresult 함수 사용해서 결과 txt_result에 입력하기
            self.postresult("Process Done!")

     

    반응형
Designed by Tistory.