개요
– 본 글에서는 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. 만들어진 결과물을 실제 투자에 어떻게 활용했는지는 다음 글을 통해 실제 투자 후기를 알려드리겠다.