PYTHON/AUTO TRADE SYSTEM

[자동 매매 시스템 구축하기] 주문 정보 관리하기 (6) - 정정 주문 데이터 로직 확인하기 ①

  • -

지난 게시글에서는 사전에 생성했던 tableWidget 객체이며 주문 정보 데이터를 입력할 객체인   tableWidget_2  에 주문과 관련된 정보를 입력하는 기능을 구현했다. 사실 주문의 접수와 체결로 인해 발생하는 여러 데이터를 처리하고 GUI를 통해 나타내도록 하는 기능은 지금까지 모두 구현했다고 봐도 무방하지만, 이번 게시글에서는 더 나아가 단순한 주문 접수 외에 정정 주문과 취소 주문이 이루어졌을 때 데이터가 어떤 절차를 거쳐 발생하게 되며 우리는 그 데이터를 어떻게 처리해야 하는지에 대해 살펴볼 예정이다.

 

 

정정 주문과 취소 주문의 동작 로직

  def recieve_chejandata  함수 내에서, 1차적으로   sGubun  에 의해 국내주식 잔고통보인가 아니면 주식체결통보인가를 구분해낸다. 이후   sGubun  이 주식체결통보라면 그 안에서는 "접수" 데이터인가 "체결" 데이터인가에 따라 구분하게 되는데, 일반적으로 이 두 가지 데이터는 서로 각각 다르게 발생한다. 예를 들어 매수 주문을 접수하게 되면 접수한 당시에는 "접수" 데이터만 발생하고, 해당 주문 건이 체결된 경우에만 "체결" 데이터가 발생하게 된다.

하지만  정정 주문이나 취소 주문 등의 경우에는 "접수"와 "체결" 외에도 "확인"과 "미체결 클리어"라는 데이터가 새롭게 등장한다. 이 두 가지 데이터가 발생하면서부터 데이터를 처리하는 것이 다소 복잡하게 이루어지는데, 가장 먼저 기본적인 형태의 예시부터 살펴보도록 하자.

 

① 신규 주문 접수 시의 동작 로직

신규 주문 접수는 데이터가 발생하는 수많은 형태 중에서도 가장 간단한 형태로 데이터가 발생하는데, 그 생김새는 아래와 같다. 굳이 굳이 텍스트로 적어보자면 접수 시에는 "접수" 데이터와 함께 주문번호가 생성되고 해당 주문이 체결되면 "체결" 데이터와 함께 체결량 데이터가 전달된다.

우리가 이 과정에서 눈여겨 봐야 할 것은 신규 주문 접수와 해당 접수 건을 원인으로 발생한 체결 데이터는 별도의 원주문번호 데이터를 전달하지 않는다는 것이다. 이를 눈여겨봐야 하는 것은 바로 우리는 원주문번호가 0000000인 경우에는 신규로 접수된 주문이며 기존에 접수된 주문으로부터 발생한 체결 데이터라는 것을 확인할 수 있기 때문이다.

 

 

② 신규 주문 접수 후 정정 주문 시의 동작 로직

기본적으로 정정 주문의 경우에는 "접수" → "확인" → "미체결 클리어"라는 세 개의 단계에 따라 데이터가 처리되는데, 여기서 "접수"를 제외한 나머지 두 단계가 다소 생소하게 느껴질 것이다. 아래의 사진을 참고해보면 매수 정정 내에서 "접수", "확인" ,"접수"라는 세 개의 데이터가 순서대로 발생하고 있음을 확인할 수 있다.

그 중에서 첫 번째에 위치한 "접수"는 매수 정정 주문에 대한 정보를 나타내는 것이며, "확인"은 접수된 정정 주문에 대한 데이터를 다시 한 번 출력하는 것이다. 하지만 맨 마지막 "접수"를 보면 신규 주문시에 접수했던 데이터와 동일한 데이터를 갖고 있다는 것을 확인할 수 있는데, 저 데이터가 바로 미체결 클리어라고 부르는 데이터이다.

