본문 바로가기
AyoProject/Ayotera-Trade

[AT] 29. 우량주 종목 자동 예측 및 선정 (4-3)

by 청양호박이 2021. 2. 18.

지난 시간에 이어서 저평가 종목을 추출하는 로직을 구현해 보겠습니다. 

 

  • 우량주 종목 선정을 위한 기반 데이터 수집 방안 (전자공시시스템 활용) - 1개 종목기준
  • 정리된 데이터 수집 방안에 대해서 코스피, 코스닥 전체 종목에 대한 데이터를 DBMS에 적재
  • 우량주 종목 선정 로직을 적용하여 대상 종목 추출
  • 추출된 우량주 종목에 대해서 저평가 종목 추출
  • 추출된 종목에 대해서 실험실 진행

 

우량주 종목에 대해서 저평가 종목을 추출하는 내용을 완성하기 위해서는 아래의 추가 단계가 필요하고, 앞으로 당분간은 해당 구성을 구현해 보도록 하겠습니다. 

 

앞으로 구성해야할 전체 구성입니다. 현재 완료되어있는 부분과 안되어 있는 부분을 단계별로 확인해 보면....

 

  1. Dart OpenData로 부터 전체 종목의 재무제표정보 추출 (완료)
  2. 영업이익률 / 현금흐름표를 기준으로 우량주 종목을 선정 (완료)
  3. 선정된 우량주 종목을 Database에 저장 (완료)
  4. 키움증권 API로 부터 3번에 저장된 우량주 종목에 대한 지표를 추출 (진행) -> 전체종목으로 변경
  5. 추출된 종목별 지표를 Database에 저장 (진행) -> 전체종목으로 변경
  6. 통합 이력기록을 위해서 작업 이력을 이력테이블 생성 및 저장
  7. 저평가 종목을 포함한 최종 종목을 선정 및 화면에 렌더링

 

이번에는 전체 종목에 대해서 추가 Hyper Parameter를 키움증권 API를 통해서 받아오고, 그 정보를 종목별 지표 저장하는 table에 저장해 보도록 하겠습니다.

 

 

1. 관련 TR확인


우선 키움증권에서 제공하는 OpenAPI를 통해서 주식기본정보를 가져오기 위해서는 KOA StudioSA를 통해서 적당한 Transaction을 확인합니다. 확인해보면 TR목록 중에서... opt10001 : 주식기본정보요청 이라는 TR이 있습니다. 

 

[Input] : 종목코드를 넣어주면....

[Output] : 종목명, PER, EPS, ROE, PBR, 시가총액 등 다양한 정보를 리턴해 줍니다. 

 

그럼 사용할 대상 TR은 확인을 완료했습니다. 

 

 

2. opt10001 사용 코드 적용


실로 오랫만에 키움증권 TR사용을 위한 코드를 작성해 봅니다. 하지만 당황하지 않고 차근차근 구현해 봅니다. 현재는 코드분리 없이 한개의 python 파일을 통해서 작성하기 때문에... 각 부분별로 작성합니다. 

 

[Class 함수 부분]

    # tr전체 이벤트 처리
    def trEvent(self, sScrNo, sRQName, sTrCode, sRecordName, sPreNext, nDataLength, sErrorCode, sMessage, sSplmMsg):
        # sScrNo(화면번호), sRQName(사용자구분), sTrCode(Tran명), sRecordName(레코드명), sPreNext(연속조회 유무)
        if sPreNext == '2': self.haveNext = True
        else: self.haveNext = False

        if sRQName == 'AT_opt10001':
            self.AT_opt10001(sTrCode, sRQName)
        elif sRQName == 'AT_opt10081':
            self.AT_opt10081(sTrCode, sRQName)
        # 이벤트 처리에는 항상 EventLoop Exit( )구현
        self.TrEventLoop.exit()

    def AT_opt10001(self, sTrCode, sRQName):
        self.opt10001_res["종목명"] = self.GetCommData(sTrCode, sRQName, 0, "종목명")
        self.opt10001_res["ROE"] = self.GetCommData(sTrCode, sRQName, 0, "ROE")
        self.opt10001_res["PBR"] = self.GetCommData(sTrCode, sRQName, 0, "PBR")
        self.opt10001_res["시가총액"] = self.GetCommData(sTrCode, sRQName, 0, "시가총액")

