2. 해외선물/2-1. 해외선물 자동매매 연구

(키움증권 해외선물 자동매매 파이썬) 12. 주문가능수량 조회 (opw30011)

봄이오네 2023. 10. 5. 08:06
반응형
목 차

1. 들어가며
2. 사전설명
   1) 주문가능수량 조회의 필요성
   2) 청산가능수량 조회의 필요성
   3) WKOA Studio에서 TR 확인하기
3. 코드 설명
4. 전체코드
5. 마치며

 

1. 들어가며

지난 글에서는 "Tr 목록" 중 하나인 opw30009(예수금 및 증거금 현황조회)을 통해 해외선물 매매시 필요한 "주문가능금액" 조회 방법을 알아보았다. 나중에 설명하겠지만, "주문가능금액 조회"로 진입을 할지, 청산을 할지 여부에 대한 코드가 각각 다르게 적용되니, 천천히 이해하면서 숙지하도록 하자.

 

이번 글에서는 "Tr 목록" 설명의 마지막인 opw30011(주문가능수량 조회)에 대해 알아보고자 한다. 영웅문G의 "화면번호 4520"에서 "신청" 버튼을 누르면 주문가능수량이 조회된다.

 

그림1. 영웅문G(모의계좌)에서 주문가능수량 조회

 

※ opw30011(주문가능수량 조회)는 예전에 설명(https://springcoming.tistory.com/161)하였다. 중복된 내용은 생략한다.


2. 사전설명

1) 주문가능수량 조회의 필요성

사실 opw30011(주문가능수량 조회)를 시스템에 넣어야 할지 고민되었다. 주문가능수량을 고정 변수로 선언하여 굳이 "TR목록"을 사용할 필요가 없을 거 같은데 말이다.

 

그런데, 해외선물 종목에 따라 증거금이 서로 다르고, 종목을 바꿀 때마다 일일히 "고정 변수"로 선언했던 주문가능수량을 변경해 주어야 하는 불편함이 존재한다.

 

예를 들어보자. 미니 나스닥(NQZ23)의 위탁증거금은 18,484달러이고, 마이크로 미니 나스닥(MNQZ23)의 위탁증거금은 1,848달러이다. 애석하게도 1,000달러 손실이 발생했다고 하면, 현재 주문가능금액은 17,484달러(18,484-1,000)이다. 17,484달러로는 미니 나스닥 1거래가 불가하며 마이크로 나스닥 9계약으로 진입을 해야 한다. 즉, 손실이 발생하면 기존에 설정했던 미니 "1"계약을 마이크로 나스닥의 "9"로 변경해 주어야 한다. 상당히 귀찮다. 그냥 opw30011(주문가능수량  조회)을 이용해서 종목에 상관없이 주문가능수량 값을 구하자.

 

2) 청산가능수량 조회의 필요성

위탁증거금이 1,000달러인 종목이 있다고 하자. 주문가능금액은 2,800달러이고, long 포지션으로 2계약 진입했다고 하자. +300달러 수익이 발생하면, 계좌에 있는 금액은 3,100달러(2,800+300)가 된다. 청산을 하려고 "매도"에서 "주문가능수량"을 누르면 short 포지션의 "3"계약이 나와서, 생각없이 청산을 해버린다. 어떤게 실수냐고? 스위칭 하려는 의도가 없었다고 가정하자. long 2계약 진입했으면, short 2계약으로 청산하는게 맞다. 그런데 short 3계약으로 청산하면... long 2계약이 청산되고, 신규로 short 1계약으로 진입이 되어 버린다는 것이다. 필자도 가끔씩 하는 어이없는 실수이다.

 

이런 실수를 방지하기 위해 "청산가능수량"을 계산하여 "청산"할 때 필요한 수량만 청산하는 것이다.

 

3) WKOA Studio에서 TR 확인하기

< 그림2 >에서 입력/출력되는 데이터를 확인해보자. input 정보는 7가지 데이터를 입력(SetInputValue) 및 요청(CommRqData)해 주어야 한다. 우리에게 필요한 output 데이터는 2가지를 수신(GetCommData)받으면 된다.

 

< 그림 2 >는 WKOA Studio에서 opw30011(주문가능수량 조회)를 실행한 결과이다.

① 입력/출력데이터를 먼저 확인한다. input 7가지, output 2가지 등을 확인하자.

② OpenAPI-W를 통해 키움서버에 데이터 입력(SetInputValue) 및 요청(CommRqData)하는 형태이다.

③ 위의 ②의 형태이다. input 7가지 데이터를 입력하고, 조회(요청이 이루어짐)를 한다.

④ 위의 ③에서 조회한 내용에 대한 결과이다. 진입 전이라, "주문가능수량"이 1이다.

 

여기서 input 7가지 데이터를 설명한다.

① 계좌번호 : 10자리 입력

② 계좌비밀번호 : 4자리 입력

③ 비밀번호입력매체 : < 그림2 >의 ②를 보면, 비밀번호 입력매체는 "00"으로 입력하라고 한다.

