8 minute read

pytrader.py

이전 포스팅에서 기본 클래스(kiwoom.py)와 UI를 제작하였다.

이제 UI들에 기능을 넣어주고, kiwoom.py의 데이터들을 구현하는 코드를 작성한다.

pytrader.py - UI 불러오기 및 init() 생성


import os

import sys



from PyQt5.QtWidgets import *

from PyQt5.QtCore import *

from PyQt5 import uic



from PyQt5.QtWidgets import QApplication, QWidget



from kiwoom import Kiwoom           # 키움증권 함수/공용 (싱글턴)



from Qthread_1 import Thread1       # 계좌평가잔고내역 가져오기

from Qthread_2 import Thread2       # 계좌 관리

from Qthread_3 import Thread3       # 자동 매매 시작하기



from News_all import secondwindow           # 웹 크롤링 팝업 창

from Division_Stock import Thirdwindow      # 분할 자동 매매 팝업 창

from Kiwoom_Stock import Forthwindow        # 키움 조건식 자동 매매 팝업 창



form_class = uic.loadUiType("pytrader.ui")[0]



class Login_Machnine(QMainWindow, QWidget, form_class) :

    def __init__(self, *args, **kwargs) :

        print("Login Machine 실행")

        super(Login_Machnine, self).__init__(*args, **kwargs)

        form_class.__init__(self)

        self.setUI()

        

        ### 계좌평가잔고내역 초기 세팅

        self.label_l1.setText(str("총매입금액"))

        self.label_l2.setText(str("총평가금액"))

        self.label_l3.setText(str("추정예탁자산"))

        self.label_l4.setText(str("총평가손익금액"))

        self.label_l5.setText(str("총수익률(%)"))

        ###

        

        self.searchItemTextEdit2.setAlignment(Qt.AlignRight)

        

        self.buy_price.setAlignment(Qt.AlignRight)

        self.buy_price.setDecimals(0)

        self.n_o_stock.setAlignment(Qt.AlignRight)

        self.n_o_stock.setDecimals(0)

        self.profit_price.setAlignment(Qt.AlignRight)

        self.profit_price.setDecimals(0)

        self.loss_price.setAlignment(Qt.AlignRight)

        self.loss_price.setDecimals(0)

        

        self.login_event_loop = QEventLoop()

        

        ### 키움증권 로그인

        self.k = Kiwoom()

        self.set_signal_slot()                  # 키움 로그인을 위한 명령어 전송 시 받는 공간 할당

        self.signal_login_commConnect()

        ###

        

        ### 이벤트 생성 및 진행

        self.call_account.clicked.connect(self.c_acc)               # 계좌 정보 가져오기

        self.acc_manage.clicked.connect(self.a_manage)              # 계좌 정보 가져오기

        self.Auto_start.clicked.connect(self.auto)                  # 자동 매매 시작하기

        self.div_stock.clicked.connect(self.Division)               # 분할 매매 시작하기

        self.Kiwoom_auto.clicked.connect(self.Kiwoom_ra)            # 키움 조건식 자동매매 시작하기

        ###

        

        self.CRR.clicked.connect(self.Crolling)                     # 웹 크롤링

        

        ### 부가기능 1 : 종목선택, 새로운 종목 추가 및 삭제

        self.k.kiwoom.OnReceiveTrData.connect(self.trdata_slot)     # 키움서버 데이터 받는 곳

        self.additemlast.clicked.connect(self.searchItem2)          # 종목 추가

        self.Deletecode.clicked.connect(self.deletecode)            # 종목 삭제

        ###

        

        ### 부가기능 2 : 데이터베이스화, 저장, 삭제, 불러오기

        self.Getanal_code = []                                      # 불러온 파일 저장

        self.Save_Stock.clicked.connect(self.Save_selected_code)    # 종목 저장

        self.Del_Stock.clicked.connect(self.delete_code)            # 종목 삭제

        self.Load_Stock.clicked.connect(self.Load_code)             # 종목 불러오기

        ###



라이브러리를 불러오고, 여러 기능들(계좌 관리, 자동 매매 등)을 수행하는 Thread들을 연결한다. (각 스레드들은 차후 포스팅)


from_class.__init__(self)

self.setUI()

위의 코드 부분은 앞서 제작했던 UI를 불러온다.

setUI() 메서드는 다음과 같이 구성된다.


def setUI(self) :

    self.setupUi(self)


계좌평가잔고내역 세팅

