본문 바로가기
파이썬/주식

Python으로 보통주, 우선주 괴리율을 이용한 투자전략 - 2

by 행복론자 2020. 6. 6.

이전 이야기

2020/06/06 - [Python/Stock] - Python으로 보통주, 우선주 괴리율을 이용한 투자전략 - 1

 

Python으로 보통주, 우선주 괴리율을 이용한 투자전략 - 1

최근 주식시장이 강세입니다. 많이 오른 종목도 있고 상대적으로 안 오른 종목들도 있는데 이번에 살펴볼 방법은 제 짧은 지식으로 생각해본 보통주, 우선주 괴리율 투자법입니다. 먼저 보통주�

jsp-dev.tistory.com

 

 

지난글에는 우선주-보통주 매핑을 구했습니다.

이를 바탕으로 가격정보를 얻어와 괴리율을 구해보겠습니다.

바로 코드로 보면 다음과 같습니다. 

    def calc_disparity_rate_signal(self):
        print('calc_disparity_rate_signal start')
        self.get_all_codes_names()
        for preferred_stock_name in self.stock_map.keys():
            time.sleep(1.5)
            try:
                common_stock_name = self.stock_map[preferred_stock_name]
                preferred_stock_code = self.code_dict[preferred_stock_name]
                common_stock_code = self.code_dict[common_stock_name]

                preferred_stock_df = self.get_price_data(preferred_stock_code, '20200604')
                common_stock_df = self.get_price_data(common_stock_code, '20200604')
                common_stock_df = common_stock_df.rename(
                    columns={'open': 'open_common', 'high': 'high_common', 'low': 'low_common', 'close': 'close_common',
                             'volume': 'volume_common'})

                concat_df = pd.concat([preferred_stock_df, common_stock_df], axis=1)
                concat_df['disparity_rate'] = (concat_df['close_common'] - concat_df['open']) / concat_df['open'] * 100
                concat_df['close_ma200'] = concat_df['close'].rolling(window=200).mean()
                concat_df['close_common_ma20'] = concat_df['close_common'].rolling(window=20).mean()
                concat_df['disparity_rate_ma200'] = concat_df['disparity_rate'].rolling(window=200).mean()
                concat_df['signal'] = np.where((concat_df['disparity_rate'] > concat_df['disparity_rate_ma200']) & (
                        concat_df['close'] < concat_df['close_ma200']) & (concat_df['close_common'] > concat_df['close_common_ma20']), 1, 0)

                buy_signal = concat_df[-1:]['signal'].values[0]

                if buy_signal:
                    print(preferred_stock_name, common_stock_name)
                    print(concat_df[-1:]['signal'].values[0], end='\n\n')
                    concat_df.to_excel(preferred_stock_name+'.xlsx')

            except Exception as e:
                print('error on ', preferred_stock_name)
                print(traceback.format_exc())
                        
            print('ends')

 

 

이름을 대충 알아볼 수 있도록 지었는데 이걸 보는 누군가도 그렇게 느낄지는 모르겠습니다.

아무튼 이 전략은 추출한 111건의 우선주-보통주 매핑 중 다음 조건에 해당하는 기업을 추출합니다.

 

1. 괴리율이라는 수치를 대상으로 200일 이동평균을 구하고 현재 괴리율이 이 값보다 큰 경우

> 보통주가 많이 올라서 차이가 발생했다고 가정  

 

2. 우선주 가격이 200일 이동평균보다 작을 때  

> 가격 하락 상태

 

3. 보통주 가격이 20일 이동평균보다 클 때

> 보통주가 상승 추세일 때

 

전체 코드는 다음과 같습니다.

지난번에 말씀드린 것처럼 기본 베이스가 되는 코드들은 class101 수업에서 사용한 코드이고

우선주 추출 및 위의 calc_disparity_rate_signal 함수만 제가 만들었습니다.

import sys
import traceback
from PyQt5.QtWidgets import *
from PyQt5.QAxContainer import *
from PyQt5.QtCore import *
import re
from datetime import date, timedelta
import time
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

