주식자동매매 41강. 실시간 자동매매 구현(3), 자동 매매 시작 GUI(Push Button) 및 스레드 스크립트 생성
본문 바로가기

주식 자동매매 강의/기초반(모든 코딩의 뿌리)

주식자동매매 41강. 실시간 자동매매 구현(3), 자동 매매 시작 GUI(Push Button) 및 스레드 스크립트 생성

반응형

40강에서 종목 선정 후 매수/매도 가격을 QtableWidget에 입력까지 완료하였습니다. 이제는 QtableWidget에 입력된 값을 불러와 실시간 자동매매를 하기 위한 기초작업을 다지겠습니다.

 

[유튜브 강의, 링크]와 같이 보시면 많은 도움이 되실 겁니다.

 

1. 자동매매를 위한 GUI(Push Button) 생성

 아래 그림과 같이 Push Button을 생성 후 objectName은 Auto_start라고 입력하였으며, 버튼 이름은 자동매매 시작이라고 정의 하였습니다. 혹시 이름이 마음에 들지 않으시면 원하는 이름으로 교체하시면 됩니다. 

자동매매를 위한 GUI(Push Button) 생성
자동매매를 위한 GUI 생성

 

2. 자동매매를 위한 스레드 스크립트 생성

 오랜만에 스레드 스크립트를 생성합니다. Qthread_1에서는 계좌평가잔고내역을 가져왔으며, Qthread_2에서는 계좌관리 정보를 가져왔습니다. 이번에는 Qthread_3 스크립트를 생성하여 자동매매 시작을 위한 코드를 구성해 보겠습니다.

 

(1) Main Script에 추가될 내용

 Main Script에서 자동매매 시작을 위한 Qthread_3을 사용할 수 있게 불러옵니다.

  - form Qthread_3 import Thread3 : Qthread_3 스크립트에서 Thread3 클래스를 가져옵니다. 

 

자동매매 시작을 위한 Qthread_3 불러오기
자동매매 시작을 위한 Qthread_3 불러오기

  앞서 만든 Push Button인 Auto_start를 클릭하면 self.auto라는 함수가 실행되게 합니다.

  - self.Auto_start.clicked.connect(self.suto)

 

push button 클릭 시 특정 함수 실행
push button 클릭 시 특정 함수 실행

  auto 함수가 호출되면, Thread3 클래스가 실행되게 합니다.

  - def auto(self): auto 함수 생성

  - h3 = Thread3(self) : Thread3(self) 기능을 h3에 인스턴스화 합니다.

  - h3.start() : h3 객체를 실행합니다.

auto 함수 생성
auto 함수 생성

(2)Qhtread_3 구성하기

 Qhtread_3.py라는 스크립트를 만듭니다. 혹시.... 이거 못 만드시는 분은 강의를 처음부터 정주행 하기를 바랍니다. 

 그리고 4개의 함수를 불러옵니다. os부터 Kiwoom까지는 이전 강의에서 계속 설명 드렸던 부분이라 생략... 하려고 했으나 다시 한번 더 말씀드리겠습니다.

 - improt os : 현재 디렉터리가 어디인지 알려주는 함수입니다. 텍스트로 저장한(DB) 값들을 불러올 때 특정 디렉터리에 저장되어 있는지 확인하는 과정 시 사용됩니다.

 - from PyQt5.QtCore import * : 스레드 함수를 사용하기 위한 클래스를 불러옵니다. 여기 *안에 QThread함수도 담겨 있습니다. 

 - from kiwoom import Kiwoom : 우리가 만든 절대 언어이죠? 여기에는 키움 서버에 명령을 전송하기 위한 레지스터가 저장되어 있습니다.

 - from KiwoomType import * : 이 부분은 실시간 정보를 받아오기 위해 반드시 필요한 FID 번호를 저장한 곳입니다. 이 부분은 다음 강의에서 상세히 다루도록 하겠습니다. 그만큼 중요하기 때문입니다. 

 