④ 종목코드 : 미니 나스닥('23.12월)을 입력(NQZ23)

⑤ 매도수구분 : < 그림2 >의 ②를 보면, 1: 매도, 2:매수이다. 매수 주문가능수량을 알아보기 위해 "2"를 넣었다.

⑥ 해외주문유형 : < 그림2 >의 ②를 보면, 1:시장가, 2:지정가, 3.stop, 4,stoplimit이다. "1"을 입력하여 시장가 조회

⑦ 주문표시가격 : 이 부분이 제일 헷갈렸다. < 그림2 >의 ②를 보면, 주문가격 입력(예시:6AM16,  종목의 경우 0.725)로 설명이 나오는데... 문제는 필자는 "시장가"로 진입하려고 한다. 빈칸(" ")으로 설정하니 시장가로 주문가능수량이 조회가 되었다.

 

그림2. WKOA Studio에서 조회한 모의계좌의 주문가능수량

 


3. 코드 설명

이번부터는 "기존 코드"에서 중복되는 내용은 생략하고, 추가되는 코드만 설명하려고 한다. 코드를 누적해서 작성하다보니, 첨부해야되는 사진, 중복되는 설명 등이 너무 많이져서이니, 양해해 주시리라 믿는다.

 ※ 추가된 코드 위주로 설명한다.

 

그림3-1. OpenAPI-W에 데이터 입력/조회한다.

 

43줄~53줄 : < 그림2 >에서 확인하였듯이, input 데이터 입력/데이터 요청을 위해 함수를 선언한다.

44줄~50줄 : 119줄에서 "함수실행" 명령을 실행하면, opw30011(주문가능수량 조회)를 조회하기 위한 input 데이터 7가지를 입력(SetInputValue)해줄 명령어를 미리 만든다.

51줄 : 44줄~50줄에서 입력된 7가지 input 데이터를 OpenAPI-W에 요청(CommRqData)한다.

52줄~53줄 : 키움서버에서 데이터를 받을 때까지 "대기(waiting)"해주는 명령어를 정의(52줄) 및 실행(53줄)한다.

 

 

그림3-2. OpenAPI-W를 통해 데이터를 수신받아 변수에 담아준다.

 

85줄~96줄 : 51줄에서 7가지 데이터를 요청(CommRqData)하면 키움서버에서 반응(이벤트)하여 데이터를 제공해주는데, 그 데이터를 수신(GetCommData)받을 함수를 선언한다.

86줄~89줄 : 받아온 데이터 중 86줄에서 주문가능수량(orderable_qty), 87줄에서 청산가능수량(liquidable_qty)의 변수에 담는다. (주문가능수량과 통화코드는 필자가 임의로 작성한 것이다)

91줄~94줄 : 전역변수로 활용하기 위해 86줄~87줄의 변수 앞에 self를 붙인다.

96줄 : 53줄에서 데이터를 모두 수신받기 위해 정지(loop)해 놓은 명령어를 종료(exit)한다.

 

 

그림3-3. 주문가능수량 조회를 위해 43줄의 함수를 실행한다.

 

119줄 : 43줄의 함수를 실행해 주는 코드이다. 43줄에서 받아줄 input 7가지 데이터를 119줄에서 정의해준다.

 


4. 전체코드

아래 < 접은글 >의 코드를 실행하려면, 시세조회 이용료($185)를 지불하여야 하며, 119줄에서 사용자의 계좌번호 10자리와 비밀번호 4자리를 넣어주어야 한다.

 

 

더보기
import sys
from PyQt5.QAxContainer import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

class btl_system():
    def __init__(self):
        self.kiwoom = QAxWidget("KFOPENAPI.KFOpenAPICtrl.1")
        print("로그인 시작!")

        self.kiwoom.dynamicCall("CommConnect(1)")  # CommConnect() : 괄호 안에 자동(1)을 넣는다.
        self.kiwoom.OnEventConnect.connect(self.login_Connect)
        self.kiwoom.OnReceiveTrData.connect(self.trdata_get)

        self.login_event_loop = QEventLoop()
        self.login_event_loop.exec_()

        self.kiwoom.dynamicCall("GetCommonFunc(QString, QString)", "ShowAccountWindow", "")  # 계좌번호 입력창을 띄우는 내부함수

    def login_Connect(self, err_code):
        if err_code == 0:
            print('로그인 성공했습니다!')
        else:
            print('로그인 실패했습니다!')
        self.login_event_loop.exit()

    ########## 키움서버에 TR 요청하는 함수 모음 ##########
    def rq_data_opc10002(self, stock_code_num, time_unit):  # 분봉 이미지 만들기
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "종목코드", stock_code_num)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "시간단위", time_unit)
        self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opc_10002", "opc10002", "", "1002")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def rq_data_opw30009(self, deposit_num1, password_2, password_enter3):  # 주문가능금액, 실현손익, 미실현손익
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "계좌번호", deposit_num1)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호", password_2)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호입력매체", password_enter3)
        self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opw_30009", "opw30009", "", "3009")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def rq_data_opw30011(self, deposit_num1, password_2, password_enter3, futures_code4, sell_buy_gubunm5, order_type6, order_price7):  # 주문가능수량
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "계좌번호", deposit_num1)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호", password_2)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호입력매체", password_enter3)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "종목코드", futures_code4)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "매도수구분", sell_buy_gubunm5)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "해외주문유형", order_type6)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "주문표시가격", order_price7)  # "시장가"로 주문할 경우, 함수 실행시 빈칸("")으로 요청
        self.kiwoom.dynamicCall("CommRqData(QString, QString, QString,QString)", "opw_30011", "opw30011", "", "3011")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def rq_data_opw30012(self, deposit_num1, password_2, password_enter3):  # 계좌 현재가, 매도수구분, 보유수량, 매입단가, 계좌현재가, 평가손익
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "계좌번호", deposit_num1)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호", password_2)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호입력매체", password_enter3)
        self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opw_30012", "opw30012", "", "3012")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    ########## OnReceiveTrData을 통해 수신받은 데이터 함수  ##########
    def trdata_get(self, scrno, rqname, trcode, recordname, prenext):
        if rqname == "opc_10002":
            self.open_price_one_ago = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", 1, "시가").strip()))
            self.close_price_one_ago = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", 1, "현재가").strip()))
            self.tr_event_loop.exit()

        elif rqname == "opw_30009":  # 주문가능금액, 실현손익, 미실현손익
            orderable_money = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "주문가능금액").strip())
            withdrawal_money = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "인출가능금액").strip())
            realized_pl = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "선물청산손익").strip())
            unrealized_pl = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "선물평가손익").strip())

            self.orderable_money = orderable_money / 100
            self.withdrawal_money = withdrawal_money / 100
            self.realized_pl = realized_pl / 100
            self.unrealized_pl = unrealized_pl / 100

            print(self.orderable_money)

            self.tr_event_loop.exit()

        elif rqname == "opw_30011":  # 주문가능수량
            orderable_qty = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "주문가능수량").strip())
            liquidable_qty = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "청산가능수량").strip())
            orderable_money_30011 = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "주문가능금액").strip())
            currency_code = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "통화코드").strip()

            self.orderable_qty = orderable_qty
            self.liquidable_qty = liquidable_qty
            self.orderable_money_30011 = orderable_money_30011 / 100
            self.currency_code = currency_code

            self.tr_event_loop.exit()

        elif rqname == "opw_30012":  # 계좌 현재가, 매도수구분, 보유수량, 매입단가, 계좌현재가, 평가손익
            stock_code_d = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "종목코드").strip()
            sell_buy_gubun_d = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "매도수구분").strip()
            my_qty_d = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "수량").strip())
            my_price_d = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "매입가격").strip())
            current_price_d = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "현재가격").strip()))
            estimate_pl_d = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "평가손익").strip())

            self.stock_code_d = stock_code_d
            self.sell_buy_gubun_d = sell_buy_gubun_d
            self.my_qty_d = my_qty_d
            self.my_price_d_30012 = my_price_d
            self.current_price_d_30012 = current_price_d
            self.estimate_pl_d_30012 = estimate_pl_d / 100

            self.tr_event_loop.exit()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    btl = btl_system()

    btl.rq_data_opw30011("계좌번호 10자리", "계좌비밀번호 4자리", "00", "NQZ23", "2", "1", "")  # 주문가능수량 (매수를 위한 주문가능수량 조회)

    app.exec_()

 