여기서 역시 눈여겨봐야 하는 것은 원주문번호가 0000000인 주문 데이터가 맨 마지막에 발생하게 되는데, 해당 데이터는 새롭게 접수된 주문이 아닌 기존에 접수되어 있던 데이터(주문)을 취소했다는 의미로 발생하는 데이터인 것이다. 우리는 지금 "접수" 데이터가 발생했을 때 해당 데이터를   self.order_info  변수에 입력하도록 하고 있는데, 아래와 같이 기존에 접수된 주문이 취소되었음을 확인하는 데이터인 "접수" 데이터가 발생하게 되면 이 데이터 역시   self.order_info  변수에 입력하게 된다는 것이다.

그렇다면 우리는 여기서 원주문번호가 0000000인 경우와 0000000이 아닌 경우를 나누어서 데이터를 입력하도록 해야 한다. 다시 말해, 원주문번호가 0000000인 경우   self.order_info  변수 내에 데이터를 입력하도록 하고, 원주문번호가 000000이 아닌 경우라면   self.order_info  내에 새로운 주문 번호를 입력해줘야 한다는 것이다. 이 내용은 다소 복잡하니, 주문이 접수되고 진행될 때 발생하는 데이터의 로직을 살펴본 후에 한 번에 제작해보도록 하자. 

이와 관련하여 실제로 본인의 로그 파일에 해당 데이터가 남아 있는 기록이 있어 확인해본 결과, 아래의 사진과 같은 로그 기록이 출력되었음을 확인할 수 있다. 아래의 사진에서 실제로 동작하는 로그 데이터들이 어떠한 절차에 따라 발생하는지 확인하면 좋을 것 같다.

 

 

③ 신규 주문 접수 후 일정 수량 체결 후 정정 주문 시의 동작 로직

두 번째 유형에 비해 한 단계 더 복잡한 유형의 데이터가 발생하게 되는데 여기서 주의해야 하는 점은 새롭게 발생하는 데이터라 하더라도 미체결 잔량 데이터만 변경될 뿐 기존의 주문량은 맨 처음에 접수되었을 당시의 데이터가 전달된다는 것이다. 또한 데이터를 처리하게 되는 단계 역시 "접수" → "확인" → "체결" → "체결"의 방식으로 발생하는데, 여기서 세 번째 자리에 위치해 있는 "체결"이 바로 미체결 클리어 단계에 해당한다.

하지만 이 경우, 위의 ② 신규 주문 이후 정정 주문을 접수했을 때의 데이터 발생 로직에서 살펴봤던 것보다 한 층 더 어려운 절차를 따라 데이터들이 발생한다는 것을 한 눈에 확인할 수 있다. 이 경우 역시, 앞서 살펴봤던 내용과 마찬가지로 정정 주문이 발생했을 때, 그 원주문번호가 이미   self.order_info  변수 내에 입력되어 있는지 아닌지 등과 같은 정보를 사전에 확인함으로써   self.order_info  변수와   tableWidget_2  객체가 올바르게 동작하도록 구현할 수 있다.

추가적으로, 취소 주문이 발생했을 경우에 데이터가 발생하는 로직에 대해서도 설명하고자 했으나, 사실 주문 취소 이후에는 또 새로운 주문을 접수해야 하기 때문에 본인의 경우에는 취소 주문을 따로 접수하지 않고 정정 주문만을 구현해서 사용하고 있기 때문에 별도로 살펴보지 않도록 하겠다. 취소 주문 기능을 구현해야 하는데 어떤 데이터가 발생하는지 모르는 경우에는 단순하게 자동 매매 프로그램을 실행시킨 상태에서 영웅문(HTS)에 접속해서 임의로 주문을 넣어보면 그에 따른 주문 데이터가 파이썬의 로그로 출력되니, 그를 참고해서 기능을 구현하면 된다.

 

 