class Kiwoom(QAxWidget):  # QAxWidget 클래스로부터 dynamicCall, setControl, OnEventConnect 를 상속받음
    def __init__(self):
        super().__init__()
        self._create_kiwoom_instance()  # 키움증권 OpenAPI 를 정상적으로 사용할 수 있도록 처리함
        self._set_signal_slots()        # 이벤트 처리메소드 등록

        self.code_dict = {}  # key: 종목명 - value: 종목코드 매핑
        self.stock_map = {}  # key: 우선주 - value: 보통주 매핑

        self.day = (date.today() - timedelta(1)).strftime('%Y%m%d')
        self.stock_info = pd.DataFrame(columns=("Open","High","Low","Close","Volume"))

    def _create_kiwoom_instance(self):
        self.setControl("KHOPENAPI.KHOpenAPICtrl.1")      # 이거 대신 obj = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1") 가능

    def _set_signal_slots(self):
        self.OnEventConnect.connect(self._event_connect)  # 콜백함수 등록
        self.OnReceiveTrData.connect(self._receive_tr_data)

    def _receive_tr_data(self, screen_no, rqname, trcode, record_name, next, unused1, unused2, unused3, unused4):
        # print("_receive_tr_data!!!")
        if next == '2':
            self.remained_data = True
        else:
            self.remained_data = False

        if rqname == "opt10081_req":
            self._opt10081(rqname, trcode)
        elif rqname == "opw00001_req":
            print("opw00001_req!!!")
            print("Get an de_deposit!!!")
            # self._opw00001(rqname, trcode)
        elif rqname == "opw00018_req":
            print("opw00018_req!!!")
            print("Get the possessed item !!!!")
            # self._opw00018(rqname, trcode)
        elif rqname == "opt10074_req":
            print("opt10074_req!!!")
            print("Get the profit")
            # self._opt10074(rqname, trcode)
        elif rqname == "opw00015_req":
            print("opw00015_req!!!")
            print("deal list!!!!")
            # self._opw00015(rqname, trcode)
        elif rqname == "opt10076_req":
            print("opt10076_req")
            print("chegyul list!!!!")
            # self._opt10076(rqname, trcode)
        elif rqname == "opt10073_req":
            print("opt10073_req")
            print("Get today profit !!!!")
            # self._opt10073(rqname, trcode)
        elif rqname == "opt10080_req":
            print("opt10080_req!!!")
            print("Get an de_deposit!!!")
            # self._opt10080(rqname, trcode)

        try:
            self.tr_event_loop.exit()
        except AttributeError:
            pass

    def _event_connect(self, err_code):
        if err_code == 0:
            print("로그인 성공")
        else:
            print("로그인 에러코드 : " + str(err_code))

        self.login_event_loop.exit()

    def comm_connect(self):
        self.dynamicCall("CommConnect()")    # 로그인창 띄우기
        self.login_event_loop = QEventLoop() # 이벤트 루프 생성
        self.login_event_loop.exec_()        # 프로그램 흐름을 일시중지하고 이벤트만 처리할 수 있는 상태로 만듬

    # tr 입력값을 서버 통신 전에 입력
    # ex. SetInputValue("종목코드","000660")
    def set_input_value(self, id, value):
        self.dynamicCall("SetInputValue(QString,QString)", id, value)

    def get_all_codes_names(self):
        ret = self.dynamicCall("GetCodeListByMarket(QString)", ["0"])  # 맨뒤 인자는 시장구분, 모든 코드들을 가져옴
        kospi_code_list = ret.split(';')

        for code in kospi_code_list:
            name = self.dynamicCall("GetMasterCodeName(QString)", [code])  # 맨뒤는 종목코드, 코드에 따른 종목명을 가져옴

            regex = re.compile('[우][a-zA-Z]{0,1}$')
            match = regex.search(name)
            self.code_dict[name] = code

            if match:
                # 우선주 이름
                preferred_stock_name = name

                # 우선주에서 보통주 이름 구하기
                common_stock_name = re.sub('\d{0,1}[우]{0,1}[a-zA-Z]{0,1}$', '', name)

                # 우선주 이름 <> 보통주 매핑
                self.stock_map[preferred_stock_name] = common_stock_name

        # 네이밍 검증 전 출력
        print(len(self.stock_map), self.stock_map)

        # 네이밍 검증, '보통주 + 우' 형태가 아닌 경우는 걸러냄
        del_list = []
        for code in self.stock_map.keys():
            common_stock_name = self.stock_map[code]
            if common_stock_name not in self.code_dict.keys():
                del_list.append(code)

        for del_code in del_list:
            print( self.stock_map[del_code])
            del self.stock_map[del_code]

        # 네이밍 검증 이후 출력
        print(len(self.stock_map), self.stock_map)

    # 사용방법
    # code: 종목코드(ex. '005930' )
    # start : 기준일자. (ex. '20200424') => 20200424 일자 까지의 모든 open, high, low, close, volume 데이터 출력
    def get_price_data(self, code, start):
        print('get_price_data!', code, start)
        self.ohlcv = {'date': [], 'open': [], 'high': [], 'low': [], 'close': [], 'volume': []}
        self.set_input_value("종목코드", code)
        self.set_input_value("기준일자", start)
        self.set_input_value("수정주가구분", 1)
        self.comm_rq_data("opt10081_req", "opt10081", 0, "0101")

        # 이 밑에는 한번만 가져오는게 아니고 싹다 가져오는거다.

        while self.remained_data == True:
            # time.sleep(TR_REQ_TIME_INTERVAL)
            self.set_input_value("종목코드", code)
            self.set_input_value("기준일자", start)
            self.set_input_value("수정주가구분", 1)
            self.comm_rq_data("opt10081_req", "opt10081", 2, "0101")

        time.sleep(1)
        # data 비어있는 경우
        if self.ohlcv['date'][0] == '':
            return []

        if self.ohlcv['date'] == '':
            return []

        df = pd.DataFrame(self.ohlcv, columns=['open', 'high', 'low', 'close', 'volume'], index=self.ohlcv['date'])
        # print(df)
        return df

    def _get_repeat_cnt(self, trcode, rqname):
        ret = self.dynamicCall("GetRepeatCnt(QString, QString)", trcode, rqname)
        return ret

    def _opt10081(self, rqname, trcode):
        # print('_opt10081')
        # 몇번 반복 실행 할지 설정
        ohlcv_cnt = self._get_repeat_cnt(trcode, rqname)

        # 하나의 row씩 append
        for i in range(ohlcv_cnt):
            date = self._get_comm_data(trcode, rqname, i, "일자")
            open = self._get_comm_data(trcode, rqname, i, "시가")
            high = self._get_comm_data(trcode, rqname, i, "고가")
            low = self._get_comm_data(trcode, rqname, i, "저가")
            close = self._get_comm_data(trcode, rqname, i, "현재가")
            volume = self._get_comm_data(trcode, rqname, i, "거래량")

            self.ohlcv['date'].append(date)
            self.ohlcv['open'].append(int(open))
            self.ohlcv['high'].append(int(high))
            self.ohlcv['low'].append(int(low))
            self.ohlcv['close'].append(int(close))
            self.ohlcv['volume'].append(int(volume))

    def _get_comm_data(self, code, field_name, index, item_name):
        ret = self.dynamicCall("GetCommData(QString, QString, int, QString", code, field_name, index, item_name)
        return ret.strip()

    # tr을 서버에 전송한다
    def comm_rq_data(self, rqname, trcode, next, screen_no):
        self.dynamicCall("CommRqData(QString, QString, int, QString)", rqname, trcode, next, screen_no)
        time.sleep(1)
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def calc_disparity_rate_signal(self):
        print('calc_disparity_rate_signal start')        
        for preferred_stock_name in self.stock_map.keys():
            time.sleep(1.5)
            try:
                common_stock_name = self.stock_map[preferred_stock_name]
                preferred_stock_code = self.code_dict[preferred_stock_name]
                common_stock_code = self.code_dict[common_stock_name]

                preferred_stock_df = self.get_price_data(preferred_stock_code, '20200604')
                common_stock_df = self.get_price_data(common_stock_code, '20200604')
                common_stock_df = common_stock_df.rename(
                    columns={'open': 'open_common', 'high': 'high_common', 'low': 'low_common', 'close': 'close_common',
                             'volume': 'volume_common'})

                concat_df = pd.concat([preferred_stock_df, common_stock_df], axis=1)
                concat_df['disparity_rate'] = (concat_df['close_common'] - concat_df['open']) / concat_df['open'] * 100
                concat_df['close_ma200'] = concat_df['close'].rolling(window=200).mean()
                concat_df['close_common_ma20'] = concat_df['close_common'].rolling(window=20).mean()
                concat_df['disparity_rate_ma200'] = concat_df['disparity_rate'].rolling(window=200).mean()
                concat_df['signal'] = np.where((concat_df['disparity_rate'] > concat_df['disparity_rate_ma200']) & (
                        concat_df['close'] < concat_df['close_ma200']) & (concat_df['close_common'] > concat_df['close_common_ma20']), 1, 0)

                buy_signal = concat_df[-1:]['signal'].values[0]

                if buy_signal:
                    print(preferred_stock_name, common_stock_name)
                    print(concat_df[-1:]['signal'].values[0], end='\n\n')
                    concat_df.to_excel(preferred_stock_name+'.xlsx')

            except Exception as e:
                print('error on ', preferred_stock_name)
                print(traceback.format_exc())
                        
            print('ends')