다음으로, UI를 우리가 지정한 이름으로 세팅해준다.


### 계좌평가잔고내역 초기 세팅

self.label_l1.setText(str("총매입금액"))

self.label_l2.setText(str("총평가금액"))

self.label_l3.setText(str("추정예탁자산"))

self.label_l4.setText(str("총평가손익금액"))

self.label_l5.setText(str("총수익률(%)"))

###

label_l1, label_l2, …, label_l5는 Qdesigner에서 지정한 박스의 이름들이며, 각각의 박스에 지정된 텍스트들을 넣는다.

위의 텍스트를 넣는 과정을 하면 밑의 이미지처럼 초기 세팅이 완료된다.

image.png


거래창 세팅

다음으로 거래할 종목을 입력하고 매수, 매도를 할 수 있는 박스들의 초기값을 세팅해준다.


self.searchItemTextEdit2.setAlignment(Qt.AlignRight)



self.buy_price.setAlignment(Qt.AlignRight)

self.buy_price.setDecimals(0)

self.n_o_stock.setAlignment(Qt.AlignRight)

self.n_o_stock.setDecimals(0)

self.profit_price.setAlignment(Qt.AlignRight)

self.profit_price.setDecimals(0)

self.loss_price.setAlignment(Qt.AlignRight)

self.loss_price.setDecimals(0)

위에서 계좌평가잔고내역을 세팅해준 것과 마찬가지로 동일하게 세팅해주면, 빨간색 동그라미 부분이 세팅된다.

image.png


pytrader.py - 로그인

이제 우리는 키움 증권에 로그인 후 로그인 정보를 받아와야한다.


self.login_event_loop = QEventLoop()



### 키움증권 로그인

self.k = Kiwoom()

self.set_signal_slot()                  # 키움 로그인을 위한 명령어 전송 시 받는 공간 할당

self.signal_login_commConnect()

###

로그인 - QEventLoop()

키움 증권의 OpenAPI는 대부분 이벤트 기반의 작업으로 처리된다.

즉, 어떤 이벤트가 발생하면 그 이벤트가 제대로 처리(완료)되기 전 까지는 다음으로 넘어가지 않도록 한다.

QEventLoop() 는 이러한 역할을 수행하는 부분으로, 로그인 과정이 시작되기 전 이벤트를 대기하는 준비 과정이다.


로그인 - 객체 초기화

self.k = Kiwoom() 은 우리가 이전 포스팅에서 정의했던 키움 OpenAPI의 객체들을 초기화한다.


로그인 - 이벤트 발생 및 처리

self.set_signal_slot() 에서는 로그인 이벤트 발생 시 성공/실패를 처리하는 부분으로 다음과 같이 구성된다.


def set_signal_slot(self) :

    self.k.kiwoom.OnEventConnect.connect(self.login_slot)

그리고, login_slot


def login_slot(self, errCode) :

    if errCode == 0 :

        print("로그인 성공")

        self.statusbar.showMessage("로그인 성공")

        self.get_account_info()                     # 로그인 성공 시 계좌정보 가져오기

        

    elif errCode == -100 :

        print("사용자 정보교환 실패")

    elif errCode == -101 :

        print("서버접속 실패")

    elif errCode == -102 :

        print("버전처리 실패")

        

    self.login_event_loop.exit()                    # 로그인 완료 시 로그인 창 닫기

위처럼 되고, get_account_info()


def get_account_info(self) :

    account_list = self.k.kiwoom.dynamicCall("GetLoginInfo(String)", "ACCNO")

    for i in account_list.split(';') :

        self.accComboBox.addItem(i)


앞서 이벤트가 발생하면 self.signal_login_commConnect() 을 수행하는데, CommConnect() 메서드로 키움 OpenAPI에 로그인 요청을 보내 로그인 작업을 처리하고 결과를 반환한다.


def signal_login_commConnect(self) :

    self.k.kiwoom.dynamicCall("CommConnect()")      # 데이터 전송 함수

    

    

    self.login_event_loop.exec_()                   # 로그인이 완료될 때까지 반복

pytrader.py - 이벤트 생성 및 진행

로그인을 완료했다면 이제 UI 내부의 기능들을 수행할 수 있도록 해줘야한다.

계좌 정보 및 계좌 관리 정보


self.call_account.clicked.connect(self.c_acc)               # 계좌 정보 가져오기

self.acc_manage.clicked.connect(self.a_manage)              # 계좌 관리 정보 가져오기

