yfinance로 미국 주식 기본 지표 스크래핑하기

개요

– 본 글에서는 yfinance를 활용해 나스닥에 상장된 미국 기업들의 PER, PBR, ROE 등 기본 지표를 수집하는 웹 스크래핑 코드에 대해 소개한다. 또한 기본 지표를 근거로 저PER, 저PBR 기업을 분류하여 파일로 저장한다.

– ‘주식 종목 선정을 어떻게 할 것인가?’ 내게는 이게 가장 어려운 문제였다. 시작이 반이라는 말의 무게를 가장 크게 느꼈던 순간이 어떤 종목에 투자할지 고민할 때였던 것 같다.

– 고민에 고민을 거듭한 끝에 내가 내린 결론은 ‘숫자에 근거한 투자를 하자’ 였다. 기업의 실적에 기반한 PER, PBR, ROE, 영업이익 상승률 등의 기본 지표를 바탕으로 저평가된 기업들을 선별하는 것

– 다음 고민은 ‘이를 위해서는 최대한 많은 기업들의 기본 지표들을 수집해야 하는데 어떤 기업부터 찾아나가야 하지?’ 였다. 내가 만약 반도체 분야에서 일한다면 엔비디아를 시작으로 TSMC, 마이크론.. 등 기업들의 기본 지표를 수집하면 되는 걸까?

– 문제는 이걸 일일이 하기에는 시간이 너무 오래 걸린다는 것. 그래서 찾아보니 파이썬의 yfinance 라이브러리를 이용하면 나스닥에 상장된 기업들의 기본 지표를 받아올 수 있으며, ~사이트에서는 나스닥에 상장된 미국 기업들의 목록을 다운로드할 수 있다. 이를 이용해서 나스닥에 상장된 미국 기업들의 기본 지표를 스크래핑하는 코드를 만들었다. 결과물은 엑셀 파일로 저장한다.

사용자 환경

– python : 3.8.10

– tqdm : 4.65.0

– pandas : 2.0.3

– numpy : 1.24.4

– datetime 5.5

– requests : 2.31.0

코드

# 출처가 https://companiesmarketcap.com/ 인 경우

import yfinance as yf
from tqdm import tqdm
import pandas as pd
import numpy as np
from datetime import datetime
import time
import threading
import os
import requests

### 변수들 모음

# 미국 주식들 시가총액에 따른 순위 엑셀 다운로드
url = "https://companiesmarketcap.com/usa/largest-companies-in-the-usa-by-market-cap/?download=csv"  # 다운로드받을 csv 파일 주소
filename = "us_stocks.csv"  # 넷상에서 다운받은 주식 목록 파일명

# User-Agent 설정
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"  # User-Agent 설정. 스팸봇으로 오해하지 않도록
header = {"User-Agent": user_agent}  # 요청 헤더 설정

# 상수(파일 저장 경로 같은) 등 모음
DIR_STOCK_US = "C:/coding/DA/재테크/미국기업목록/us_stocks.csv"  # 기업 목록 저장
DIR_STOCK_INDICATOR = "C:/coding/DA/재테크/stocks_basic_indicators_CMC.xlsx"  # 작업 후 다 만들어진 파일 저장할 경로
lock = threading.Lock()  # 락
start_time = time.time()  # 시작 시간
num_cpu = os.cpu_count()  # 일 나눠서할 cpu 개수
cnt_stock_lists = os.path.isfile(
    DIR_STOCK_US
)  # 기업 목록 저장된 엑셀파일이 로컬 파일에 잘 저장됐는지 확인

# 날짜 관련
now = datetime.now()  # 오늘 날짜 가져오기
today = str(now).split()[0]  # 긴 형식의 날짜 ex) 2024-10-27 (2024년 10월 27일)

# User-Agent 설정
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"  # User-Agent 설정. 스팸봇으로 오해하지 않도록
header = {"User-Agent": user_agent}  # 요청 헤더 설정

# 스레드로 데이터프레임 따로 작업한 후 concat할 데이터프레임 모아놓는 리스트들
list_dfs = list()


### 함수들 모음
# 기업목록 csv 파일 다운로드
def download_stock_list():
    try:
        response = requests.get(url, headers=header)
        response.raise_for_status()

        # 파일 저장
        with open(DIR_STOCK_US, "wb") as f:
            f.write(response.content)
            print("기업 목록 다운로드 완료")

    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error 발생: {e}")

    except Exception as e:
        print(f"다운로드 중 오류 발생: {e}")