Qhtread_3 구성하기
Qhtread_3 구성하기

 다음으로 Thread3 클래스를 구성하고 다시 말씀드리지만 클래스 시작 시 대문자로 하셔야 됩니다.

 - class Thread3(QThread) : Thread3라는 클래스를 만들었으며, 스레드 기능을 수행하기 위해 QThread라는 부모 클래스의 상속을 받습니다. 다시 말씀드리지만 상속은 정말 좋은 시스템입니다. 내가 아무것도 가지고 있지 않아도 상속만 받는다면 부모가 이뤄놓은 모든 것을 누릴 수 있기 때문입니다.

 - def __init__(self, parent) : 자식 클래스에서 부모 클래스의 내용을 모두 사용하고 싶을 때 사용하는 문구입니다. parent는 부모의 윈도우 창을 사용하기 위한 매개변수이며, init은 클래스 생성자를 이용하여 self라는 인스턴스에 parent라는 매개변수를 넣어 사용하겠다는 의미입니다.

 - supter().__init__(parent) : 부모 클래스를 초기화하며, 반드시 초기화 작업을 하셔야 부모의 기능을 사용하실 수 있습니다.

 - self.k = Kiwoom() : 키움에 명령을 전송하기 위한 다양한 기능 들을 메타 클래스 기반 싱글턴이라는 마법의 언어(Kiwoom)에서 self.k로 인스턴스화 합니다. 앞으로 self.k만 치시면 Kiwoom() 클래스 기능을 모두 사용하실 수 있습니다.

 

 여기까지 작성하시면 기본 기능은 다 되신 거고, 주문 전송 등을 하기 위해선는 현재 계좌 정보가 필요합니다. 각 Thread는 통신을 하지 못하기 때문에 Thread3에서 계좌번호를 알 수 있도록 앞서 만든 GUI에 입력된 계좌번호를 가져옵니다.

 - account = self.parent.accComboBox.currentText() : 우선 Thread3에서 부모 GUI에 접속하고 싶으면 앞서 설정한 parent 객체를 사용합니다. self.parent를 입력하면 부모 GUI 사용이 가능합니다. 그리고 부모 GUI에 accComboBox에 입력된 내용을 가져오기 위해 currentText() 함수를 사용하고 최종적으로 account에 입력합니다.

 - self.account_num = account : account는 self에 담겨져 있지 않기 때문에 현재 스크립트에 어디서든/누구나 계좌번호를 확인하지 못합니다. 따라서, self.account_num을 만들어 관련 내용을 넣습니다.

계좌 번호
계좌 번호

 

3. 요약 및 코드

 앞선 강의와 상당히 중복되는 부분이 많지만 다시 한번 복습하는 차원에서 상세히 말씀드렸습니다. 혹시 이해가 되지 않으시면, 모든 내용이 더욱 자세히 설명되어 있는 앞선 강의를 다시 한 번 더 정주행 하시길 바랍니다. 이번 강의에서는 자동매매를 위한 GUI push button 생성 및 Thread3을 만드는 과정을 진행하였습니다. 감사합니다.

 

반응형

Main Script 코드

 

import os                         # 현재 디렉토리 확인 기능
import sys                        # system specific parameters and functions : 파이썬 스크립트 관리
from PyQt5.QtWidgets import *     # GUI의 그래픽적 요소를 제어       하단의 terminal 선택, activate py37_32,  pip install pyqt5,   전부다 y
from PyQt5 import uic             # ui 파일을 가져오기위한 함수
from PyQt5.QtCore import *        # eventloop/스레드를 사용 할 수 있는 함수 가져옴.

################# 부가 기능 수행(일꾼) #####################################
from kiwoom import Kiwoom          # 키움증권 함수/공용 방 (싱글턴)
from Qthread_1 import Thread1      # 계좌평가잔고내역 가져오기
from Qthread_2 import Thread2      # 계좌 관리
from Qthread_3 import Thread3      # 자동매매 시작

#=================== 프로그램 실행 프로그램 =========================#

form_class = uic.loadUiType("ALBA.ui")[0]             # 만들어 놓은 ui 불러오기