기존에 opt10081의 코드가 약간 섞여있지만, 그냥 추가해서 작성하겠습니다. 코드 상으로는... sRQName이 AT_opt10001로 호출이 오면 AT_opt10001 함수를 호출하고 EventLoop를 종료합니다. 

 

해당 AT_opt10001함수는 GetCommData를 통해서 가져온 데이터를 response변수에 저장을 해주고 종료합니다. 그렇다면 해당 함수를 호출하는 부분을 확인해 보겠습니다.

 

[호출 본문]

    # opt10001 TR요청
    kd.opt10001_res = {}
    kd.SetInputValue("종목코드", "005930")
    # kd.SetInputValue("종목코드", "066570")
    kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")
    print(kd.opt10001_res["종목명"], type(kd.opt10001_res["종목명"]))
    print(kd.opt10001_res["ROE"], type(kd.opt10001_res["ROE"]))
    print(kd.opt10001_res["PBR"], type(kd.opt10001_res["PBR"]))
    print(kd.opt10001_res["시가총액"], type(kd.opt10001_res["시가총액"]))

response변수를 비어있는 dict type으로 생성하고, SetInputValue를 통해서 종목코드를 input값으로 넣어줍니다. 그리고나서는 CommRqData를 통해서 원하는 호출명을 추가하여 호출합니다. 

 

코드 내부적으로는 CommRqData를 호출하면, Transaction을 보내고 EventLoop를 생성하여 응답을 기다리게 됩니다. 서버에서 응답이 오면 위의 trEvent가 호출이 되고... 해당 response변수에 값을 넣어주게 되는 것이지요~~

 

음청 간단하죠?? 위의 코드는 샘플로 [삼성전자] 종목에 대해서 원하는 기본정보를 가져와보는 소스입니다.

 

[호출 결과]

삼성전자 <class 'str'>
8.7 <class 'str'>
2.26 <class 'str'>
5068345 <class 'str'>

추가적으로 모든 결과는 string type으로 온다는 것도 같이 확인이 가능하네요~~

 



 

 

3. Table 생성


해당 결과를 저장하기 위해서는 적합한 Table을 추가해야 합니다. 종목 기본 정보이기 때문에 아래의 이름으로 테이블을 생성해 주겠습니다. 추가적으로 모든 결과값은 string type이기 때문에 역시나 string으로 지정하겠습니다. 

 

[stocks_basic_info]

CREATE TABLE stocks_basic_info(
	stocks_id VARCHAR(10) PRIMARY KEY,
	stocks_roe VARCHAR(10),
	stocks_pbr VARCHAR(10),
	stocks_marketcap VARCHAR(30),
	create_date DATETIME,
	update_yn VARCHAR(1),
	update_data DATETIME
);

담백하게 종목id를 primary key로 하고 나머지는 적당한 길이의 varchar로 설정합니다. 또한, 최초 저장되는 일자와 매일 업데이트 여부를 체크하기 위해서 update여부에 대한 정보를 추가하도록 구성합니다.

 

 

4 Table 저장 로직 구현


전체 종목에 대해서 기본정보를 저장하도록 변경하였기 때문에, 전체 종목이 저장되어있는 table을 통해서 구현하도록 하겠습니다. Database안에 stocks_info라는 테이블이 있는데... 해당 테이블은 코스피와 코스닥 종목이 모두 저장되기 때문에 아주 적합한 포인트 입니다.

 