계좌 정보 및 계좌 관리 정보를 가져오는 부분이다.

call_account, acc_manage는 버튼 박스의 이름으로 해당 박스를 누르면 아래의 메서드를 실행한다.

c_acc 메서드와 a_manage 메서드는 다음과 같다.


def c_acc(self) :

    print("선택 계좌 정보 가져오기")

    h1 = Thread1(self)

    h1.start()

    

def a_manage(self) :

    print("계좌 관리")

    h2 = Thread2(self)

    h2.start()

해당 계좌의 정보들을 출력해준다. 현재 거래하는 종목이 없기에 계좌 내역 및 계좌 관리에 아무것도 뜨지 않았다.

image.png

각 스레드(Thread1, Thread2)는 차후 포스팅에서 설명한다.


매매 및 시황 분석


self.Auto_start.clicked.connect(self.auto)                  # 자동 매매 시작하기

self.div_stock.clicked.connect(self.Division)               # 분할 매매 시작하기

self.Kiwoom_auto.clicked.connect(self.Kiwoom_ra)            # 키움 조건식 자동매매 시작하기



self.CRR.clicked.connect(self.Crolling)                     # 웹 크롤링

각 매매를 수행 or 수행하는 윈도우로 이동한다.

계좌 정보 및 계좌 관리 정보를 가져오기와 동일하다.


def auto(self) :

    print("자동매매 시작")

    h3 = Thread3(self)

    h3.start()



def Division(self) :

    print("분할매매 하기")

    self.third = Thirdwindow()

    

def Kiwoom_ra(self) :

    print("키움조건식 자동매매")

    self.forth = Forthwindow()



def Crolling(self) :

    print("뉴스 가져오기")

    self.second = secondwindow()

해당 부분은 다음의 이미지 부분을 구현한다.

image.png


pytrader.py - 부가기능 1 : 종목 선택, 추가, 삭제

해당 부분에서는 주식 거래를 할 수 있도록 특정 종목을 추가 및 삭제하는 기능을 수행한다.


self.k.kiwoom.OnReceiveTrData.connect(self.trdata_slot)     # 키움서버 데이터 받는 곳

self.additemlast.clicked.connect(self.searchItem2)          # 종목 추가

self.Deletecode.clicked.connect(self.deletecode)            # 종목 삭제

서버 데이터 수신

만약 사용자가 특정 TR 요청을 서버에 전송하면, API는 이를 서버에서 호출되어 데이터를 받아 처리한다.

이를 수신했을 때 수행하는 이벤트가 OnReceiveTrData 이고, 서버에서 데이터를 받을 때 마다 trdata_slot 메서드가 호출된다.

trdata_slot 에서는 주식기본정보요청 이라는 TR 요청을 하고 그에대한 응답으로 특정 종목의 정보를 받아오는 역할을 수행한다.


def trdata_slot(self, sCrNo, sRQName, sTrCode, sRecordName, sPrevNext) :

    if sTrCode == "opt10001" :

        if sRQName == "주식기본정보요청" :

            currentPrice = abs(int(self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, 0, "현재가")))

            D_R = (self.k.kiwoom.dynamicCall("GetCommData(QStirng, QString, int, QString)", sTrCode, sRQName, 0, "신용비율")).strip()

            row_count = self.buylast.rowCount()

            

            self.buylast.setItem(row_count-1, 2, QTableWidgetItem(str(format(currentPrice, ","))))

            self.buylast.setItem(row_count-1, 3, QTableWidgetItem(str(D_R)))

            

            self.buylast.item(row_count-1, 2).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(row_count-1, 3).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

특정 종목에 대한 정보를 받아오고 그 종목의 현재가 및 신용비율을 제작했던 UI의 특정 공간(buylast)에 넣는다.


종목 추가 및 삭제

위에서 특정 종목에 대한 데이터를 수신하는 메서드를 만들었는데, 어떤 종목에 대한 데이터를 수신할 것인지 우리는 알아야한다.

