Project : Price and EPS NTM

Quant|2021. 12. 9. 20:39
반응형

🌒 Problem

평소 자주 사용하던 Koyfin에서 일부 회사 데이터를 유료로 제공하게 되었다.

🌓 Do

EPS 값과 가격 데이터만 있으면 충분히 나도 구현 가능할 것이다

 

🌔 Prepare

가격 데이터는 야후 파이낸스 api에서 제공해주므로 eps 데이터만 구해주면 된다.

 

🌕 Code

# Ticker 와 살펴볼 시작 날을 정한다

ticker = 'aapl'
date = '2020-01-01'

EPS 데이터 준비

야후 파이낸스에서 제공하는 eps는 가장 최근 분기가 아니라
현재 분기 전 2분기까지만 제공한다.

EX ) 애플 3분기 실적 2.0이라면, 애플 1분기 실적 1.8까지만 제공
따라서 가장 최근 분기와 그 이전 분기를 nasdaq.com에서 크롤링해 온다
또한 NTM 계산을 해야하므로 다음 분기들의 Consensus 값들도 크롤링한다.

NTM 계산방법 : 21년도 3분기 NTM
 = 21년도 4분기 eps + 22년도 1분기 eps + 22년도 2분기 eps + 22년도 3분기 eps 
# 셀레니움 설치

!pip install selenium
!apt-get update
!apt install chromium-chromedriver
#셀레니움 라이브러리 import

from selenium import webdriver
from urllib.request import urlopen
from bs4 import BeautifulSoup as bs
from urllib.parse import quote_plus
from selenium.webdriver.common.keys import Keys
import time
from IPython.display import Image
import urllib
import time
# Colab에선 웹브라우저 창이 뜨지 않으므로 별도 설정한다.
# Colab 기본 설정 
options = webdriver.ChromeOptions()
options.add_argument('--headless')        # Head-less 설정
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36'
options.add_argument('user-agent={0}'.format(user_agent))
driver = webdriver.Chrome('chromedriver', options=options)

#해당 url로 이동 ex : MSFT
url = "https://www.nasdaq.com/market-activity/stocks/"+ticker+"/earnings" 
driver.implicitly_wait(10)
driver.get(url)

consensus EPS 크롤링

import pandas as pd

# consensus EPS 크롤링
driver.implicitly_wait(10)
table = driver.find_element_by_class_name('earnings-forecast__section--quarterly')

# 통채로 추출한 테이블에서 필요한 consensus EPS 부분만 따로 추출
tbody = table.find_element_by_tag_name("tbody")
rows = tbody.find_elements_by_tag_name("tr")
eps = []

for index, value in enumerate(rows):
    body=value.find_elements_by_tag_name("td")[0]
    eps.append(body.text)

#다음 5개 분기의 consensus의 정확한 날을 모르므로, 최대한 미래 값으로 설정하였다.
eps_dict = {
    '2099-01-01': eps[0],
    '2099-01-02': eps[1],
    '2099-01-03': eps[2],
    '2099-01-04': eps[3],
    '2099-01-05': eps[4],
}

# dataframe 형태의 다음 5분기에 대한 consensus eps
eps = pd.DataFrame.from_dict(eps_dict, orient='index').rename(columns={0:'epsactual'})

실행 결과

현재 분기와 이전 분기 EPS 크롤링

import datetime

# 지금까지 발표된 eps 크롤링
table = driver.find_element_by_class_name('earnings-surprise__table')

# 문자열 형태로 받은 eps 데이터를 띄어쓰기 단위로 나눔
table_list = table.text.split()

# 15번째와 21번째에 현재분기와 이전분기 발표 날짜가 저장되어있고
## 여기서 사용하는 날짜 형식은 "2021-12-09" 이므로 해당 형태로 바꾸어준다. 
table_list[15] = table_list[15][6:] + '-' + table_list[15][:2] + '-' + table_list[15][3:5] 
table_list[21] = table_list[21][6:] + '-' + table_list[21][:2] + '-' + table_list[21][3:5]

# 크롤링한 eps 데이터를 딕셔너리 형태로 바꾸고, 이를 다시 dataframe 형태로 바꿔준다.
eps_b = {
    table_list[15] : table_list[16]
    ,
    table_list[21] : table_list[22]
}

# 날짜를 index로 설정하고, eps열의 이름을 'epsactual' 로 지정
eps_second = pd.DataFrame.from_dict(eps_b, orient='index').rename(columns={0:'epsactual'})

# 날짜가 내림차순으로 정렬 되어있어 다시 역순으로 정렬
eps_second = eps_second.iloc[::-1]

실행 결과

# 가격 데이터와 eps 데이터를 불러오기 위한 
## 야후 파이낸스 api 설치
!pip install yahoo_fin
!pip install yahoo_fin --upgrade
import pandas as pd
import yahoo_fin.stock_info as si

# price history
price = si.get_data(ticker)

# earnings history 
earnings_hist = si.get_earnings_history(ticker)

