기업을 분석하는데는 기본적으로 여러 데이터가 필요합니다. 그 중에서도 기업의 기본 (펀더멘털) 이라 할 수 있는 재무데이터 분석이 필수적이라 할 수 있죠.
하지만 분석할 기업은 많습니다. 2021-11-29 일을 기준으로 한국거래소 (KRX) 에 상장된 종목만해도 2,481 개에 달합니다. 이정도면 기업의 이름만 확인하기에도 벅찬 양이네요.
다행히도 우리에겐 반복작업에 능한 컴퓨터가 있죠. 그리고 전자공시시스템 (DART : Data Analysis, Retrieval and Transfer System) 에서 제공하는 OPEN DART API 가 있습니다. 그래서 개발을 원하는 누구나가 DART 에서 제공하는 데이터를 활용하여 개발이 가능합니다.
본 프로젝트를 하려면 우선 OPEN DART 에 가입하여 API 인증키를 발급하는 것이 필요하니, 아래의 사이트에서 가입하셔야 합니다.
https://opendart.fss.or.kr/intro/main.do
이번 프로젝트에선 파이썬의 OpenDartReader 패키지로 원하는 재무적 조건을 가진 종목을 추려내보고자 합니다.
종목을 추려내려면 우선 분류할 데이터가 필요하겟죠?? 분류할 데이터는 PER, PBR 과 같은 지표를 기준으로 삼을 예정이며, 이를 위해선 지표들을 구성하는 순이익, 자산, 자본 등등의 raw data 가 필요합니다.
Raw Data 를 추출해봅시다.
1. OpenDartReader?
OpenDartReader 란 패키지 이름에서도 알 수 있듯이, 전자공시시스템 Dart 를 읽을 수 있게 해주는 Open Dart API 서비스를 더 쉽게 이용할 수 있도록 하는 파이썬의 패키지 (오픈소스 라이브러리) 입니다. 이 패키지를 활용하면 단 1줄로 기업의 재무데이터를 읽어 올 수 있습니다. 물론, 이걸 사용자의 입맛에 맞게 가공하려면 추가적인 조치가 필요하겠죠.
기본적인 설치 방법이나 사용법은 아래의 홈페이지 참조하세요.
https://github.com/FinanceData/OpenDartReader
제가 도움을 많이 받은건 이 페이지와 이 페이지의 레퍼런스 매뉴얼의 3. finstate 부분 이었습니다.
2. 활용
OpenDartReader 에서 반환값은 데이터프레임 형태로 column 은 아래 그림과 같습니다. 그 중 빨강 박스 표기한게 제가 데이터 추출할 때 활용한 column 입니다.
원본만 불러오는 걸 단순하게 코드로 나타내보면 아래와 같습니다.
import OpenDartReader
import pandas as pd
api_key = ' ' # API Key 입력
stock_name = 'LG전자'
dart = OpenDartReader(api_key)
df1 = pd.DataFrame()
# LG전자 2016년의 1분기보고서 전체항목 불러오기
df1 = df1.append(dart.finstate_all(stock_name, 2016, reprt_code='11013', fs_div='CFS'))
print(df1) # 결과를 터미널에 보여준다.
# to_excel() 시, 파일 경로까지 함께 넣어주면 더 관리하기 좋다.
fileName = 'C:/Projects/stocks_sorting/data/result_LG전자.xlsx'
df1.to_excel(excel_writer=fileName)
그 결과값은 아래와 같은데, 터미널로 나타내기엔 양이 많아 제대로 확인이 불가하죠. 주로 to_excel() 을 사용하셔서 엑셀로 저장하신뒤 확인하시는걸 추천드립니다.
rcept_no reprt_code bsns_year corp_code sj_div sj_nm \
0 20160516002710 11013 2016 00401731 BS 재무상태표
1 20160516002710 11013 2016 00401731 BS 재무상태표
2 20160516002710 11013 2016 00401731 BS 재무상태표
3 20160516002710 11013 2016 00401731 BS 재무상태표
4 20160516002710 11013 2016 00401731 BS 재무상태표
.. ... ... ... ... ... ...
202 20160516002710 11013 2016 00401731 SCE 자본변동표
203 20160516002710 11013 2016 00401731 SCE 자본변동표
204 20160516002710 11013 2016 00401731 SCE 자본변동표
205 20160516002710 11013 2016 00401731 SCE 자본변동표
206 20160516002710 11013 2016 00401731 SCE 자본변동표
account_id account_nm \
0 ifrs_CurrentAssets 유동자산
1 ifrs_CashAndCashEquivalents 현금및현금성자산
2 -표준계정코드 미사용- 금융기관예치금
3 ifrs_TradeAndOtherCurrentReceivables 매출채권
4 -표준계정코드 미사용- 기타수취채권
.. ... ...
202 ifrs_Equity 기말자본
203 ifrs_Equity 기말자본
204 ifrs_Equity 기말자본
205 ifrs_Equity 기말자본
206 ifrs_Equity 기말자본
account_detail thstrm_nm \
0 - 제 15 기 1분기말
1 - 제 15 기 1분기말
2 - 제 15 기 1분기말
3 - 제 15 기 1분기말
4 - 제 15 기 1분기말
.. ... ...
202 자본 [member]|지배기업의 소유주에게 귀속되는 자본 [member] 제 15 기 1분기
203 자본 [member]|지배기업의 소유주에게 귀속되는 자본 [member]|기타자본구성요소 제 15 기 1분기
204 자본 [member]|지배기업의 소유주에게 귀속되는 자본 [member]|기타포괄손... 제 15 기 1분기
205 자본 [member]|지배기업의 소유주에게 귀속되는 자본 [member]|납입자본 제 15 기 1분기
206 자본 [member]|지배기업의 소유주에게 귀속되는 자본 [member]|이익잉여금 제 15 기 1분기
thstrm_amount frmtrm_nm frmtrm_amount bfefrmtrm_nm bfefrmtrm_amount \
0 17378859000000 제 14 기말 16397613000000 제 13 기말 17482698000000
1 3294782000000 제 14 기말 2710156000000 제 13 기말 2244406000000
2 87649000000 제 14 기말 87454000000 제 13 기말 67700000000
3 6838192000000 제 14 기말 7093352000000 제 13 기말 7683915000000
4 564178000000 제 14 기말 654141000000 제 13 기말 633219000000
.. ... ... ... ... ...
202 11641224000000 제 14 기 11626572000000 제 13 기 11719423000000
203 -210343000000 제 14 기 -210343000000 제 13 기 -210412000000
204 -1207576000000 제 14 기 -1171979000000 제 13 기 -1143557000000
205 3992348000000 제 14 기 3992348000000 제 13 기 3992348000000
206 9066795000000 제 14 기 9016546000000 제 13 기 9081044000000
ord thstrm_add_amount frmtrm_q_nm frmtrm_q_amount frmtrm_add_amount
0 1 NaN NaN NaN NaN
1 2 NaN NaN NaN NaN
2 15 NaN NaN NaN NaN
3 16 NaN NaN NaN NaN
4 31 NaN NaN NaN NaN
.. .. ... ... ... ...
202 25 NaN 제 14 기 1분기 11455909000000 NaN
203 25 NaN 제 14 기 1분기 -210412000000 NaN
204 25 NaN 제 14 기 1분기 -1250105000000 NaN
205 25 NaN 제 14 기 1분기 3992348000000 NaN
206 25 NaN 제 14 기 1분기 8924078000000 NaN
[207 rows x 20 columns]
이렇게 엑셀로 보시면 확인하기 더 좋습니다.
이제 여기서 원하는걸 추출할 차례입니다.
데이터 프레임에 조건을 걸어서 그 조건에 맞는 위치를 찾는 방식으로 하였습니다.
예를 들면,
# df2 초기화. 정리된 data 를 넣을 곳. 빈 index 를 넣으면 추가가 안되므로 하나 넣는다.
df2 = pd.DataFrame(columns=['유동자산'], index=['1900-01-01'])
# 빈 데이터프레임 생성
df1 = pd.DataFrame()
# 빈 데이터프레임에 어느 종목의 재무데이터 로드
df1 = df1.append(dart.finstate_all(stock_name, i, reprt_code=k, fs_div='CFS'))
# 재무상태표에서 유동자산에 해당하는 조건 입력
condition = (df1.sj_nm == '재무상태표') & (df1.account_nm == '유동자산')
# 조건에 맞는 항목을 thstrm_amount 에서 찾음
current_assets[j] = int(df1.loc[condition].iloc[0]['thstrm_amount'])
# 위의 값을 옮기고자 하는 데이터프레임 df2 에 추가
df2.loc[path_string] = [current_assets[j]]
# 원본 dataframe 에도 영향을 끼치게끔 (inplace=True) 첫 행 drop.
df2.drop(['1900-01-01'], inplace=True)
# 그 다음 종목의 데이터를 얻기 위해 df2 재초기화
df2 = pd.DataFrame(columns=['유동자산'], index=['1900-01-01'])
처럼 말이죠.
3. 코드
위 코드에서 조건과 column 을 추가하고 각 종목에 대해 루프를 돌리면 각 종목들의 재무데이터를 추출할 수 있습니다.
전 아래의 데이터를 추출하였습니다.
- '유동자산'
- '부채총계'
- '자본총계'
- '매출액'
- '매출총이익'
- '영업이익'
- '당기순이익'
- '영업활동현금흐름'
- '잉여현금흐름' (산출하는 방법이 다양하여 단순하게 [영업활동 현금흐름 - 투자활동 현금흐름] 으로 하였습니다.)
이에 대한 전체 코드는 아래와 같습니다.
import OpenDartReader
import pandas as pd
import time
api_key = ' ' # OpenDart API 에서 받는 KEY 입력
# 얻고자 하는 종목명 리스트 형태로 입력
stock_names = ['삼성전자', 'LG전자', '현대자동차']
dart = OpenDartReader(api_key)
# 데이터 초기화. df2 는 정리된 데이터를 모으는 곳이며 얻고자 하는 데이터 이름을
# column 화 시킴. 빈 index 를 넣으면 추가가 안되므로 임으로 하나 넣자.
df2 = pd.DataFrame(columns=['유동자산', '부채총계', '자본총계', '매출액', '매출총이익', '영업이익',
'당기순이익', '영업활동현금흐름', '잉여현금흐름'], index=['1900-01-01'])
# '11013'=1분기보고서, '11012' =반기보고서, '11014'=3분기보고서, '11011'=사업보고서
reprt_code = ['11013', '11012', '11014', '11011']
# 데이터를 얻기위한 반복문 시작
for stocks in stock_names:
# 정리 완료된 파일 저장하기 위한 경로 및 이름. result_종목이름.xlsx 형태로 저장된다.
fileName = f'C:/Projects/stocks_sorting/data/result_{str(stocks)}.xlsx'
for i in range(2015, 2022): # OpenDart는 2015년부터 정보를 제공한다.
# 더미 리스트 초기화. 1 ~ 4 분기 데이터를 합할 예정이므로 4 크기 만큼의 리스트 선언.
current_assets = [0, 0, 0, 0] # 유동자산
liabilities = [0, 0, 0, 0] # 부채총계
equity = [0, 0, 0, 0] # 자본총계
revenue = [0, 0, 0, 0] # 매출액
grossProfit = [0, 0, 0, 0] # 매출총이익
income = [0, 0, 0, 0] # 영업이익
net_income = [0, 0, 0, 0] # 당기순이익
cfo = [0, 0, 0, 0] # 영업활동현금흐름
cfi = [0, 0, 0, 0] # 투자활동현금흐름
fcf = [0, 0, 0, 0] # 잉여현금흐름 : 편의상 영업활동 - 투자활동 현금흐름으로 계산
for j, k in enumerate(reprt_code):
df1 = pd.DataFrame() # Raw Data
if str(type(dart.finstate_all('004840', i, reprt_code=k, fs_div='CFS'))) == "<class 'NoneType'>":
pass
# 타입이 NoneType 이 아니면 읽어온다.
else:
df1 = df1.append(dart.finstate_all(stock_name, i, reprt_code=k, fs_div='CFS'))
# 재무상태표 부분
condition = (df1.sj_nm == '재무상태표') & (df1.account_nm == '유동자산') # 유동자산
condition_2 = (df1.sj_nm == '재무상태표') & (df1.account_nm == '부채총계') # 부채총계
condition_3 = (df1.sj_nm == '재무상태표') & \
((df1.account_nm == '자본총계') | (df1.account_nm == '반기말자본') | (df1.account_nm == '3분기말자본') | (df1.account_nm == '분기말자본') | (df1.account_nm == '1분기말자본')) #자본총계
# 손익계산서 부분
condition_4 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & ((df1.account_nm == '매출액') | (df1.account_nm == '수익(매출액)'))
condition_5 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & (df1.account_nm == '매출총이익')
condition_6 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & \
((df1.account_nm == '영업이익(손실)') | (df1.account_nm == '영업이익'))
condition_7 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & \
((df1.account_nm == '당기순이익(손실)') | (df1.account_nm == '당기순이익') | \
(df1.account_nm == '분기순이익') | (df1.account_nm == '분기순이익(손실)') | (df1.account_nm == '반기순이익') | (df1.account_nm == '반기순이익(손실)') | \
(df1.account_nm == '연결분기순이익') | (df1.account_nm == '연결반기순이익')| (df1.account_nm == '연결당기순이익')|(df1.account_nm == '연결분기(당기)순이익')|(df1.account_nm == '연결반기(당기)순이익')|\
(df1.account_nm == '연결분기순이익(손실)'))
# 현금흐름표 부분
condition_8 = (df1.sj_nm == '현금흐름표') & ((df1.account_nm == '영업활동으로 인한 현금흐름') | (df1.account_nm == '영업활동 현금흐름'))
condition_9 = (df1.sj_nm == '현금흐름표') & ((df1.account_nm == '투자활동으로 인한 현금흐름') | (df1.account_nm == '영업활동 현금흐름'))
current_assets[j] = int(df1.loc[condition].iloc[0]['thstrm_amount'])
liabilities[j] = int(df1.loc[condition_2].iloc[0]['thstrm_amount'])
equity[j] = int(df1.loc[condition_3].iloc[0]['thstrm_amount'])
revenue[j] = int(df1.loc[condition_4].iloc[0]['thstrm_amount'])
grossProfit[j] = int(df1.loc[condition_5].iloc[0]['thstrm_amount'])
income[j] = int(df1.loc[condition_6].iloc[0]['thstrm_amount'])
net_income[j] = int(df1.loc[condition_7].iloc[0]['thstrm_amount'])
cfo[j] = int(df1.loc[condition_8].iloc[0]['thstrm_amount'])
cfi[j] = int(df1.loc[condition_9].iloc[0]['thstrm_amount'])
fcf[j] = (cfo[j] - cfi[j])
if k == '11013': # 1분기.
path_string = str(i) + '-03-31'
elif k == '11012': # 2분기
path_string = str(i) + '-06-30'
elif k == '11014': # 3분기
path_string = str(i) + '-09-30'
else: # 4분기. 1 ~ 3분기 데이터를 더한다음 사업보고서에서 빼야 함
path_string = str(i) + '-12-30'
revenue[j] = revenue[j] - (revenue[0] + revenue[1] + revenue[2])
grossProfit[j] = grossProfit[j] - (grossProfit[0] + grossProfit[1] + grossProfit[2])
income[j] = income[j] - (income[0] + income[1] + income[2])
net_income[j] = net_income[j] - (net_income[0] + net_income[1] + net_income[2])
fcf[j] = fcf[j] - (fcf[0] + fcf[1] + fcf[2])
# 데이터프레임에 저장하는 부분
df2.loc[path_string] = [current_assets[j], liabilities[j], equity[j],
revenue[j], grossProfit[j], income[j], net_income[j], cfo[j], fcf[j]]
df2.tail() # 데이터프레임 끝에 저장한다.
# 너무 잦은 요청이 있을 경우 OpenDart API 측에서 IP 를 차단하니 텀을 둔다.
time.sleep(0.1)
# 원본 dataframe 에도 영향을 끼치게끔 (inplace=True) 첫 행 drop.
df2.drop(['1900-01-01'], inplace=True) # 첫 행 drop
# 파일 저장. fileName 에 명시된 경로에 각 종목코드별로 다른 이름으로 저장
df2.to_excel(fileName)
# 그 다음 종목의 데이터를 얻기 위해 df2 재초기화
df2 = pd.DataFrame(columns=['유동자산', '부채총계', '자본총계', '매출액', '매출총이익', '영업이익',
'당기순이익', '영업활동현금흐름', '잉여현금흐름'], index=['1900-01-01']) # 정리된 Data. 빈 index 를 넣으면 추가가 안되므로 하나 넣자.
이제 설명이 필요한 부분은 2군데 정도 있는거 같네요. 먼저, 조건을 만드는 condition 선언에 대해서 입니다.
# 재무상태표 부분
condition = (df1.sj_nm == '재무상태표') & (df1.account_nm == '유동자산') # 유동자산
condition_2 = (df1.sj_nm == '재무상태표') & (df1.account_nm == '부채총계') # 부채총계
condition_3 = (df1.sj_nm == '재무상태표') & \
((df1.account_nm == '자본총계') | (df1.account_nm == '반기말자본') | (df1.account_nm == '3분기말자본') | (df1.account_nm == '분기말자본') | (df1.account_nm == '1분기말자본')) #자본총계
# 손익계산서 부분
condition_4 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & ((df1.account_nm == '매출액') | (df1.account_nm == '수익(매출액)'))
condition_5 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & (df1.account_nm == '매출총이익')
condition_6 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & \
((df1.account_nm == '영업이익(손실)') | (df1.account_nm == '영업이익'))
condition_7 = ((df1.sj_nm == '손익계산서') | (df1.sj_nm == '포괄손익계산서')) & \
((df1.account_nm == '당기순이익(손실)') | (df1.account_nm == '당기순이익') | \
(df1.account_nm == '분기순이익') | (df1.account_nm == '분기순이익(손실)') | (df1.account_nm == '반기순이익') | (df1.account_nm == '반기순이익(손실)') | \
(df1.account_nm == '연결분기순이익') | (df1.account_nm == '연결반기순이익')| (df1.account_nm == '연결당기순이익')|(df1.account_nm == '연결분기(당기)순이익')|(df1.account_nm == '연결반기(당기)순이익')|\
(df1.account_nm == '연결분기순이익(손실)'))
# 현금흐름표 부분
condition_8 = (df1.sj_nm == '현금흐름표') & ((df1.account_nm == '영업활동으로 인한 현금흐름') | (df1.account_nm == '영업활동 현금흐름'))
condition_9 = (df1.sj_nm == '현금흐름표') & ((df1.account_nm == '투자활동으로 인한 현금흐름') | (df1.account_nm == '영업활동 현금흐름'))
각 종목별로 원본을 불러왔을 때 같은 개념을 두고도 표현되는 단어가 조금씩 다릅니다.
처음엔 account_id 컬럼에서 표준계정코드를 불러오는게 더 나을거 같단 생각도 하였으나, 위와 마찬가지로 account_id 가 다르거나 없는 경우가 있어서 위와 같은 방법을 택했습니다.
그래서 조건식이 꽤 많고 지저분하죠. 혹시 더 좋은 방법이 있다면 댓글 부탁드립니다.
사례를 표로 표시해보았습니다. 실제 더 많은 기업대상으로 하면 조건이 더 추가될 수 있습니다.
정리하고 보니 당기순이익의 표현이 참 다양하네요 ;;
종목 데이터를 활용할 때, 1년단위가 아닌 특정시점에서 4분기 데이터를 활용할 계획이므로
'thstrm_amount' 컬럼의 값을 활용합니다. 여기서 추출된 값은 분기별 데이터를 의미합니다. (사업보고서는 1년단위 데이터)
손익계산서에서 4분기의 값을 얻고 싶다면 '4분기 = 사업보고서 -(1분기 + 2분기 + 3분기)' 를 해야합니다.
if k == '11013': # 1분기.
path_string = str(i) + '-03-31'
elif k == '11012': # 2분기
path_string = str(i) + '-06-30'
elif k == '11014': # 3분기
path_string = str(i) + '-09-30'
else: # 4분기. 1 ~ 3분기 데이터를 더한다음 사업보고서에서 빼야 함
path_string = str(i) + '-12-30'
revenue[j] = revenue[j] - (revenue[0] + revenue[1] + revenue[2])
grossProfit[j] = grossProfit[j] - (grossProfit[0] + grossProfit[1] + grossProfit[2])
income[j] = income[j] - (income[0] + income[1] + income[2])
net_income[j] = net_income[j] - (net_income[0] + net_income[1] + net_income[2])
fcf[j] = fcf[j] - (fcf[0] + fcf[1] + fcf[2])
3. 한계점
위에서도 말씀 드렸지만, OpenDartReader 는 파이썬에서 Dart 의 데이터를 쉽게 얻어올 수 있는 API 이며 약간의 가공을 거치면 원하는 데이터를 추출이 가능하죠. 그래도 한계점은 존재합니다. 구체적으론 OpenDartReader 의 한계가 아니라 OpenDart API 자체의 한계라 볼 수 있겠네요. 2가지 정도 있는거 같습니다.
1. 2015년 데이터부터 제공
opendart 홈페이지의 상장기업 재무제표 개발가이드에보면 2015년부터 정보제공이라고 명시되어 있습니다.
퀀트투자에선 데이터 양이 많아야 그나마 퀀트 전략 백테스팅의 유효성이 좀 더 입증 될 텐데 아쉽네요.
이 API 기반으로 데이터베이스를 구축하면 2015년부터 2020년까지, 총 6년의 데이터를 확보할 수 있겠습니다.
2. 분당 크롤링 횟수 1000회 제한
분당 크롤링 횟수 1000회로 제한되며 그 이상 할 경우 dart 측에서 자체적으로 24시간 IP 차단하는 정책이 있습니다.
그 때문에 코드에 time.sleep() 를 넣어줬습니다.
파이썬의 연산속도가 무한하다 가정했을 때,
크롤링 요청간격을 대략적으로 계산해보면 1000/60 = 16.67 이란 초당 크롤링 횟수가 나오며, 이의 역수는
0.06 입니다. 이 의미는 1회 크롤링 간격을 0.06를 초과하게끔 정하면 dart 정책에 위배되지 않게 크롤링을 할 수 있다는 게 되겠네요.
이번 포스팅은 여기에서 마치겠습니다.
다음 내용은 pykrx 를 사용해서 시가총액 column 추가하기를 할 예정입니다.
'프로젝트 > [ing]_백테스팅_툴' 카테고리의 다른 글
OpenDartReader 로 종목을 분류해보자 (3) (0) | 2021.12.16 |
---|---|
OpenDartReader 로 종목을 분류해보자 (2) (0) | 2021.12.03 |
[python] 인터넷 연결 안되있으면 컴퓨터 다시 시작하는 프로그램 (0) | 2021.10.21 |
[투자지표] 나쁜 투자 종목을 피해보자 (1). - PER (0) | 2021.10.18 |
[python] 백트레이더(Backtrader) 로 데이터 추출 및 전략 세우기 (0) | 2021.08.22 |