[키움 자동매매 프로그램] - 계좌 관리(Qthread_2.py) 구현
Qthread_2.py 구현
pytrader.py에서 계좌 관리 부분을 구현하는 코드이다.
우리가 가진 주식 종목이 얼마나 위험한지 역배열인지 간단하게 파악해 볼 수 있는 코드를 구현한다.
Qthread_2.py - init 생성
from PyQt5.QtCore import *
from kiwoom import Kiwoom
from PyQt5.QtWidgets import *
from PyQt5.QtTest import *
from datetime import datetime, timedelta
class Thread2(QThread) :
def __init__(self, parent) :
super().__init__(parent)
self.parent = parent
self.k = Kiwoom()
self.Find_down_Screen = "1200" # 계좌평가잔고내역을 받기 위한 스크린
self.code_in_all = None # 1600개 코드 중 1개의 코드, 계속해서 갱신
self.Predic_Screen = "1400" # 일봉차트를 가져오기 위한 스크린
self.calcul_data = [] # 받아온 종목의 다양한 값(현재가/고가/저가 등)을 계산
self.second_filter = [] # 역배열인지 확인
self.Predic_start = [] # 미래 예측
self.k.kiwoom.OnReceiveTrData.connect(self.trdata_slot)
self.detail_account_info_event_loop = QEventLoop()
### 기관, 외국인 평균가 가져오기
self.C_K_F_class()
###
### 역배열 평가
self.Invers_arrangement()
###
### GUI에 삽입
column_head = ["종목코드", "종목명", "위험도", "역배열"]
colCount = len(column_head)
rowCount = len(self.k.acc_portfolio)
self.parent.Danger_wd.setColumnCount(colCount)
self.parent.Danger_wd.setRowCount(rowCount)
self.parent.Danger_wd.setHorizontalHeaderLabels(column_head)
idx2 = 0
for i in self.k.acc_portfolio.keys() :
self.parent.Danger_wd.setItem(idx2, 0, QTableWidgetItem(str(i)))
self.parent.Danger_wd.setItem(idx2, 1, QTableWidgetItem(self.k.acc_portfolio[i]["종목명"]))
self.parent.Danger_wd.setItem(idx2, 2, QTableWidgetItem(self.k.acc_portfolio[i]["위험도"]))
self.parent.Danger_wd.setItem(idx2, 3, QTableWidgetItem(self.k.acc_portfolio[i]["역배열"]))
self.parent.Danger_wd.item(idx2, 0).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.parent.Danger_wd.item(idx2, 1).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.parent.Danger_wd.item(idx2, 2).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.parent.Danger_wd.item(idx2, 3).setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
idx2 += 1
앞으로의 포스팅에서는 객체 초기화, 스크린 번호 등 기본이 되는 값들에 대한 설명은 생략할 예정이다.
앞서 설명했던 어떤 정보를 수신하기 위해 필요한 이벤트인 OnReceiveTrData, QEventLoop로 이벤트를 대기하고 처리될 때 까지 기다린다.
기관 및 외국인 평균가 가져오기
우리가 보유하고 있는 주식이 안전한지 위험한지 판단하기 위해 기관 및 외국인 평균가를 조회하여 비교해본다.
def C_K_F_class(self) :
code_list = []
for code in self.k.acc_portfolio.keys() :
code_list.append(code)
print(f"계좌 포함 종목 : {code_list}")
for idx, code in enumerate(code_list) :
QTest.qWait(1000)
self.k.kiwoom.dynamicCall("DisconnectRealData(QString)", self.Find_down_Screen) # 해당 스크린 끊고 다시 시작
self.code_in_all = code # 종목코드 선언(중간에 코드 정보를 받아오기 위함)
print(f"{idx+1} / {len(code_list)} : 종목 검사 중 코드이름 : {self.code_in_all}")
date_today = datetime.today().strftime("%Y%m%d")
date_prev = datetime.today() - timedelta(10) # 10일전의 데이터를 받아옴. 필요에 따라일수 변경 가능
date_prev = date_prev.strftime("%Y%m%d")
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", code)
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "시작일자", date_prev)
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종료일자", date_today)
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "기관추정단가구분", "1")
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "외인추정단가구분", "1")
self.k.kiwoom.dynamicCall("CommRqData(String, String, int, String)", "종목별기관매매추이요청", "opt10045", "0", self.Find_down_Screen)
self.detail_account_info_event_loop.exec_()
현재 보유하고 있는 주식 종목의 종목 코드를 가져온다.
만약 보유하고 있는 주식이 많다면 키움 서버에 과도한 요청을 보내 서버가 이를 차단 or 데이터 전송 중단을 할 수 있기에 QTest.qWait(1000)으로 1초에 한 번 요청을 보내도록 설계한다.
이전 요청에 연결됬었던 데이터를 해제하여 스크린 번호를 초기화 후, 우리가 보유한 종목에 대해 데이터를 조회한다.
보유한 종목에 대해 종목코드, 데이터 조회 시작 일자(10일, 변경 가능), 데이터 조회 종료 일자, 기관추정단가구분, 외인추정단가구분을 요청 데이터로 지정, 종목별기관매매추이요청으로 API에 데이터 요청을 보낸다.
이렇게 서버에서 얻어온 데이터는 위험도를 평가할 때 사용된다.
주식일봉차트 가져오기
기본적인 틀은 기관 및 외국인 평가를 가져올 때와 비슷하다.
def Invers_arrangement(self) :
code_list = []
for code in self.k.acc_portfolio.keys() :
code_list.append(code)
print(f"계좌 포함 종목 : {code_list}")
for idx, code in enumerate(code_list) :
QTest.qWait(1000)
self.code_in_all = code # 종목코드 선언(중간에 코드 정보를 받아오기 위함)
self.k.kiwoom.dynamicCall("DisconnectRealData(QString)", self.Predic_Screen)
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "종목코드", code)
self.k.kiwoom.dynamicCall("SetInputValue(QString, QString)", "수정주가구분", "1")
self.k.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", "주식일봉차트조회", "opt10081", "0", self.Predic_Screen)
self.detail_account_info_event_loop.exec_()
현재 보유하고 있는 종목의 종목코드, 수정주가 여부를 요청 데이터로 지정하고 주식일봉차트조회로 API에 요청을 보낸다.
여기서 얻어온 데이터는 역배열인지 아닌지를 판단할 때 사용된다.
서버 데이터 요청 및 수신
위에서 기관 및 외국인 평균가와 주식일봉차트에 대한 데이터를 얻기 위한 데이터를 요청하였고, 이에 대한 응답을 받아야한다.
코드가 길기 때문에, 요청 이름이 종목별기관매매추이요청인 경우와 주식일봉차트조회인 경우를 나눠서 설명한다.
trdata_slot - 종목별기관매매추이요청
def trdata_slot(self, sScrNo, sRQName, sTrCode, sRecordName, sPrevNext) :
if sRQName == "종목별기관매매추이요청" :
cnt2 = self.k.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", sTrCode, sRQName)
self.calcul2_data = []
self.calcul2_data2 = []
self.calcul2_data3 = []
self.calcul2_data4 = []
for i in range(cnt2) :
institutional_trading = (self.k.kiwoom.dynamicCall("GetCommData(String, String, int, String)", sTrCode, sRQName, i, "기관일별순매매수량"))
institutional_trading_avg = (self.k.kiwoom.dynamicCall("GetCommData(String, String, int, String)", sTrCode, sRQName, 0, "기관추정평균가"))
forgin_trading = (self.k.kiwoom.dynamicCall("GetCommData(String, String, int, String)", sTrCode, sRQName, i, "외인일별순매매수량"))
forgin_trading_avg = (self.k.kiwoom.dynamicCall("GetCommData(String, String, int, String)", sTrCode, sRQName, 0, "외인추정평균가"))
per = (self.k.kiwoom.dynamicCall("GetCommData(String, String, int, String)", sTrCode, sRQName, i, "등락율"))
close = (self.k.kiwoom.dynamicCall("GetCommData(String, String, int, String)", sTrCode, sRQName, i, "종가"))
self.calcul2_data.append(int(institutional_trading.strip()))
self.calcul2_data2.append(abs(int(close.strip())))
self.calcul2_data2.append(abs(int(institutional_trading_avg.strip())))
self.calcul2_data2.append(abs(int(forgin_trading_avg.strip())))
self.calcul2_data3.append(int(forgin_trading.strip()))
self.calcul2_data4.append(float(per.strip()))
self.institutional_trading_batch(self.calcul2_data, self.calcul2_data3)
self.detail_account_info_event_loop.exit()
데이터 요청 이름이 종목별기관매매추이요청일 경우 수행되는 부분이다.
우리가 보유 종목에 대해 데이터를 요청하였고 받아온 정보는 기관일별순매매수량, 기관추정평균가, 외인일별순매매수량, 외인추정평균가, 등락율, 종가이다.
기관일별순매매수량과 외인일별순매매수량을 이용하여 위험도를 평가하는데 위험도를 평가하는 메서드는 다음과 같다.
def institutional_trading_batch(self, a, c) :
a = a[0:4]
c = c[0:4]
if all(x < 0 for x in a) and all(x < 0 for x in c) :
self.k.acc_portfolio[self.code_in_all].update({"위험도" : "손절"})
elif all(x < 0 for x in a[:3]) and all(x < 0 for x in c[:3]) :
self.k.acc_portfolio[self.code_in_all].update({"위험도" : "주의"})
elif all(x < 0 for x in a[:2]) and all(x < 0 for x in c[:2]) :
self.k.acc_portfolio[self.code_in_all].update({"위험도" : "관심"})
else :
self.k.acc_portfolio[self.code_in_all].update({"위험도" : "낮음"})
a는 기관의 거래 동향 데이터, c는 외국인의 거래 동향 데이터로 보유 종목의 위험도를 평가한다.
최근 4일간, 최근 3일간, 최근 2일간, 그외로 경우를 나누고 거래 동향이 음수, 양수인지에 따라 위험도를 평가하는 로직이다.
trdata_slot - 주식일봉차트조회
위험도를 평가하였으므로 다음은 역배열인지를 평가한다.
아래의 코드를 종목별기관매매추이요청 알고리즘 밑에 넣으면 된다.
elif sRQName == "주식일봉차트조회" :
code = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, 0, "종목코드")
code = code.strip()
cnt = self.k.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", sTrCode, sRQName)
for i in range(cnt) :
data = []
current_price = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "현재가")
value = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "거래량")
trading_value = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "거래대금")
date = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "일자")
start_price = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "시가")
high_price = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "고가")
low_price = self.k.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, i, "저가")
data.append("")
data.append(current_price.strip())
data.append(value.strip())
data.append(trading_value.strip())
data.append(date.strip())
data.append(start_price.strip())
data.append(high_price.strip())
data.append(low_price.strip())
data.append("")
self.Predic_start.append(int(current_price.strip()))
self.calcul_data.append(data.copy())
if self.calcul_data == None or len(self.calcul_data) < 210 :
self.k.acc_portfolio[self.code_in_all].update({"역배열" : "데이터 X"})
else :
total_five_price = []
total_twenty_price = []
total_sixty_price = []
total_onehtwenty_prices = []
for k in range(10) :
total_five_price.append(sum(self.Predic_start[k : 5+k]) / 5)
for k in range(10) :
total_twenty_price.append(sum(self.Predic_start[k : 20+k]) / 20)
for k in range(10) :
total_sixty_price.append(sum(self.Predic_start[k : 60+k]) / 60)
for k in range(10) :
total_onehtwenty_prices.append(sum(self.Predic_start[k : 120+k]) / 120)
add_item = 0
for k in range(10) :
if (float(total_twenty_price[0]) < float(total_twenty_price[9])) and
(float(total_sixty_price[0]) < float(total_sixty_price[9])) and
(float(total_onehtwenty_prices[0]) < float(total_onehtwenty_prices[9])) :
if float(total_twenty_price[k]) < float(total_sixty_price[k]) and
float(total_sixty_price[k]) < float(total_onehtwenty_prices[k]) :
add_item += 1
else :
pass
if add_item >= 8 :
self.k.acc_portfolio[self.code_in_all].update({"역배열" : "O"})
else :
self.k.acc_portfolio[self.code_in_all].update({"역배열" : "X"})
self.calcul_data.clear()
self.Predic_start.clear()
self.detail_account_info_event_loop.exit()
데이터 요청 이름이 주식일봉차트조회인 경우 요청된 종목 코드와, 일봉 데이터의 반복 개수를 확인하고 종목의 현재가, 거래량, 거래대금, 일자, 시가, 고가, 저가 데이터를 가져온다.
얻어온 데이터를 리스트에 개별 데이터로 저장하고, 만약 얻어온 데이터가 충분하지 않을경우 데이터 X로 데이터의 부족함을 알려준다.
이제 이평선을 계산하는데, 5일, 20일, 60일 ,120일 이평선을 계산하여 각각의 리스트에 넣고,
역배열인지 아닌지를 판단한다.
역배열 조건은 두 가지인데,
- 20일, 60일, 120일 이평선이 증가(처음 값보다 마지막 값이 크면 증가세라 판단)
- 특정 일자(k)에서 20 < 60 < 120 순으로 이평선 값이 정렬
위의 두 조건을 만족시키면 역배열 값을 증가시키고, 해당 값이 10일 중 8일 이상일 경우 역배열로 판단한다.
위의 코드들을 수행하면 다음과 같은 결과를 볼 수 있다.
정리
해당 코드를 작성함으로써 우리는 보유한 종목에 대한 일봉 데이터 및 기관/외국인 매매 추세를 분석하여
우리의 종목이 위험한지 안전한지를 자동으로 판단한다.
종목의 상태를 기반으로 특정 종목이 해당 알고리즘에 위험하다고 판단되면 자동매매 포트폴리오에서 매도를 하는 등의 전략으로 업데이트 할 수 있다.