본문 바로가기
파이썬으로 종목 스크리너 만들기

뇌동매매 금지 - 11. S-RIM을 이용한 목표주가 계산해보기 - 2. S-RIM 데이터 크롤링하기

by 유티끌 2022. 11. 26.

이번 포스팅에서는 S-RIM 데이터를 어떻게 구해볼 것인지, 스크리닝 시에 어떻게 할 것인지를 알아봅니다.


11. S-RIM을 이용한 목표주가 계산해보기 - 2. S-RIM 데이터 크롤링하기

그럼 이제 어떻게 S-RIM 데이터를 구해야할지, 이것은 제가 사용하는 스크리너에서 어떻게 사용할 수 있을지를 생각해야합니다.

사실 계산식은 책에 나와있어서 계산하는 것은 문제가 아닌데, 자본총계(특히 지배주주지분)과 평균 ROE를 구하는게 쉬운일은 아니어보였습니다. 그래서 조금 고민하면서 구글링을 하던 도중에, 아래 위키페이지를 알게 되었습니다.

https://wikidocs.net/94787

책을 작성하려고 위키독스를 작성하시는 것 같았지만, 뭔가 멈춰있는 그런 느낌이었습니다...?

이 위키의 저자가 만든 라이브러리가 있길래, 이것을 사용하면 쉽게 구할 수 있겠다 싶었습니다.

https://github.com/sharebook-kr/pystocklib

알고보니, 제가 이미 사용하고 있는 pykrx 라이브러리를 만드신 분들이 만들어놓은 코드였죠...!! 넘나 감사했습니다. 이분들이 정말 감사할 따름입니다.

라이브러리 사용방법은 위키독스에 나와있습니다만... 페이지를 하나하나 호출해서 크롤링하는 이 방식으로는 꽤나 많은 시간이 걸리겠다 싶었습니다... 애초에 티커를 하나하나 입력해야지만이 사용할 수 있었죠.

그치만 제가 만든 스크리너에서는 충분히 사용할 수 있을 것 같았습니다.


S-RIM 데이터 크롤링하고 계산한 다음에, 엑셀로 저장하기

기존의 필터 리팩토링하기

일단 저는 기존에 종목들에 대해 우선주, 스팩주, 지주사 및 거래량 0인 종목들을 걸러내는 필터를 사용하고 있었는데요, 이것을 분리해줬습니다.

filter_by_condition.py

def drop_column(df: pd.DataFrame):
    # 스팩 주식 드랍
    df.drop(
        df[df["종목명"].str.contains("스팩")].index,
        inplace=True
    )
    # 우선주 드랍
    df.drop(
        df[df["종목명"].str.endswith(("우", "우B", "우C", "(전환)"))].index,
        inplace=True
    )

    # 지주사 드랍
    df.drop(
        df[df["종목명"].str.endswith(("홀딩스", "지주", "지주회사"))].index,
        inplace=True
    )

    return df

def drop_volume(df: pd.DataFrame):
    # 직전 거래일의 거래량이 0인 경우는 어떠한 이유에서 거래정지가 되어있을 확률이 높음
    df = df[df["거래량"] > 0]

    return df

코스피, 코스닥 종목들의 종목코드, 종목명 얻어오기

다음은 종목들의 정보들을 얻을 차례입니다.

korean_market_factor_data.py

    def __get_korean_stock_ticker_and_name(self, date, market):
        stock_list = pd.DataFrame({'종목코드': self.stock.get_market_ticker_list(date, market=market)})
        stock_list['종목명'] = stock_list['종목코드'].map(lambda x: stock.get_market_ticker_name(x))
        stock_list['업종'] = stock_list['종목명'].map(lambda x: self.fdr_data[self.fdr_data["Name"] == x]["Sector"].iloc[0])

        return stock_list

아래 메소드에서 위 메소드를 호출합니다.

    def get_stock_ticker_and_name_list(self, market):

        today = self.__get_date()

        return filter_data.drop_column(self.__get_korean_stock_ticker_and_name(today, market))

이 때, return 문에서, 컬럼들을 걸러내는 메소드를 이용합니다. 컬럼들을 걸러내기 위해서는 종목명을 알고 있어야하기 때문에, 일단 종목코드와 종목명을 얻어야했습니다.