# 각 주식 지표 데이터 가져오기(기업들 PER, PBR, ROE, EPS, 영업이익 가져오기)
def add_stock_info(data):
    global cnt
    data_reset = data.reset_index(drop=True)
    for i in tqdm(range(len(data_reset))):  # 2024/04/10(수) 추가
        try:  # 해당 값들이 있는 경우에만 가져오기
            # yfinance를 사용하여 주식 데이터를 가져오기
            symbol = data_reset.loc[i]["Symbol"]
            stock_data = yf.Ticker(symbol)  # ticker.upper() 오류
            income_statement = (
                stock_data.financials
            )  # 재무제표에서 영업이익 가져오기. Financial Statement
            stock_info = stock_data.info
            # PER(주가수익비율) 정보 가져오기
            data_reset.loc[i, "PER"] = stock_info.get("forwardPE", 0)
            # PBR(주가순자산비율) 정보 가져오기
            data_reset.loc[i, "PBR"] = stock_info.get("priceToBook", 0)
            # ROE(자기자본이익률) 정보 가져오기
            data_reset.loc[i, "ROE"] = stock_info.get("returnOnEquity", 0)
            # EPS(주당순이익) 정보 가져오기
            data_reset.loc[i, "EPS"] = stock_info.get("trailingEps", 0)
            # 가장 최근 년도 영업이익 가져오기
            if len(income_statement.loc["Operating Income"]) >= 1:
                data_reset.loc[i, "OperatingIncome1"] = income_statement.loc[
                    "Operating Income"
                ][0]
                # 가장 최근 년도 -1년 영업이익 가져오기
            if len(income_statement.loc["Operating Income"]) >= 2:
                data_reset.loc[i, "OperatingIncome2"] = income_statement.loc[
                    "Operating Income"
                ][1]
        except Exception as e:
            # print(f"에러 심볼 : {symbol}") # 디버깅용
            continue

        # 기본 지표들 평가
        if (
            data_reset.loc[i, "OperatingIncome1"]
            and data_reset.loc[i, "OperatingIncome2"]
        ):  # 작년, 재작년 영업이익이 존재하는 경우
            data_reset.loc[i, "OIIR"] = (
                data_reset.loc[i, "OperatingIncome1"]
                / data_reset.loc[i, "OperatingIncome2"]
            )  # Operating Income Increasing Ratio. 영업이익 상승 비율
            if (
                data_reset.loc[i, "OIIR"] >= 1.05
            ):  # 영업이익 소폭 증가 주식 판별. 영업이익 5% 상승 기준
                data_reset.loc[i, "LROI"] = True
                data_reset.loc[i, "평가충족"] += 1
            if (
                data_reset.loc[i, "OIIR"] >= 1.10
            ):  # 영업이익 대폭 증가 주식 판별. 영업이익 10% 상승 기준
                data_reset.loc[i, "HROI"] = True
                data_reset.loc[i, "평가충족"] += 1
        if (
            data_reset.loc[i, "PER"]
            and (type(data_reset.loc[i, "PER"]) != str)
            and (data_reset.loc[i, "PER"] <= 9)  # PER만 자꾸 str이 있다고 떠서...
        ):  # 저PER 주식 판별 및 추가. PER 9 기준
            data_reset.loc[i, "LPER"] = True
            data_reset.loc[i, "평가충족"] += 1
        if data_reset.loc[i, "PBR"] and (
            data_reset.loc[i, "PBR"] <= 1.5
        ):  # 저PBR 주식 판별 및 추가. PBR 1.5 기준
            data_reset.loc[i, "LPBR"] = True
            data_reset.loc[i, "평가충족"] += 1
        if data_reset.loc[i, "ROE"] and (
            data_reset.loc[i, "ROE"] >= 0.07
        ):  # 고ROE 주식 판별 및 추가. ROE 7% 기준
            data_reset.loc[i, "HROE"] = True
            data_reset.loc[i, "평가충족"] += 1
        if data_reset.loc[i, "EPS"] and (
            data_reset.loc[i, "EPS"] >= 5
        ):  # 고EPS 주식 판별 및 추가. EPS 5 기준
            data_reset.loc[i, "HEPS"] = True
            data_reset.loc[i, "평가충족"] += 1

    lock.acquire()
    list_dfs.append(data_reset.reset_index(drop=True))
    lock.release()