728x90

 

 

정정 주문으로 발생하는 데이터 입력 기능도 구현해보자.

정정 주문으로부터 발생하는 데이터의 로직은 바로 위에 있으니 그 내용을 참고하는 것으로 하고, 여러 번 이야기했듯이 정정 주문으로 발생하는 데이터는 원주문번호가 0000000인가 아닌가라는 추가적인 기준에 따라 데이터를 한 번 더 구분할 것이다.
※ Line : 29, 30


    def receive_chejandata(self, Gubun, ItemCnt, FidList):
        """
        Gubun(string) - 0:주문체결통보, 1:국내주식잔고통보, 4:파생상품잔고통보
        FidList(string) - 데이터 구분은 ;이다.
        """
        if Gubun == "0":
            accno = self.__getchejandata("9201").strip()        ## 계좌번호
            orderno = self.__getchejandata("9203").strip()      ## 주문번호
            item_code = self.__getchejandata("9001").strip()    ## 종목코드
            order_gubun = self.__getchejandata("912").strip()   ## 주문 구분(JJ:주식주문, FJ:선옵, JG:주식잔고, FG:선옵잔고)
            order_status = self.__getchejandata("913").strip()  ## 주문 상태
            item_name = self.__getchejandata("302").strip()     ## 종목명
            order_quan = self.__getchejandata("900").strip()    ## 주문 수량
            order_pri = self.__getchejandata("901").strip()     ## 주문 가격
            remained_quan = self.__getchejandata("902").strip() ## 미체결량
            signed_vol = self.__getchejandata("903").strip()    ## 체결대금
            origin_ordno = self.__getchejandata("904").strip()  ## 원주문번호
            order_bns = self.__getchejandata("905").strip()     ## 주문 구분(+:매수, -:매도)
            trade_gubun = self.__getchejandata("906").strip()   ## 매수 구분(시장가, 지정가, 보통 등)
            signed_num = self.__getchejandata("909").strip()    ## 체결 번호
            signed_pri = self.__getchejandata("910").strip()    ## 체결가
            signed_quan = self.__getchejandata("911").strip()   ## 체결량
            today_com = self.__getchejandata("938").strip()     ## 당일매매 수수료
            today_tax = self.__getchejandata("939").strip()     ## 당일 매매 세금

            if order_status == "접수":

                if origin_ordno == "0000000":
                    pass

                idx = len(self.order_info['item_code'].to_list())

                self.order_info.loc[idx, 'item_code'] = item_code
                self.order_info.loc[idx, 'item_name'] = item_name
                self.order_info.loc[idx, 'bns'] = order_bns
                self.order_info.loc[idx, 'origin_code'] = origin_ordno
                self.order_info.loc[idx, 'order_gubun'] = order_gubun
                self.order_info.loc[idx, 'trade_gubun'] = trade_gubun
                self.order_info.loc[idx, 'order_quan'] = order_quan
                self.order_info.loc[idx, 'signed_quan'] = signed_quan
                self.order_info.loc[idx, 'remained_quan'] = remained_quan
                self.order_info.loc[idx, 'order_info'] = ""
                print("self.order_info")
                print(self.order_info)

                self.tableWidget_2.setRowCount(idx)
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_item_code, QTableWidgetItem(str(item_code)))
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_item_name, QTableWidgetItem(str(item_name)))
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_origin_code, QTableWidgetItem(str(origin_ordno)))
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_order_price, QTableWidgetItem(str(order_pri)))
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_order_quan, QTableWidgetItem(str(order_quan)))
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_trade_quan, QTableWidgetItem(str(signed_quan)))
                self.tableWidget_2.setItem(idx, dv.tableWidget_2_remained_quan, QTableWidgetItem(str(remained_quan)))

 