if __name__ == "__main__":
    app = QApplication(sys.argv)
    kiwoom = Kiwoom()
    kiwoom.comm_connect()
    # kiwoom.get_price_data('005930', '20200603')
    kiwoom.get_all_codes_names()
    kiwoom.calc_disparity_rate_signal()
    sys.exit(app.exec_())

 

 

문제는 이 코드를 실행하다보면 에러가 납니다. 

아무리 sleep을 넣어도 Too many request 에러가 납니다.

너무 많은 요청을 보낸다는데 그렇게까지 많은지는 모르겠습니다..

따라서 전체가 다 돌아가지는 않고 중간에 에러가 납니다. 

이 문제는 Kiwoom 사용법에 익숙해지면 해결할 수 있을 것 같아 우선은 문제인 상태로 올립니다.

 

코드를 실행하면 위 매수조건에 해당하는 기업들(우선주명)이 나옵니다 

 

 

하지만 재무정보를 보면 다 사고 싶은 기업들은 아닙니다.

몇 개만 추려보면 다음과 같았습니다.  (개인적인 견해라 주식 추천이 아닙니다. 저만 살겁니다.)

 

추가로 코드를 작성한 시점은 06-05이라 06-04 종가를 기준으로 추출했고

첨부한 캡쳐는 06-06에 한거라 06-05 종가입니다.

 

 

1. 대신증권우 

 

 

2. 미래에셋대우우 

 

 

3. 삼성화재우

 

 

4. 현대차우

 

 

 

삼성화재를 제외하면 거래량도 좀 있고 배당도 좋고 실적도 나쁘지 않은 기업만 추려봤습니다.

훌륭한 배당 및 시세차익을 기대해볼 수 있는 우선주-보통주 괴리율 투자 전략이었습니다.

 

가정대로 실제로 돈을 벌지는 두고 봐야겠습니다!

 

 

제 블로그에 방문해주셔서 감사합니다.
좋아요, 댓글은 제가 글을 쓰는데 큰 힘이 됩니다.
아래는 쿠팡 링크이고 쿠팡 파트너스 활동을 통해 일정 수익이 발생할 수 있음을 알려 드립니다.

반응형
이 포스팅은 쿠팡파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

댓글