5. 마치며

opw30011(주문가능수량 조회)을 통해 증거금에 따른 진입/청산 가능수량 조회에 대해 알아보았다. 글을 작성하다가 생각난 것 중 하나인데, opw30011 조회를 통해서도 "주문가능금액"의 조회가 가능하다. 기존에는 opw30009를 통해서 조회했는데, CommRqData 조회 횟수를 줄일 수 있을 것 같은 좋은 생각이 든다. 후후훗! 왜 그동안 opw30009(예수금및증거금 현황조회)에서만 "주문가능금액"만 조회했는지;;;;

 

당초 코드를 작성할 때 WKOA Studio에서 각각의 TR목록에 대한 "output"을 꼼꼼히 보았다고 했는데, 필자가 못 봤나보다. 이번 기회에 코드 수정(주문가능금액을 조회하기 위한 코드 변경, opw30009 → opw30011)을 고민해 봐야겠다. 나중에 설명하겠지만, CommRqData 조횟수 제한 때문에 코드를 짤 때 조회시간(대기 5초 정도)을 고려해 주어야 한다. 글을 쓰다보면, 머리 속에서 더 많은 좋은 생각이 나는 것 같다.ㅎㅎㅎ

 

opw30011(주문가능수량 조회)를 마지막으로 자동매매 시스템에서 활용되는 "TR목록"에 대한 설명은 마무리 하려고 한다. 다음 글에서는 SendOrder 함수에 대해 알아보자.

 

반응형