그렇다면 이제 주문 상태가 "접수"이고 원주문번호가 "0000000"인 주문은 ①신규 주문 접수 시와 ②정정 주문 시 마지막 단계에서 발생하는 기존 접수 주문을 취소하는 미체결 클리어 발생 시 두 경우밖에 없다. 그럼 이 두 가지 유형을 구분해주면 되는데, 둘 간의 가장 큰 차이점은 바로   self.order_info  변수 내에 해당 원주문번호가 있는가 없는가를 기준으로 구분할 수 있다. 다시 말해, 신규 주문 접수 시 원주문번호가 "0000000"의 형태로 전달되기 때문에 해당 종목코드로 저장되어 있는 원주문번호 "0000000"인 주문 건이 없다면 데이터를 입력하면 되고, 동일한 종목코드에 대해 원주문번호를 "0000000"로 하는 데이터가 있다면 데이터를 입력하지 않도록 하는 것이다.
※ Line : 5~9

if order_status == "접수":

    if origin_ordno == "0000000":

        ## 사전에 입력된 종목인데 원주문번호가 0000000인 경우 = 정정 주문의 미체결 클리어
        if origin_ordno in self.order_info['origin_code']:
            pass

        ## 입력되지 않은 종목인데 원주문번호가 000000인 경우 = 신규 주문
        elif origin_ordno not in self.order_info['origin_code']:
            pass

 

위 코드에서 Line 5~9 에 해당하는 구간이 바로 원주문번호가 "0000000"일 때, 해당 주문코드가   self.order_info  변수 내에 존재하는지 존재하지 않는지를 판단하는 구간에 해당한다. 하지만 원주문번호가 "0000000"인 주문이 self.order_info 변수 내에 존재 여부를 판단하기 전에 앞서, 우리는 해당 종목코드로 접수된 주문이 있는지도 확인해줘야 한다. 왜냐하면 우리는 한 종목만을 거래할 것이 아니기 때문이다. 다시 말해, "000020"이라는 종목과 "000040"이라는 종목 두 종목에 대해 신규 접수된 주문이 있을 때, 두 종목코드 모두 원주문번호를 "0000000"로 하는 데이터가 입력되어 있을 것이다. 만약 이 때 "000020"이라는 종목에 대해서만 정정 주문을 접수한다고 가정하면, 우리는 새로운 데이터를 "000020"에 입력해야 할지 "000040"에 입력해야 할지 알 수 없다. 더 나아가, 둘 다 원주문번호를 "0000000"로 하고 있기 때문에, "000020" 종목에 대한 정정 주문이 모두 완료되었고 새로운 주문번호를 입력했음에도 불구하고 "000040" 좀옥에 대해서는 "0000000"이라는 원주문번호를 갖고 있기 때문에 Line 5~6의 코드는 동작하게 된다. 이에 대한 해결 방법은 간단하게도, 종목코드에 대한 부분도 추가적으로 조건문을 설정해주면 된다. 너무 복잡해보이지만, 아래의 도표를 참고해보자. 

왼쪽의 도표는 000020 종목에 대해 접수된 주문이 없는 상태에서 신규 주문이 접수되어 데이터를 입력하는 모습이고, 오른쪽의 도표는 000020 종목에 대한 접수된 주문이 있는 상태에서 정정 주문이 접수되어 데이터를 입력하는 모습이다. 이 때 [신규 주문]과 [미체결 클리어]에서 발생하는 데이터를 보면 종목코드는 000020, 원주문번호는 0000000, 주문 유형은 접수로 동일하다. 즉, 그냥 동일한 데이터가 발생하게 된다.

이 때 신규 주문인가 정정 주문인가를 구분해낼 수 있는 가장 좋은 방법은 바로 기존에 접수된 주문이 있는지를 판단하는 것이다. 우리는 이 접수 데이터를   self.order_info   변수 안에 입력해두었기 때문에, 어렵지 않게 구분해낼 수 있다는 것이다.
※ Line : 28~60