그럼 구현해야 하는 단계는,

 

  1. stocks_info에 저장되어있는 모든 종목을 구해서
  2. 각 종목별로 opt_10001을 호출하여 정보를 가져오고
  3. 가져온 정보를 stocks_basic_info 테이블에 저장

 

이렇게 3단계를 거쳐서 완성하게 되겠습니다. 그럼 1번 부터 차례로 구현해 보겠습니다.

 

[1: stocks_info에 저장되어있는 종목 구하기 - 불가로 아래 변경적용함]

 

AT프로그램의 로직상, 매일 새벽에 1번은 전체 종목을 가져오는 배치가 구동이 됩니다... 왜냐하면, 전날에 신규로 상장되는 회사도 있을테고... 없어지는 회사도 있을가능성이 있기 때문입니다. 해당 배치가 돌면, 당일에 업데이트 되는 항목은... 즉 상장되어 있는 종목은 update_date에 날짜가 오늘날짜로 변경되어 있습니다. 

 

따라서, stocks_info에 저장되어있는 종목을 전체 가져오기 위해서는 아래의 코드를 사용하겠습니다.

now = datetime.datetime.now()
nowDate = now.strftime('%Y-%m-%d')
# print(nowDate)
sql_dml = '''
SELECT stocks_id FROM stocks_info WHERE stocks_id!='' AND DATE(update_data) = %s
'''
sql_data = (nowDate)
mariadb_cur.execute(sql_dml, sql_data)
sList = mariadb_cur.fetchall()

우선 오늘날짜를 가져와서 시:분:초에 대해서는 빼주고 년-월-일 형식으로 저장을 합니다. 그리고 sql_dml 에는 오늘날짜를 전달해서 같은 날에 대해서만 데이터를 가져오도록 합니다.

 

이렇게 하면... 아래와 같이 데이터를 정상적으로 가져오게 됩니다.

 

각 각 DB에서 쿼리한 결과와... 프로그램에서 출력한 결과입니다. 정확하게 동일하네요~~

 

 

[2: 각 종목별로 opt_10001을 호출하여 정보 가져오기]

[3: 가져온 정보를 stocks_basic_info 테이블에 저장]

 

이제 전체종목에 대해서 list를 확보했으니... 1개씩 loop를 돌면서 opt_10001을 호출해서 정보를 가져오면 되겠습니다. 저장하는 로직을 분리해서 작성하기가 까다로워 하나로 작성을 하겠습니다. 관련한 코드는 아래와 같습니다.

 

그런데... 두둥!!! 키움증권에서는 OpenAPI를 통해서 Transaction을 요청할 경우 여러가지 제약사항이 존재합니다. 그 중에서도 대량의 요청을 하게 될 경우 시스템의 부하방지를 위해서 여러가지 제약이 있다고 가이드를 해주고요. 자세한 내용은 제가 작성한 아래의 시험용 글을 확인해주세요.

 

2021/02/18 - [AyoProject/Ayotera-Trade] - [AT][Error] 키움증권 OpenAPI 호출 제한

 

[AT][Error] 키움증권 OpenAPI 호출 제한

이번에는 현재 키움증권의 OpenAPI의 호출에 대한 제한에 대해서 알아보고자 합니다. 키움증권에서는 OpenAPI에 대한 과도한 Transaction의 방지를 위해서 아래와 같이 KOA Studio에 조회제한에 대한 제약

ayoteralab.tistory.com

이 테스트를 통해서 안정적으로 시스템을 돌리기 위해서는 적어도 1시간에 1,000건으로 제약이 되어야 합니다. 따라서 우리가 생각하는 모든 종목 데이터에 대해서 정보를 가져오기 위해서는 3,000건이상이기 때문에 적어도 x4의 시간이 소요되게 됩니다.

 

최소 3시간.... 이것은 아무래도 불가능하다고 보여집니다. 따라서 저는 로직을 변경하여, 우량주로 판단된 아이만 기본정보를 가져오도록 하겠습니다. 그 항목은 아래에서 다루고... 실제로 호출하여 정보를 가져오는 로직은 동일하기 때문에 그대로 이어서 해보겠습니다. 

 