# 파일 저장
def save_df():
    df_all = pd.concat(
        list_dfs, ignore_index=True
    )  # 각 스레드가 작업 끝낸 데이터프레임 합쳐주기
    if os.path.isfile(DIR_STOCK_INDICATOR):
        df_nasdaq_indicators = pd.read_excel(
            DIR_STOCK_INDICATOR, index_col=0
        )  # 이미 저장된 나스닥 기본지표 파일
        df_nasdaq_indicators = pd.concat(
            [df_nasdaq_indicators, df_all], ignore_index=True
        )  # concat 하면서 기존 index는 무시하고 초기화한다.
        df_nasdaq_indicators.to_excel(DIR_STOCK_INDICATOR, index=True, index_label="No")
    else:
        df_all.to_excel(DIR_STOCK_INDICATOR, index=True, index_label="No")

    print("저장이 완료되었습니다.")  # 저장 완료 문구


# 시간 체크
def check_time():
    global start_time
    runningtime = int(time.time() - start_time)  # 현재 시간 - 시작 시간
    runningtime_hour = runningtime // 3600  # 시간
    runningtime -= runningtime_hour * 3600
    runningtime_minute = runningtime // 60  # 분
    runningtime -= runningtime_minute * 60
    runningtime_second = runningtime  # 초
    print(f"실행시간 {runningtime_hour}:{runningtime_minute}:{runningtime_second}")


# 주식 지표들 저장할 데이터프레임 생성
def make_df():
    df_stocks_nyse = pd.read_csv(
        DIR_STOCK_US, index_col=0
    )  # 시가총액순 기업들을 df_stocks라는 이름의 DataFrame에 저장
    df_stocks_nyse = df_stocks_nyse.assign(
        DATE=today,
        PER=0,
        PBR=0,
        ROE=0,
        EPS=0,
        OperatingIncome1=0,
        OperatingIncome2=0,
        # 6개 평가 항목
        평가충족=0,
        LPER=False,  # Low PER. 낮은 PER
        LPBR=False,  # Low PBR. 낮은 PBR
        HROE=False,  # High ROE. 높은 ROE
        HEPS=False,  # High EPS. 높은 EPS
        LROI=False,  # Low Rise Of Income. 영업이익 소폭 증가
        HROI=False,  # High Rise of Income
        OIIR=False,  # 전 영업이익/전전 영업이익 = 영업이익 상승률
        소감="'-",
    )  # DataFrane에 PER, PBR, ROE, EPS 컬럼 추가 + 영업이익, 소감 추가
    return df_stocks_nyse


# 멀티 쓰레딩
def multi_thread():
    df_stocks_nyse = make_df()
    ranges = np.array_split(
        np.array(list(df_stocks_nyse.index)), num_cpu
    )  # 각 쓰레드에 할당하기 위해 종목들 스플릿하기
    thread_list = []  # 실행시킬 쓰레드들 목록
    df_stocks_for_thread = []  # 미국주식 목록 데이터프레임 쪼개서 담을 리스트

    for c in range(1, num_cpu + 1):
        globals()[f"df_stocks_split{c}"] = df_stocks_nyse.loc[
            ranges[c - 1][0] : ranges[c - 1][-1] + 1
        ]
        df_stocks_for_thread.append(eval(f"df_stocks_split{c}"))

    # 멀티쓰레딩 수행
    for d in df_stocks_for_thread:  # d는 dataframe
        t = threading.Thread(
            target=add_stock_info,
            args=(d,),
        )
        thread_list.append(t)

    for t1 in thread_list:
        t1.start()

    for t2 in thread_list:
        t2.join()


if __name__ == "__main__":
    if (not cnt_stock_lists) or (
        now.month % 2 == 0
    ):  # 기업 리스트 파일이 없거나, 짝수 달이라면 기업 목록 CSV 파일 다운로드
        download_stock_list()

    multi_thread()  # 멀티 쓰레딩

    save_df()  # 데이터프레임 엑셀로 저장

    check_time()  # 실행시간 출력

코드 설명

– 코드에서 각 단락이 어떤 기능을 구현한 것인지에 대해서는 내 깃허브에 간략하게 적어놓았다.