주식 거래를 위해 종목에 대해 간단한 정보(현재가, 신용비율 등)를 수신함과 동시에 우리가 지정한 정보들(매수가, 매수수량, 익절가, 손절가)들을 같이 묶어 추가하는 메서드를 구성한다.


    def searchItem2(self) :

        item_name = self.searchItemTextEdit2.toPlainText()

        if item_name != "" :

            for code in self.k.All_Stock_Code.keys() :

                if item_name == self.k.All_Stock_Code[code]["종목명"] :

                    self.new_code = code



            row_count = self.buylast.rowCount()

            for row in range(row_count) :

                existing_code = self.buylast.item(row, 0)

                if existing_code and existing_code.text() == self.new_code:

                    print(f"이미 추가된 종목: {item_name}")

                    return



            column_head = ["종목코드", "종목명", "현재가", "신용비율", "매수가", "매수수량", "익절가", "손절가"]

            col_count = len(column_head)

            

            self.buylast.setColumnCount(col_count)

            self.buylast.setRowCount(row_count+1)

            self.buylast.setHorizontalHeaderLabels(column_head)

            

            self.buylast.setItem(row_count, 0, QTableWidgetItem(str(self.new_code)))

            self.buylast.setItem(row_count, 1, QTableWidgetItem(str(item_name)))

            self.buylast.setItem(row_count, 4, QTableWidgetItem(str(format(int(self.buy_price.value()), ","))))

            self.buylast.setItem(row_count, 5, QTableWidgetItem(str(format(int(self.n_o_stock.value()), ","))))

            self.buylast.setItem(row_count, 6, QTableWidgetItem(str(format(int(self.profit_price.value()), ","))))

            self.buylast.setItem(row_count, 7, QTableWidgetItem(str(format(int(self.loss_price.value()), ","))))

            

            self.buylast.item(row_count, 0).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(row_count, 1).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(row_count, 4).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(row_count, 5).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(row_count, 6).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(row_count, 7).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            

            self.getItemInfo(self.new_code)



        else :

            print("종목 이름 필요")

종목명은 사용자가 직접 입력한 값(item_name)이고, All_Stock_Code 는 모든 종목의 코드를 저장해놓은 Dictionary로 사용자가 입력한 종목명에 해당하는 종목 코드(new_code)를 반환한다.

특정 종목에 대한 정보를 수신했으므로 서버에서 수신한 데이터(종목코드, 종목명 등)와 우리가 지정한 데이터(매수가, 매수수량 등)를 묶어 UI에 전시한다.

여기서 우리는 현재가와 신용비율에 대한 정보는 키움 서버에 요청해서 데이터를 받아야 알 수 있으므로 이를 처리하는 메서드인 getItemInfo 메서드를 생성한다.



def getItemInfo(self, new_code) :

    self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", new_code)

    self.k.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "주식기본정보요청", "opt10001", 0, "100")

해당 메서드에서 주식기본정보요청으로 trdata_slot 메서드가 실행되고 서버에서 데이터를 수신받아 현재가와 신용비율에 대한 정보를 얻어올 수 있는 것이다.

예를 들어, “카카오” 종목을 추가한다면 다음과 같이 된다. (매수 가격, 매수 수량, 익절 가격, 손절 가격은 0이다.)

image.png


종목 추가 코드 다음은 종목 삭제 메서드이다.


def deletecode(self) :

    x = self.buylast.selectedIndexes()

    if len(x) == 0 :

        print("삭제할 종목이 없거나 삭제할 종목을 클릭")

    else :

        self.buylast.removeRow(x[0].row())

위에서 종목 추가 메서드를 구현하면서 거래와 동시에 이루어지는 것이 아닌 단순히 어떤 종목을 추가하였는지 보기 위한 기능만 구현하였다.

그렇다면 삭제할 때에는 단순히 우리가 보는 UI에서 없애주면 되기 때문에 간단하게 구현할 수 있다.

pytrader.py - 부가기능 2 : 데이터베이스화

위의 과정을 매 실행때마다 반복하는 것은 굉장히 비효율적인 방법이다.

사용자가 특정 종목에 대한 정보를 추가했다면 이를 데이터베이스에 저장, 후에 불러온다면 훨씬 편하게 사용할 수 있다.

따라서 우리가 추가했던 종목을 데이터베이스화하는 부가기능을 추가한다.


self.Getanal_code = []                                      # 불러온 파일 저장

self.Save_Stock.clicked.connect(self.Save_selected_code)    # 종목 저장

self.Del_Stock.clicked.connect(self.delete_code)            # 종목 삭제

self.Load_Stock.clicked.connect(self.Load_code)             # 종목 불러오기


데이터베이스 - 저장

먼저 Save_selected_code를 보면 다음과 같다.