1,000건에 대한 제약을 기준으로 그래도 30분 내에는 정상으로 동작하기 때문에... 그리고 추천주는 아무리 많아도 500개는 넘지 않을 것이기 때문에... 우리의 코드는 1,000건을 30분에 완료하는 기준으로 진행하도록 하겠습니다. 

    print("start : ", datetime.datetime.now())

    table_insert_cnt = 0
    sql_dml = '''
            INSERT INTO stocks_basic_info(stocks_id, stocks_roe, stocks_pbr, stocks_marketcap, create_date)
            VALUES (%s, %s, %s, %s, NOW())
            ON DUPLICATE KEY UPDATE update_yn='Y', update_data=NOW()
    '''
    for i in sList:
        # opt10001 TR요청
        kd.opt10001_res = {}
        kd.SetInputValue("종목코드", i[0])
        kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")

        sql_data = (kd.opt10001_res["종목코드"],kd.opt10001_res["ROE"],kd.opt10001_res["PBR"],kd.opt10001_res["시가총액"])

        try:
            mariadb_cur.execute(sql_dml, sql_data)
            table_insert_cnt += 1
        except mariadb.Error as error:
            print(error)

        print(table_insert_cnt, ", ", datetime.datetime.now())

        time.sleep(1.8)

    mariadb_conn.commit()

소요시간을 측정하기 위해서 일단 시간을 달아놓긴 했습니다. 위에 미리 생성해둔 table에 저장은 sql_data에 각 항목을 지정해서 저장을 수행합니다. 이제는 크게 코드로 설명을 할 것은 없어보이네요~

 

결과만 확인해 보겠습니다!!! 

133
start :  2021-02-18 10:35:24.908000
1 ,  2021-02-18 10:35:24.942000
...
...
...
131 ,  2021-02-18 10:39:22.943000
132 ,  2021-02-18 10:39:24.774000
133 ,  2021-02-18 10:39:26.604000
end :  2021-02-18 10:39:28.415000

총 4분 4초가량 소요가 되었습니다. 그리고 DB에는 해당테이블에 아래와 같이 깔끔하게 데이터가 들어가 있습니다.

마지막 133번째 데이터가 동일한 시간에 잘 insert가 되었습니다. 그렇다면 데이터도 맞는지... KOI Studio를 통해서 확인해 보겠습니다. 

 

해당 데이터는 서호전기 종목이였습니다. ROE / PBR / 시가총액 모두 정상적으로 입력되었습니다. 

 

 

[1: stocks_info에 저장되어있는 종목 구하기 - 변경적용버전]

 

변경되는 내용은 크게 다르지 않습니다. 쿼리만 바꾸면 되겠네요~~

sql_dml = '''
SELECT stocks_id FROM quarter_superior_stocks WHERE bsns_year = %s AND bsns_quarter = %s
'''
sql_data = ('2020', '1')
mariadb_cur.execute(sql_dml, sql_data)
sList = mariadb_cur.fetchall()

음청나게 담백하게 바뀌었습니다. 어차피 우량주종목관련 table은 모두 년도(bsns_year), 분기(bsns_quarter)로 동작하기 때문에 오늘 날짜를 가져오는 로직은 삭제하였습니다. 

 

2020년 1분기 기준으로 추천된 종목은 133건입니다. 

각 각 DB에서 쿼리한 결과와... 프로그램에서 출력한 결과입니다. 역시나 정확하게 동일하네요~~

 

이렇게 구현이 완료되었습니다. 이제 최종적으로 영업이익률 + 현금흐름표 + PBR + ROE + 시가총액의 기준으로 판단된 종합 우량주 종목을 추출하여 화면으로 뿌려주는 부분을 다음에 구현해 보겠습니다.

 

- Ayotera Lab -

댓글