이렇게 얻은 정보들을, 실질적으로 프로세스를 수행할 파이썬 파일에서 이용하도록 가져온다음 concat 으로 하나로 합칩니다.

crawling_data_for_s_rim.py

import datetime
import time
import pandas as pd
import pystocklib.srim as srim
from extract_data.extract import Extract
from export_data import ExportToData

kospi_data = extractor.factor_data.get_stock_ticker_and_name_list("KOSPI")
kosdaq_data = extractor.factor_data.get_stock_ticker_and_name_list("KOSDAQ")

kospi_kosdaq_data = pd.concat([kospi_data, kosdaq_data])

S-RIM 계산에 필요한 데이터들을 크롤링해와서 기업가치 계산하기

먼저 크롤링을 진행하기 전에, import문 바로 하단에, 계산용 함수를 선언합니다.

crawling_data_for_s_rim.py

def calculate_company_value(net_worth, roe, k, discount_roe=1.0):
    if discount_roe == 1.0:
        value = net_worth + (net_worth * (roe - k)) / k
    else:
        excess_earning = net_worth * (roe - k) * 0.01
        mul = discount_roe / (1.0 + k * 0.01 - discount_roe)
        value = net_worth + excess_earning * mul

    return value

라이브러리에 기업가치 및 적정주가를 계산해주는 메소드가 존재하지만, 호출할때마다 자본총계 및 예상ROE, 요구수익률을 크롤링하므로 불필요한 호출이 발생하게 됩니다.

계산식에 따르면 ROE감소추정치만 바뀌고 나머지는 동일한 값만 있으면 되므로, 계산식을 별도로 만들어줍니다. (계산식 자체는 라이브러리 안에서 복사해서 빼냈습니다;)

다음으로는, 종목정보에서 종목코드만 뽑아 새로운 데이터프레임 객체를 만들고, index를 리셋합니다.
그 후에 ROE감소추정치를 미리 배열로 만듭니다.

net_worth_and_roe_list = kospi_kosdaq_data[["종목코드"]]
net_worth_and_roe_list.reset_index(drop=True, inplace=True)
require_rate_of_return = [1.0, 0.9, 0.8]

이 이후로는, 매 종목별로 반복문을 돌며 크롤링을 실행하고, S-RIM을 계산합니다. 자세한 로직은 아래와 같습니다.

  • 자본총계와 예상ROE를 크롤링합니다.
    • 이 때, 값을 구할 수 없는 종목도 있을 수 있으므로, try-catch로 감싸줍니다. 값을 구하지 못하는 경우에는 0을 넣습니다.
  • 다음으로, ROE감소추정치 배열의 총 길이만큼 다시 반복해가면서 기업가치를 계산합니다.
  • 계산한 값들을 데이터프레임에 저장합니다.
for i in range(len(net_worth_and_roe_list)):
    print(f'{net_worth_and_roe_list.loc[i, "종목코드"]}, {i+1}/{len(net_worth_and_roe_list.index.values.tolist())}')

    s_rim_values = []

    try:
        net_worth = srim.reader.get_net_worth(net_worth_and_roe_list.loc[i, "종목코드"])
    except:
        net_worth = 0

    try:
        roe = srim.reader.get_roe(net_worth_and_roe_list.loc[i, "종목코드"])
    except:
        roe = 0

    for discount_roe in require_rate_of_return:
        s_rim_values.append(calculate_company_value(net_worth, roe, 10, discount_roe))

    net_worth_and_roe_list.loc[i, "net_worth"] = net_worth
    net_worth_and_roe_list.loc[i, "average_roe"] = roe
    net_worth_and_roe_list.loc[i, "s-rim_value"] = s_rim_values[0]
    net_worth_and_roe_list.loc[i, "s-rim_value_0.9"] = s_rim_values[1]
    net_worth_and_roe_list.loc[i, "s-rim_value_0.8"] = s_rim_values[2]

여기서 calculate_company_value(net_worth, roe, 10, discount_roe) 에 요구수익률 자리에 10을 넣고 있습니다. 책의 저자도 그렇고, 라이브러리 자체 로직에서도 BBB- 회사채금리를 크롤링하는 메소드가 있지만, 저는 그냥 종목당 적어도 10%는 얻자! 라는 생각으로, 상수값 10을 그냥 넣었습니다.