def receive_chejandata(self, Gubun, ItemCnt, FidList):
    """
    Gubun(string) - 0:주문체결통보, 1:국내주식잔고통보, 4:파생상품잔고통보
    FidList(string) - 데이터 구분은 ;이다.
    """
    if Gubun == "0":
        accno = self.__getchejandata("9201").strip()        ## 계좌번호
        orderno = self.__getchejandata("9203").strip()      ## 주문번호
        item_code = self.__getchejandata("9001").strip()    ## 종목코드
        order_gubun = self.__getchejandata("912").strip()   ## 주문 구분(JJ:주식주문, FJ:선옵, JG:주식잔고, FG:선옵잔고)
        order_status = self.__getchejandata("913").strip()  ## 주문 상태
        item_name = self.__getchejandata("302").strip()     ## 종목명
        order_quan = self.__getchejandata("900").strip()    ## 주문 수량
        order_pri = self.__getchejandata("901").strip()     ## 주문 가격
        remained_quan = self.__getchejandata("902").strip() ## 미체결량
        signed_vol = self.__getchejandata("903").strip()    ## 체결대금
        origin_ordno = self.__getchejandata("904").strip()  ## 원주문번호
        order_bns = self.__getchejandata("905").strip()     ## 주문 구분(+:매수, -:매도)
        trade_gubun = self.__getchejandata("906").strip()   ## 매수 구분(시장가, 지정가, 보통 등)
        signed_num = self.__getchejandata("909").strip()    ## 체결 번호
        signed_pri = self.__getchejandata("910").strip()    ## 체결가
        signed_quan = self.__getchejandata("911").strip()   ## 체결량
        today_com = self.__getchejandata("938").strip()     ## 당일매매 수수료
        today_tax = self.__getchejandata("939").strip()     ## 당일 매매 세금

        if order_status == "접수":

            ## ①신규 주문 접수 또는 ②정정 주문 접수(미체결 클리어)
            if origin_ordno == "0000000":

                ## 종목코드 데이터가 없고 원주문번호가 0000000 = 신규 주문 접수
                if item_code not in self.order_info['item_code']:
                    idx = len(self.order_info['item_code'].to_list())

                    self.order_info.loc[idx, 'item_code'] = item_code
                    self.order_info.loc[idx, 'item_name'] = item_name
                    self.order_info.loc[idx, 'bns'] = order_bns
                    self.order_info.loc[idx, 'origin_code'] = origin_ordno
                    self.order_info.loc[idx, 'order_gubun'] = order_gubun
                    self.order_info.loc[idx, 'trade_gubun'] = trade_gubun
                    self.order_info.loc[idx, 'order_quan'] = order_quan
                    self.order_info.loc[idx, 'signed_quan'] = signed_quan
                    self.order_info.loc[idx, 'remained_quan'] = remained_quan
                    self.order_info.loc[idx, 'order_info'] = ""
                    print("self.order_info")
                    print(self.order_info)

                    self.tableWidget_2.setRowCount(idx)
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_item_code, QTableWidgetItem(str(item_code)))
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_item_name, QTableWidgetItem(str(item_name)))
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_origin_code, QTableWidgetItem(str(origin_ordno)))
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_order_price, QTableWidgetItem(str(order_pri)))
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_order_quan, QTableWidgetItem(str(order_quan)))
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_trade_quan, QTableWidgetItem(str(signed_quan)))
                    self.tableWidget_2.setItem(idx, dv.tableWidget_2_remained_quan, QTableWidgetItem(str(remained_quan)))


                ## 종목코드 데이터가 있고 원주문번호가 0000000 = 정정 주문 접수(미체결 클리어)
                elif item_code in self.order_info['item_code']:
                    pass

 

이제 다음 게시글에서는 주문 번호가 0000000이 아닌 경우에 대해서는 어떠한 구조로 동작하도록 구현할 것인지에 대해 살펴보도록 하자.

 

 


728x90
반응형
Contents

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

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