– 아래에는 각 사용자마다 수정해야하는 부분(파일 저장할 경로 등), 유의해야하는 점에 대해 설명한다.

사용자마다 변경할 변수들

1. filename = “us_stocks.csv” # 넷상에서 다운받은 주식 목록 파일명

– 해당 변수는 스크래핑할 미국 기업 주식 목록을 다운로드해서 어떤 이름으로 저장할지를 나타낸다. 각자 마음에 드는 이름으로 변경해도 무방하다.

– 나는 미국 기업 주식 목록을 이곳에서 다운로드하고 있다. 해당 웹사이트는 국가별 주식의 시가총액 순 기업 목록 데이터를 제공한다. 해당 웹사이트에서 다운로드 버튼을 클릭하면 기업 목록을 csv 파일로 다운로드할 수 있다. 아쉬운 점은 국가별 주식이 ‘해당 국가의 시장에 상장된 기업들’이 아니라 ‘해당 국가 소속된 기업들’ 이라는 점이다. 그래서 TSMC 같은 기업은 존재하지 않는다.

미국 기업 주식 목록를 다운로드 받을 수 있는 사이트
다운로드 버튼을 클릭하면 미국 기업 목록을 다운로드할 수 있다.

2. DIR_STOCK_US = “C:/coding/DA/재테크/미국기업목록/us_stocks.csv” # 기업 목록 저장

– 1의 이름으로 된 미국 기업 주식 목록을 어떤 경로에 저장할지 나타내는 변수

3. DIR_STOCK_INDICATOR = “C:/coding/DA/재테크/stocks_basic_indicators_CMC.xlsx” # 작업 후 다 만들어진 파일 저장할 경로

– 모든 종목을 스크래핑해서 만들어진 결과물을 어떤 경로에 저장할지 나타내는 변수

4. num_cpu = os.cpu_count() # 일 나눠서할 cpu 개수

– 멀티쓰레딩으로 일을 나눠서 할 cpu의 개수이다. 나는 그냥 내 cpu 수만큼 했다.

– 아무래도 수천 개의 주식의 기본 지표를 스크래핑 하다 보니 시간이 꽤 걸린다. 시간을 더 줄이기 위해 멀티쓰레딩을 활용한다.

5. 기본 지표 기준 : ‘# 기본 지표들 평가’ 아랫부분

– 아래 기준은 내가 정한 기준이다. 각자 입맛에 맞게 변경하자.

  • 저PER 기준 : PER 9 이하
  • 저PBR 기준 : PBR 1.5 이하
  • 고ROE 기준 : ROE 7% 이상
  • 고EPS 기준 : EPS 5 이상
  • 영업이익 소폭 상승 기준 : 5% 상승
  • 영업이익 대폭 상승 기준 : 10% 상승

– 영업이익은 최근 2개년 영업이익을 가져온다.

결과물

코드가 실행되는 화면
완성된 결과물

– 평가충족 컬럼 : 총 6개 항목에 대해 평가하며, 그 중 몇 가지를 충족하는지를 나타낸다.

(LPER : 저 PER, LPBR : 저PBR, HROE : 고ROE, HEPS : 고EPS, LROI : 영업이익 소폭 상승, HROI : 영업이익 대폭 상승)

– 소감 컬럼 : 각 주식들에 대해 어떻게 생각하는지 자기 생각, 분석하고 느낀점을 적기 위해 만들었다.

유의할 점

1. 종종 코드를 실행시키다 보면 봇으로 오인받아 중간에 프로그램이 멈추기도 한다.

2. 404 Client Error가 화면에 자주 나오는데 나도 원인을 잘 모르겠다. 실적 기록이 짧은 기업에 대해서는 야후 파이낸스에 제대로 된 데이터가 없어서 오류가 뜨는 게 아닐까?

3. 2의 이유로 시간이 실행 시간이 들쭉날쭉하다. 어쩔 때는 15분 안에 끝나지만, 어떨 때는 50분이 걸리기도 한다. 404 Client Error가 뜰 때마다 화면에 tqdm이 다시 표시되기 위해 걸리는 시간 때문인 것 같기도 한데, 나중에 코드를 수정하던가 해야겠다.

4. 만들어진 결과물을 실제 투자에 어떻게 활용했는지는 다음 글을 통해 실제 투자 후기를 알려드리겠다.

참고 자료

  1. 국 기업 목록
  2. 내 깃허브