def Save_selected_code(self) :

    for row in range(self.buylast.rowCount()) :

        code_n = self.buylast.item(row, 0).text()

        name = self.buylast.item(row, 1).text().strip()

        price = self.buylast.item(row, 2).text()

        dept = self.buylast.item(row, 3).text()

        buy = self.buylast.item(row, 4).text()

        n_o_stock = self.buylast.item(row, 5).text()

        profit = self.buylast.item(row, 6).text()

        loss = self.buylast.item(row, 7).text()

        

        

        f = open("dist/Selected_code.txt", "a", encoding="utf8")        # "a" : 달아 쓰기, "w" : 덮어 쓰기

        f.write("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (code_n, name, price, dept, buy, n_o_stock, profit, loss))

        f.close()

buylast에 추가된 종목들의 종목코드, 이름, 가격 등을 Selected_code.txt라는 간단한 데이터베이스에 넣어 저장한다.

종목이 저장되면 다음과 같이 메모장에 저장된다.

image.png

우리는 이제 이 간단한 데이터베이스를 이용하여 불러온 뒤 사용할 수 있다.


데이터베이스 - 삭제

데이터베이스를 만들었으므로 저장된 데이터를 불러오거나 삭제할 수 있어야 한다.

먼저 삭제를 보면 다음과 같다.


def delete_code(self) :

    if os.path.exists("dist/Selected_code.txt") :

        os.remove("dist/Selected_code.txt")

삭제를 누르면 데이터베이스의 모든 데이터가 삭제되도록 구현하였다.


데이터베이스 - 불러오기

데이터베이스에 저장된 자동매매할 종목들을 불러온다.


def Load_code(self) :

    if os.path.exists("dist/Selected_code.txt") :

        f = open("dist/Selected_code.txt", "r", encoding="utf8")

        lines = f.readlines()

        

        if not lines :

            msg = QMessageBox()

            msg.setIcon(QMessageBox.Warning)

            msg.setText("파일이 비어있습니다.")

            msg.setWindowTitle("경고")

            msg.exec_()

            

        for line in lines :

            if line != "" :

                ls = line.split("\t")

                t_code = ls[0]

                t_name = ls[1]

                current_price = ls[2]

                dept = ls[3]

                buy = ls[4]

                n_o_stock = ls[5]

                profit = ls[6]

                loss = ls[7].split("\n")[0]

                

                self.Getanal_code.append([t_code, t_name, current_price, dept, buy, n_o_stock, profit, loss])

        f.close()

        

        column_head = ["종목코드", "종목명", "현재가", "신용비율", "매수가", "매수수량", "익절가", "손절가"]

        colCount = len(column_head)

        rowCount = len(self.Getanal_code)

        

        self.buylast.setColumnCount(colCount)                               # 행 개수

        self.buylast.setRowCount(rowCount)                                  # 열 개수

        self.buylast.setHorizontalHeaderLabels(column_head)                 # 행 이름 삼입

        self.buylast.setSelectionMode(QAbstractItemView.SingleSelection)

        

        for index in range(rowCount) :

            self.buylast.setItem(index, 0, QTableWidgetItem(str(self.Getanal_code[index][0])))

            self.buylast.setItem(index, 1, QTableWidgetItem(str(self.Getanal_code[index][1])))

            self.buylast.setItem(index, 2, QTableWidgetItem(str(self.Getanal_code[index][2])))

            self.buylast.setItem(index, 3, QTableWidgetItem(str(self.Getanal_code[index][3])))

            self.buylast.setItem(index, 4, QTableWidgetItem(str(self.Getanal_code[index][4])))

            self.buylast.setItem(index, 5, QTableWidgetItem(str(self.Getanal_code[index][5])))

            self.buylast.setItem(index, 6, QTableWidgetItem(str(self.Getanal_code[index][6])))

            self.buylast.setItem(index, 7, QTableWidgetItem(str(self.Getanal_code[index][7])))

            

            self.buylast.item(index, 0).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 1).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 2).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 3).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 4).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 5).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 6).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

            self.buylast.item(index, 7).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)

데이터베이스에 저장된 데이터를 가져오고 UI에 전시한다.

자동매매 프로그램에서 가장 기본이 되는 창을 만들고, 기능을 구현하였다.

현재 계좌평가잔고내역 조회계좌 관리가 Thread로 구현되어 있다.

그렇기에 종목을 넣고 자동매매를 할려고 해도 계좌 정보가 없기에 실행되지 않는다.

다음 포스팅에서는 계좌평가잔고내역 조회를 구현한 Thread를 분석한다.