class Login_Machnine(QMainWindow, QWidget, form_class):       # QMainWindow : PyQt5에서 윈도우 생성시 필요한 함수

    def __init__(self, *args, **kwargs):                      # Main class의 self를 초기화 한다.

        print("Login Machine 실행합니다.")
        super(Login_Machnine, self).__init__(*args, **kwargs)
        form_class.__init__(self)                            # 상속 받은 from_class를 실행하기 위한 초기값(초기화)
        self.setUI()                                         # UI 초기값 셋업 반드시 필요

        ### 초기 셋팅 : 계좌평가잔고내역
        self.label_11.setText(str("총매입금액"))
        self.label_12.setText(str("총평가금액"))
        self.label_13.setText(str("추정예탁자산"))
        self.label_14.setText(str("총평가손익금액"))
        self.label_15.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()  # 이때 QEventLoop()는 block 기능을 가지고 있다.

        ####키움증권 로그인 하기
        self.k = Kiwoom()                     # Kiwoom()을 실행하며 상속 받는다. 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)            # 자동매매 시작


        ################# 부가기능 1 : 종목선택하기 새로운 종목 추가 및 삭제
        self.k.kiwoom.OnReceiveTrData.connect(self.trdata_slot)           # 키움서버 데이터 받는 곳
        self.additmelast.clicked.connect(self.searchItem2)                # 종목 추가
        self.Deletcode.clicked.connect(self.deltecode)                        # 종목 삭제


        ################# 부가기능 2 : 데이터베이스화 하기, 저장, 삭제, 불러오기
        self.Getanal_code = []                                                 # 불러온 파일 저장
        self.Save_Stock.clicked.connect(self.Save_selected_code)               # 종목 저장
        self.Del_Stock.clicked.connect(self.delet_code)                        # 종목 삭제
        self.Load_Stock.clicked.connect(self.Load_code)                        # 종목 불러오기
        ####################

    def Load_code(self):

        if os.path.exists("dist/Selected_code.txt"):
            f = open("dist/Selected_code.txt", "r", encoding="utf8")
            lines = f.readlines()  # 여러 종목이 저장되어 있다면 모든 항목을 가져온다.
            for line in lines:
                if line != "":                     # 만약에 line이 비어 있지 않다면
                    ls = line.split("\t")          # \t(tap)로 구분을 지어 놓는다.
                    t_code = ls[0]
                    t_name = ls[1]
                    curren_price = ls[2]
                    dept = ls[3]
                    mesu = ls[4]
                    n_o_stock = ls[5]
                    profit = ls[6]
                    loss = ls[7].split("\n")[0]
                    self.Getanal_code.append([t_code, t_name, curren_price, dept, mesu, 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])))



    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()
            mesu = 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" 덮어 쓴다. files라느 파이썬 페키지 볼더를 만든다.
            f.write("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (code_n, name, price, dept, mesu, n_o_stock, profit, loss))  # t는 tap을 의미한다.
            f.close()

    def delet_code(self):

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

    def deltecode(self):
        x = self.buylast.selectedIndexes()  # 리스트로 선택된 행번호와 열번호가 x에 입력된다.
        self.buylast.removeRow(x[0].row())


    def searchItem2(self):            # 종목추가시 사용됨.
        itemName = self.searchItemTextEdit2.toPlainText()
        if itemName != "":
            for code in self.k.All_Stock_Code.keys():  # 포트폴리오에 저장된 코드들을 실시간 등록
                # 주식체결 정보 가져오기(틱 데이터) : 현재가, 전일대비, 등락률, 매도호가, 매수호가, 거래량, 누적거래량, 고가, 시가, 저가
                if itemName == self.k.All_Stock_Code[code]['종목명']:
                    self.new_code = code


        column_head = ["종목코드", "종목명", "현재가", "신용비율", "매수 가격", "매수 수량", "익절 가격", "손절 가격"]
        colCount = len(column_head)
        row_count = self.buylast.rowCount()

        self.buylast.setColumnCount(colCount)  # 행 갯수
        self.buylast.setRowCount(row_count+1)  # colum_haed가 한 행을 잡아 먹는다. 실제 입력 되는 값은 1행 부터이다.
        self.buylast.setHorizontalHeaderLabels(column_head)  # 행의 이름 삽입

        self.buylast.setItem(row_count, 0, QTableWidgetItem(str(self.new_code))) # 실제 입력값은 1행부터이나 0부터 들어가야 된다.
        self.buylast.setItem(row_count, 1, QTableWidgetItem(str(itemName)))
        ################## 더블 스핀 박스 내용 읽기
        self.buylast.setItem(row_count, 4, QTableWidgetItem(str(int(self.buy_price.value()))))
        self.buylast.setItem(row_count, 5, QTableWidgetItem(str(int(self.n_o_stock.value()))))
        self.buylast.setItem(row_count, 6, QTableWidgetItem(str(int(self.profit_price.value()))))
        self.buylast.setItem(row_count, 7, QTableWidgetItem(str(int(self.loss_price.value()))))


        self.getItemInfo(self.new_code)



    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")

    def setUI(self):
        self.setupUi(self)                # UI 초기값 셋업

    def set_signal_slot(self):
        self.k.kiwoom.OnEventConnect.connect(self.login_slot)  # 내가 알고 있는 login_slot에다가 특정 값을 던져 준다.

    def signal_login_commConnect(self):
        self.k.kiwoom.dynamicCall("CommConnect()")  # 네트워크적 서버 응용프로그램에 데이터를 전송할 수 있게 만든 함수
        self.login_event_loop.exec_()  # 로그인이 완료될 때까지 계속 반복됨. 꺼지지 않음.

    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()  # 로그인이 완료되면 로그인 창을 닫는다.

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

        for n in account_list.split(';'):
            self.accComboBox.addItem(n)

    def c_acc(self):
        print("선택 계좌 정보 가져오기")
        ##### 1번 일꾼 실행
        h1 = Thread1(self)
        h1.start()

    def a_manage(self):
        print("계좌 관리")
        h2 = Thread2(self)
        h2.start()


    def auto(self):
        print("자동매매 시작")
        h3 = Thread3(self)
        h3.start()



    def trdata_slot(self, sScrNo, 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(QString, QString, int, QString)", sTrCode, sRQName, 0, "신용비율")).strip()
                row_count = self.buylast.rowCount()
                self.buylast.setItem(row_count - 1, 2, QTableWidgetItem(str(currentPrice)))
                self.buylast.setItem(row_count - 1, 3, QTableWidgetItem(str(D_R)))



if __name__=='__main__':             # import된 것들을 실행시키지 않고 __main__에서 실행하는 것만 실행 시킨다.
                                     # 즉 import된 다른 함수의 코드를 이 화면에서 실행시키지 않겠다는 의미이다.

    app = QApplication(sys.argv)     # PyQt5로 실행할 파일명을 자동으로 설정, PyQt5에서 자동으로 프로그램 실행
    CH = Login_Machnine()            # Main 클래스 myApp으로 인스턴스화
    CH.show()                        # myApp에 있는 ui를 실행한다.
    app.exec_()                      # 이벤트 루프

QThread_3.py 스크립트

 

import os                                   # 현재 디렉토리 확인 기능
from PyQt5.QtCore import *                  # 쓰레드 함수를 불러온다.
from kiwoom import Kiwoom                   # 로그인을 위한 클래스
from kiwoomType import *


class Thread3(QThread):
    def __init__(self, parent):   # 부모의 윈도우 창을 가져올 수 있다.
        super().__init__(parent)  # 부모의 윈도우 창을 초기화 한다.
        self.parent = parent      # 부모의 윈도우를 사용하기 위한 조건

        ################## 키움서버 함수를 사용하기 위해서 kiwoom의 능력을 상속 받는다.
        self.k = Kiwoom()
        ##################

        ################## 사용되는 변수
        account = self.parent.accComboBox.currentText()  # 콤보박스 안에서 가져오는 부분
        self.account_num = account
        # 계좌번호 가져오는 부분은 Qthread_3 분리 시 로그인 후 계좌번호를 가져오는 함수로 교체된다. Lecture_0529.py
반응형

.link_tit