## index가 [4:] 인 이유는 미래 4개 분기의 eps 데이터를 불러오는데 
### None 값으로 가져오기 때문에 잘라버림
earnings_hist = earnings_hist[4:]

# list 형식으로 불러온 것을 dataframe 형태로 바꿔줌
earnings = pd.DataFrame(earnings_hist)

# 미래 -> 과거 순에서 과거 -> 미래 순으로 바꿈
earnings = earnings[['startdatetime','epsactual']].iloc[::-1]

# 날짜 10글자로 조정
earnings['startdatetime']  = earnings['startdatetime'].str.slice(start=0, stop=10)

# startdatetime -> Date 로 이름 변경
earnings = earnings.rename({'startdatetime': 'Date'}, axis=1)

# 날짜 data를 index로 설정
earnings = earnings.set_index('Date',inplace = False)

#크롤링한 eps data와 실제 eps를 합침
EPS_plus_forcast = earnings.append(eps_second, ignore_index=False)

## 날짜와 eps 가 중복되는 경우가 발생하는데, 가장 밑에 있는 요소를 살리고 
## 나머지는 다 지우도록 설정
EPS_plus_forcast = EPS_plus_forcast.loc[~EPS_plus_forcast.index.duplicated(keep='last')]
### ex : "2021-01-01" "2.1" , "2021-01-01" "2.3" 인 경우 
### 후자를 선택하고 이전 값은 제거


EPS_plus_forcast = EPS_plus_forcast.append(eps, ignore_index=False)
EPS_plus_forcast

실행 결과

# 왜인지는 모르겠는데 eps가 지수표현이 되어 출력돼서 
# 소수점 3글자만 출력하도록 설정
## 1.0+e0 -> 1.000
pd.options.display.float_format = '{:.3f}'.format
Pandas의 rolling 사용하면 NTM을 쉽게 계산할 수 있다.
하지만 rolling 기능은 자기 자신과 이전 N개의 합을 계산하므로,
EPS를 뒤집어서 계산해야 한다.

또한 NTM은 자기 자신의 eps는 포함하지 않으므로, rolling 이후, 자기 자신을 빼주어야 한다.
# ntm 계산용으로 eps 뒤집기
ntm_tmp = EPS_plus_forcast.iloc[::-1] 

# 자기 자신도 포함해서 더해버려서 5개를 더하고 자기자신을 빼면 미래의 4개가 됨
ntm_tmp = ntm_tmp['epsactual'].rolling(window = 5, min_periods=5).sum()

# ntm 다시 처음으로 되돌리기
ntm_tmp = ntm_tmp.iloc[::-1] 

# eps와 ntm의 요소가 str로 되어있는 경우가 있어 
# float 형태로 바꿔준다
ntm_tmp = ntm_tmp.astype('float')
EPS_plus_forcast = EPS_plus_forcast.astype('float')

# rolling 값에서 자기 자신 값을 빼준다.
NTM = ntm_tmp.to_frame(name='epsactual').sub(EPS_plus_forcast, fill_value = 0, axis = 0)

 가격 데이터와 EPS 데이터를 합쳐 하나의 dataframe으로 정리한다. 

# NTM과 가격데이터 융합
df = price.join(NTM)

# 실적 발표한 날 이전의 날들은 
## 그 이전 실적발표한 날의 NTM으로 설정한다
df = df.fillna(method='ffill')

# dataframe의 모든 요소를 출력
pd.set_option('display.max_row', None)

df.tail()

실행 결과

데이터 시각화

import matplotlib.pyplot as plt

# 처음 설정한 날짜부터 나타내도록 dataframe을 자른다. 
df = df.loc[date:]

# 가격 데이터(종가)를 그래프화
ax=df.plot(kind='line', y='close', color='DarkBlue', figsize=(20,10))

# NTM을 그래프화
ax2=df.plot(kind='line', y='epsactual', secondary_y=True,color='Red', ax=ax)

ax.set_ylabel('Price')
ax2.set_ylabel('EPS')

plt.show()

짜잔~

🌗 Part to supplement

사이트마다 다음 분기의 Consensus가 극명하게 달랐고 특히 'DOCU' 같은 경우 

nasdaq.com, 야후 파이낸스, investing 세 사이트 모두 달랐다.

 

이 문제는 추후 보완해볼 예정이다.

Koyfin의 NTM 계산 방법과 api와 사이트에서 제공하는 데이터를 사용해

내가 산출한 결과와 미세한 차이는 있지만, 엄청 크지는 않다.

반응형

'Quant' 카테고리의 다른 글

[Project] 증권사 리포트에 따라 사면 어떻게 될까??  (35) 2022.03.11
[Quant Strategy] Pair Trading with Long/Short  (17) 2022.02.20
[금융상품] 채권  (18) 2021.10.26
[금융상품] 원유  (20) 2021.10.18
[금융 상품] 금 / 은  (187) 2021.10.13

댓글()