여기서 바로 적정주가를 계산했을 수도 있었는데요, 기업은 1년에 정해진 시기에만 추가상장을 통한 증자나, 액면분할을 실행하는게 아니기 때문에, 각각의 분기별로 최신 상장주식수를 확인해서 나누어주는 것이 정확할 것 같았습니다.

엑셀로 저장하기

기존에 만들어놓은, 엑셀로 저장하는 메소드를 이용해서, 원하는 위치, 시트이름, 데이터를 파라미터로 넘겨서 엑셀파일을 만들어줍니다.

exporter.export_to_excel(
    "crawling_data/net_worth_and_roe_list_for_s_rim.xlsx",
    "s_rim",
    net_worth_and_roe_list
)

이렇게 하면 2천여개의 기업에 대한 S-RIM 기업가치 계산은 종료하게 됩니다.


crawling_data_for_s_rim.py 의 전체 코드는 아래와 같습니다.

import datetime
import time
from extract_data.extract import Extract
import pandas as pd
import pystocklib.srim as srim
from export_data import ExportToData


def calculate_company_value(net_worth, roe, k, discount_roe=1.0):
    if discount_roe == 1.0:
        value = net_worth + (net_worth * (roe - k)) / k
    else:
        excess_earning = net_worth * (roe - k) * 0.01
        mul = discount_roe / (1.0 + k * 0.01 - discount_roe)
        value = net_worth + excess_earning * mul

    return value

start = time.time()
extractor = Extract()
exporter = ExportToData()

kospi_data = extractor.factor_data.get_stock_ticker_and_name_list("KOSPI")
kosdaq_data = extractor.factor_data.get_stock_ticker_and_name_list("KOSDAQ")

kospi_kosdaq_data = pd.concat([kospi_data, kosdaq_data])

net_worth_and_roe_list = kospi_kosdaq_data[["종목코드"]]
net_worth_and_roe_list.reset_index(drop=True, inplace=True)
require_rate_of_return = [1.0, 0.9, 0.8]

for i in range(len(net_worth_and_roe_list)):
    print(f'{net_worth_and_roe_list.loc[i, "종목코드"]}, {i+1}/{len(net_worth_and_roe_list.index.values.tolist())}')

    s_rim_values = []

    try:
        net_worth = srim.reader.get_net_worth(net_worth_and_roe_list.loc[i, "종목코드"])
    except:
        net_worth = 0

    try:
        roe = srim.reader.get_roe(net_worth_and_roe_list.loc[i, "종목코드"])
    except:
        roe = 0

    for discount_roe in require_rate_of_return:
        s_rim_values.append(calculate_company_value(net_worth, roe, 10, discount_roe))

    net_worth_and_roe_list.loc[i, "net_worth"] = net_worth
    net_worth_and_roe_list.loc[i, "average_roe"] = roe
    net_worth_and_roe_list.loc[i, "s-rim_value"] = s_rim_values[0]
    net_worth_and_roe_list.loc[i, "s-rim_value_0.9"] = s_rim_values[1]
    net_worth_and_roe_list.loc[i, "s-rim_value_0.8"] = s_rim_values[2]

exporter.export_to_excel(
    "crawling_data/net_worth_and_roe_list_for_s_rim.xlsx",
    "s_rim",
    net_worth_and_roe_list
)


end = time.time()
sec = (end - start)

result_list = str(datetime.timedelta(seconds=sec)).split(".")
print(f"Total extracting time : {result_list[0]} ---------------------")

이렇게 크롤링을 진행하는데, 2천여개 종목을 크롤링하는데 1시간정도 걸렸습니다.
크롤링 데이터는 연간 재무재표 데이터를 기준으로 하고 있습니다. 따라서 연 1회만 크롤링하면 될 것으로 생각하는데요, 저는 이 크롤링으로 생성한 엑셀파일을 데이터베이스 처럼 이용하고자 합니다.

다음 포스트에서는 이렇게 계산한 S-RIM 기업가치를 통해서 분기별 데이터에 대한 적정주가를 계산해보고, 스크리닝 조건식을 만들어봅니다.

반응형

댓글