-
Notifications
You must be signed in to change notification settings - Fork 51
/
search_index.json
9 lines (9 loc) · 188 KB
/
search_index.json
1
2
3
4
5
6
7
8
9
[
["index.html", "R을 이용한 퀀트 투자 포트폴리오 만들기 Welcome 지은이 소개 머리말 이 책의 구성 이 책에서 다루지 않은 주제 도움이 될 만한 자료들 이 책의 지원 페이지 종목과 관련된 유의사항 세션정보", " R을 이용한 퀀트 투자 포트폴리오 만들기 이현열 2021-01-24 Welcome 현재 개정판이 작업중에 있습니다. 2021년 2월 즈음 만나보실 수 있습니다. R을 이용한 퀀트 투자 포트폴리오 만들기 구매 링크 본 페이지는 R을 이용한 퀀트 투자 포트폴리오 만들기의 웹사이트 입니다. 책의 수정 사항이 있을시 즉시 반영할 예정이며, 책에서 다루지 못했던 추가적인 내용도 지속적으로 업데이트 할 예정입니다. 패스트캠퍼스에서 본 책의 내용을 바탕으로 강의가 진행중이니, 수강을 원하시는 분은 참조하시기 바랍니다. 강의 링크 책 발간 이후 업데이트 내용은 다음과 같습니다. 2021년 1월 24일: 네이버증권의 차트가 플래쉬를 사용하지 않음에 따라, 해당 부분을 새롭게 작성했습니다. 2021년 1월 17일: 한국거래소가 사이트를 개편함에 따라 5장 [한국거래소의 산업별 현황 및 개별지표 크롤링] 부분을 새롭게 작성했습니다. 2020년 1월 17일: 9장 [퀀트 전략을 이용한 종목선정 (기본)]과 10장 [퀀트 전략을 이용한 종목선정 (심화)]에서 재무제표를 이용한 전략의 경우, 1~4월에는 최근년도 데이터가 일부 종목에 대해서만 들어옵니다. 따라서 해당 기간에는 전전년도 데이터를 사용해야 하며, 이를 고려하도록 코드를 변경하였습니다. 2020년 9월 26일: 5장 [한국거래소의 산업별 현황 및 개별지표 크롤링] 부분이 R 4.0 이상 버젼에서 잘 작동하지 않는 문제가 발생하고 있습니다. R 3.6 버젼에서 실행해주시길 바랍니다. (https://rstudio.cloud/ 에서는 원하는 R 버젼으로 프로그램 실행이 가능합니다.) 해당 버젼에서도 안될 경우 session을 재시작하면 제대로 실행이 됩니다. 2020년 9월 26일: 4장 [기업공시채널에서 오늘의 공시 불러오기]에서 POST 부분의 url이 기존 http://kind.krx.co.kr/disclosure/todaydisclosure.do 에서 https://dev-kind.krx.co.kr/disclosure/todaydisclosure.do 로 변경되었습니다. 2020년 4월 27일: 9장 [금융 데이터 수집하기 (심화)]에 [DART의 Open API를 이용한 데이터 수집하기] 챕터를 추가하였습니다. 이를 통해 더욱 다양한 데이터를 수집할 수 있습니다. 2020년 4월 7일: 각 페이지 하단에 질문/답변 기능을 추가하였습니다. 이제 블로그나 이메일, SNS 보다는 웹북에 질문을 남겨주시기 바랍니다. 2020년 3월 22일: 11장 [포트폴리오 구성]에 실무에서 많이 사용되는 인덱스 포트폴리오 및 인핸스드 인덱스 포트폴리오 구성 방법을 추가하였습니다. 2020년 3월 22일: 8장 [데이터 분석 및 시각화하기]에서 종목정보 시각화 이전에 ggplot() 기초 챕터를 추가하였습니다. 이로써 기존에 해당 패키지를 모르던 분도 쉽게 배울수 있도록 하였습니다. 2020년 3월 15일: 6장 [금융 데이터 수집하기 (심화)]에 [재무제표 및 가치지표 크롤링]에서 사용하는 페이지가 크롤러의 접근을 막음에 따라, user_agent() 를 이용하여 웹브라우저 인자를 추가해 주었습니다. 2020년 1월 19일: 5장의 [거래소 데이터 정리하기] 부분에서 substr() 함수 대신 stringr 패키지의 str_sub() 함수를 사용하여 코드를 훨씬 간결하게 표현했습니다. 또한 종목코드 끝이 0이 아닐 경우 우선주인 점을 이용하여 더욱 쉽게 클렌징 처리를 하였습니다. 2020년 1월 18일: 야후 파이낸스 웹페이지의 구조가 바뀌어 동적 크롤링을 통해서만 데이터 수집이 가능하게 되었습니다. 이는 본 책에서는 다루지 않으므로, 6장 [금융 데이터 수집하기 (심화)]에서 해당 부분을 삭제하였습니다. 지은이 소개 이현열 한양대학교에서 경영학을 전공하고, 카이스트 대학원에서 금융공학 석사 학위를 받았다. 졸업 후 증권사에서 주식운용, 자산운용사에서 퀀트 포트폴리오 매니저, 보험사에서 데이터 분석 업무를 거쳐, 현재 핀테크 스타트업에서 퀀트 및 자산배분 리서치 업무를 하고 있다. 평소 꾸준한 SNS와 블로그 활동으로 퀀트 아이디어 및 백테스트 결과 등을 공유하면서 퀀트 투자의 대중화를 위해 노력하고 있다. 한양대학교 재무금융 박사 과정을 수료했으며, 패스트캠퍼스에서 R과 퀀트 투자 강의를 맡고 있다. 지은 책으로는 《스마트베타》(2017)가 있다. 머리말 퀀트 투자 중 팩터에 관한 이론적 내용을 다룬 《SMART BETA(스마트 베타): 감정을 이기는 퀀트 투자》(김병규, 이현열, 워터베어프레스, 2017) 출간 이후 강의와 세미나를 통해많은 분들을 만났고, 공통적인 어려움을 느낄 수 있었습니다. 기관 투자자들이 손쉽게 데이터를 구할 수 있는 것과는 다르게, 일반 투자자들은 퀀트 투자를 하기 위한 데이터를 구하는 시작점부터 어려움을 겪는다는 것이었습니다. 그러나 프로그래밍을 이용하면 일반 투자자들도 얼마든지 금융 데이터 수집 및 처리, 퀀트 모델 개발, 포트폴리오 분석 및 자동화 등이 가능합니다. 이 책을 읽는 독자분들이 스스로 이러한 퀀트 투자 프로세스를 만들 수 있기를 바라는 마음으로 책을 구성했습니다. 또한 실제 전문 투자자들이 사용하는 기술들도 포함했으니 책의 내용을 넘어 더욱 훌륭한 모델을 만드는 데 도움이 되시리라 생각합니다. 이 책에서 데이터 수집을 위해 주로 다루는 크롤링은 웹페이지의 데이터를 가져오는 것입니다. 기존에 책 발간 이후 참고자료로 사용된 페이지 중 형태가 바뀐곳이 많아 개정판을 작성하게 되었습니다. 수정된 내역은 다음과 같습니다. 야후 파이낸스의 웹페이지 구조가 바뀌어 크롤링이 사실상 어렵게 되었고, 책 내용에서 제외하였습니다. 한국거래소 사이트가 개편되어 해당 부분은 바뀐 페이지에 맞게 새로 작성하였습니다. 네이버 증권의 주가 데이터 출처가 변경되어 새로 작성하였습니다. 일부 페이지가 크롤러의 접근을 막음에 따라 user_agent() 함수를 사용해 크롤링이 가능해지게 하였습니다. 많은 문의가 있었던 DART 크롤링에 대한 내용을 추가했습니다. 포트폴리오 구성 부분에 실무에서 많이 사용되는 인덱스 포트폴리오 및 인핸스드 인덱스 포트폴리오 구성 방법을 추가하였습니다. ggplot2 패키지의 기본적인 사용법을 추가하였습니다. 일부 코드를 수정하여 더욱 데이터 처리를 쉽게 하거나, 종목 선택을 강건하게 하였습니다. 앞으로도 페이지 변경 등 코드를 수정해야 하거나 추가된 내용이 있을 경우 책의 공식 페이지인 https://hyunyulhenry.github.io/quant_cookbook/ 에 즉각적으로 업데이트 할 예정이며, 질문사항이 있을 경우 페이지에 남겨주시면 답변드리고 있습니다. 어느때보다 주식과 투자에 대한 관심이 뜨거워진 지금, 유행이라는 파도에 휩쓸리는 투자보다는 데이터를 이용한 객관적이고 장기적인 투자로 성공하시길 기원합니다. 2021년 1월 이현열([email protected]) 이 책의 구성 이 책은 API와 크롤링을 통한 금융 데이터 수집, 투자 종목 선택 및 포트폴리오 구성, 백테스트와 성과 분석으로 이루어져 있습니다. CHAPTER 1 퀀트 투자의 심장: 데이터와 프로그래밍 퀀트 투자란 무엇인지, 왜 프로그래밍이 필요한지, 여러 언어 중 R을 사용해야 하는 이유에 대해 살펴봅니다. CHAPTER 2 크롤링을 위한 기본 지식 크롤링을 통한 데이터 수집에 앞서 인코딩, 웹의 동작 방식, HTML에 대한 기본 정보와 데이터 처리에 편리한 R 코드를 살펴봅니다. CHAPTER 3 API를 이용한 데이터 수집 API를 통한 데이터 수집과 getSymbols() 함수의 사용 방법에 대해 살펴봅니다. CHAPTER 4 크롤링 이해하기 크롤링이 무엇인가에 대해 살펴보며, GET과 POST 방식을 이용한 간단한 예제를 살펴봅니다. CHAPTER 5 금융 데이터 수집하기 기본 한국거래소에서 제공하는 데이터를 크롤링하는 방법, 섹터의 구성종목을 수집하는 방법에 대해 살펴봅니다. CHAPTER 6 금융 데이터 수집하기 심화 퀀트 투자의 핵심 자료인 수정주가, 재무제표 및 가치지표를 크롤링하는 방법을 살펴봅니다. CHAPTER 7 데이터 정리하기 앞에서 수집한 주가, 재무제표, 가치지표를 하나의 파일로 정리하는 방법을 살펴봅니다. CHAPTER 8 데이터 분석 및 시각화하기 수집한 데이터를 바탕으로 dplyr 패키지를 이용한 데이터 분석 및 ggplot2 패키지를 이용한 데이터 시각화, 인터랙티브 그래프를 나타내는 방법을 살펴봅니다. CHAPTER 9 퀀트 전략을 이용한 종목 선정 기본 베타에 대한 이해 및 기본적 팩터인 저변동성, 모멘텀, 밸류, 퀄리티를 이용한 종목 선정에 대해 살펴봅니다. CHAPTER 10 퀀트 전략을 이용한 종목 선정 심화 단순 종목 선정을 넘어 실무에서 사용되는 섹터 중립 포트폴리오 및 이상치 제거와 팩터 결합 방법, 마법공식 및 멀티팩터에 대해 살펴봅니다. CHAPTER 11 포트폴리오 구성 최적화 패키지를 이용한 포트폴리오 구성에서 가장 대중적으로 사용되는 최소분산 포트폴리오, 최대분산효과 포트폴리오, 위험균형 포트폴리오를 구현합니다. 또한 실무에서 많이 사용되는 인덱스 포트폴리오 및 인핸스드 인덱스 포트폴리오 구성 방법을 살펴봅니다. CHAPTER 12 포트폴리오 백테스트 Return.portfolio() 함수를 이용한 백테스트 방법에 대해 살펴보겠습니다. CHAPTER 13 성과 및 위험 평가 포트폴리오의 수익률을 바탕으로 성과 및 위험 평가에 사용되는 각종 지표에 대해 알아보며, 4팩터 회귀분석을 통한 요인 분석을 실행합니다. 이 책에서 다루지 않은 주제 이 책은 R을 기본적으로 사용할 줄 아는 독자를 대상으로 작성되었습니다. 따라서 내용의 효율적 전달을 위해 R과 R Studio 설치, 기초적인 프로그래밍 등의 내용은 생략했습니다. 따라서 프로그래밍을 처음 접하는 독자라면 프로그래밍 기초를 먼저 익히신후 본 책을 읽으시길 추천드립니다. 또한 이 책에서는 프로그램 언어로 R을 이용했기 때문에 Python 혹은 다른 언어를 사용하는 분들에게는 직접적으로 도움이 되지 않을 수 있다고 생각할 수 있습니다. 그러나 투자에 필요한 금융 데이터 수집을 어디서 어떻게 하는지, 종목 선택을 어떻게 하고 포트폴리오를 어떻게 구성하는지에 대한 이론적 내용을 이해한 후 본인들이 사용하는 언어로 구현해보는 것도 좋은 도전이 될 것입니다. 도움이 될 만한 자료들 먼저 팩터 투자와 관련하여 심화된 내용을 알고 싶은 분은 저의 이전 책 및 책에서 인용된 논문을 읽어볼 것을 권합니다. R 프로그래밍과 관련하여 기초부터 tidyverse 패키지까지 이해하는 데 도움이 될만한 책 목록은 다음과 같습니다. 《SMART BETA(스마트 베타): 감정을 이기는 퀀트 투자》(김병규, 이현열, 워터베어프레스, 2017) 《손에 잡히는 R 프로그래밍》(가렛 그롤먼드, 한빛미디어, 2015) 《R Cookbook》(폴 티터, 인사이트, 2012) 《R을 활용한 데이터 과학》(해들리 위컴, 개럿 그롤문드, 인사이트, 2019) 《Do it! 쉽게 배우는 R 데이터 분석》(김영우, 이지스퍼블리싱, 2017) 《ggplot2: R로 분석한 데이터를 멋진 그래픽으로》(해들리 위컴, 프리렉, 2017) 이 책의 지원 페이지 이 책은 R의 bookdown 패키지로 작성되어 웹페이지 및 GitHub 저장소에 공유되어 있습니다. 따라서 책에 포함되어 있는 각종 코드를 웹페이지에 방문하여 얻으실 수 있습니다. 웹페이지: https://hyunyulhenry.github.io/quant_cookbook GitHub 저장소: https://github.com/hyunyulhenry/quant_cookbook 크롤링 대상 웹페이지의 구조가 바뀌어 코드의 수정이 필요할 경우 즉각적으로 반영할 것이며, 인쇄본에서 다루지 않은 내용도 추가적으로 업데이트될 예정입니다. 또한 bookdown 패키지를 이용하여 책을 집필하고자 하는 분들에게도 많은 도움이 될 것입 니다. 이 외에도 퀀트 투자 혹은 R을 이용한 투자 활용법 등의 내용은 저자의 블로그에 많은글들이 있으니 참조하기 바랍니다. Henry’s Quantopia: http://henryquant.blogspot.com 종목과 관련된 유의사항 팩터 모델을 이용한 종목 선택과 관련된 CHAPTER에서는 해당 조건으로 선택된 종목들이 나열되어 있습니다. 그러나 이는 해당 종목에 대한 매수 추천이 아님을 밝히며, 데이터를 받은 시점의 종목이기에 독자 여러분이 책을 읽는 시점에서 선택된 종목과는 상당한 차이가 있습니다. 또한 이 책에서 다루는 모델을 이용하여 투자를 할 경우, 이로 인한 이익과 손해는 본인에게 귀속됨을 알립니다. 세션정보 본 책에서 사용된 R 버젼 및 각종 정보는 다음과 같습니다. ## R version 3.6.3 (2020-02-29) ## Platform: x86_64-pc-linux-gnu (64-bit) ## Running under: Ubuntu 16.04.7 LTS ## ## Matrix products: default ## BLAS: /usr/lib/atlas-base/atlas/libblas.so.3.0 ## LAPACK: /usr/lib/atlas-base/atlas/liblapack.so.3.0 ## ## locale: ## [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C ## [3] LC_TIME=C.UTF-8 LC_COLLATE=C.UTF-8 ## [5] LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8 ## [7] LC_PAPER=C.UTF-8 LC_NAME=C ## [9] LC_ADDRESS=C LC_TELEPHONE=C ## [11] LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C ## ## attached base packages: ## [1] stats graphics grDevices utils datasets ## [6] methods base ## ## other attached packages: ## [1] showtext_0.9 showtextdb_3.0 sysfonts_0.8.1 ## ## loaded via a namespace (and not attached): ## [1] compiler_3.6.3 magrittr_1.5 bookdown_0.20 ## [4] htmltools_0.5.0 tools_3.6.3 yaml_2.2.1 ## [7] stringi_1.5.3 rmarkdown_2.3 knitr_1.30 ## [10] stringr_1.4.0 digest_0.6.25 xfun_0.17 ## [13] rlang_0.4.7 evaluate_0.14 "],
["퀀트-투자의-심장-데이터와-프로그래밍.html", "Chapter 1 퀀트 투자의 심장: 데이터와 프로그래밍 1.1 데이터 구하기 1.2 퀀트 투자와 프로그래밍 1.3 R 프로그램 1.4 퀀트 투자에 유용한 R 패키지", " Chapter 1 퀀트 투자의 심장: 데이터와 프로그래밍 몇 년 전까지만 하더라도 퀀트 투자는 일반 투자자들에게 매우 낯선 영역이었지만, 최근에는 각종 커뮤니티와 매체를 통해 많은 사람들에게 익숙한 단어가 되었습니다. 퀀트 투자에서 ‘퀀트’란 모형을 기반으로 금융상품의 가격을 산정하거나, 이를 바탕으로투자를 하는 사람을 말합니다. 퀀트(Quant)라는 단어가 ‘계량적’을 의미하는 퀀티터티브(Quantitative)의 앞 글자를 따왔음을 생각하면 쉽게 이해가 될 것입니다. 일반적으로 투자자들이 산업과 기업을 분석해 가치를 매기는 정성적인 투자법과는 달리, 퀀트 투자는 수학과 통계를 기반으로 전략을 만들고 이를 바탕으로 투자하는 정량적인 투자법을 의미합니다. 이처럼 데이터를 수집·가공한 후 이를 바탕으로 모델을 만들고 실행하는 단계는 데이터 과학의 업무 흐름도와 매우 유사합니다. 해들리 위컴 (Hadley Wickham)(Grolemund and Wickham 2018)에 따르면 데이터 과학의 업무 과정은 그림 1.1과 같습니다. 그림 1.1: 데이터 과학 업무 과정 데이터 과학자들은 프로그래밍을 통해 데이터를 불러온 후 이를 정리하고, 원하는 결과를 찾기 위해 데이터를 변형하거나 시각화하고 모델링합니다. 이러한 결과를 바탕으로 타인과 소통하는 일련의 과정을 거칩니다. 퀀트 투자의 단계 역시 이와 매우 유사합니다. 투자에 필요한 주가, 재무제표 등의 데이터를 수집해 정리한 후 필요한 지표를 얻기 위해 가공합니다. 그 후 각종 모형을 이용해 투자 종목을 선택하거나 백테스트를 수행하며, 이를 바탕으로 실제로 투자하고 성과를 평가합니다. 따라서 퀀트 투자는 데이터 과학이 금융에 응용된 사례라고도 볼수 있으며, 퀀트 투자의 중심에는 데이터와 프로그래밍이 있습니다. 이 책에서도 데이터 과학의 업무 단계와 동일하게 데이터 불러오기, 데이터별로 정리하고 가공하기, 시각화를 통해 데이터의 특징 파악하기, 퀀트 모델을 이용해 종목 선택하기, 백테스트를 실시한 후 성과 및 위험 평가하기에 대해 알아보겠습니다. 이에 앞서 이 CHAPTER에서는 퀀트 투자의 심장이라고 할 수 있는 데이터를 어떻게 얻을 수 있 는지, 왜 프로그래밍을 해야 하는지, 그중에서도 R이 무엇인지에 대해 간략히 살펴보겠습니다. 1.1 데이터 구하기 퀀트 투자에 필요한 데이터는 여러 데이터 제공업체의 서비스를 이용해서 매우 쉽게 구할 수 있습니다. 해외 데이터 수집에는 블룸버그 혹은 Factset, 국내 데이터 수집에는 DataGuide가 흔히 사용됩니다. 물론 비용을 더 지불한다면 단순 데이터 수집뿐만 아니라 즉석에서 백테스트 및 성과 평가까지 가능합니다. Factset에서 판매하는 Alpha Testing 혹은 S&P Global에서 판매하는 ClariFI(그림 1.2)를 사용한다면, 전 세계 주식을 대상으로 원하는 전략의 백테스트 결과를 마우스 몇 번 클릭해서 얻을 수 있습니다. 그림 1.2: ClariFI®의 백테스트 기능 데이터 제공업체를 이용하는 방법의 최대 단점은 바로 비용입니다. 블룸버그 단말기는 1년 사용료가 대리 한 명의 연봉과 비슷해, 흔히 ‘블대리’라고 부르기도 합니다. 국내 데이터 업체의 사용료는 이보다 저렴하기는 하지만, 역시 1년 사용료가 수백만 원 정도로, 일반 개인 투자자가 감당하기에는 부담이 됩니다. 해외데이터는 Quandl1이나 tiingo2등의 업체가 제공하는 서비스를 이용하면 상대적으로 저렴한 가격에 데이터를 구할 수 있습니다. 물론 대형 데이터 제공업체에 비해 데 이터의 종류가 적고 기간은 짧은 편이지만, 대부분의 일반 투자자가 사용하기에는 충분한 데이터를 얻을 수 있습니다. tiingo에서는 전 세계 64,386개 주식의 30년 이상 가격 정보, 21,352개 주식의 12년 이상 재무정보를 월 $10에 받을 수 있으며, 한정된 종목과 용량에 대해서는 무료로 데이터를 받을 수도 있습니다. 더군다나 API를 통해 프로그램 내에서 직접 데이터를 받을 수 있어 편리합니다. 그러나 아쉽게도 이러한 데이터에서 한국 시장의 정보는 소외되어 있습니다. 따라서 돈을 들이지 않고 국내 데이터를 얻기 위해서는 직접 발품을 파는 수밖에 없습니다.야후 파이낸스3 혹은 국내 금융 웹사이트에서 제공하는 정보를 크롤링해 데이터를 수집할 수 있습니다. 그림 1.3: NAVER 금융 제공 재무정보 이러한 정보를 잘만 활용한다면 장기간의 주가 및 재무정보를 무료로 수집할 수 있습니다. 물론 데이터 제공업체가 제공하는 깔끔한 형태의 데이터가 아니므로 클렌징 작업이 필요하고 상장폐지된 기업의 데이터를 구하기 힘들다는 단점이 있습니다. 그러나 비용이 들지 않는 데다 현재 시점에서 투자 종목을 선택할 때는 상장폐지된 기업의 정 보가 필요하지 않는다는 점을 고려하면 이는 큰 문제가 되지 않습니다. 1.2 퀀트 투자와 프로그래밍 우리가 구한 데이터는 연구나 투자에 바로 사용할 수 있는 형태로 주어지는 경우가 거의 없습니다. 따라서 데이터를 목적에 맞게 처리하는 과정을 거쳐야 하며, 이를 흔히 데이터 클렌징 작업이라고 합니다. 또한 정제된 데이터를 활용한 투자 전략의 백테스트나 종목 선정을 위해서 프로그래밍은 필수입니다. 물론 모든 퀀트 투자에서 프로그래 밍이 필수인 것은 아닙니다. 엑셀을 이용해도 간단한 형태의 백테스트 및 종목 선정은 얼마든지 가능합니다. 그러나 응용성 및 효율성의 측면에서 엑셀은 매우 비효율적입니다. 데이터를 수집하고 클렌징 작업을 할 때 대상이 몇 종목 되지 않는다면 엑셀을 이용해도 충분히 가능합니다. 그러나 종목 수가 수천 종목을 넘어간다면 데이터를 손으로 일일이 처리하기가 사실상 불가능에 가깝습니다. 이러한 단순 반복 작업은 프로그래밍을 이용한다면 훨씬 효율적으로 수행할 수 있습니다. 백테스트에서도 프로그래밍이 훨씬 효율적입니다. 과거 12개월 누적수익률이 높은 종목에 투자하는 모멘텀 전략의 백테스트를 한다고 가정합시다. 처음에는 엑셀로 백테스트를 하는 것이 편하다고 생각할 수 있습니다. 그러나 만일 12개월이 아닌 6개월 누적 수익률로 백테스트를 하고자 한다면 어떨까요? 엑셀에서 다시 6개월 누적수익률을 구하기 위해 명령어를 바꾸고 드래그하는 작업을 반복해야 할 것입니다. 그러나 프로그래밍을 이용한다면 n = 12였던 부분을 n = 6으로 변경한 후 단지 클릭 한 번만으로 새로운 백테스트가 완료됩니다. 전체 데이터가 100MB 정도라고 가정할 때, 투자 전략이 계속해서 늘어날 경우는 어떨까요? 엑셀에서 A라는 전략을 백테스트하기 위해서는 해당 데이터로 작업한 후 저장할 것입니다. 그 후 B라는 전략을 새롭게 백테스트하려면 해당 데이터를 새로운 엑셀 파일에 복사해 작업한 후 다시 저장해야 합니다. 결과적으로 10개의 전략만 백테스트 하더라도 100MB짜리 엑셀 파일이 10개, 즉 1GB 정도의 엑셀 파일이 쌓이게 됩니다. 만일 데이터가 바뀔 경우 다시 10개 엑셀 시트의 데이터를 일일이 바꿔야 하는 귀찮음도 감수해야 합니다. 물론 하나의 엑셀 파일 내에서 모든 전략을 수행할 수도 있지만, 이러한 경우 속도가 상당히 저하되는 문제가 있습니다. 프로그래밍을 이용한다면 어떨까요? 백테스트를 수행하는 프로그래밍 스크립트는 불과 몇 KB에 불과하므로, 10개의 전략에 대한 스크립트 파일을 합해도 1MB가 되지 않습니다. 데이터가 바뀌더라도 원본 데이터 파일 하나만 수정해주면 됩니다. 물론 대부분의 사람들에게 프로그래밍은 낯선 도구입니다. 그러나 퀀트 투자에 필요한 프로그래밍은 매우 한정적이고 몇 가지 기능을 반복적으로 쓰기 때문에 몇 개의 단어와 구문만 익숙해지면 사용하는 데 큰 어려움이 없습니다. 또한 전문 개발자들의 프로그래밍에 비하면 상당히 쉬운 수준이므로, 비교적 빠른 시간 내에 원하는 전략을 테스트하고 수행하는 정도의 능력을 갖출 수도 있습니다. 1.3 R 프로그램 인간이 사용하는 언어의 종류가 다양하듯이, 프로그래밍 언어의 종류 역시 다양합니다. 대략 700여 개 이상의 프로그래밍 언어 중4 대중적으로 사용하는 언어는 그리 많지 않으므로, 대중성과 효율성을 위해 사용량이 많은 언어를 이용하는 것이 좋습니다. 그림 1.5는 프로그래밍 언어의 사용 순위5입니다. 이 중 R과 Python은 매우 대중적인 언어입니다. 해당 언어가 많이 사용되는 가장 큰 이유는 무료인 데다 일반인들이 사용하기에도 매우 편한 형태로 구성되어 있기 때문입니다. 그림 1.4: 2017년 기준 프로그래밍 언어 사용 통계 순위 이러한 프로그래밍 언어 중 이 책에서는 R을 이용합니다. R의 장점은 무료라는 점 이외에도 타 언어와 비교할 수 없이 다양한 패키지가 있다는 점입니다. R은 두터운 사용자층을 기반으로 두고 있어 상상할 수 없을 정도로 패키지가 많으며, 특히 통계나 계량분석과 관련된 패키지는 독보적이라고 할 수 있습니다. 그림 1.5: CRAN 등록 패키지 수 1.4 퀀트 투자에 유용한 R 패키지 R에는 여러 연구자와 실무자의 헌신적인 노력 덕분에 금융 연구와 퀀트 투자를 위한 다양한 패키지가 만들어져 있으며, 누구나 무료로 이용할 수 있습니다. 이 책에서 사용되는 패키지 중 중요한 것은 다음과 같습니다. 각 패키지에 대한 자세한 설명은 구글에서 패키지명을 검색한 후 PDF 파일을 통해 확인할 수 있습니다. quantmod: 이름에서 알 수 있듯이 퀀트 투자에 매우 유용한 패키지입니다. API를 이용해 데이터를 다운로드하는 getSymbols() 함수는 대단히 많이 사용됩니다. 이 외에도 볼린저밴드, 이동평균선, 상대강도지수(RSI) 등 여러 기술적 지표를 주가 차트에 나타낼 수도 있습니다. PerformanceAnalytics: 포트폴리오의 성과와 위험을 측정하는 데 매우 유용한 패키지입니다. Return.portfolio() 함수는 포트폴리오 백테스트에 필수적인 함수입니다. xts: 기본적으로 금융 데이터는 시계열 형태이며, xts 패키지는 여러 데이터를 시계열 형태(eXtensible TimeSeries)로 변형해줍니다. 일별 수익률을 월별 수익률 혹은 연도별 수익률로 변환하는 apply.monthly()와 apply.yearly() 함수, 데이터들의 특정 시점을 찾아주는 endpoints() 함수 역시 백테스트에 필수적으로 사용되는 함수입니다. 이 패키지는 PerformanceAnalytics 패키지 설치 시 자동으로 설치됩니다. zoo: zoo 패키지 역시 시계열 데이터를 다루는 데 유용한 함수가 있습니다. rollapply() 함수는 apply() 함수를 전체 데이터가 아닌 롤링 윈도우 기법으로 활용할 수 있게 해주며, NA 데이터를 채워주는 na.locf() 함수는 시계열 데이터의 결측치를 보정할 때 매우 유용합니다. httr & rvest: 데이터를 웹에서 수집하기 위해서는 크롤링이 필수이며, httr과 rvest는 크롤링에 사용되는 패키지입니다. httr은 http의 표준 요청을 수행하는 패키지로서 단순히 데이터를 받는 GET() 함수와 사용자가 필요한 값을 선택해 요청하는 POST() 함수가 대표적으로 사용됩니다. rvest는 HTML 문서의 데이터를 가져오는 패키지이며, 웹페이지에서 데이터를 크롤링한 후 원하는 데이터만 뽑는데 필요한 여러 함수가 포함되어 있습니다. dplyr: 데이터 처리에 특화되어 R을 이용한 데이터 과학 분야에서 많이 사용되는 패키지입니다. C++로 작성되어 매우 빠른 처리 속도를 보이며, API나 크롤링을 통해 수집한 데이터들을 정리할 때도 매우 유용합니다. ggplot2: 데이터를 시각화할 때 가장 많이 사용되는 패키지입니다. 물론 R에서 기본적으로 내장된 plot() 함수를 이용해도 시각화가 가능하지만, 해당 패키지를 이용하면 훨씬 다양하고 깔끔하게 데이터를 그림으로 표현할 수 있습니다. 이 외에도 이 책에서는 다양한 패키지를 사용했으며, 아래의 코드를 실행하면 설치되지 않은 패키지를 설치할 수 있습니다. pkg = c('magrittr', 'quantmod', 'rvest', 'httr', 'jsonlite', 'readr', 'readxl', 'stringr', 'lubridate', 'dplyr', 'tidyr', 'ggplot2', 'corrplot', 'dygraphs', 'highcharter', 'plotly', 'PerformanceAnalytics', 'nloptr', 'quadprog', 'RiskPortfolios', 'cccp', 'timetk', 'broom', 'stargazer', 'timeSeries') new.pkg = pkg[!(pkg %in% installed.packages()[, "Package"])] if (length(new.pkg)) { install.packages(new.pkg, dependencies = TRUE)} References "],
["크롤링을-위한-기본-지식.html", "Chapter 2 크롤링을 위한 기본 지식 2.1 인코딩의 이해와 R에서 UTF-8 설정하기 2.2 웹의 동작 방식 2.3 HTML과 CSS This is page heading Page heading: size 1 Page heading: size 2 Unordered List Ordered List Major Stock Indices and US ETF a tag & href attribute img tag & src attribute S&P 500 Dow Jones Industrial Average NASDAQ Composite My Header 2.4 파이프 오퍼레이터(%>%) 2.5 오류에 대한 예외처리", " Chapter 2 크롤링을 위한 기본 지식 프로그래밍에 익숙한 분들도 크롤링은 생소한 경우가 많습니다. 기본적인 프로그래밍에 관한 책과 강의가 굉장히 많지만 크롤링을 다루는 자료는 접하기 힘들기 때문입니다. 크롤링은 기계적인 단계가 많기 때문에 조금만 연습해도 활용할 수 있는 기술입니다. 그러나 복잡한 웹페이지나 데이터 내용을 수집하려면 인코딩, 통신 구조에 대한 지 식이 필요할 때가 있습니다. 이 CHAPTER에서는 크롤링을 하기 위해 사전에 알고 있으면 도움이 되는 인코딩, 웹의 동작 방식, HTML과 CSS에 대해 알아보겠습니다. 그리고 실제 크롤링 시 유용한 파이프 오퍼레이터와 오류에 대한 예외처리도 알아보겠습니다. 2.1 인코딩의 이해와 R에서 UTF-8 설정하기 2.1.1 인간과 컴퓨터 간 번역의 시작, ASCII R에서 스크립트를 한글로 작성해 저장한 후 이를 다시 불러올 때, 혹은 한글로 된 데이터를 크롤링하면 오류가 뜨거나 읽을 수 없는 문자로 나타나는 경우가 종종 있습니다. 이는 한글 인코딩 때문에 발생하는 문제이며, 이러한 현상을 흔히 ‘인코딩이 깨졌다’라고 표현합니다. 인코딩이란 사람이 사용하는 언어를 컴퓨터가 사용하는 0과 1로 변환하는 과정을 말하며, 이와 반대의 과정을 디코딩이라고 합니다. 이렇듯 사람과 컴퓨터 간의 언어를 번역하기 위해 최초로 사용된 방식이 아스키(ASCII: American Standard Code for Information Interchange)입니다. 0부터 127까지 총 128개 바이트에 알파벳과 숫자, 자주 사용되는 특수문자 값을 부여하고, 문자가 입력되면 이에 대응되는 바이트가 저장됩니다. 그러나 아스키의 ‘American’이라는 이름에서 알 수 있듯이 이는 영어의 알파벳이 아닌 다른 문자를 표현하는 데 한계가 있으며, 이를 보완하기 위한 여러 방법이 나오게 되었습니다. 그림 1.3: 아스키 코드 표 2.1.2 한글 인코딩 방식의 종류 인코딩에 대한 전문적인 내용은 이 책의 범위를 넘어가며, 크롤링을 위해서는 한글을 인코딩하는 데 쓰이는 EUC-KR과 CP949, UTF-8 정도만 이해해도 충분합니다. 만일 ‘알’이라는 단어를 인코딩한다면 어떤 방법이 있을까요? 먼저 ‘알’이라는 문자 자체에 해당하는 코드를 부여해 나타내는 방법이 있습니다. 아니면 이를 구성하는 모음과 자음을 나누어 ㅇ, ㅏ, ㄹ 각각에 해당하는 코드를 부여하고 이를 조합할 수도 있습니다. 전자와 같이 완성된 문자 자체로 나타내는 방법을 완성형, 후자와 같이 각 자모로 나타내는 방법을 조합형이라고 합니다. 한글 인코딩 중 완성형으로 가장 대표적인 방법은 EUC-KR입니다. EUC-KR은 현대 한글에서 많이 쓰이는 문자 2,350개에 번호를 붙인 방법입니다. 그러나 2,350개 문자로 모든 한글 자모의 조합을 표현하기 부족해, 이를 보완하고자 마이크로소프트가 도입한 방법이 CP949입니다. CP949는 11,720개 한글 문자에 번호를 붙인 방법으로 기존 EUC-KR보다 나타낼 수 있는 한글의 개수가 훨씬 많아졌습니다. 윈도우의 경우 기본 인코딩이 CP949로 되어 있습니다. 조합형의 대표적 방법인 UTF-8은 모음과 자음 각각에 코드를 부여한 후 조합해 한글을 나타냅니다. 조합형은 한글뿐만 아니라 다양한 언어에 적용할 수 있다는 장점이 있어 전 세계 웹페이지의 대부분이 UTF-8로 만들어지고 있습니다. 그림 2.1: 웹페이지에서 사용되는 인코딩 비율 2.1.3 R에서 UTF-8 설정하기 윈도우에서는 기본 인코딩이 CP949로 이루어져 있으며, 일부 국내 웹사이트는 EUC-KR로 인코딩이 된 경우도 있습니다. 반면 R의 여러 함수는 인코딩이 UTF-8로 이루어져 있어, 인코딩 방식의 차이로 인해 스크립트 작성 및 크롤링 과정에서 오류가 발생하는 경우가 종종 있습니다. 만일 CP949 인코딩을 그대로 사용하면 미리 저장되었던 한글 스크립트가 깨져 나오는 일이 발생할 수 있습니다. 이를 방지하기 위해 그림 2.2과 같이 기본 인코딩을 UTF-8로 변경해주는 것이 좋습니다. R Studio의 [Tools → Global Options] 메뉴에서 [Code → Saving] 항목 중 [Default text encodings] 항목을 통해 기본 인코딩을 UTF-8로 변경합니다. 그림 2.2: 인코딩 변경 해당 방법으로도 해결되지 않는다면 그림 2.3와 같이 [File → Reopen with Encoding] 메뉴에서 [UTF-8] 항목을 선택하고 [Set as default encoding for source files] 항목을 선택한 후 [OK]를 클릭합니다. UTF-8로 인코딩이 설정된 후 파일을 다시 엽니다. 그림 2.3: 인코딩 변경 후 재시작 2.2 웹의 동작 방식 크롤링은 웹사이트의 정보를 수집하는 과정입니다. 따라서 웹이 어떻게 동작하는지 이해할 필요가 있습니다. 그림 2.4: 웹 환경 구조 먼저 클라이언트란 여러분의 데스크톱이나 휴대폰과 같은 장치와 크롬이나 파이어폭스와 같은 소프트웨어를 의미합니다. 서버는 웹사이트와 앱을 저장하는 컴퓨터를 의미합니다. 클라이언트가 특정 정보를 요구하는 과정을 ‘요청’이라고 하며, 서버가 해당 정보를 제공하는 과정을 ‘응답’이라고 합니다. 그러나 클라이언트와 서버가 연결되어 있지 않다면 둘 사이에 정보를 주고받을 수 없으며, 이를 연결하는 공간이 바로 인터넷입니다. 또한 건물에도 고유의 주소가 있는 것처럼, 각 서버에도 고유의 주소가 있는데 이것이 인터넷 주소 혹은 URL입니다. 여러분이 네이버에서 경제 기사를 클릭하는 경우를 생각해봅시다. 클라이언트는 사용자인 여러분이고, 서버는 네이버이며, URL은 www.naver.com이 됩니다. 경제 기사를 클릭하는 과정이 요청이며, 클릭 후 해당 페이지를 보여주는 과정이 응답입니다. 2.2.1 HTTP 클라이언트가 각기 다른 방법으로 데이터를 요청한다면, 서버는 해당 요청을 알아듣지 못할 것입니다. 이를 방지하기 위해 규정된 약속이나 표준에 맞추어 데이터를 요청해야 합니다. 이러한 약속을 HTTP(HyperText Transfer Protocol)라고 합니다. 클라이언트가 서버에게 요청의 목적이나 종류를 알리는 방법을 HTTP 요청 방식(HTTP Request Method)이라고 합니다. HTTP 요청 방식은 크게 표 2.1와 같이 GET, POST, PUT, DELETE라는 네 가지로 나눌 수 있지만 크롤링에는 GET과 POST 방식이 대부분 사용되므로 이 두 가지만 알아도 충분합니다. GET 방식과 POST 방식의 차이 및 크롤링 방법은 CHAPTER 4에서 자세하게 다루겠습니다. 표 2.1: HTTP 요청 방식과 설명 요청방식 주소 GET 특정 정보 조회 POST 새로운 정보 등록 PUT 기존 특정 정보 갱신 DELETE 기존 특정 정보 삭제 인터넷을 사용하다 보면 한 번쯤 ‘이 페이지를 볼 수 있는 권한이 없음(HTTP 오류 403 - 사용할 수 없음)’ 혹은 ‘페이지를 찾을 수 없음(HTTP 오류 404 - 파일을 찾을 수 없음)’이라는 오류를 본 적이 있을 겁니다. 여기서 403과 404라는 숫자는 클라이언트의 요청에 대한 서버의 응답 상태를 나타내는 HTTP 상태 코드입니다. HTTP 상태 코드는 100번대부터 500번대까지 있으며, 성공적으로 응답을 받을 시 200번 코드를 받게 됩니다. 각 코드에 대한 내용은 HTTP 상태 코드를 검색하면 확인할 수 있으며, 크롤링 과정에서 오류가 발생할 시 해당 코드를 통해 어떤 부분에서 오류가 발생했는지 확인이 가능합니다. 표 2.2: HTTP 상태 코드 그룹 별 내용 코드 주소 내용 1xx Informational (조건부 응답) 리퀘스트를 받고, 처리 중에 있음 2xx Success (성공) 리퀘스트를 정상적으로 처리함 3xx Redirection (리디렉션) 리퀘스트 완료를 위해 추가 동작이 필요함 4xx Client Error (클라이언트 오류) 클라이언트 요청을 처리할 수 없어 오류 발생 5xx Server Error (서버 오류) 서버에서 처리를 하지 못하여 오류 발생 2.3 HTML과 CSS 클라이언트와 서버가 데이터를 주고받을 때는 디자인이라는 개념이 필요하지 않습니다. 그러나 응답받은 정보를 사람이 확인하려면 보기 편한 방식으로 바꾸어줄 필요가 있는데 웹페이지가 그러한 역할을 합니다. 웹페이지의 제목, 단락, 목록 등 레이아웃을 잡아주는 데 쓰이는 대표적인 마크업 언어가 HTML(HyperText Markup Language)입니다. HTML을 통해 잡혀진 뼈대에 글자의 색상이나 폰트, 배경색, 배치 등 화면을 꾸며주는 역할을 하는 것이 CSS(Cascading Style Sheets)입니다. 우리의 목적은 웹페이지를 만드는 것이 아니므로 HTML과 CSS에 대해 자세히 알 필요는 없습니다. 그러나 크롤링하고자 하는 데이터가 웹페이지의 어떤 태그 내에 위치하고 있는지, 어떻게 크롤링하면 될지 파악하기 위해서는 HTML과 CSS에 대한 기본적인 지식은 알아야 합니다. 메모장에서 HTML 코드를 입력한 후 ‘파일명.html’로 저장하면 해당 코드가 웹페이지에서 어떻게 나타나는지 확인할 수 있습니다. 2.3.1 HTML 기본 구조 HTML은 크게 메타 데이터를 나타내는 head와 본문을 나타내는 body로 나누어집니다. head에서 title은 웹페이지에서 나타나는 제목을 나타내며 body 내에는 본문에 들어갈 각종 내용들이 포함되어 있습니다. <html> <head> <title>Page Title</title> </head> <body> <h2> This is page heading </h2> <p> THis is first paragraph text </p> </body> </html> Page Title This is page heading THis is first paragraph text 그림 2.5: HTML 기본 구조 2.3.2 태그와 속성 HTML 코드는 태그와 속성, 내용으로 이루어져 있습니다. 크롤링한 데이터에서 특정 태그의 데이터만을 찾는 방법, 특정 속성의 데이터만을 찾는 방법, 뽑은 자료에서 내용만을 찾는 방법 등 내용을 찾는 방법이 모두 다르기 때문에 태그와 속성에 대해 좀 더 자세히 살펴보겠습니다. 그림 2.6: HTML 구성 요소 분석 꺾쇠(<>)로 감싸져 있는 부분을 태그라고 하며, 여는 태그 <>가 있으면 반드시 이를 닫는 태그인 </>가 쌍으로 있어야 합니다. 속성은 해당 태그에 대한 추가적인 정보를 제공해주는 것으로, 뒤에 속성값이 따라와야 합니다. 내용은 우리가 눈으로 보는 텍스트 부분을 의미합니다. 앞의 HTML 코드는 문단을 나타내는 <p> 그, 정렬을 나타내는 align 속성과 center를 통해 가운데 정렬을 지정하며, 내용에는 ‘퀀트 투자 Cookbook’을 나타내고, </p> 태그를 통해 태그를 마쳤습니다. 2.3.3 h 태그와 p 태그 h 태그는 폰트의 크기를 나타내는 태그이며, p 태그는 문단을 나타내는 태그입니다. 이를 사용한 간단한 예제는 다음과 같습니다. h 태그의 숫자가 작을수록 텍스트 크기는 커지는 것이 확인되며, 숫자는 1에서 6까지 지원됩니다. p 태그를 사용하면 각각의 문단이 만들어지는 것이 확인됩니다. <html> <body> <h1>Page heading: size 1</h1> <h2>Page heading: size 2</h2> <h3>Page heading: size 3</h3> <p>Quant Cookbook</p> <p>By Henry</p> </body> </html> Page heading: size 1 Page heading: size 2 Page heading: size 3 Quant Cookbook By Henry 그림 2.7: h 태그와 p 태그 예제 2.3.4 리스트를 나타내는 ul 태그와 ol 태그 ul과 ol 태그는 리스트(글머리 기호)를 만들 때 사용됩니다. ul은 순서가 없는 리스트(unordered list), ol은 순서가 있는 리스트(ordered list)를 만듭니다. <html> <body> <h2> Unordered List</h2> <ul> <li>Price</li> <li>Financial Statement</li> <li>Sentiment</li> </ul> <h2> Ordered List</h2> <ol> <li>Import</li> <li>Tidy</li> <li>Understand</li> <li>Communicate</li> </ol> </body> </html> Unordered List Price Financial Statement Sentiment Ordered List Import Tidy Understand Communicate 그림 2.8: 리스트 관련 태그 예제 ul 태그로 감싼 부분은 글머리 기호가 순서가 없는 •으로 표현되며, ol 태그로 감싼 부분은 숫자가 순서대로 표현됩니다. 각각의 리스트는 li를 통해 생성됩니다. 2.3.5 table 태그 table 태그는 표를 만드는 태그입니다. <html> <body> <h2>Major Stock Indices and US ETF</h2> <table> <tr> <th>Country</th> <th>Index</th> <th>ETF</th> </tr> <tr> <td>US</td> <td>S&P 500</td> <td>IVV</td> </tr> <tr> <td>Europe</td> <td>Euro Stoxx 50</td> <td>IEV</td> </tr> <tr> <td>Japan</td> <td>Nikkei 225</td> <td>EWJ</td> </tr> <tr> <td>Korea</td> <td>KOSPI 200</td> <td>EWY</td> </tr> </table> </body> </html> Major Stock Indices and US ETF Country Index ETF US S&P 500 IVV Europe Euro Stoxx 50 IEV Japan Nikkei 225 EWJ Korea KOSPI 200 EWY 그림 2.9: table 태그 예제 table 태그 내의 tr 태그는 각 행을 의미합니다. 각 셀의 구분은 th 혹은 td 태그를 통해 구분할 수 있습니다. th 태그는 진하게 표현되므로 주로 테이블의 제목에 사용되고, td 태그는 테이블의 내용에 사용됩니다. 2.3.6 a 태그와 src 태그 및 속성 a 태그와 src 태그는 다른 태그와는 다르게, 혼자 쓰이기보다는 속성과 결합해 사용됩니다. a 태그는 href 속성과 결합해 다른 페이지의 링크를 걸 수 있습니다. src 태그는 img 속성과 결합해 이미지를 불러옵니다. <html> <body> <h2>a tag & href attribute</h2> <p>HTML links are defined with the a tag. The link address is specified in the href attribute:</p> <a href="https://henryquant.blogspot.com/">Henry's Quantopia</a> <h2>img tag & src attribute</h2> <p>HTML images are defined with the img tag, and the filename of the image source is specified in the src attribute:</p> <img src="https://cran.r-project.org/Rlogo.svg", width="180",height="140"> </body> </html> a tag & href attribute HTML links are defined with the a tag. The link address is specified in the href attribute: Henry's Quantopia img tag & src attribute HTML images are defined with the img tag, and the filename of the image source is specified in the src attribute: 그림 2.10: a 태그와 src 태그 예제 a 태그 뒤 href 속성의 속성값으로 연결하려는 웹페이지 주소를 입력한 후 내용을 입력하면, 내용 텍스트에 웹페이지의 링크가 추가됩니다. img 태그 뒤 src 속성의 속성값에는 불러오려는 이미지 주소를 입력하며, width 속성과 height 속성을 통해 이미지의 가로세로 길이를 조절할 수도 있습니다. 페이지 내에서 링크된 주소를 모두 찾거나, 모든 이미지를 저장하려고 할 때 속성값을 찾으면 손쉽게 원하는 작업을 할 수 있습니다. 2.3.7 div 태그 div 태그는 화면의 전체적인 틀(레이아웃)을 만들 때 주로 사용하는 태그입니다. 단독으로도 사용될 수 있으며, 꾸밈을 담당하는 style 속성과 결합되어 사용되기도 합니다. <html> <body> <div style="background-color:black;color:white"> <h5>First Div</h5> <p>Black backgrond, White Color</p> </div> <div style="background-color:yellow;color:red"> <h5>Second Div</h5> <p>Yellow backgrond, Red Color</p> </div> <div style="background-color:blue;color:grey"> <h5>Second Div</h5> <p>Blue backgrond, Grey Color</p> </div> </body> </html> First Div Black backgrond, White Color Second Div Yellow backgrond, Red Color Second Div Blue backgrond, Grey Color 그림 2.11: div 태그 예제 div 태그를 통해 총 세 개의 레이아웃으로 나누어진 것을 알 수 있습니다. style 속성 중 background-color는 배경 색상을, color는 글자 색상을 의미하며, 각 레이아웃마다 다른 스타일이 적용되었습니다. 2.3.8 CSS CSS는 앞서 설명했듯이 웹페이지를 꾸며주는 역할을 합니다. head에서 각 태그에 CSS 효과를 입력하면 본문의 모든 해당 태그에 CSS 효과가 적용됩니다. 이처럼 웹페이지를 꾸미기 위해 특정 요소에 접근하는 것을 셀렉터(Selector)라고 합니다. <html> <head> <style> body {background-color: powderblue;} h4 {color: blue;} </style> </head> <body> <h4>This is a heading</h4> <p>This is a first paragraph.</p> <p>This is a second paragraph.</p> </body> </html> body {background-color: powderblue;} h4 {color: blue;} This is a heading This is a first paragraph. This is a second paragraph. 그림 2.12: css 예제 head 태그 사이에 여러 태그에 대한 CSS 효과가 정의되었습니다. 먼저 body의 전체 배경 색상을 powderblue로 설정했으며, h4 태그의 글자 색상은 파란색(blue)으로 설정했습니다. body 태그 내에서 style에 태그를 주지 않더라도, CSS 효과가 모두 적용되었음이 확인됩니다. 2.3.9 클래스와 id CSS를 이용하면 본문의 모든 태그에 효과가 적용되므로, 특정한 요소(Element)에만 동일한 효과를 적용할 수 없습니다. 클래스 속성을 이용하면 동일한 이름을 가진 클래스에는 동일한 효과가 적용됩니다. <html> <style> .index { background-color: tomato; color: white; padding: 10px; } .desc { background-color: moccasin; color: black; padding: 10px; } </style> <div> <h2 class="index">S&P 500</h2> <p class="desc"> Market capitalizations of 500 large companies having common stock listed on the NYSE, NASDAQ, or the Cboe BZX Exchange</p> </div> <div> <h2>Dow Jones Industrial Average</h2> <p>Value of 30 large, publicly owned companies based in the United States</p> </div> <div> <h2 class="index">NASDAQ Composite</h2> <p class="desc">The composition of the NASDAQ Composite is heavily weighted towards information technology companies</p> <div> </html> .index { background-color: tomato; color: white; padding: 10px; } .desc { background-color: moccasin; color: black; padding: 10px; } S&P 500 Market capitalizations of 500 large companies having common stock listed on the NYSE, NASDAQ, or the Cboe BZX Exchange Dow Jones Industrial Average Value of 30 large, publicly owned companies based in the United States NASDAQ Composite The composition of the NASDAQ Composite is heavily weighted towards information technology companies 그림 2.13: class 예제 셀렉터를 클래스에 적용할 때는 클래스명 앞에 마침표(.)를 붙여 표현합니다. 위 예제에서 index 클래스는 배경 색상이 tomato, 글자 색상은 흰색, 여백은 10px로 정의되었습니다. desc 클래스는 배경 색상이 moccasin, 글자 색상은 검은색, 여백은 10px로 정의되었습니다. 본문의 첫 번째(S&P 500)와 세 번째(NASDAQ Composite) 레이아웃의 h2 태그 뒤에는 index 클래스를, p 태그 뒤에는 desc 클래스를 속성으로 입력했습니다. 따라서 해당 레이아웃에만 CSS 효과가 적용되며, 클래스 값이 없는 두 번째 레이아웃에는 효과가 적용되지 않습니다. id 또한 이와 비슷한 역할을 하며, HTML 내에서 여러 개의 클래스가 정의될 수 있는 반면, id는 단 하나만 사용하기를 권장합니다. <html> <head> <style> /* Style the element with the id "myHeader" */ #myHeader { background-color: lightblue; color: black; padding: 15px; text-align: center; } </style> </head> <body> <!-- A unique element --> <h1 id="myHeader">My Header</h1> </body> </html> /* Style the element with the id \"myHeader\" */ #myHeader { background-color: lightblue; color: black; padding: 15px; text-align: center; } My Header 그림 2.14: id 예제 셀렉터를 id에 적용할 때는 클래스명 앞에 샵(#)를 붙여 표현하며, 페이지에서 한 번만 사용된다는 점을 제외하면 클래스와 사용 방법이 거의 동일합니다. 클래스나 id 값을 통해 원하는 내용을 크롤링하는 경우도 많으므로, 각각의 이름 앞에 마침표(.)와 샵(#) 을 붙여야 한다는 점을 꼭 기억하기 바랍니다. HTML과 관련해 추가적인 정보가 필요하거나 내용이 궁금하다면 아래 웹사이트를 참고하기 바랍니다. w3schools: https://www.w3schools.in/html-tutorial/ 웨버 스터디: http://webberstudy.com/ 2.4 파이프 오퍼레이터(%>%) 파이프 오퍼레이터는 R에서 동일한 데이터를 대상으로 연속으로 작업하게 해주는 오퍼레이터(연산자)입니다. 크롤링에 필수적인 rvest 패키지를 설치하면 자동으로 magrittr 패키지가 설치되어 파이프 오퍼레이터를 사용할 수 있습니다. 흔히 프로그래밍에서 x라는 데이터를 F()라는 함수에 넣어 결괏값을 확인하고 싶으면 F(x)의 방법을 사용합니다. 예를 들어 3과 5라는 데이터 중 큰 값을 찾으려면 max(3,5)를 통해 확인합니다. 이를 통해 나온 결괏값을 또 다시 G()라는 함수에 넣어 결괏값을 확인하려면 비슷한 과정을 거칩니다. max(3,5)를 통해 나온 값의 제곱근을 구하려면 result = max(3,5)를 통해 첫 번째 결괏값을 저장하고, sqrt(result)를 통해 두 번째 결괏값을 계산합니다. 물론 sqrt(max(3,5))와 같은 표현법으로 한 번에 표현할 수 있습니다. 이러한 표현의 단점은 계산하는 함수가 많아질수록 저장하는 변수가 늘어나거나 괄호가 지나치게 길어진다는 것입니다. 그러나 파이프 오퍼레이터인 %>%를 사용하면 함수 간의 관계를 매우 직관적으로 표현하고 이해할 수 있습니다. 이를 정리하면 아래 표 2.3와 같습니다. 표 2.3: 파이프 오퍼레이터의 표현과 내용 비교 내용 표현.방법 F(x) x %>% F G(F(x)) x %>% F %>% G 간단한 예제를 통해 파이프 오퍼레이터의 사용법을 살펴보겠습니다. 먼저 다음과 같은 10개의 숫자가 있다고 가정합니다. x = c(0.3078, 0.2577, 0.5523, 0.0564, 0.4685, 0.4838, 0.8124, 0.3703, 0.5466, 0.1703) 우리가 원하는 과정은 다음과 같습니다. 각 값들의 로그값을 구할 것 로그값들의 계차를 구할 것 구해진 계차의 지수값을 구할 것 소수 둘째 자리까지 반올림할 것 입니다. 즉 log(), diff(), exp(), round()에 대한 값을 순차적으로 구하고자 합니다. x1 = log(x) x2 = diff(x1) x3 = exp(x2) round(x3, 2) ## [1] 0.84 2.14 0.10 8.31 1.03 1.68 0.46 1.48 0.31 첫 번째 방법은 단계별 함수의 결괏값을 변수에 저장하고 저장된 변수를 다시 불러와 함수에 넣고 계산하는 방법입니다. 전반적인 계산 과정을 확인하기에는 좋지만 매번 변수에 저장하고 불러오는 과정이 매우 비효율적이며 코드도 불필요하게 길어집니다. round(exp(diff(log(x))), 2) ## [1] 0.84 2.14 0.10 8.31 1.03 1.68 0.46 1.48 0.31 두 번째는 괄호를 통해 감싸는 방법입니다. 앞선 방법에 비해 코드는 짧아졌지만, 계산 과정을 알아보기에는 매우 불편한 방법으로 코드가 짜여 있습니다. library(magrittr) x %>% log() %>% diff() %>% exp() %>% round(., 2) ## [1] 0.84 2.14 0.10 8.31 1.03 1.68 0.46 1.48 0.31 마지막으로 파이프 오퍼레이터를 사용하는 방법입니다. 코드도 짧으며 계산 과정을 한눈에 파악하기도 좋습니다. 맨 왼쪽에는 원하는 변수를 입력하며, %>% 뒤에는 차례대로 계산하고자 하는 함수를 입력합니다. 변수의 입력값을 ()로 비워둘 경우, 오퍼레이터의 왼쪽에 있는 값이 입력 변수가 됩니다. 반면 round()와 같이 입력값이 두 개 이상 필요하면 마침표(.)가 오퍼레이터의 왼쪽 값으로 입력됩니다. 파이프 오퍼레이터는 크롤링뿐만 아니라 모든 코드에 사용할 수 있습니다. 이를 통해 훨씬 깔끔하면서도 데이터 처리 과정을 직관적으로 이해할 수 있습니다. 2.5 오류에 대한 예외처리 크롤링을 이용해 데이터를 수집할 때 일반적으로 for loop 구문을 통해 수천 종목에 해당하는 웹페이지에 접속해 해당 데이터를 읽어옵니다. 그러나 특정 종목에 해당하는 페이지가 없거나, 단기적으로 접속이 불안정할 경우 오류가 발생해 루프를 처음부터 다시 실행해야 하는 번거로움이 있습니다. tryCatch() 함수를 이용하면 예외처리, 즉 오류가 발생할 경우 이를 무시하고 넘어갈 수 있습니다. tryCatch() 함수의 구조는 다음과 같습니다. result = tryCatch({ expr }, warning = function(w) { warning-handler-code }, error = function(e) { error-handler-code }, finally = { cleanup-code }) 먼저 expr는 실행하고자 하는 코드를 의미합니다. warning은 경고를 나타내며, warning-handler-code는 경고 발생 시 실행할 구문을 의미합니다. 이와 비슷하게 error와 error-handler-code는 각각 오류와 오류 발생 시 실행할 구문을 의미합니다. finally는 오류의 여부와 관계 없이 무조건 수행할 구문을 의미하며, 생략할 수도 있습니다. number = data.frame(1,2,3,"4",5, stringsAsFactors = FALSE) str(number) ## 'data.frame': 1 obs. of 5 variables: ## $ X1 : num 1 ## $ X2 : num 2 ## $ X3 : num 3 ## $ X.4.: chr "4" ## $ X5 : num 5 먼저 number 변수에는 1에서 5까지 값이 입력되어 있으며, 다른 값들은 형태가 숫자인 반면 4는 문자 형태입니다. for (i in number) { print(i^2) } ## [1] 1 ## [1] 4 ## [1] 9 ## Error in i^2: non-numeric argument to binary operator for loop 구문을 통해 순서대로 값들의 제곱을 출력하는 명령어를 실행하면 문자 4는 제곱을 할 수 없어 오류가 발생하게 됩니다. tryCatch() 함수를 사용하면 이처럼 오류가 발생하는 루프를 무시하고 다음 루프로 넘어갈 수 있게 됩니다. for (i in number) { tryCatch({ print(i^2) }, error = function(e) { print(paste('Error:', i)) }) } ## [1] 1 ## [1] 4 ## [1] 9 ## [1] "Error: 4" ## [1] 25 expr 부분은 print(i^2)이며, error-handler-code 부분은 오류가 발생한 i를 출력합니다. 해당 코드를 실행하면 문자 4에서 오류가 발생함을 알려준 후 루프가 멈추지 않고 다음으로 진행됩니다. "],
["api를-이용한-데이터-수집.html", "Chapter 3 API를 이용한 데이터 수집 3.1 API를 이용한 Quandl 데이터 다운로드 3.2 getSymbols() 함수를 이용한 API 다운로드", " Chapter 3 API를 이용한 데이터 수집 이 CHAPTER와 다음 CHAPTER에서는 본격적으로 데이터를 수집하는 방법을 배우겠습니다. 먼저 API를 이용해 데이터를 수집하는 방법을 살펴봅니다. API 제공자는 본인이 가진 데이터베이스를 다른 누군가가 쉽게 사용할 수 있는 형태로 가지고 있으며, 해당 데이터베이스에 접근할 수 있는 열쇠인 API 주소를 가진 사람은 이를 언제든지 사용할 수 있습니다. 그림 1.3: API 개념 API는 API 주소만 가지고 있다면 데이터를 언제, 어디서, 누구나 쉽게 이용할 수 있다는 장점이 있습니다. 또한 대부분의 경우 사용자가 필요한 데이터만을 가지고 있으므로 접속 속도가 빠르며, 데이터를 가공하는 번거로움도 줄어듭니다. 해외에는 금융 데이터를 API의 형태로 제공하는 업체가 많으므로, 이를 잘만 활용한다면 매우 손쉽게 퀀트 투자에 필요한 데이터를 수집할 수 있습니다. 3.1 API를 이용한 Quandl 데이터 다운로드 데이터 제공업체 Quandl은 일부 데이터를 무료로 제공하며 API를 통해서 다운로드할 수 있습니다.6 이 책에서는 예제로 애플(AAPL)의 주가를 다운로드해보겠습니다. csv 형식의 API 주소는 다음과 같습니다. https://www.quandl.com/api/v3/datasets/WIKI/AAPL/data.csv?api_key=xw3NU3xLUZ7vZgrz5QnG 위 주소를 웹 브라우저 주소 창에 직접 입력하면 csv 형식의 파일이 다운로드되며, 파일을 열어보면 애플의 주가 데이터가 있습니다. 그림 2.1: API 주소를 이용한 데이터 다운로드 그러나 웹 브라우저에 해당 주소를 입력해 csv 파일을 다운로드하고 csv 파일을 다시 R에서 불러오는 작업은 무척이나 비효율적입니다. R에서 API 주소를 이용해 직접 데이터를 다운로드할 수 있습니다. url.aapl = "https://www.quandl.com/api/v3/datasets/WIKI/AAPL/data.csv?api_key=xw3NU3xLUZ7vZgrz5QnG" data.aapl = read.csv(url.aapl) head(data.aapl) ## Date Open High Low Close Volume ## 1 2018-03-27 173.7 175.2 166.9 168.3 38962839 ## 2 2018-03-26 168.1 173.1 166.4 172.8 36272617 ## 3 2018-03-23 168.4 169.9 164.9 164.9 40248954 ## 4 2018-03-22 170.0 172.7 168.6 168.8 41051076 ## 5 2018-03-21 175.0 175.1 171.3 171.3 35247358 ## 6 2018-03-20 175.2 176.8 174.9 175.2 19314039 ## Ex.Dividend Split.Ratio Adj..Open Adj..High Adj..Low ## 1 0 1 173.7 175.2 166.9 ## 2 0 1 168.1 173.1 166.4 ## 3 0 1 168.4 169.9 164.9 ## 4 0 1 170.0 172.7 168.6 ## 5 0 1 175.0 175.1 171.3 ## 6 0 1 175.2 176.8 174.9 ## Adj..Close Adj..Volume ## 1 168.3 38962839 ## 2 172.8 36272617 ## 3 164.9 40248954 ## 4 168.8 41051076 ## 5 171.3 35247358 ## 6 175.2 19314039 url에 해당 주소를 입력한 후 read.csv() 함수를 이용해 간단하게 csv 파일을 불러올 수 있습니다. 3.2 getSymbols() 함수를 이용한 API 다운로드 이전 예에서 API 주소를 이용하면 매우 간단하게 데이터를 수집할 수 있음을 살펴보았습니다. 그러나 이 방법에는 단점도 있습니다. 먼저 원하는 항목에 대한 API를 일일이 얻기가 힘듭니다. 또한 Quandl의 경우 무료로 얻을 수 있는 정보에 제한이 있으며, 다운로드 양에도 제한이 있습니다. 이 방법으로 한두 종목의 데이터를 수집할 수 있지만, 전 종목의 데이터를 구하기는 사실상 불가능합니다. 다행히 야후 파이낸스 역시 주가 데이터를 무료로 제공하며, quantmod 패키지의 getSymbols() 함수는 해당 API에 접속해 데이터를 다운로드합니다. 3.2.1 주가 다운로드 getSymbols() 함수의 기본적인 사용법은 매우 간단합니다. 괄호 안에 다운로드하려는 종목의 티커를 입력하면 됩니다. library(quantmod) getSymbols('AAPL') ## [1] "AAPL" head(AAPL) ## AAPL.Open AAPL.High AAPL.Low AAPL.Close ## 2007-01-03 3.082 3.092 2.925 2.993 ## 2007-01-04 3.002 3.070 2.994 3.059 ## 2007-01-05 3.063 3.079 3.014 3.038 ## 2007-01-08 3.070 3.090 3.046 3.053 ## 2007-01-09 3.087 3.321 3.041 3.306 ## 2007-01-10 3.384 3.493 3.337 3.464 ## AAPL.Volume AAPL.Adjusted ## 2007-01-03 1238319600 2.582 ## 2007-01-04 847260400 2.639 ## 2007-01-05 834741600 2.620 ## 2007-01-08 797106800 2.633 ## 2007-01-09 3349298400 2.852 ## 2007-01-10 2952880000 2.988 먼저 getSymbols() 함수 내에 애플의 티커인 AAPL을 입력합니다. 티커와 동일한 변수인 AAPL이 생성되며, 주가 데이터가 다운로드된 후 xts 형태로 입력됩니다. 다운로드 결과로 총 6개의 열이 생성됩니다. Open은 시가, High는 고가, Low는 저가, Close는 종가를 의미합니다. 또한 Volume은 거래량을 의미하며, Adjusted는 배당이 반영된 수정주가를 의미합니다. 이 중 가장 많이 사용되는 데이터는 Adjusted, 즉 배당이 반영된 수정주가입니다. chart_Series(Ad(AAPL)) Ad() 함수를 통해 다운로드한 데이터에서 수정주가만을 선택한 후 chart_Series() 함수를 이용해 시계열 그래프를 그릴 수도 있습니다. 시계열 기간을 입력하지 않으면 2007년 1월부터 현재까지의 데이터가 다운로드되며, 입력 변수를 추가해서 원하는 기간의 데이터를 다운로드할 수도 있습니다. data = getSymbols('AAPL', from = '2000-01-01', to = '2018-12-31', auto.assign = FALSE) head(data) ## AAPL.Open AAPL.High AAPL.Low AAPL.Close ## 2000-01-03 0.9364 1.0045 0.9079 0.9994 ## 2000-01-04 0.9665 0.9877 0.9035 0.9152 ## 2000-01-05 0.9263 0.9872 0.9196 0.9286 ## 2000-01-06 0.9475 0.9554 0.8482 0.8482 ## 2000-01-07 0.8616 0.9018 0.8527 0.8884 ## 2000-01-10 0.9107 0.9129 0.8460 0.8728 ## AAPL.Volume AAPL.Adjusted ## 2000-01-03 535796800 0.8622 ## 2000-01-04 512377600 0.7895 ## 2000-01-05 778321600 0.8010 ## 2000-01-06 767972800 0.7317 ## 2000-01-07 460734400 0.7664 ## 2000-01-10 505064000 0.7529 from에는 시작시점을 입력하고 to에는 종료시점을 입력하면 해당 기간의 데이터가 다운로드됩니다. getSymbols() 함수를 통해 다운로드한 데이터는 자동으로 티커와 동일한 변수명에 저장됩니다. 만일 티커명이 아닌 원하는 변수명에 데이터를 저장하려면 auto.assign 인자를 FALSE로 설정해주면 다운로드한 데이터가 원하는 변수에 저장됩니다. ticker = c('FB', 'NVDA') getSymbols(ticker) ## [1] "FB" "NVDA" head(FB) ## FB.Open FB.High FB.Low FB.Close FB.Volume ## 2012-05-18 42.05 45.00 38.00 38.23 573576400 ## 2012-05-21 36.53 36.66 33.00 34.03 168192700 ## 2012-05-22 32.61 33.59 30.94 31.00 101786600 ## 2012-05-23 31.37 32.50 31.36 32.00 73600000 ## 2012-05-24 32.95 33.21 31.77 33.03 50237200 ## 2012-05-25 32.90 32.95 31.11 31.91 37149800 ## FB.Adjusted ## 2012-05-18 38.23 ## 2012-05-21 34.03 ## 2012-05-22 31.00 ## 2012-05-23 32.00 ## 2012-05-24 33.03 ## 2012-05-25 31.91 head(NVDA) ## NVDA.Open NVDA.High NVDA.Low NVDA.Close ## 2007-01-03 24.71 25.01 23.19 24.05 ## 2007-01-04 23.97 24.05 23.35 23.94 ## 2007-01-05 23.37 23.47 22.28 22.44 ## 2007-01-08 22.52 23.04 22.13 22.61 ## 2007-01-09 22.64 22.79 22.14 22.17 ## 2007-01-10 21.93 23.47 21.60 23.26 ## NVDA.Volume NVDA.Adjusted ## 2007-01-03 28870500 22.11 ## 2007-01-04 19932400 22.01 ## 2007-01-05 31083600 20.63 ## 2007-01-08 16431700 20.78 ## 2007-01-09 19104100 20.38 ## 2007-01-10 27718600 21.39 한 번에 여러 종목의 주가를 다운로드할 수도 있습니다. 위 예제와 같이 페이스북과 엔비디아의 티커인 FB와 NVDA를 ticker 변수에 입력하고 getSymbols() 함수에 티커를 입력한 변수를 넣으면 두 종목의 주가가 순차적으로 다운로드됩니다. 3.2.2 국내 종목 주가 다운로드 getSymbols() 함수를 이용하면 미국뿐 아니라 국내 종목의 주가도 다운로드할 수 있습니다. 국내 종목의 티커는 총 6자리로 구성되어 있으며, 해당 함수에 입력되는 티커는 코스피 상장 종목의 경우 티커.KS, 코스닥 상장 종목의 경우 티커.KQ의 형태로 입력해야 합니다. 다음은 코스피 상장 종목인 삼성전자 데이터의 다운로드 예시입니다. getSymbols('005930.KS', from = '2000-01-01', to = '2018-12-31') ## [1] "005930.KS" tail(Ad(`005930.KS`)) ## 005930.KS.Adjusted ## 2018-12-20 38293 ## 2018-12-21 38293 ## 2018-12-24 38442 ## 2018-12-26 37996 ## 2018-12-27 38250 ## 2018-12-28 38700 삼성전자의 티커인 005930에 .KS를 붙여 getSymbols() 함수에 입력하면 티커명에 해당하는 005930.KS 변수명에 데이터가 저장됩니다. 변수명에 마침표(.)가 있으므로 Ad() 함수를 통해 수정주가를 확인하려면 변수명 앞뒤에 억음 부호(`)를 붙여야 합니다. 국내 종목은 종종 수정주가에 오류가 발생하는 경우가 많아서 배당이 반영된 값보다는 단순 종가(Close) 데이터를 사용하기를 권장합니다. tail(Cl(`005930.KS`)) ## 005930.KS.Close ## 2018-12-20 38650 ## 2018-12-21 38650 ## 2018-12-24 38800 ## 2018-12-26 38350 ## 2018-12-27 38250 ## 2018-12-28 38700 Cl() 함수는 Close, 즉 종가만을 선택하며, 사용 방법은 Ad() 함수와 동일합니다. 비록 배당을 고려할 수는 없지만, 전반적으로 오류가 없는 데이터를 사용할 수 있습니다. 다음은 코스닥 상장종목인 셀트리온제약의 예시이며, 티커인 068670에 .KQ를 붙여 함수에 입력합니다. 역시나 데이터가 다운로드되어 티커명의 변수에 저장됩니다. getSymbols("068760.KQ", from = '2000-01-01', to = '2018-12-31') ## [1] "068760.KQ" tail(Cl(`068760.KQ`)) ## 068760.KQ.Close ## 2018-12-20 NA ## 2018-12-21 NA ## 2018-12-24 NA ## 2018-12-26 NA ## 2018-12-27 NA ## 2018-12-28 NA 3.2.3 FRED 데이터 다운로드 미국 연방준비은행에서 관리하는 Federal Reserve Economic Data(FRED)는 미국 및 각국의 중요 경제지표 데이터를 살펴볼 때 가장 많이 참조되는 곳 중 하나입니다. getSymbols() 함수를 통해 FRED 데이터를 다운로드할 수 있습니다. 먼저 미 국채 10년물 금리를 다운로드하는 예제를 살펴보겠습니다. getSymbols('DGS10', src='FRED') ## [1] "DGS10" chart_Series(DGS10) 미 국채 10년물 금리에 해당하는 티커인 DGS10을 입력해주며, 데이터 출처에 해당하는 src에 FRED를 입력해줍니다. FRED에서 제공하는 API를 이용해 데이터가 다운로드되며, chart_Series() 함수를 통해 금리 추이를 살펴볼 수 있습니다. 각 항목별 티커를 찾는 방법은 매우 간단합니다. 먼저 FRED의 웹사이트7원 하는 데이터를 검색합니다. 만일 원/달러 환율에 해당하는 티커를 찾고자 한다면 그림 3.1와 같이 이에 해당하는 South Korea / U.S. Foreign Exchange Rate를 검색해 원하는 페이지에 접속합니다. 이 중 페이지 주소에서 /series/ 다음에 위치하는 DEXKOUS가 해당 항목의 티커입니다. 그림 3.1: FRED 사이트 내 원/달러 환율의 티커 확인 getSymbols('DEXKOUS', src='FRED') ## [1] "DEXKOUS" tail(DEXKOUS) ## DEXKOUS ## 2021-01-08 1089 ## 2021-01-11 1097 ## 2021-01-12 1100 ## 2021-01-13 1098 ## 2021-01-14 1098 ## 2021-01-15 1099 해당 티커를 입력하면, FRED 웹사이트와 동일한 데이터가 다운로드됩니다. 이 외에도 509,000여 개의 방대한 FRED 데이터를 해당 함수를 통해 손쉽게 R에서 다운로드할 수 있습니다. 자세한 내용은 https://docs.quandl.com/ 에서 확인할 수 있습니다.↩︎ https://fred.stlouisfed.org/↩︎ "],
["크롤링-이해하기.html", "Chapter 4 크롤링 이해하기 4.1 GET과 POST 방식 이해하기 4.2 크롤링 예제", " Chapter 4 크롤링 이해하기 API를 이용하면 데이터를 매우 쉽게 수집할 수 있지만, 국내 주식 데이터를 다운로드 하기에는 한계가 있으며, 원하는 데이터가 API의 형태로 제공된다는 보장도 없습니다. 따라서 우리는 필요한 데이터를 얻기 위해 직접 찾아 나서야 합니다. 각종 금융 웹사이트에는 주가, 재무정보 등 우리가 원하는 대부분의 주식 정보가 제공되고 있으며, API를 활용할 수 없는 경우에도 크롤링을 통해 이러한 데이터를 수집할 수 있습니다. 크롤링 혹은 스크래핑이란 웹사이트에서 원하는 정보를 수집하는 기술입니다. 대부분의 금융 웹사이트는 간단한 형태로 작성되어 있어, 몇 가지 기술만 익히면 어렵지 않게 데이터를 크롤링할 수 있습니다. 이 CHAPTER에서는 크롤링에 대한 간단한 설명과 예제를 살펴보겠습니다. 크롤링을 할 때 주의해야 할 점이 있습니다. 특정 웹사이트의 페이지를 쉬지 않고 크롤링하는 행위를 무한 크롤링이라고 합니다. 무한 크롤링은 해당 웹사이트의 자원을 독점하게 되어 타인의 사용을 막게 되며 웹사이트에 부하를 주게 됩니다. 일부 웹사이트에서는 동일한 IP로 쉬지 않고 크롤링을 할 경우 접속을 막아버리는 경우도 있습니다. 따라서 하나의 페이지를 크롤링한 후 1~2초 가량 정지하고 다시 다음 페이지를 크롤링하는 것이 좋습니다. 4.1 GET과 POST 방식 이해하기 우리가 인터넷에 접속해 서버에 파일을 요청하면, 서버는 이에 해당하는 파일을 우리에게 보내줍니다. 크롬과 같은 웹 브라우저는 이러한 과정을 사람이 수행하기 편하고 시각적으로 보기 편하도록 만들어진 것이며, 인터넷 주소는 서버의 주소를 기억하기 쉽게 만든 것입니다. 우리가 서버에 데이터를 요청하는 형태는 다양하지만 크롤링에서는 주로 GET과 POST 방식을 사용합니다. 그림 1.3: 클라이언트와 서버 간의 요청/응답 과정 4.1.1 GET 방식 GET 방식은 인터넷 주소를 기준으로 이에 해당하는 데이터나 파일을 요청하는 것입니다. 주로 클라이언트가 요청하는 쿼리를 앰퍼샌드(&) 혹은 물음표(?) 형식으로 결합해 서버에 전달합니다. 한경컨센서스8에 접속한 후 상단 탭에서 [기업] 탭을 선택하면, 주소 끝부분에 ?skinType=business가 추가되며 이에 해당하는 페이지의 내용을 보여줍니다. 즉, 해당 페이지는 GET 방식을 사용하고 있으며 입력 종류는 skinType, 이에 해당하는 [기업] 탭의 입력값은 business임을 알 수 있습니다. 그림 2.1: 한경 컨센서스 기업 REPORT 페이지 이번에는 [파생] 탭을 선택해봅니다. 역시나 웹사이트 주소 끝부분이 ?skinType=derivative로 변경되며, 해당 주소에 맞는 내용이 나타납니다. 여러 다른 탭들을 클릭해보면 ?skinType= 뒷부분의 입력값이 변함에 따라 이에 해당하는 페이지로 내용 이 변경되는 것을 알 수 있습니다. 다시 [기업] 탭을 선택한 후 다음 페이지를 확인하기 위해 하단의 [2]를 클릭합니다. 기존 주소인 ?skinType=business 뒤에 추가로 sdate와 edate, now_page 쿼리가 추가됩니다. sdate는 검색 기간의 시작시점, edate는 검색 기간의 종료시점, now_ page는 현재 페이지를 의미하며, 원하는 데이터를 수기로 입력해도 이에 해당하는 페이지의 데이터를 보여줍니다. 이처럼 GET 방식으로 데이터를 요청하면 웹페이지 주소를 수정해 원하는 종류의 데이터를 받아올 수 있습니다. 그림 2.4: 쿼리 추가로 인한 url의 변경 4.1.2 POST 방식 POST 방식은 사용자가 필요한 값을 추가해서 요청하는 방법입니다. GET 방식과 달리 클라이언트가 요청하는 쿼리를 body에 넣어서 전송하므로 요청 내역을 직접 볼 수 없습니다. 한국거래소 상장공시시스템9에 접속해 [전체메뉴보기]를 클릭하고 [상장법인상세정보] 중 [상장종목현황]을 선택합니다. 웹페이지 주소가 바뀌며, 상장종목현황이 나타납니다. 그림 4.1: 상장공시시스템의 상장종목현황 메뉴 이번엔 조회일자를 [2017-12-28]로 선택한 후 [검색]을 클릭합니다. 페이지의 내용은 선택일 기준으로 변경되었지만, 주소는 변경되지 않고 그대로 남아 있습니다. GET 방식에서는 선택 항목에 따라 웹페이지 주소가 변경되었지만, POST 방식을 사용해 서버에 데이터를 요청하는 해당 웹사이트는 그렇지 않은 것을 알 수 있습니다. POST 방식의 데이터 요청 과정을 살펴보려면 개발자 도구를 이용해야 하며, 크롬에서는 [F12]키를 눌러 개발자 도구 화면을 열 수 있습니다. 개발자 도구 화면에서 다시 한번 [검색]을 클릭해봅니다. [Network] 탭을 클릭하면, [검색]을 클릭함과 동시에 브라우저와 서버 간의 통신 과정을 살펴볼 수 있습니다. 이 중 listedIssueStatus.do라는 항목이 POST 형태임을 알 수 있습니다. 그림 4.2: 크롬 개발자도구의 Network 화면 해당 메뉴를 클릭하면 통신 과정을 좀 더 자세히 알 수 있습니다. 가장 하단의 Form Data에는 서버에 데이터를 요청하는 내역이 있습니다. method에는 readListIssueStatus, selDate에는 2017-12-28이라는 값이 있습니다. 그림 2.5: POST 방식의 서버 요청 내역 이처럼 POST 방식은 요청하는 데이터에 대한 쿼리가 GET 방식처럼 URL을 통해 전송되는 것이 아닌 body를 통해 전송되므로, 이에 대한 정보는 웹 브라우저를 통해 확인할 수 없습니다. 4.2 크롤링 예제 일반적인 크롤링은 httr 패키지의 GET() 혹은 POST() 함수를 이용해 데이터를 다운로드한 후 rvest 패키지의 함수들을 이용해 원하는 데이터를 찾는 과정으로 이루어집니다. 이 CHAPTER에서는 GET 방식으로 금융 실시간 속보의 제목을 추출하는 예제, POST 방식으로 기업공시채널에서 오늘의 공시를 추출하는 예제, 태그와 속성, 페이지 내비게이션 값을 결합해 국내 상장주식의 종목명 및 티커를 추출하는 예제를 학습해 보겠습니다. 4.2.1 금융 속보 크롤링 크롤링의 간단한 예제로 금융 속보의 제목을 추출해보겠습니다. 먼저 네이버 금융에 접속한 후 [뉴스 → 실시간 속보]10를 선택합니다. 이 중 뉴스의 제목에 해당하는 텍스트만 추출하고자 합니다. 뉴스 제목 부분에 마우스 커서를 올려둔 후 마우스 오른쪽 버튼을 클릭하고 [검사]를 선택하면 개발자 도구 화면이 나타납니다. 여기서 해당 글자가 HTML 내에서 어떤 부분에 위치하는지 확인할 수 있습니다. 해당 제목은 dl 태그 → dd 태그의 articleSubject 클래스 → a 태그 중 title 속성에 위치하고 있습니다. 태그와 속성의 차이가 이해되지 않은 분은 CHAPTER 2를 다시 살펴보기 바랍니다. 그림 2.6: 실시간 속보의 제목 부분 html 먼저 해당 페이지의 내용을 R로 불러옵니다. library(rvest) library(httr) url = 'https://finance.naver.com/news/news_list.nhn?mode=LSS2D&section_id=101&section_id2=258' data = GET(url) print(data) 먼저 url 변수에 해당 주소를 입력한 후 GET() 함수를 이용해 해당 페이지의 내용을 받아 data 변수에 저장합니다. data 변수를 확인해보면 Status가 200, 즉 데이터가 이상 없이 받아졌으며, 인코딩(charset)은 EUC-KR 타입으로 되어 있습니다. 우리는 개발자 도구 화면을 통해 제목에 해당하는 부분이 dl 태그 → dd 태그의 articleSubject 클래스 → a 태그 중 title 속성에 위치하고 있음을 살펴보았습니다. 이를 활용해 제목 부분만을 추출하는 방법은 다음과 같습니다. data_title = data %>% read_html(encoding = 'EUC-KR') %>% html_nodes('dl') %>% html_nodes('.articleSubject') %>% html_nodes('a') %>% html_attr('title') read_html() 함수를 이용해 해당 페이지의 HTML 내용을 읽어오며, 인코딩은 EUC-KR로 설정합니다. html_nodes() 함수는 해당 태그를 추출하는 함수이며 dl 태그에 해당하는 부분을 추출합니다. html_nodes() 함수를 이용해 articleSubject 클래스에 해당하는 부분을 추출할 수 있으며, 클래스 속성의 경우 이름 앞에 마침표(.)를 붙여주어야 합니다. html_nodes() 함수를 이용해 a 태그를 추출합니다. html_attr() 함수는 속성을 추출하는 함수이며 title에 해당하는 부분만을 추출합니다. 위 과정을 거쳐 data_title에는 실시간 속보의 제목만이 저장됩니다. 이처럼 개발자 도구 화면을 통해 내가 추출하고자 하는 데이터가 HTML 중 어디에 위치하고 있는지 먼저 확인하면 어렵지 않게 해당 데이터를 읽어올 수 있습니다. print(data_title) ## [1] "버티느냐 마느냐... 2주새 30% " ## [2] "“태어났더니 금수저”... 돌도 안된" ## [3] "10억 계좌 40%, 1000만원 계" ## [4] "\\"96층에 사람있어요\\"…삼성전자 '게" ## [5] "이번주 실적시즌…4분기 실적보다 '이" ## [6] "지금 사면 100% 수익?...'빚투" ## [7] "430억 등치고 호화 생활한 회장님 " ## [8] "\\"인튜이티브 서지컬, 코로나19 장기" ## [9] "총수 공백으로 빠진 주가, 여전히 완" ## [10] "[해외주식 톺아보기]전기차, 비야디(" ## [11] "\\"세종공업, 주요 고객사의 수소차 라" ## [12] "법원, 현대차증권 `파킹거래` 인정…" ## [13] "[표] 주요국 증시 주간 동향" ## [14] "상장사 10세 이하 '金수저' 주주 " ## [15] "세 회사로 쪼갠 대림산업…DL·DL이" ## [16] "새해 22% 오른 LG…목표주가 15" ## [17] "[표] 주간 주요 증시 지표" ## [18] "LG에너지솔루션, 그룹 재편에 IPO" ## [19] "\\"지니언스, 국내 네트워크 접근제어 " ## [20] "[단독]1,200만 가입자 발판삼아·" 4.2.2 기업공시채널에서 오늘의 공시 불러오기 한국거래소 상장공시시스템에 접속한 후 [오늘의 공시 → 전체 → 더보기]를 선택해 전체 공시내용을 확인할 수 있습니다. 그림 4.3: 오늘의공시 확인하기 해당 페이지에서 날짜를 변경하면 페이지의 내용은 해당일의 공시로 변경되지만 URL은 변경되지 않습니다. 이처럼 POST 방식은 요청하는 데이터에 대한 쿼리가 body의 형태를 통해 전송되므로, 개발자 도구 화면을 통해 해당 쿼리에 대한 내용을 확인해야 합니다. 개발자 도구 화면을 연 상태에서 조회일자를 [2018-12-28]로 선택한 후 [검색]을 클릭하고 [Network] 탭의 todaydisclosure.do 항목을 살펴보면 Form Data를 통해 서버에 데이터를 요청하는 내역을 확인할 수 있습니다. 여러 항목 중 selDate 부분이 우리가 선택한 일자로 설정되어 있습니다. 그림 2.9: POST 방식의 데이터 요청 POST 방식으로 쿼리를 요청하는 방법을 코드로 나타내면 다음과 같습니다. library(httr) library(rvest) Sys.setlocale("LC_ALL", "English") url = 'https://dev-kind.krx.co.kr/disclosure/todaydisclosure.do' data = POST(url, body = list( method = 'searchTodayDisclosureSub', currentPageSize = '15', pageIndex = '1', orderMode = '0', orderStat = 'D', forward = 'todaydisclosure_sub', chose = 'S', todayFlag = 'Y', selDate = '2018-12-28' )) data = read_html(data) %>% html_table(fill = TRUE) %>% .[[1]] Sys.setlocale("LC_ALL", "Korean") 한글(korean)로 작성된 페이지를 크롤링하면 오류가 발생하는 경우가 종종 있으므로 Sys.setlocale() 함수를 통해 로케일 언어를 영어(English)로 설정합니다. POST() 함수를 통해 해당 url에 원하는 쿼리를 요청하며, 쿼리는 body 내에 리스트 형태로 입력해줍니다. 해당 값은 개발자 도구 화면의 Form Data와 동일하게 입력해주며, marketType과 같이 값이 없는 항목은 입력하지 않아도 됩니다. read_html() 함수를 이용해 해당 페이지의 HTML 내용을 읽어옵니다. html_table() 함수는 테이블 형태의 데이터를 읽어오는 함수입니다. 셀 병합이 된 열이 있으므로 fill=TRUE를 추가합니다. .[[1]]를 통해 첫 번째 리스트를 선택합니다. 한글을 읽기 위해 Sys.setlocale() 함수를 통해 로케일 언어를 다시 Korean으로 변경합니다. 저장된 데이터를 확인하면 화면과 동일한 내용이 출력됩니다. print(head(data)) ## NA NA ## 1 18:32 이노와이즈 ## 2 18:26 에스제이케이 ## 3 18:11 아이엠텍 ## 4 18:10 시그넷이브이 ## 5 18:09 ## 6 18:09 ## NA ## 1 최대주주변경 ## 2 증권 발행결과(자율공시)(제3자배정 유상증자) ## 3 [정정]유상증자결정(제3자배정) ## 4 유형자산 양수 결정 ## 5 자기주식매매신청내역(코스닥시장) ## 6 대량매매내역(코스닥시장) ## NA NA ## 1 화신테크 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 2 에스제이케이 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 3 아이엠텍 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 4 시그넷이브이 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 5 코스닥시장본부 ## 6 코스닥시장본부 POST 형식의 경우 body에 들어가는 쿼리 내용을 바꾸어 원하는 데이터를 받을 수 있습니다. 만일 2020년 9월 18일 공시를 확인하고자 한다면 위의 코드에서 selDate만 2020-09-18로 변경해주면 됩니다. 아래 코드의 출력 결과물을 2019년 9월 18일 공시와 비교하면 동일한 결과임을 확인할 수 있습니다. Sys.setlocale("LC_ALL", "English") url = 'https://dev-kind.krx.co.kr/disclosure/todaydisclosure.do' data = POST(url, body = list( method = 'searchTodayDisclosureSub', currentPageSize = '15', pageIndex = '1', orderMode = '0', orderStat = 'D', forward = 'todaydisclosure_sub', chose = 'S', todayFlag = 'Y', selDate = '2020-09-18' )) data = read_html(data) %>% html_table(fill = TRUE) %>% .[[1]] Sys.setlocale("LC_ALL", "Korean") print(head(data)) ## NA NA ## 1 18:24 KMH ## 2 18:19 대한그린파워 ## 3 18:19 대한그린파워 ## 4 18:17 이더블유케이 ## 5 18:08 ## 6 18:08 ## NA ## 1 소송등의제기(전환사채발행금지가처분) ## 2 증권 발행결과(자율공시)(제31회차 CB) ## 3 전환사채(해외전환사채포함)발행후만기전사채취득(제28회차) ## 4 전환사채권발행결정(제4회차) ## 5 자기주식매매신청내역(코스닥시장) ## 6 대량매매내역(코스닥시장) ## NA NA ## 1 케이엠에이치 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 2 퍼시픽바이오 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 3 퍼시픽바이오 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 4 이더블유케이 공시차트\\r\\n\\t\\t\\t\\t\\t주가차트 ## 5 코스닥시장본부 ## 6 코스닥시장본부 4.2.3 네이버 금융에서 주식티커 크롤링 태그와 속성, 페이지 내비게이션 값을 결합해 국내 상장주식의 종목명 및 티커를 추출하는 방법을 알아보겠습니다. 네이버 금융에서 [국내증시 → 시가총액] 페이지에는 코스피와 코스닥의 시가총액별 정보가 나타나 있습니다. 코스피: https://finance.naver.com/sise/sise_market_sum.nhn?sosok=0&page=1 코스닥: https://finance.naver.com/sise/sise_market_sum.nhn?sosok=1&page=1 또한 종목명을 클릭해 이동하는 페이지의 URL을 확인해보면, 끝 6자리가 각 종목의 거래소 티커임도 확인이 됩니다. 티커 정리를 위해 HTML에서 확인해야 할 부분은 총 두 가지입니다. 먼저 하단의 페이지 내비게이션을 통해 코스피와 코스닥 시가총액에 해당하는 페이지가 각각 몇 번째 페이지까지 있는지 알아야 합니다. 아래와 같은 항목 중 [맨뒤]에 해당하는 페이지가 가장 마지막 페이지입니다. 그림 4.4: 페이지 네비게이션 [맨뒤]에 마우스 커서를 올려두고 마우스 오른쪽 버튼을 클릭한 후 [검사]를 선택하면 개발자 도구 화면이 열립니다. 여기서 해당 글자가 HTML 내에서 어떤 부분에 위치하는지 확인할 수 있습니다. 해당 링크는 pgRR 클래스 → a 태그 중 href 속성에 위치하며, page= 뒷부분의 숫자에 위치하는 페이지로 링크가 걸려 있습니다. 그림 2.12: HTML 내 페이지 네비게이션 부분 종목명 링크에 해당하는 주소 중 끝 6자리는 티커에 해당합니다. 따라서 각 링크들의 주소를 알아야 할 필요도 있습니다. 그림 4.5: 네이버 금융 시가총액 페이지 삼성전자에 마우스 커서를 올려둔 후 마우스 오른쪽 버튼을 클릭하고 [검사]를 선택합니다. 개발자 도구 화면을 살펴보면 해당 링크가 tbody → td → a 태그의 href 속성에 위치하고 있음을 알 수 있습니다. 위 정보들을 이용해 데이터를 다운로드하겠습니다. 아래 코드에서 i = 0일 경우 코스피에 해당하는 URL이 생성되고, i = 1일 경우 코스닥에 해당하는 URL이 생성됩니다. 먼저 코스피에 해당하는 데이터를 다운로드하겠습니다. library(httr) library(rvest) i = 0 ticker = list() url = paste0('https://finance.naver.com/sise/', 'sise_market_sum.nhn?sosok=',i,'&page=1') down_table = GET(url) 빈 리스트인 ticker 변수를 만들어줍니다. paste0() 함수를 이용해 코스피 시가총액 페이지의 url을 만듭니다. GET() 함수를 통해 해당 페이지 내용을 받아 down_table 변수에 저장합니다. 가장 먼저 해야 할 작업은 마지막 페이지가 몇 번째 페이지인지 찾아내는 작업입니다. 우리는 이미 개발자 도구 화면을 통해 해당 정보가 pgRR 클래스의 a 태그 중 href 속성에 위치하고 있음을 알고 있습니다. navi.final = read_html(down_table, encoding = 'EUC-KR') %>% html_nodes(., '.pgRR') %>% html_nodes(., 'a') %>% html_attr(., 'href') read_html() 함수를 이용해 해당 페이지의 HTML 내용을 읽어오며, 인코딩은 EUC-KR로 설정합니다. html_nodes() 함수를 이용해 pgRR 클래스 정보만 불러오며, 클래스 속성이므로 앞에 마침표(.)를 붙입니다. html_nodes() 함수를 통해 a 태그 정보만 불러옵니다. html_attr() 함수를 통해 href 속성을 불러옵니다. 이를 통해 navi.final에는 해당 부분에 해당하는 내용이 저장됩니다. print(navi.final) ## [1] "/sise/sise_market_sum.nhn?sosok=0&page=32" 이 중 우리가 알고 싶은 내용은 page= 뒤에 있는 숫자입니다. 해당 내용을 추출하는 코드는 다음과 같습니다. navi.final = navi.final %>% strsplit(., '=') %>% unlist() %>% tail(., 1) %>% as.numeric() strsplit() 함수는 전체 문장을 특정 글자 기준으로 나눕니다. page= 뒷부분 의 데이터만 필요하므로 =를 기준으로 문장을 나눠줍니다. unlist() 함수를 통해 결과를 벡터 형태로 변환합니다. tail() 함수를 통해 뒤에서 첫 번째 데이터만 선택합니다. as.numeric() 함수를 통해 해당 값을 숫자 형태로 바꾸어줍니다. print(navi.final) ## [1] 32 코스피 시가총액 페이지는 32번째 페이지까지 있으며, for loop 구문을 이용하면 1페이지부터 navi.final, 즉 32 페이지까지 모든 내용을 읽어올 수 있습니다. 먼저 코스피의 첫 번째 페이지에서 우리가 원하는 데이터를 추출하는 방법을 살펴보겠습니다. i = 0 # 코스피 j = 1 # 첫번째 페이지 url = paste0('https://finance.naver.com/sise/', 'sise_market_sum.nhn?sosok=',i,"&page=",j) down_table = GET(url) i와 j에 각각 0과 1을 입력해 코스피 첫 번째 페이지에 해당하는 url을 생성합니다. GET() 함수를 이용해 해당 페이지의 데이터를 다운로드합니다. Sys.setlocale("LC_ALL", "English") table = read_html(down_table, encoding = "EUC-KR") %>% html_table(fill = TRUE) table = table[[2]] Sys.setlocale("LC_ALL", "Korean") Sys.setlocale() 함수를 통해 로케일 언어를 English로 설정합니다. read_html() 함수를 통해 HTML 정보를 읽어옵니다. html_table() 함수를 통해 테이블 정보를 읽어오며, fill=TRUE를 추가해줍니다. table 변수에는 리스트 형태로 총 세 가지 테이블이 저장되어 있습니다. 첫 번째 리스트에는 거래량, 시가, 고가 등 적용 항목이 저장되어 있고 세 번째 리스트에는 페이지 내비게이션 테이블이 저장되어 있으므로, 우리에게 필요한 두 번째 리스트만을 table 변수에 다시 저장합니다. 한글을 읽기 위해 Sys.setlocale() 함수를 통해 로케일 언어를 다시 Korean으로 변경합니다. 저장된 테이블 내용을 확인하면 다음과 같습니다. print(head(table)) ## N 종목명 현재가 전일비 등락률 액면가 시가총액 ## 1 NA ## 2 1 삼성전자 86,800 1,300 -1.48% 100 5,181,771 ## 3 2 SK하이닉스 128,500 3,000 -2.28% 5,000 935,483 ## 4 3 LG화학 975,000 13,000 -1.32% 5,000 688,275 ## 5 4 삼성전자우 77,600 0 0.00% 100 638,560 ## 6 5 NAVER 343,500 21,000 +6.51% 100 564,245 ## 상장주식수 외국인비율 거래량 PER ROE 토론실 ## 1 NA <NA> <NA> NA ## 2 5,969,783 55.49 30,430,330 23.70 8.69 NA ## 3 728,002 50.16 3,914,900 32.60 4.25 NA ## 4 70,592 43.70 385,932 91.10 1.84 NA ## 5 822,887 80.26 3,006,672 21.19 N/A NA ## 6 164,263 56.80 3,258,379 68.39 10.56 NA 이 중 마지막 열인 토론실은 필요 없는 열이며, 첫 번째 행과 같이 아무런 정보가 없는 행도 있습니다. 이를 다음과 같이 정리해줍니다. table[, ncol(table)] = NULL table = na.omit(table) print(head(table)) ## N 종목명 현재가 전일비 등락률 액면가 시가총액 ## 2 1 삼성전자 86,800 1,300 -1.48% 100 5,181,771 ## 3 2 SK하이닉스 128,500 3,000 -2.28% 5,000 935,483 ## 4 3 LG화학 975,000 13,000 -1.32% 5,000 688,275 ## 5 4 삼성전자우 77,600 0 0.00% 100 638,560 ## 6 5 NAVER 343,500 21,000 +6.51% 100 564,245 ## 10 6 현대차 257,000 7,500 -2.84% 5,000 549,127 ## 상장주식수 외국인비율 거래량 PER ROE ## 2 5,969,783 55.49 30,430,330 23.70 8.69 ## 3 728,002 50.16 3,914,900 32.60 4.25 ## 4 70,592 43.70 385,932 91.10 1.84 ## 5 822,887 80.26 3,006,672 21.19 N/A ## 6 164,263 56.80 3,258,379 68.39 10.56 ## 10 213,668 31.29 2,071,264 61.41 4.32 이제 필요한 정보는 6자리 티커입니다. 티커 역시 개발자 도구 화면을 통해 tbody → td → a 태그의 href 속성에 위치하고 있음을 알고 있습니다. 티커를 추출하는 코드는 다음과 같습니다. symbol = read_html(down_table, encoding = 'EUC-KR') %>% html_nodes(., 'tbody') %>% html_nodes(., 'td') %>% html_nodes(., 'a') %>% html_attr(., 'href') print(head(symbol, 10)) ## [1] "/item/main.nhn?code=005930" ## [2] "/item/board.nhn?code=005930" ## [3] "/item/main.nhn?code=000660" ## [4] "/item/board.nhn?code=000660" ## [5] "/item/main.nhn?code=051910" ## [6] "/item/board.nhn?code=051910" ## [7] "/item/main.nhn?code=005935" ## [8] "/item/board.nhn?code=005935" ## [9] "/item/main.nhn?code=035420" ## [10] "/item/board.nhn?code=035420" read_html() 함수를 통해 HTML 정보를 읽어오며, 인코딩은 EUC-KR로 설정합니다. html_nodes() 함수를 통해 tbody 태그 정보를 불러옵니다. 다시 html_nodes() 함수를 통해 td와 a 태그 정보를 불러옵니다. html_attr() 함수를 이용해 href 속성을 불러옵니다. 이를 통해 symbol에는 href 속성에 해당하는 링크 주소들이 저장됩니다. 이 중 마지막 6자리 글자만 추출하는 코드는 다음과 같습니다. library(stringr) symbol = sapply(symbol, function(x) { str_sub(x, -6, -1) }) print(head(symbol, 10)) ## /item/main.nhn?code=005930 /item/board.nhn?code=005930 ## "005930" "005930" ## /item/main.nhn?code=000660 /item/board.nhn?code=000660 ## "000660" "000660" ## /item/main.nhn?code=051910 /item/board.nhn?code=051910 ## "051910" "051910" ## /item/main.nhn?code=005935 /item/board.nhn?code=005935 ## "005935" "005935" ## /item/main.nhn?code=035420 /item/board.nhn?code=035420 ## "035420" "035420" sapply() 함수를 통해 symbol 변수의 내용들에 function()을 적용하며, stringr 패키지의 str_sub() 함수를 이용해 마지막6자리 글자만 추출합니다. 결과를 살펴보면 티커에 해당하는 마지막 6글자만 추출되지만 동일한 내용이 두 번 연속해 추출됩니다. 이는 main.nhn?code=에 해당하는 부분은 종목명에 설정된 링크, board.nhn?code=에 해당하는 부분은 토론실에 설정된 링크이기 때문입니다. symbol = unique(symbol) print(head(symbol, 10)) ## [1] "005930" "000660" "051910" "005935" "035420" ## [6] "005380" "006400" "207940" "068270" "035720" unique() 함수를 이용해 중복되는 티커를 제거하면 우리가 원하는 티커 부분만 깔끔하게 정리됩니다. 해당 내용을 위에서 구한 테이블에 입력한 후 데이터를 다듬는 과정은 다음과 같습니다. table$N = symbol colnames(table)[1] = '종목코드' rownames(table) = NULL ticker[[j]] = table 위에서 구한 티커를 N열에 입력합니다. 해당 열 이름을 종목코드로 변경합니다. na.omit() 함수를 통해 특정 행을 삭제했으므로, 행 이름을 초기화해줍니다. ticker의 j번째 리스트에 정리된 데이터를 입력합니다. 위의 코드에서 i와 j 값을 for loop 구문에 이용하면 코스피와 코스닥 전 종목의 티커가 정리된 테이블을 만들 수 있습니다. 이를 전체 코드로 나타내면 다음과 같습니다. data = list() # i = 0 은 코스피, i = 1 은 코스닥 종목 for (i in 0:1) { ticker = list() url = paste0('https://finance.naver.com/sise/', 'sise_market_sum.nhn?sosok=',i,'&page=1') down_table = GET(url) # 최종 페이지 번호 찾아주기 navi.final = read_html(down_table, encoding = "EUC-KR") %>% html_nodes(., ".pgRR") %>% html_nodes(., "a") %>% html_attr(.,"href") %>% strsplit(., "=") %>% unlist() %>% tail(., 1) %>% as.numeric() # 첫번째 부터 마지막 페이지까지 for loop를 이용하여 테이블 추출하기 for (j in 1:navi.final) { # 각 페이지에 해당하는 url 생성 url = paste0( 'https://finance.naver.com/sise/', 'sise_market_sum.nhn?sosok=',i,"&page=",j) down_table = GET(url) Sys.setlocale("LC_ALL", "English") # 한글 오류 방지를 위해 영어로 로케일 언어 변경 table = read_html(down_table, encoding = "EUC-KR") %>% html_table(fill = TRUE) table = table[[2]] # 원하는 테이블 추출 Sys.setlocale("LC_ALL", "Korean") # 한글을 읽기위해 로케일 언어 재변경 table[, ncol(table)] = NULL # 토론식 부분 삭제 table = na.omit(table) # 빈 행 삭제 # 6자리 티커만 추출 symbol = read_html(down_table, encoding = "EUC-KR") %>% html_nodes(., "tbody") %>% html_nodes(., "td") %>% html_nodes(., "a") %>% html_attr(., "href") symbol = sapply(symbol, function(x) { str_sub(x, -6, -1) }) symbol = unique(symbol) # 테이블에 티커 넣어준 후, 테이블 정리 table$N = symbol colnames(table)[1] = "종목코드" rownames(table) = NULL ticker[[j]] = table Sys.sleep(0.5) # 페이지 당 0.5초의 슬립 적용 } # do.call을 통해 리스트를 데이터 프레임으로 묶기 ticker = do.call(rbind, ticker) data[[i + 1]] = ticker } # 코스피와 코스닥 테이블 묶기 data = do.call(rbind, data) http://hkconsensus.hankyung.com/↩︎ http://kind.krx.co.kr/↩︎ https://finance.naver.com/news/news_list.nhn?mode=LSS2D&section_id=101&section_id2=258↩︎ "],
["금융-데이터-수집하기-기본.html", "Chapter 5 금융 데이터 수집하기 (기본) 5.1 한국거래소의 산업별 현황 및 개별지표 크롤링 5.2 WICS 기준 섹터정보 크롤링", " Chapter 5 금융 데이터 수집하기 (기본) API와 크롤링을 이용한다면 비용을 지불하지 않고 얼마든지 금융 데이터를 수집할 수있습니다. 이 CHAPTER에서는 금융 데이터를 받기 위해 필요한 주식티커를 구하는 방법과 섹터별 구성종목을 크롤링하는 방법을 알아보겠습니다. 5.1 한국거래소의 산업별 현황 및 개별지표 크롤링 앞 CHAPTER의 예제를 통해 네이버 금융에서 주식티커를 크롤링하는 방법을 살펴보았습니다. 그러나 이 방법은 지나치게 복잡하고 시간이 오래 걸립니다. 반면 한국거래소에서 제공하는 업종분류 현황과 개별종목 지표 데이터를 이용하면 훨씬 간단하게 주식티커 데이터를 수집할 수 있습니다. KRX 정보데이터시스템 http://data.krx.co.kr/ 에서 [기본통계 → 주식 → 세부안내] 부분 [12025] 업종분류 현황 [12021] 개별종목 해당 데이터들을 크롤링이 아닌 [Excel] 버튼을 클릭해 엑셀 파일로 받을 수도 있습니다. 그러나 매번 엑셀 파일을 다운로드하고 이를 R로 불러오는 작업은 상당히 비효율적이며, 크롤링을 이용한다면 해당 데이터를 R로 직접 불러올 수 있습니다. 5.1.1 업종분류 현황 크롤링 먼저 업종분류 현황에 해당하는 페이지에 접속한 후 개발자 도구 화면을 열고 [다운로드] 버튼을 클릭한 후 [CSV]를 누릅니다. [Network] 탭에는 generate.cmd와 download.cmd 두 가지 항목이 있습니다. 거래소에서 엑셀 데이터를 받는 과정은 다음과 같습니다. http://data.krx.co.kr/comm/fileDn/download_excel/download.cmd 에 원하는 항목을 쿼리로 발송하면 해당 쿼리에 해당하는 OTP(generate.cmd)를 받게 됩니다. 부여받은 OTP를 http://data.krx.co.kr/에 제출하면 이에 해당하는 데이터(download.cmd)를 다운로드하게 됩니다. 먼저 1번 단계를 살펴보겠습니다. 그림 1.3: OTP 생성 부분 General 항목의 Request URL의 앞부분이 원하는 항목을 제출할 주소입니다. Form Data에는 우리가 원하는 항목들이 적혀 있습니다. 이를 통해 POST 방식으로 데이터를 요청함을 알 수 있습니다. 다음으로 2번 단계를 살펴보겠습니다. 그림 2.1: OTP 제출 부분 General 항목의 Request URL은 OTP를 제출할 주소입니다. Form Data의 OTP는 1번 단계에서 부여받은 OTP에 해당합니다. 이 역시 POST 방식으로 데이터를 요청합니다. 위 과정을 코드로 나타내면 다음과 같습니다. library(httr) library(rvest) library(readr) gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' gen_otp_data = list( mktId = 'STK', trdDd = '20210108', money = '1', csvxls_isNo = 'false', name = 'fileDown', url = 'dbms/MDC/STAT/standard/MDCSTAT03901' ) otp = POST(gen_otp_url, query = gen_otp_data) %>% read_html() %>% html_text() gen_otp_url에 원하는 항목을 제출할 URL을 입력합니다. 개발자 도구 화면에 나타는 쿼리 내용들을 리스트 형태로 입력합니다. 이 중 mktId의 STK는 코스피에 해당하는 내용이며, 코스닥 데이터를 받고자 할 경우 KSQ를 입력해야 합니다. POST() 함수를 통해 해당 URL에 쿼리를 전송하면 이에 해당하는 데이터를 받게 됩니다. read_html()함수를 통해 HTML 내용을 읽어옵니다. html_text() 함수는 HTML 내에서 텍스트에 해당하는 부분만을 추출합니다. 이를 통해 OTP 값만 추출하게 됩니다. 위의 과정을 거쳐 생성된 OTP를 제출하면, 우리가 원하는 데이터를 다운로드할 수 있습니다. down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd' down_sector_KS = POST(down_url, query = list(code = otp), add_headers(referer = gen_otp_url)) %>% read_html(encoding = 'EUC-KR') %>% html_text() %>% read_csv() OTP를 제출할 URL을 down_url에 입력합니다. POST() 함수를 통해 위에서 부여받은 OTP 코드를 해당 URL에 제출합니다. add_headers() 구문을 통해 리퍼러(referer)를 추가해야 합니다. 리퍼러란 링크를 통해서 각각의 웹사이트로 방문할 때 남는 흔적입니다. 거래소 데이터를 다운로드하는 과정을 살펴보면 첫 번째 URL에서 OTP를 부여받고, 이를 다시 두번째 URL에 제출했습니다. 그런데 이러한 과정의 흔적이 없이 OTP를 바로 두번째 URL에 제출하면 서버는 이를 로봇으로 인식해 데이터를 반환하지 않습니다. 따라서 add_headers() 함수를 통해 우리가 거쳐온 과정을 흔적으로 남겨 야 데이터를 반환하게 되며 첫 번째 URL을 리퍼러로 지정해줍니다. read_html()과 html_text() 함수를 통해 텍스트 데이터만 추출합니다. EUC-KR로 인코딩이 되어 있으므로 read_html() 내에 이를 입력해줍니다. read_csv() 함수는 csv 형태의 데이터를 불러옵니다. print(down_sector_KS) ## # A tibble: 917 x 8 ## 종목코드 종목명 시장구분 업종명 종가 대비 등락률 ## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> ## 1 095570 AJ네트웍… KOSPI 서비스업… 4540 -155 -3.3 ## 2 006840 AK홀딩스… KOSPI 기타금융… 25350 150 0.6 ## 3 027410 BGF KOSPI 기타금융… 4905 -25 -0.51 ## 4 282330 BGF리테… KOSPI 유통업 141000 4500 3.3 ## 5 138930 BNK금융… KOSPI 기타금융… 5780 0 0 ## 6 001460 BYC KOSPI 섬유의복… 324500 10500 3.34 ## 7 001465 BYC우 KOSPI 섬유의복… 157500 10000 6.78 ## 8 001040 CJ KOSPI 기타금융… 102500 7600 8.01 ## 9 079160 CJ CGV KOSPI 서비스업… 26150 300 1.16 ## 10 00104K CJ4우(… KOSPI 기타금융… 81400 5300 6.96 ## # … with 907 more rows, and 1 more variable: ## # 시가총액 <dbl> 위 과정을 통해 down_sector 변수에는 산업별 현황 데이터가 저장되었습니다. 코스닥 시장의 데이터도 다운로드 받도록 하겠습니다. gen_otp_data = list( mktId = 'KSQ', # 코스닥으로 변경 trdDd = '20210108', money = '1', csvxls_isNo = 'false', name = 'fileDown', url = 'dbms/MDC/STAT/standard/MDCSTAT03901' ) otp = POST(gen_otp_url, query = gen_otp_data) %>% read_html() %>% html_text() down_sector_KQ = POST(down_url, query = list(code = otp), add_headers(referer = gen_otp_url)) %>% read_html(encoding = 'EUC-KR') %>% html_text() %>% read_csv() 코스피 데이터와 코스닥 데이터를 하나로 합치도록 합니다. down_sector = rbind(down_sector_KS, down_sector_KQ) 이를 csv 파일로 저장하겠습니다. ifelse(dir.exists('data'), FALSE, dir.create('data')) write.csv(down_sector, 'data/krx_sector.csv') 먼저 ifelse() 함수를 통해 data라는 이름의 폴더가 있으면 FALSE를 반환하고, 없으면 해당 이름으로 폴더를 생성해줍니다. 그 후 앞서 다운로드한 데이터를 data 폴더 안에 krx_sector.csv 이름으로 저장합니다. 해당 폴더를 확인해보면 데이터가 csv 형태로 저장되어 있습니다. 5.1.2 개별종목 지표 크롤링 개별종목 데이터를 크롤링하는 방법은 위와 매우 유사하며, 요청하는 쿼리 값에만 차이가 있습니다. 개발자 도구 화면을 열고 [CSV] 버튼을 클릭해 어떠한 쿼리를 요청하는지 확인합니다. 그림 2.7: 개별지표 OTP 생성 부분 이 중 tboxisuCd_finder_stkisu0_6, isu_Cd, isu_Cd2 등의 항목은 조회 구분의 개별추이 탭에 해당하는 부분이므로 우리가 원하는 전체 데이터를 받을 때는 필요하지 않은 요청값입니다. 이를 제외한 요청값을 산업별 현황 예제에 적용하면 해당 데이터 역시 손쉽게 다운로드할 수 있습니다. library(httr) library(rvest) library(readr) gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' gen_otp_data = list( searchType = '1', mktId = 'ALL', trdDd = '20210108', csvxls_isNo = 'false', name = 'fileDown', url = 'dbms/MDC/STAT/standard/MDCSTAT03501' ) otp = POST(gen_otp_url, query = gen_otp_data) %>% read_html() %>% html_text() down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd' down_ind = POST(down_url, query = list(code = otp), add_headers(referer = gen_otp_url)) %>% read_html(encoding = 'EUC-KR') %>% html_text() %>% read_csv() print(down_ind) ## # A tibble: 2,345 x 11 ## 종목코드 종목명 종가 대비 등락률 EPS PER ## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 060310 3S 2245 -45 -1.97 NA NA ## 2 095570 AJ네트웍… 4540 -155 -3.3 982 4.62 ## 3 006840 AK홀딩스… 25350 150 0.6 2168 11.7 ## 4 054620 APS홀딩… 7500 -150 -1.96 NA NA ## 5 265520 AP시스템… 26000 -100 -0.38 671 38.8 ## 6 211270 AP위성 8100 -250 -2.99 51 159. ## 7 027410 BGF 4905 -25 -0.51 281 17.5 ## 8 282330 BGF리테… 141000 4500 3.3 8763 16.1 ## 9 138930 BNK금융… 5780 0 0 1647 3.51 ## 10 001460 BYC 324500 10500 3.34 33265 9.75 ## # … with 2,335 more rows, and 4 more variables: ## # BPS <dbl>, PBR <dbl>, 주당배당금 <dbl>, ## # 배당수익률 <dbl> 위 과정을 통해 down_ind 변수에는 개별종목 지표 데이터가 저장되었습니다. 해당 데이터 역시 csv 파일로 저장하겠습니다. write.csv(down_ind, 'data/krx_ind.csv') 5.1.3 최근 영업일 기준 데이터 받기 위 예제의 쿼리 항목 중 date와 schdate 부분을 원하는 일자로 입력하면(예: 20190104) 해당일의 데이터를 다운로드할 수 있으며, 전 영업일 날짜를 입력하면 가장 최근의 데이터를 받을 수 있습니다. 그러나 매번 해당 항목을 입력하기는 번거로우므로 자동으로 반영되게 할 필요가 있습니다. 네이버 금융의 [국내증시 → 증시자금동향]에는 이전 2영업일에 해당하는 날짜가 있으며, 자동으로 날짜가 업데이트되어 편리합니다. 따라서 해당 부분을 크롤링해 쿼리 항목에 사용할 수 있습니다. 그림 2.9: 최근 영업일 부분 크롤링하고자 하는 데이터가 하나거나 소수일때는 HTML 구조를 모두 분해한 후 데이터를 추출하는 것보다 Xpath를 이용하는 것이 훨씬 효율적입니다. Xpath란 XML 중 특정 값의 태그나 속성을 찾기 쉽게 만든 주소라 생각하면 됩니다. 예를 들어 R 프로그램이 저장된 곳을 윈도우 탐색기를 이용해 이용하면 C:\\Program Files\\R\\R-3.4.2 형태의 주소를 보이는데 이것은 윈도우의 path 문법입니다. XML 역시 이와 동일한 개념의 Xpath가 있습니다. 웹페이지에서 Xpath를 찾는 법은 다음과 같습니다. 그림 5.1: Xpath 복사하기 먼저 크롤링하고자 하는 내용에 마우스 커서를 올린 채 마우스 오른쪽 버튼을 클릭한 후 [검사]를 선택합니다. 그러면 개발자 도구 화면이 열리며 해당 지점의 HTML 부분이 선택됩니다. 그 후 HTML 화면에서 마우스 오른쪽 버튼을 클릭하고 [Copy → Copy Xpath]를 선택하면 해당 지점의 Xpath가 복사됩니다. //*[@id="type_0"]/div/ul[2]/li/span //*[@id=\"type_0\"]/div/ul[2]/li/span 위에서 구한 날짜의 Xpath를 이용해 해당 데이터를 크롤링하겠습니다. library(httr) library(rvest) library(stringr) url = 'https://finance.naver.com/sise/sise_deposit.nhn' biz_day = GET(url) %>% read_html(encoding = 'EUC-KR') %>% html_nodes(xpath = '//*[@id="type_1"]/div/ul[2]/li/span') %>% html_text() %>% str_match(('[0-9]+.[0-9]+.[0-9]+') ) %>% str_replace_all('\\\\.', '') print(biz_day) ## [1] "20210120" 페이지의 url을 저장합니다. GET() 함수를 통해 해당 페이지 내용을 받습니다. read_html() 함수를 이용해 해당 페이지의 HTML 내용을 읽어오며, 인코딩은 EUC-KR로 설정합니다. html_node() 함수 내에 위에서 구한 Xpath를 입력해서 해당 지점의 데이터를 추출합니다. html_text() 함수를 통해 텍스트 데이터만을 추출합니다. str_match() 함수 내에서 정규표현식11을 이용해 숫자.숫자.숫자 형식의 데이터를 추출합니다. str_replace_all() 함수를 이용해 마침표(.)를 모두 없애줍니다. 이처럼 Xpath를 이용하면 태그나 속성을 분해하지 않고도 원하는 지점의 데이터를 크롤링할 수 있습니다. 위 과정을 통해 yyyymmdd 형태의 날짜만 남게 되었습니다. 이를 위의 date와 schdate에 입력하면 산업별 현황과 개별종목 지표를 최근일자 기준으로 다운로드하게 됩니다. 전체 코드는 다음과 같습니다. library(httr) library(rvest) library(stringr) library(readr) # 최근 영업일 구하기 url = 'https://finance.naver.com/sise/sise_deposit.nhn' biz_day = GET(url) %>% read_html(encoding = 'EUC-KR') %>% html_nodes(xpath = '//*[@id="type_1"]/div/ul[2]/li/span') %>% html_text() %>% str_match(('[0-9]+.[0-9]+.[0-9]+') ) %>% str_replace_all('\\\\.', '') # 코스피 업종분류 OTP 발급 gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' gen_otp_data = list( mktId = 'STK', trdDd = biz_day, # 최근영업일로 변경 money = '1', csvxls_isNo = 'false', name = 'fileDown', url = 'dbms/MDC/STAT/standard/MDCSTAT03901' ) otp = POST(gen_otp_url, query = gen_otp_data) %>% read_html() %>% html_text() # 코스피 업종분류 데이터 다운로드 down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd' down_sector_KS = POST(down_url, query = list(code = otp), add_headers(referer = gen_otp_url)) %>% read_html(encoding = 'EUC-KR') %>% html_text() %>% read_csv() # 코스닥 업종분류 OTP 발급 gen_otp_data = list( mktId = 'KSQ', trdDd = biz_day, # 최근영업일로 변경 money = '1', csvxls_isNo = 'false', name = 'fileDown', url = 'dbms/MDC/STAT/standard/MDCSTAT03901' ) otp = POST(gen_otp_url, query = gen_otp_data) %>% read_html() %>% html_text() # 코스닥 업종분류 데이터 다운로드 down_sector_KQ = POST(down_url, query = list(code = otp), add_headers(referer = gen_otp_url)) %>% read_html(encoding = 'EUC-KR') %>% html_text() %>% read_csv() down_sector = rbind(down_sector_KS, down_sector_KQ) ifelse(dir.exists('data'), FALSE, dir.create('data')) write.csv(down_sector, 'data/krx_sector.csv') # 개별종목 지표 OTP 발급 gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' gen_otp_data = list( searchType = '1', mktId = 'ALL', trdDd = biz_day, # 최근영업일로 변경 csvxls_isNo = 'false', name = 'fileDown', url = 'dbms/MDC/STAT/standard/MDCSTAT03501' ) otp = POST(gen_otp_url, query = gen_otp_data) %>% read_html() %>% html_text() # 개별종목 지표 데이터 다운로드 down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd' down_ind = POST(down_url, query = list(code = otp), add_headers(referer = gen_otp_url)) %>% read_html(encoding = 'EUC-KR') %>% html_text() %>% read_csv() write.csv(down_ind, 'data/krx_ind.csv') 5.1.4 거래소 데이터 정리하기 위에서 다운로드한 데이터는 중복된 열이 있으며, 불필요한 데이터 역시 있습니다. 따라서 하나의 테이블로 합친 후 정리할 필요가 있습니다. 먼저 다운로드한 csv 파일을 읽어옵니다. down_sector = read.csv('data/krx_sector.csv', row.names = 1, stringsAsFactors = FALSE) down_ind = read.csv('data/krx_ind.csv', row.names = 1, stringsAsFactors = FALSE) read.csv() 함수를 이용해 csv 파일을 불러옵니다. row.names = 1을 통해 첫 번째 열을 행 이름으로 지정하고, stringsAsFactors = FALSE를 통해 문자열 데이터가 팩터 형태로 변형되지 않게 합니다. intersect(names(down_sector), names(down_ind)) ## [1] "종목코드" "종목명" "종가" "대비" ## [5] "등락률" 먼저 intersect() 함수를 통해 두 데이터 간 중복되는 열 이름을 살펴보면 종목코드와 종목명 등이 동일한 위치에 있습니다. setdiff(down_sector[, '종목명'], down_ind[ ,'종목명']) ## [1] "ESR켄달스퀘어리츠" "NH프라임리츠" ## [3] "롯데리츠" "맥쿼리인프라" ## [5] "맵스리얼티1" "모두투어리츠" ## [7] "미래에셋맵스리츠" "바다로19호" ## [9] "베트남개발1" "신한알파리츠" ## [11] "에이리츠" "엘브이엠씨홀딩스" ## [13] "이리츠코크렙" "이지스레지던스리츠" ## [15] "이지스밸류리츠" "제이알글로벌리츠" ## [17] "케이탑리츠" "코람코에너지리츠" ## [19] "하이골드12호" "하이골드3호" ## [21] "한국ANKOR유전" "한국패러랠" ## [23] "GRT" "JTC" ## [25] "SBI핀테크솔루션즈" "SNK" ## [27] "골든센츄리" "글로벌에스엠" ## [29] "넥스틴" "뉴프라이드" ## [31] "로스웰" "미투젠" ## [33] "소마젠(Reg.S)" "씨케이에이치" ## [35] "에스앤씨엔진그룹" "엑세스바이오" ## [37] "오가닉티코스메틱" "윙입푸드" ## [39] "이스트아시아홀딩스" "잉글우드랩" ## [41] "컬러레이" "코오롱티슈진" ## [43] "크리스탈신소재" "헝셩그룹" setdiff() 함수를 통해 두 데이터에 공통적으로 없는 종목명, 즉 하나의 데이터에만 있는 종목을 살펴보면 위와 같습니다. 해당 종목들은 선박펀드, 광물펀드, 해외종목 등 일반적이지 않은 종목들이므로 제외하는 것이 좋습니다. 따라서 둘 사이에 공통적으로 존재하는 종목을 기준으로 데이터를 합쳐주겠습니다. KOR_ticker = merge(down_sector, down_ind, by = intersect(names(down_sector), names(down_ind)), all = FALSE ) merge() 함수는 by를 기준으로 두 데이터를 하나로 합치며, 공통으로 존재하는 종목코드, 종목명, 종가, 대비, 등락률을 기준으로 입력해줍니다. 또한 all 값을 TRUE로 설정하면 합집합을 반환하고, FALSE로 설정하면 교집합을 반환합니다. 공통으로 존재하는 항목을 원하므로 여기서는 FALSE를 입력합니다. KOR_ticker = KOR_ticker[order(-KOR_ticker['시가총액']), ] print(head(KOR_ticker)) ## 종목코드 종목명 종가 대비 등락률 ## 327 005930 삼성전자 89700 -900 -0.99 ## 45 000660 SK하이닉스 133000 4000 3.10 ## 1076 051910 LG화학 1000000 38000 3.95 ## 328 005935 삼성전자우 78600 -1400 -1.75 ## 299 005380 현대차 259000 -2000 -0.77 ## 1928 207940 삼성바이오로직스 830000 12000 1.47 ## 시장구분 업종명 시가총액 EPS PER BPS ## 327 KOSPI 전기전자 5.355e+14 3166 28.33 37528 ## 45 KOSPI 전기전자 9.682e+13 2943 45.19 65836 ## 1076 KOSPI 화학 7.059e+13 4085 244.80 217230 ## 328 KOSPI 전기전자 6.468e+13 NA NA NA ## 299 KOSPI 운수장비 5.534e+13 11310 22.90 253001 ## 1928 KOSPI 의약품 5.492e+13 3067 270.62 65812 ## PBR 주당배당금 배당수익률 ## 327 2.39 1416 1.58 ## 45 2.02 1000 0.75 ## 1076 4.60 2000 0.20 ## 328 NA 1417 1.80 ## 299 1.02 4000 1.54 ## 1928 12.61 0 0.00 데이터를 시가총액 기준으로 내림차순 정렬할 필요도 있습니다. order() 함수를 통해 상대적인 순서를 구할 수 있습니다. R은 기본적으로 오름차순으로 순서를 구하므로 앞에 마이너스(-)를 붙여 내림차순 형태로 바꿉니다. 결과적으로 시가총액 기준 내림차 순으로 해당 데이터가 정렬됩니다. 마지막으로 스팩, 우선주 종목 역시 제외해야 합니다. library(stringr) KOR_ticker[grepl('스팩', KOR_ticker[, '종목명']), '종목명'] ## [1] "엔에이치스팩14호" "유안타제3호스팩" ## [3] "케이비제18호스팩" "삼성스팩2호" ## [5] "교보8호스팩" "엔에이치스팩17호" ## [7] "유안타제6호스팩" "미래에셋대우스팩3호" ## [9] "케이비제20호스팩" "DB금융스팩8호" ## [11] "유안타제5호스팩" "SK6호스팩" ## [13] "대신밸런스제8호스팩" "케이비17호스팩" ## [15] "유안타제7호스팩" "교보10호스팩" ## [17] "IBKS제13호스팩" "대신밸런스제7호스팩" ## [19] "한화에스비아이스팩" "미래에셋대우스팩 5호" ## [21] "하이제5호스팩" "SK4호스팩" ## [23] "신한제6호스팩" "에이치엠씨제5호스팩" ## [25] "한국제7호스팩" "하나금융15호스팩" ## [27] "유안타제4호스팩" "한화플러스제1호스팩" ## [29] "하나머스트제6호스팩" "상상인이안1호스팩" ## [31] "엔에이치스팩18호" "IBKS제14호스팩" ## [33] "하나금융14호스팩" "엔에이치스팩16호" ## [35] "미래에셋대우스팩4호" "SK5호스팩" ## [37] "케이비제19호스팩" "신영스팩6호" ## [39] "에이치엠씨제4호스팩" "대신밸런스제9호스팩" ## [41] "엔에이치스팩13호" "하나금융16호스팩" ## [43] "유진스팩5호" "교보9호스팩" ## [45] "상상인이안제2호스팩" "키움제5호스팩" ## [47] "이베스트스팩5호" "신영스팩5호" ## [49] "유진스팩4호" "한국제8호스팩" ## [51] "IBKS제12호스팩" "이베스트이안스팩1호" KOR_ticker[str_sub(KOR_ticker[, '종목코드'], -1, -1) != 0, '종목명'] ## [1] "삼성전자우" "현대차2우B" ## [3] "LG화학우" "현대차우" ## [5] "LG생활건강우" "LG전자우" ## [7] "삼성SDI우" "아모레퍼시픽우" ## [9] "미래에셋대우2우B" "삼성화재우" ## [11] "한국금융지주우" "신영증권우" ## [13] "CJ4우(전환)" "삼성전기우" ## [15] "한화3우B" "아모레G3우(전환)" ## [17] "현대차3우B" "신풍제약우" ## [19] "대신증권우" "SK케미칼우" ## [21] "CJ제일제당 우" "SK이노베이션우" ## [23] "LG우" "삼성물산우B" ## [25] "대림산업우" "두산퓨얼셀1우" ## [27] "금호석유우" "S-Oil우" ## [29] "NH투자증권우" "두산우" ## [31] "SK우" "CJ우" ## [33] "아모레G우" "솔루스첨단소재1우" ## [35] "녹십자홀딩스2우" "대신증권2우B" ## [37] "유한양행우" "한화솔루션우" ## [39] "SK디스커버리우" "미래에셋대우우" ## [41] "호텔신라우" "코오롱인더우" ## [43] "롯데지주우" "두산퓨얼셀2우B" ## [45] "부국증권우" "두산2우B" ## [47] "GS우" "솔루스첨단소재2우B" ## [49] "대교우B" "대한항공우" ## [51] "롯데칠성우" "유화증권우" ## [53] "삼성중공우" "LG하우시스우" ## [55] "BYC우" "유안타증권우" ## [57] "티와이홀딩스우" "일양약품우" ## [59] "남양유업우" "세방우" ## [61] "한진칼우" "대상우" ## [63] "하이트진로2우B" "코리아써우" ## [65] "한화우" "대덕전자1우" ## [67] "SK증권우" "덕성우" ## [69] "현대건설우" "한화투자증권우" ## [71] "태영건설우" "넥센타이어1우B" ## [73] "삼양사우" "코오롱우" ## [75] "삼양홀딩스우" "유유제약1우" ## [77] "DB하이텍1우" "남선알미우" ## [79] "NPC우" "SK네트웍스우" ## [81] "루트로닉3우C" "서울식품우" ## [83] "넥센우" "성신양회우" ## [85] "대덕1우" "계양전기우" ## [87] "금호산업우" "대한제당우" ## [89] "태양금속우" "코오롱글로벌우" ## [91] "한양증권우" "동원시스템즈우" ## [93] "크라운제과우" "CJ씨푸드1우" ## [95] "크라운해태홀딩스우" "대상홀딩스우" ## [97] "현대비앤지스틸우" "대원전선우" ## [99] "흥국화재우" "깨끗한나라우" ## [101] "금강공업우" "하이트진로홀딩스우" ## [103] "JW중외제약우" "KG동부제철우" ## [105] "대호피앤씨우" "노루페인트우" ## [107] "코리아써키트2우B" "진흥기업우B" ## [109] "동부건설우" "성문전자우" ## [111] "JW중외제약2우B" "유유제약2우B" ## [113] "동양우" "소프트센우" ## [115] "동양2우B" "진흥기업2우B" ## [117] "신원우" "노루홀딩스우" ## [119] "흥국화재2우B" "동양3우B" grepl() 함수를 통해 종목명에 ‘스팩’이 들어가는 종목을 찾고, stringr 패키지의 str_sub() 함수를 통해 종목코드 끝이 0이 아닌 우선주 종목을 찾을 수 있습니다. KOR_ticker = KOR_ticker[!grepl('스팩', KOR_ticker[, '종목명']), ] KOR_ticker = KOR_ticker[str_sub(KOR_ticker[, '종목코드'], -1, -1) == 0, ] 마지막으로 행 이름을 초기화한 후 정리된 데이터를 csv 파일로 저장합니다. rownames(KOR_ticker) = NULL write.csv(KOR_ticker, 'data/KOR_ticker.csv') 5.2 WICS 기준 섹터정보 크롤링 일반적으로 주식의 섹터를 나누는 기준은 MSCI와 S&P가 개발한 GICS12를 가장 많이 사용합니다. 국내 종목의 GICS 기준 정보 역시 한국거래소에서 제공하고 있으나, 이는 독점적 지적재산으로 명시했기에 사용하는 데 무리가 있습니다. 그러나 지수제공업체인 와이즈인덱스13에서는 GICS와 비슷한 WICS 산업분류를 발표하고 있습니다. WICS를 크롤링해 필요한 정보를 수집해보겠습니다. 먼저 웹페이지에 접속해 [Index → WISE SECTOR INDEX → WICS → 에너지]를 클릭합니다. 그 후 [Components] 탭을 클릭하면 해당 섹터의 구성종목을 확인할 수 있습니다. 그림 5.2: WICS 기준 구성종목 개발자도구 화면(그림 5.3)을 통해 해당 페이지의 데이터전송 과정을 살펴보도록 하겠습니다. 그림 5.3: WICS 페이지 개발자도구 화면 일자를 선택하면 [Network] 탭의 GetIndexComponets 항목을 통해 데이터 전송 과정이 나타납니다. Request URL의 주소를 살펴보면 다음과 같습니다. http://www.wiseindex.com/Index/GetIndexComponets: 데이터를 요청하는 url 입니다. ceil_yn = 0: 실링 여부를 나타내며, 0은 비실링을 의미합니다. dt=20190607: 조회일자를 나타냅니다. sec_cd=G10: 섹터 코드를 나타냅니다. 이번엔 위 주소의 페이지를 열어보겠습니다. 그림 5.4: WICS 데이터 페이지 글자들은 페이지에 출력된 내용이지만 매우 특이한 형태로 구성되어 있는데 이것은 JSON 형식의 데이터입니다. 기존에 우리가 살펴보았던 대부분의 웹페이지는 XML 형식으로 표현되어 있습니다. XML 형식은 문법이 복잡하고 표현 규칙이 엄격해 데이터의 용량이 커지는 단점이 있습니다. 반면 JSON 형식은 문법이 단순하고 데이터의 용량이 작아 빠른 속도로 데이터를 교환할 수 있습니다. R에서는 jsonlite 패키지의 fromJSON() 함수를 사용해 매우 손쉽게 JSON 형식의 데이터를 크롤링할 수 있습니다. library(jsonlite) url = 'http://www.wiseindex.com/Index/GetIndexComponets?ceil_yn=0&dt=20190607&sec_cd=G10' data = fromJSON(url) lapply(data, head) ## $info ## $info$TRD_DT ## [1] "/Date(1559833200000)/" ## ## $info$MKT_VAL ## [1] 19850082 ## ## $info$TRD_AMT ## [1] 70030 ## ## $info$CNT ## [1] 23 ## ## ## $list ## IDX_CD IDX_NM_KOR ALL_MKT_VAL CMP_CD ## 1 G10 WICS 에너지 19850082 096770 ## 2 G10 WICS 에너지 19850082 010950 ## 3 G10 WICS 에너지 19850082 267250 ## 4 G10 WICS 에너지 19850082 078930 ## 5 G10 WICS 에너지 19850082 067630 ## 6 G10 WICS 에너지 19850082 006120 ## CMP_KOR MKT_VAL WGT S_WGT CAL_WGT SEC_CD ## 1 SK이노베이션 9052841 45.61 45.61 1 G10 ## 2 S-Oil 3403265 17.14 62.75 1 G10 ## 3 현대중공업지주 2873204 14.47 77.23 1 G10 ## 4 GS 2491805 12.55 89.78 1 G10 ## 5 에이치엘비생명과학 624986 3.15 92.93 1 G10 ## 6 SK디스커버리 257059 1.30 94.22 1 G10 ## SEC_NM_KOR SEQ TOP60 APT_SHR_CNT ## 1 에너지 1 2 56403994 ## 2 에너지 2 2 41655633 ## 3 에너지 3 2 9283372 ## 4 에너지 4 2 49245150 ## 5 에너지 5 2 39307272 ## 6 에너지 6 2 10470820 ## ## $sector ## SEC_CD SEC_NM_KOR SEC_RATE IDX_RATE ## 1 G25 경기관련소비재 16.05 0 ## 2 G35 건강관리 9.27 0 ## 3 G50 커뮤니케이션서비스 2.26 0 ## 4 G40 금융 10.31 0 ## 5 G10 에너지 2.37 100 ## 6 G20 산업재 12.68 0 ## ## $size ## SEC_CD SEC_NM_KOR SEC_RATE IDX_RATE ## 1 WMI510 WMI500 대형주 69.40 89.78 ## 2 WMI520 WMI500 중형주 13.56 4.44 ## 3 WMI530 WMI500 소형주 17.04 5.78 $list 항목에는 해당 섹터의 구성종목 정보가 있으며, $sector 항목을 통해 다른 섹터의 코드도 확인할 수 있습니다. for loop 구문을 이용해 URL의 sec_cd=에 해당하는 부분만 변경하면 모든 섹터의 구성종목을 매우 쉽게 얻을 수 있습니다. sector_code = c('G25', 'G35', 'G50', 'G40', 'G10', 'G20', 'G55', 'G30', 'G15', 'G45') data_sector = list() for (i in sector_code) { url = paste0( 'http://www.wiseindex.com/Index/GetIndexComponets', '?ceil_yn=0&dt=',biz_day,'&sec_cd=',i) data = fromJSON(url) data = data$list data_sector[[i]] = data Sys.sleep(1) } data_sector = do.call(rbind, data_sector) 해당 데이터를 csv 파일로 저장해주도록 합니다. write.csv(data_sector, 'data/KOR_sector.csv') 특정한 규칙을 가진 문자열의 집합을 표현하는데 사용하는 형식 언어↩︎ https://en.wikipedia.org/wiki/Global_Industry_Classification_Standard↩︎ http://www.wiseindex.com/↩︎ "],
["금융-데이터-수집하기-심화.html", "Chapter 6 금융 데이터 수집하기 (심화) 6.1 수정주가 크롤링 6.2 재무제표 및 가치지표 크롤링 6.3 DART의 Open API를 이용한 데이터 수집하기", " Chapter 6 금융 데이터 수집하기 (심화) 지난 CHAPTER에서 수집한 주식티커를 바탕으로 이번 CHAPTER에서는 퀀트 투자의 핵심 자료인 수정주가, 재무제표, 가치지표를 크롤링하는 방법을 알아보겠습니다. 6.1 수정주가 크롤링 주가 데이터는 투자를 함에 있어 반드시 필요한 데이터이며, 인터넷에서 주가를 수집할 수 있는 방법은 매우 많습니다. 먼저 API를 이용한 데이터 수집에서 살펴본 것과 같이, getSymbols() 함수를 이용해 데이터를 받을 수 있습니다. 그러나 야후 파이낸스에서 제공하는 데이터 중 미국 주가는 이상 없이 다운로드되지만, 국내 중소형주는 주가가 없는 경우가 있습니다. 또한 단순 주가를 구할 수 있는 방법은 많지만, 투자에 필요한 수정주가를 구할 수 있는 방법은 찾기 힘듭니다. 다행히 네이버 금융에서 제공하는 정보를 통해 모든 종목의 수정주가를 매우 손쉽게 구할 수 있습니다. 6.1.1 개별종목 주가 크롤링 먼저 네이버 금융에서 특정종목(예: 삼성전자)의 [차트] 탭14을 선택합니다. 해당 차트는 주가 데이터를 받아 그래프를 그려주는 형태입니다. 따라서 해당 데이터가 어디에서 오는지 알기 위해 개발자 도구 화면을 이용합니다. URL: https://fchart.stock.naver.com/siseJson.nhn?symbol=005930&requestType=1&startTime=20191117&endTime=20210124&timeframe=day 그림 1.3: 네이버금융 차트의 통신기록 화면을 연 상태에서 [일] 탭을 선택하면 나오는 항목 중 가장 상단 항목의 Request URL이 주가 데이터를 요청하는 주소입니다. 해당 URL에 접속해보겠습니다. 그림 2.1: 주가 데이터 페이지 각 날짜별로 시가, 고가, 저가, 종가, 거래량, 외국인소지율이 있으며, 주가는 모두 수정주가 기준입니다. URL에서 symbol= 뒤에 6자리 티커만 변경하면 해당 종목의 주가 데이터가 있는 페이지로 이동할 수 있으며, 이를 통해 우리가 원하는 모든 종목의 주가 데이터를 크롤링할 수 있습니다. 또한 startTime= 에는 시작일자를, endTime= 에는 종료일자를 입력하여 원하는 기간 만큼의 데이터를 받을 수도 있습니다. library(stringr) KOR_ticker = read.csv('data/KOR_ticker.csv', row.names = 1) print(KOR_ticker$'종목코드'[1]) ## [1] 5930 KOR_ticker$'종목코드' = str_pad(KOR_ticker$'종목코드', 6, side = c('left'), pad = '0') 먼저 저장해두었던 티커 항목의 csv 파일을 불러옵니다. 종목코드를 살펴보면 005930이어야 할 삼성전자의 티커가 5930으로 입력되어 있습니다. 이는 파일을 불러오는 과정에서 0으로 시작하는 숫자들이 지워졌기 때문입니다. stringr 패키지의 str_pad() 함수를 사용해 6자리가 되지 않는 문자는 왼쪽에 0을 추가해 강제로 6자리로 만들어주도록 합니다. 다음은 첫 번째 종목인 삼성전자의 주가를 크롤링한 후 가공하는 방법입니다. library(xts) ifelse(dir.exists('data/KOR_price'), FALSE, dir.create('data/KOR_price')) ## [1] FALSE i = 1 name = KOR_ticker$'종목코드'[i] price = xts(NA, order.by = Sys.Date()) print(price) ## [,1] ## 2021-01-24 NA data 폴더 내에 KOR_price 폴더를 생성합니다. i = 1을 입력합니다. 향후 for loop 구문을 통해 i 값만 변경하면 모든 종목의 주가를 다운로드할 수 있습니다. name에 해당 티커를 입력합니다. xts() 함수를 이용해 빈 시계열 데이터를 생성하며, 인덱스는 Sys.Date()를 통해 현재 날짜를 입력합니다. library(httr) library(rvest) library(lubridate) library(stringr) library(readr) from = (Sys.Date() - years(3)) %>% str_remove_all('-') to = Sys.Date() %>% str_remove_all('-') url = paste0('https://fchart.stock.naver.com/siseJson.nhn?symbol=', name, '&requestType=1&startTime=', from, '&endTime=', to, '&timeframe=day') data = GET(url) data_html = data %>% read_html %>% html_text() %>% read_csv() print(data_html) ## # A tibble: 1,479 x 8 ## `[['날짜'` `'시가'` `'고가'` `'저가'` `'종가'` ## <chr> <dbl> <dbl> <dbl> <dbl> ## 1 <NA> NA NA NA NA ## 2 <NA> NA NA NA NA ## 3 <NA> NA NA NA NA ## 4 "[\\"20180… 48860 49700 48560 49340 ## 5 <NA> NA NA NA NA ## 6 "[\\"20180… 49220 50360 49160 50260 ## 7 <NA> NA NA NA NA ## 8 "[\\"20180… 50500 50780 49840 50780 ## 9 <NA> NA NA NA NA ## 10 "[\\"20180… 51200 51480 50900 51220 ## # … with 1,469 more rows, and 3 more variables: ## # `'거래량'` <dbl>, `'외국인소진율']` <chr>, X8 <lgl> 먼저 시작일(from)과 종료일(to)에 해당하는 날짜를 입력합니다. Sys.Date()를 통해 오늘 날짜를 불러온 후, 시작일은 years() 함수를 이용해 3년을 빼줍니다. (본인이 원하는 기간 만큼을 빼주면 됩니다.) 그 후 str_remove_all() 함수를 이용해 - 부분을 제거해 yyyymmdd 형식을 만들어 줍니다. paste0() 함수를 이용해 원하는 종목의 url을 생성합니다. url 중 티커에 해당하는 6자리 부분에 위에서 입력한 name을 설정합니다. GET() 함수를 통해 페이지의 데이터를 불러옵니다. read_html() 함수를 통해 HTML 정보를 읽어옵니다. html_text() 함수를 통해 텍스트 데이터만을 추출합니다. read_csv() 함수로 csv 형태의 데이터를 불러옵니다. 결과적으로 날짜 및 주가, 거래량, 외국인소진율 데이터가 추출됩니다. 우리에게 필요한 날짜와 종가에 해당하는 열만 선택하고, 클렌징 작업을 해주도록 하겠습니다. library(timetk) price = data_html[c(1, 5)] colnames(price) = (c('Date', 'Price')) price = na.omit(price) price$Date = parse_number(price$Date) price$Date = ymd(price$Date) price = tk_xts(price, date_var = Date) print(tail(price)) ## Price ## 2021-01-15 88000 ## 2021-01-18 85000 ## 2021-01-19 87000 ## 2021-01-20 87200 ## 2021-01-21 88100 ## 2021-01-22 86800 날짜에 해당하는 첫 번째 열과, 종가에 해당하는 다섯 번째 열만 선택해 저장합니다. 열 이름을 Date와 Price로 변경합니다. na.omit() 함수를 통해 NA 데이터를 삭제해줍니다. Date 열에서 숫자만을 추출하기 위해 readr 패키지의 parse_number() 함수를 적용합니다. 해당 함수는 문자형 데이터에서 콤마와 같은 불필요한 문자를 제거한 후 숫자형 데이터로 변경해줍니다. lubridate 패키지의 ymd() 함수를 이용하면 yyyymmdd 형태가 yyyy-mm-dd로 변경되며 데이터 형태 또한 Date 타입으로 변경됩니다. timetk 패키지의 tk_xts() 함수를 이용해 시계열 형태로 변경하며, 인덱스는 Date 열을 설정합니다. 형태를 변경한 후 해당 열은 자동으로 삭제됩니다. 데이터를 확인해보면 우리에게 필요한 형태로 정리되었습니다. write.csv(data.frame(price), paste0('data/KOR_price/', name, '_price.csv')) 마지막으로 해당 데이터를 data 폴더의 KOR_price 폴더 내에 티커_price.csv 이름으로 저장합니다. 6.1.2 전 종목 주가 크롤링 위의 코드에서 for loop 구문을 이용해 i 값만 변경해주면 모든 종목의 주가를 다운로드할 수 있습니다. 전 종목 주가를 다운로드하는 전체 코드는 다음과 같습니다. library(httr) library(rvest) library(stringr) library(xts) library(lubridate) library(readr) library(timetk) KOR_ticker = read.csv('data/KOR_ticker.csv', row.names = 1) print(KOR_ticker$'종목코드'[1]) KOR_ticker$'종목코드' = str_pad(KOR_ticker$'종목코드', 6, side = c('left'), pad = '0') ifelse(dir.exists('data/KOR_price'), FALSE, dir.create('data/KOR_price')) for(i in 1 : nrow(KOR_ticker) ) { price = xts(NA, order.by = Sys.Date()) # 빈 시계열 데이터 생성 name = KOR_ticker$'종목코드'[i] # 티커 부분 선택 from = (Sys.Date() - years(3)) %>% str_remove_all('-') # 시작일 to = Sys.Date() %>% str_remove_all('-') # 종료일 # 오류 발생 시 이를 무시하고 다음 루프로 진행 tryCatch({ # url 생성 url = paste0('https://fchart.stock.naver.com/siseJson.nhn?symbol=', name, '&requestType=1&startTime=', from, '&endTime=', to, '&timeframe=day') # 이 후 과정은 위와 동일함 # 데이터 다운로드 data = GET(url) data_html = data %>% read_html %>% html_text() %>% read_csv() # 필요한 열만 선택 후 클렌징 price = data_html[c(1, 5)] colnames(price) = (c('Date', 'Price')) price = na.omit(price) price$Date = parse_number(price$Date) price$Date = ymd(price$Date) price = tk_xts(price, date_var = Date) }, error = function(e) { # 오류 발생시 해당 종목명을 출력하고 다음 루프로 이동 warning(paste0("Error in Ticker: ", name)) }) # 다운로드 받은 파일을 생성한 폴더 내 csv 파일로 저장 write.csv(data.frame(price), paste0('data/KOR_price/', name, '_price.csv')) # 타임슬립 적용 Sys.sleep(2) } 위 코드에서 추가된 점은 다음과 같습니다. 페이지 오류, 통신 오류 등 오류가 발생할 경우 for loop 구문은 멈춰버리는데 전체 데이터를 처음부터 다시 받는 일은 매우 귀찮은 작업입니다. 따라서 tryCatch() 함수를 이용해 오류가 발생할 때 해당 티커를 출력한 후 다음 루프로 넘어가게 합니다. 또한 오류가 발생하면 xts() 함수를 통해 만들어둔 빈 데이터를 저장하게 됩니다. 마지막으로 무한 크롤링을 방지하기 위해 한 번의 루프가 끝날 때마다 2초의 타임슬립을 적용했습니다. 위 코드가 모두 돌아가는 데는 수 시간이 걸립니다. 작업이 끝난 후 data/KOR_price 폴더를 확인해보면 전 종목 주가가 csv 형태로 저장되어 있습니다. 6.2 재무제표 및 가치지표 크롤링 주가와 더불어 재무제표와 가치지표 역시 투자에 있어 핵심이 되는 데이터입니다. 해당 데이터 역시 여러 웹사이트에서 구할 수 있지만, 국내 데이터 제공업체인 FnGuide에서 운영하는 Company Guide 웹사이트15에서 손쉽게 구할 수 있습니다. 6.2.1 재무제표 다운로드 먼저 개별종목의 재무제표를 탭을 선택하면 포괄손익계산서, 재무상태표, 현금흐름표 항목이 보이게 되며, 티커에 해당하는 A005930 뒤의 주소는 불필요한 내용이므로, 이를 제거한 주소로 접속합니다. A 뒤의 6자리 티커만 변경한다면 해당 종목의 재무제표 페이지로 이동하게 됩니다. http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A005930 우리가 원하는 재무제표 항목들은 모두 테이블 형태로 제공되고 있으므로 html_table() 함수를 이용해 추출할 수 있습니다. library(httr) library(rvest) ifelse(dir.exists('data/KOR_fs'), FALSE, dir.create('data/KOR_fs')) Sys.setlocale("LC_ALL", "English") url = paste0('http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A005930') data = GET(url, user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36')) data = data %>% read_html() %>% html_table() Sys.setlocale("LC_ALL", "Korean") lapply(data, function(x) { head(x, 3)}) ## [[1]] ## IFRS(연결) 2017/12 2018/12 2019/12 2020/09 ## 1 매출액 2,395,754 2,437,714 2,304,009 1,752,555 ## 2 매출원가 1,292,907 1,323,944 1,472,395 1,066,834 ## 3 매출총이익 1,102,847 1,113,770 831,613 685,721 ## 전년동기 전년동기(%) ## 1 1,705,161 2.8 ## 2 1,086,850 -1.8 ## 3 618,311 10.9 ## ## [[2]] ## IFRS(연결) 2019/12 2020/03 2020/06 2020/09 전년동기 ## 1 매출액 598,848 553,252 529,661 669,642 620,035 ## 2 매출원가 385,545 348,067 319,062 399,705 399,939 ## 3 매출총이익 213,302 205,185 210,599 269,937 220,096 ## 전년동기(%) ## 1 8.0 ## 2 -0.1 ## 3 22.6 ## ## [[3]] ## IFRS(연결) 2017/12 2018/12 ## 1 자산 3,017,521 3,393,572 ## 2 유동자산계산에 참여한 계정 펼치기 1,469,825 1,746,974 ## 3 재고자산 249,834 289,847 ## 2019/12 2020/09 ## 1 3,525,645 3,757,887 ## 2 1,813,853 2,036,349 ## 3 267,665 324,429 ## ## [[4]] ## IFRS(연결) 2019/12 2020/03 ## 1 자산 3,525,645 3,574,575 ## 2 유동자산계산에 참여한 계정 펼치기 1,813,853 1,867,397 ## 3 재고자산 267,665 284,549 ## 2020/06 2020/09 ## 1 3,579,595 3,757,887 ## 2 1,861,368 2,036,349 ## 3 296,455 324,429 ## ## [[5]] ## IFRS(연결) 2017/12 2018/12 2019/12 ## 1 영업활동으로인한현금흐름 621,620 670,319 453,829 ## 2 당기순손익 421,867 443,449 217,389 ## 3 법인세비용차감전계속사업이익 ## 2020/09 ## 1 407,724 ## 2 198,007 ## 3 ## ## [[6]] ## IFRS(연결) 2019/12 2020/03 2020/06 ## 1 영업활동으로인한현금흐름 197,171 118,299 147,982 ## 2 당기순손익 52,270 48,849 55,551 ## 3 법인세비용차감전계속사업이익 ## 2020/09 ## 1 141,444 ## 2 93,607 ## 3 data 폴더 내에 KOR_fs 폴더를 생성합니다. Sys.setlocale() 함수를 통해 로케일 언어를 English로 설정합니다. url을 입력한 후 GET() 함수를 통해 페이지 내용을 받아오며, user_agent() 항목에 웹브라우저 구별을 입력해줍니다. 해당 사이트는 크롤러와 같이 정체가 불분명한 웹브라우저를 통한 접속이 막혀 있어, 마치 모질라 혹은 크롬을 통해 접속한 것 처럼 데이터를 요청합니다. 다양한 웹브라우저 리스트는 아래 링크에 나와있습니다. http://www.useragentstring.com/pages/useragentstring.php read_html() 함수를 통해 HTML 내용을 읽어오며, html_table() 함수를 통해 테이블 내용만 추출합니다. 로케일 언어를 다시 Korean으로 설정합니다. 위의 과정을 거치면 data 변수에는 리스트 형태로 총 6개의 테이블이 들어오게 되며, 그 내용은 표 6.1와 같습니다. 표 6.1: 재무제표 테이블 내역 순서 내용 1 포괄손익계산서 (연간) 2 포괄손익계산서 (분기) 3 재무상태표 (연간) 4 재무상태표 (분기) 5 현금흐름표 (연간) 6 현금흐름표 (분기) 이 중 연간 기준 재무제표에 해당하는 첫 번째, 세 번째, 다섯 번째 테이블을 선택합니다. data_IS = data[[1]] data_BS = data[[3]] data_CF = data[[5]] print(names(data_IS)) ## [1] "IFRS(연결)" "2017/12" "2018/12" ## [4] "2019/12" "2020/09" "전년동기" ## [7] "전년동기(%)" data_IS = data_IS[, 1:(ncol(data_IS)-2)] 포괄손익계산서 테이블(data_IS)에는 전년동기, 전년동기(%) 열이 있는데 통일성을 위해 해당 열을 삭제합니다. 이제 테이블을 묶은 후 클렌징하겠습니다. data_fs = rbind(data_IS, data_BS, data_CF) data_fs[, 1] = gsub('계산에 참여한 계정 펼치기', '', data_fs[, 1]) data_fs = data_fs[!duplicated(data_fs[, 1]), ] rownames(data_fs) = NULL rownames(data_fs) = data_fs[, 1] data_fs[, 1] = NULL data_fs = data_fs[, substr(colnames(data_fs), 6,7) == '12'] rbind() 함수를 이용해 세 테이블을 행으로 묶은 후 data_fs에 저장합니다. 첫 번째 열인 계정명에는 ‘계산에 참여한 계정 펼치기’라는 글자가 들어간 항목이 있습니다. 이는 페이지 내에서 펼치기 역할을 하는 (+) 항목에 해당하며 gsub() 함수를 이용해 해당 글자를 삭제합니다. 중복되는 계정명이 다수 있는데 대부분 불필요한 항목입니다. !duplicated() 함수를 사용해 중복되지 않는 계정명만 선택합니다. 행 이름을 초기화한 후 첫 번째 열의 계정명을 행 이름으로 변경합니다. 그 후 첫 번째 열은 삭제합니다. 간혹 12월 결산법인이 아닌 종목이거나 연간 재무제표임에도 불구하고 분기 재무제표가 들어간 경우가 있습니다. 비교의 통일성을 위해 substr() 함수를 이용해 끝 글자가 12인 열, 즉 12월 결산 데이터만 선택합니다. print(head(data_fs)) ## 2017/12 2018/12 2019/12 ## 매출액 2,395,754 2,437,714 2,304,009 ## 매출원가 1,292,907 1,323,944 1,472,395 ## 매출총이익 1,102,847 1,113,770 831,613 ## 판매비와관리비 566,397 524,903 553,928 ## 인건비 67,972 64,514 64,226 ## 유무형자산상각비 13,366 14,477 20,408 sapply(data_fs, typeof) ## 2017/12 2018/12 2019/12 ## "character" "character" "character" 데이터를 확인해보면 연간 기준 재무제표가 정리되었습니다. 문자형 데이터이므로 숫자형으로 변경합니다. library(stringr) data_fs = sapply(data_fs, function(x) { str_replace_all(x, ',', '') %>% as.numeric() }) %>% data.frame(., row.names = rownames(data_fs)) print(head(data_fs)) ## X2017.12 X2018.12 X2019.12 ## 매출액 2395754 2437714 2304009 ## 매출원가 1292907 1323944 1472395 ## 매출총이익 1102847 1113770 831613 ## 판매비와관리비 566397 524903 553928 ## 인건비 67972 64514 64226 ## 유무형자산상각비 13366 14477 20408 sapply(data_fs, typeof) ## X2017.12 X2018.12 X2019.12 ## "double" "double" "double" sapply() 함수를 이용해 각 열에 stringr 패키지의 str_replace_allr() 함수를 적용해 콤마(,)를 제거한 후 as.numeric() 함수를 통해 숫자형 데이터로 변경합니다. data.frame() 함수를 이용해 데이터 프레임 형태로 만들어주며, 행 이름은 기존 내용을 그대로 유지합니다. 정리된 데이터를 출력해보면 문자형이던 데이터가 숫자형으로 변경되었습니다. write.csv(data_fs, 'data/KOR_fs/005930_fs.csv') data 폴더의 KOR_fs 폴더 내에 티커_fs.csv 이름으로 저장합니다. 6.2.2 가치지표 계산하기 위에서 구한 재무제표 데이터를 이용해 가치지표를 계산할 수 있습니다. 흔히 사용되는 가치지표는 PER, PBR, PCR, PSR이며 분자는 주가, 분모는 재무제표 데이터가 사용됩니다. 표 6.2: 가치지표의 종류 순서 분모 PER Earnings (순이익) PBR Book Value (순자산) PCR Cashflow (영업활동현금흐름) PSR Sales (매출액) 위에서 구한 재무제표 항목에서 분모 부분에 해당하는 데이터만 선택해보겠습니다. ifelse(dir.exists('data/KOR_value'), FALSE, dir.create('data/KOR_value')) ## [1] FALSE value_type = c('지배주주순이익', '자본', '영업활동으로인한현금흐름', '매출액') value_index = data_fs[match(value_type, rownames(data_fs)), ncol(data_fs)] print(value_index) ## [1] 215051 2628804 453829 2304009 data 폴더 내에 KOR_value 폴더를 생성합니다. 분모에 해당하는 항목을 저장한 후 match() 함수를 이용해 해당 항목이 위치하는 지점을 찾습니다. ncol() 함수를 이용해 맨 오른쪽, 즉 최근년도 재무제표 데이터를 선택합니다. 다음으로 분자 부분에 해당하는 현재 주가를 수집해야 합니다. 이 역시 Company Guide 접속 화면에서 구할 수 있습니다. 불필요한 부분을 제거한 URL은 다음과 같습니다. http://comp.fnguide.com/SVO2/ASP/SVD_main.asp?pGB=1&gicode=A005930 위의 주소 역시 A 뒤의 6자리 티커만 변경하면 해당 종목의 스냅샷 페이지로 이동하게 됩니다. 그림 4.4: Company Guide 스냅샷 화면 주가추이 부분에 우리가 원하는 현재 주가가 있습니다. 해당 데이터의 Xpath는 다음과 같습니다. //*[@id="svdMainChartTxt11"] //*[@id=\"svdMainChartTxt11\"] 위에서 구한 주가의 Xpath를 이용해 해당 데이터를 크롤링하겠습니다. library(readr) url = 'http://comp.fnguide.com/SVO2/ASP/SVD_main.asp?pGB=1&gicode=A005930' data = GET(url, user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36')) price = read_html(data) %>% html_node(xpath = '//*[@id="svdMainChartTxt11"]') %>% html_text() %>% parse_number() print(price) ## [1] 86800 url을 입력한 후, GET() 함수를 이용해 데이터를 불러오며, 역시나 user_agent를 추가해 줍니다. read_html() 함수를 이용해 HTML 데이터를 불러온 후 html_node() 함수에 앞서 구한 Xpath를 입력해 해당 지점의 데이터를 추출합니다. html_text() 함수를 통해 텍스트 데이터만을 추출하며, readr 패키지의 parse_number() 함수를 적용합니다. 가치지표를 계산하려면 발행주식수 역시 필요합니다. 예를 들어 PER를 계산하는 방법은 다음과 같습니다. \\[ PER = Price / EPS = 주가 / 주당순이익\\] 주당순이익은 순이익을 전체 주식수로 나눈 값이므로, 해당 값의 계산하려면 전체 주식수를 구해야 합니다. 전체 주식수 데이터 역시 웹페이지에 있으므로 앞서 주가를 크롤링한 방법과 동일한 방법으로 구할 수 있습니다. 전체 주식수 데이터의 Xpath는 다음과 같습니다. //*[@id="svdMainGrid1"]/table/tbody/tr[7]/td[1] //*[@id=\"svdMainGrid1\"]/table/tbody/tr[7]/td[1] 이를 이용해 발행주식수 중 보통주를 선택하는 방법은 다음과 같습니다. share = read_html(data) %>% html_node( xpath = '//*[@id="svdMainGrid1"]/table/tbody/tr[7]/td[1]') %>% html_text() print(share) ## [1] "5,969,782,550/ 822,886,700" read_html() 함수와 html_node() 함수를 이용해, HTML 내에서 Xpath에 해당하는 데이터를 추출합니다. 그 후 html_text() 함수를 통해 텍스트 부분만 추출합니다. 해당 과정을 거치면 보통주/우선주의 형태로 발행주식주가 저장됩니다. 이 중 우리가 원하는 데이터는 / 앞에 있는 보통주 발행주식수입니다. share = share %>% strsplit('/') %>% unlist() %>% .[1] %>% parse_number() print(share) ## [1] 5969782550 strsplit() 함수를 통해 /를 기준으로 데이터를 나눕니다. 해당 결과는 리스트 형태로 저장됩니다. unlist() 함수를 통해 리스트를 벡터 형태로 변환합니다. .[1].[1]을 통해 보통주 발행주식수인 첫 번째 데이터를 선택합니다. parse_number() 함수를 통해 문자형 데이터를 숫자형으로 변환합니다. 재무 데이터, 현재 주가, 발행주식수를 이용해 가치지표를 계산해보겠습니다. data_value = price / (value_index * 100000000 / share) names(data_value) = c('PER', 'PBR', 'PCR', 'PSR') data_value[data_value < 0] = NA print(data_value) ## PER PBR PCR PSR ## 24.096 1.971 11.418 2.249 분자에는 현재 주가를 입력하며, 분모에는 재무 데이터를 보통주 발행주식수로 나눈 값을 입력합니다. 단, 주가는 원 단위, 재무 데이터는 억 원 단위이므로, 둘 사이에 단위를 동일하게 맞춰주기 위해 분모에 억을 곱합니다. 또한 가치지표가 음수인 경우는 NA로 변경해줍니다. 결과를 확인해보면 4가지 가치지표가 잘 계산되었습니다.16 write.csv(data_value, 'data/KOR_value/005930_value.csv') data 폴더의 KOR_value 폴더 내에 티커_value.csv 이름으로 저장합니다. 6.2.3 전 종목 재무제표 및 가치지표 다운로드 위 코드에서 for loop 구문을 이용해 URL 중 6자리 티커에 해당하는 값만 변경해주면 모든 종목의 재무제표를 다운로드하고 이를 바탕으로 가치지표를 계산할 수 있습니다. 해당 코드는 다음과 같습니다. library(stringr) library(httr) library(rvest) library(stringr) library(readr) KOR_ticker = read.csv('data/KOR_ticker.csv', row.names = 1) KOR_ticker$'종목코드' = str_pad(KOR_ticker$'종목코드', 6,side = c('left'), pad = '0') ifelse(dir.exists('data/KOR_fs'), FALSE, dir.create('data/KOR_fs')) ifelse(dir.exists('data/KOR_value'), FALSE, dir.create('data/KOR_value')) for(i in 1 : nrow(KOR_ticker) ) { data_fs = c() data_value = c() name = KOR_ticker$'종목코드'[i] # 오류 발생 시 이를 무시하고 다음 루프로 진행 tryCatch({ Sys.setlocale('LC_ALL', 'English') # url 생성 url = paste0( 'http://comp.fnguide.com/SVO2/ASP/' ,'SVD_Finance.asp?pGB=1&gicode=A', name) # 이 후 과정은 위와 동일함 # 데이터 다운로드 후 테이블 추출 data = GET(url, user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36')) %>% read_html() %>% html_table() Sys.setlocale('LC_ALL', 'Korean') # 3개 재무제표를 하나로 합치기 data_IS = data[[1]] data_BS = data[[3]] data_CF = data[[5]] data_IS = data_IS[, 1:(ncol(data_IS)-2)] data_fs = rbind(data_IS, data_BS, data_CF) # 데이터 클랜징 data_fs[, 1] = gsub('계산에 참여한 계정 펼치기', '', data_fs[, 1]) data_fs = data_fs[!duplicated(data_fs[, 1]), ] rownames(data_fs) = NULL rownames(data_fs) = data_fs[, 1] data_fs[, 1] = NULL # 12월 재무제표만 선택 data_fs = data_fs[, substr(colnames(data_fs), 6,7) == "12"] data_fs = sapply(data_fs, function(x) { str_replace_all(x, ',', '') %>% as.numeric() }) %>% data.frame(., row.names = rownames(data_fs)) # 가치지표 분모부분 value_type = c('지배주주순이익', '자본', '영업활동으로인한현금흐름', '매출액') # 해당 재무데이터만 선택 value_index = data_fs[match(value_type, rownames(data_fs)), ncol(data_fs)] # Snapshot 페이지 불러오기 url = paste0( 'http://comp.fnguide.com/SVO2/ASP/SVD_Main.asp', '?pGB=1&gicode=A',name) data = GET(url, user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36')) # 현재 주가 크롤링 price = read_html(data) %>% html_node(xpath = '//*[@id="svdMainChartTxt11"]') %>% html_text() %>% parse_number() # 보통주 발행주식수 크롤링 share = read_html(data) %>% html_node( xpath = '//*[@id="svdMainGrid1"]/table/tbody/tr[7]/td[1]') %>% html_text() %>% strsplit('/') %>% unlist() %>% .[1] %>% parse_number() # 가치지표 계산 data_value = price / (value_index * 100000000/ share) names(data_value) = c('PER', 'PBR', 'PCR', 'PSR') data_value[data_value < 0] = NA }, error = function(e) { # 오류 발생시 해당 종목명을 출력하고 다음 루프로 이동 data_fs <<- NA data_value <<- NA warning(paste0("Error in Ticker: ", name)) }) # 다운로드 받은 파일을 생성한 각각의 폴더 내 csv 파일로 저장 # 재무제표 저장 write.csv(data_fs, paste0('data/KOR_fs/', name, '_fs.csv')) # 가치지표 저장 write.csv(data_value, paste0('data/KOR_value/', name, '_value.csv')) # 2초간 타임슬립 적용 Sys.sleep(2) } 전 종목 주가 데이터를 받는 과정과 동일하게 KOR_ticker.csv 파일을 불러온 후 for loop를 통해 i 값이 변함에 따라 티커를 변경해가며 모든 종목의 재무제표 및 가치지표를 다운로드합니다. tryCatch() 함수를 이용해 오류가 발생하면 NA로 이루어진 빈 데이터를 저장한 후 다음 루프로 넘어가게 됩니다. data/KOR_fs 폴더에는 전 종목의 재무제표 데이터가 저장되고, data/KOR_value 폴더에는 전 종목의 가치지표 데이터가 csv 형태로 저장됩니다. 6.3 DART의 Open API를 이용한 데이터 수집하기 DART(Data Analysis, Retrieval and Transfer System)는 금융감독원 전자공시시스템으로써, 상장법인 등이 공시서류를 인터넷으로 제출하고, 투자자 등 이용자는 제출 즉시 인터넷을 통해 조회할 수 있도록 하는 종합적 기업공시 시스템입니다. 홈페이지에서도 각종 공시내역을 확인할 수 있지만, 해당 사이트에서 제공하는 API를 이용할 경우 더욱 쉽게 공시 내용을 수집할 수 있습니다. 6.3.1 API Key발급 및 추가하기 먼저 https://opendart.fss.or.kr/에 접속한 후 [인증키 신청/관리] → [인증키 신청]을 통해 API Key를 발급 받습니다. 그림 5.4: OpenAPI 인증키 신청 계정을 생성하고 이메일을 통해 이용자 등록을 한 후 로그인을 합니다. 그 후 [오픈API 이용현황]을 살펴보면 API Key 부분에 발급받은 Key가 있으며, 금일 몇번의 API를 요청했는지가 일일이용현황에 나옵니다. 하루 총 10,000번까지 데이터를 요청할 수 있습니다. 그림 6.1: OpenAPI 이용현황 다음으로 발급받은 API Key를 .Renviron 파일에 추가하도록 합니다. 해당 파일에는 여러 패스워드를 추가해 안전하게 관리할 수 있습니다. file.edit("~/.Renviron") .Renviron 파일이 열리면 다음과 같이 입력을 해줍니다. dart_api_key = '발급받은 API' 파일을 저장한 후 해당 파일을 적용하기 위해 R의 Session을 재시작합니다. 그 후 아래 명령어를 실행하여 API Key를 불러오도록 합니다. (재시작하지 않으면 Key를 불러올 수 없습니다.) dart_api = Sys.getenv("dart_api_key") 6.3.2 고유번호 다운로드 Open API에서 각 기업의 데이터를 받기 위해서는 종목에 해당하는 고유번호를 알아야 합니다. 이에 대한 개발가이드는 아래 페이지에 나와 있습니다. https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019018 위 페이지의 내용을 코드로 나타내보도록 합니다. library(httr) library(rvest) codezip_url = paste0( 'https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key=',dart_api) codezip_data = GET(codezip_url) print(codezip_data) ## Response [https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key=b1a630e527b0e5ff5bd58ed81b49825017fa80b8] ## Date: 2021-01-24 14:40 ## Status: 200 ## Content-Type: application/x-msdownload ## Size: 1.4 MB ## <BINARY BODY> ## NULL https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key= 뒤에 본인의 API 키를 입력합니다. GET() 함수를 통해 해당 페이지 내용을 받습니다. 다운로드 받은 내용을 확인해보면 , 즉 바이너리 형태의 데이터가 첨부되어 있습니다. 이에 대해 좀더 자세히 알아보도록 하겠습니다. codezip_data$headers[["content-disposition"]] ## [1] ": attachment; filename=CORPCODE.zip" headers의 “content-disposition” 부분을 확인해보면 CORPCODE.zip 파일이 첨부되어 있습니다. 해당 파일의 압축을 풀어 첨부된 내용을 확인합니다. tf = tempfile(fileext = '.zip') writeBin( content(codezip_data, as = "raw"), file.path(tf) ) nm = unzip(tf, list = TRUE) print(nm) ## Name Length Date ## 1 CORPCODE.xml 16083121 <NA> tempfile() 함수 통해 빈 .zip 파일을 만듭니다. writeBin() 함수는 바이너리 형태의 파일을 저장하는 함수이며, content()를 통해 첨부 파일 내용을 raw 형태로 저장합니다. 파일명은 위에서 만든 tf로 합니다. unzip() 함수를 통해 zip 내 파일 리스트를 확인합니다. zip 파일 내에는 CORPCODE.xml 파일이 있으며, read_xml() 함수를 통해 이를 불러오도록 합니다. code_data = read_xml(unzip(tf, nm$Name)) print(code_data) ## {xml_document} ## <result> ## [1] <list>\\n <corp_code>00434003</corp_code>\\n <co ... ## [2] <list>\\n <corp_code>00434456</corp_code>\\n <co ... ## [3] <list>\\n <corp_code>00430964</corp_code>\\n <co ... ## [4] <list>\\n <corp_code>00432403</corp_code>\\n <co ... ## [5] <list>\\n <corp_code>00388953</corp_code>\\n <co ... ## [6] <list>\\n <corp_code>00179984</corp_code>\\n <co ... ## [7] <list>\\n <corp_code>00420143</corp_code>\\n <co ... ## [8] <list>\\n <corp_code>00401111</corp_code>\\n <co ... ## [9] <list>\\n <corp_code>00435534</corp_code>\\n <co ... ## [10] <list>\\n <corp_code>00430186</corp_code>\\n <co ... ## [11] <list>\\n <corp_code>00430201</corp_code>\\n <co ... ## [12] <list>\\n <corp_code>00430210</corp_code>\\n <co ... ## [13] <list>\\n <corp_code>00430229</corp_code>\\n <co ... ## [14] <list>\\n <corp_code>00140432</corp_code>\\n <co ... ## [15] <list>\\n <corp_code>00426208</corp_code>\\n <co ... ## [16] <list>\\n <corp_code>00433262</corp_code>\\n <co ... ## [17] <list>\\n <corp_code>00433749</corp_code>\\n <co ... ## [18] <list>\\n <corp_code>00433785</corp_code>\\n <co ... ## [19] <list>\\n <corp_code>00196079</corp_code>\\n <co ... ## [20] <list>\\n <corp_code>00435048</corp_code>\\n <co ... ## ... 해당 파일은 HTML 형식으로 되어 있으며 중요부분은 다음과 같습니다. corp_code: 고유번호 corp_name: 종목명 corp_stock: 거래소 상장 티커 HTML의 태그를 이용해 각 부분을 추출한 후 하나의 데이터로 합치도록 하겠습니다. corp_code = code_data %>% html_nodes('corp_code') %>% html_text() corp_name = code_data %>% html_nodes('corp_name') %>% html_text() corp_stock = code_data %>% html_nodes('stock_code') %>% html_text() corp_list = data.frame( 'code' = corp_code, 'name' = corp_name, 'stock' = corp_stock, stringsAsFactors = FALSE ) html_nodes() 함수를 이용해 고유번호, 종목명, 상장티커를 선택한 후, html_text() 함수를 이용해 문자열만 추출하도록 합니다. data.frame() 함수를 통해 데이터프레임 형식으로 묶어주도록 합니다. nrow(corp_list) ## [1] 83421 head(corp_list) ## code name stock ## 1 00434003 다코 ## 2 00434456 일산약품 ## 3 00430964 굿앤엘에스 ## 4 00432403 한라판지 ## 5 00388953 크레디피아제이십오차유동화전문회사 ## 6 00179984 연방건설산업 종목수를 확인해보면 83421 개가 확인되며, 이 중 stock 열이 빈 종목은 거래소에 상장되지 않은 종목입니다. 따라서 해당 데이터는 삭제하여 거래소 상장 종목만을 남긴 후, csv 파일로 저장하도록 합니다. corp_list = corp_list[corp_list$stock != " ", ] write.csv(corp_list, 'data/corp_list.csv') 6.3.3 공시검색 6.3.3.1 전체 공시 검색 먼저 공시검색 API에 대한 이해를 위해 전체 종목의 공시를 수집하도록 하며, 해당 개발가이드는 아래 페이지에 나와 있습니다. https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019001 각종 요청인자를 통해 url을 생성 후 전송하여, 요청에 맞는 데이터를 받을 수 있습니다. 공시 검색에 해당하는 인자는 다음과 같습니다. 그림 6.2: OpenAPI 요청 인자 예시 페이지 하단에서 인자를 입력 후 [검색]을 누르면 각 인자에 맞게 생성된 url과 그 결과를 볼 수 있습니다. 그림 6.3: OpenAPI 테스트 예시 먼저 시작일과 종료일을 토대로 최근 공시 100건에 해당하는 url을 생성하도록 하겠습니다. library(lubridate) library(stringr) library(jsonlite) bgn_date = (Sys.Date() - days(7)) %>% str_remove_all('-') end_date = (Sys.Date() ) %>% str_remove_all('-') notice_url = paste0('https://opendart.fss.or.kr/api/list.json?crtfc_key=',dart_api,'&bgn_de=', bgn_date,'&end_de=',end_date,'&page_no=1&page_count=100') bgn_date에는 현재로부터 일주일 전을, end_date는 오늘 날짜를, 페이지별 건수에 해당하는 page_count에는 100을 입력하도록 합니다. 그 후 홈페이지에 나와있는 예시에 맞게 url을 작성해주도록 합니다. XML 보다는 JSON 형식으로 url을 생성 후 요청하는 것이 데이터 처리 측면에서 훨씬 효율적입니다. notice_data = fromJSON(notice_url) notice_data = notice_data[['list']] head(notice_data) ## corp_code corp_name stock_code corp_cls ## 1 00128971 대림건설 001880 Y ## 2 00689418 지와이커머스 111820 K ## 3 00138057 써니전자 004770 Y ## 4 00128971 대림건설 001880 Y ## 5 00809517 아이엠텍 226350 K ## 6 00128971 대림건설 001880 Y ## report_nm ## 1 최대주주등소유주식변동신고서(최대주주변경시) ## 2 타법인주식및출자증권취득결정 ## 3 주권관련사채권의취득결정 ## 4 최대주주변경 ## 5 [기재정정]주요사항보고서(감자결정) ## 6 [기재정정]단일판매ㆍ공급계약체결 ## rcept_no flr_nm rcept_dt rm ## 1 20210122800734 대림건설 20210122 유 ## 2 20210122900732 지와이커머스 20210122 코 ## 3 20210122800730 써니전자 20210122 유 ## 4 20210122800709 대림건설 20210122 유 ## 5 20210122000535 아이엠텍 20210122 ## 6 20210122800708 대림건설 20210122 유 fromJSON() 함수를 통해 JSON 데이터를 받은 후 list를 확인해보면 우리가 원하는 공시정보, 즉 일주일 전부터 100건의 공시 정보가 다운로드 되어 있습니다. 6.3.3.2 특정 기업의 공시 검색 이번에는 고유번호를 추가하여 원하는 기업의 공시만 확인해보록 하겠습니다. 고유번호는 위에서 다운받은 corp_list.csv 파일을 통해 확인해볼 수 있으며, 예시로 살펴볼 삼성전자의 고유번호는 00126380 입니다. bgn_date = (Sys.Date() - days(30)) %>% str_remove_all('-') end_date = (Sys.Date() ) %>% str_remove_all('-') corp_code = '00126380' notice_url_ss = paste0( 'https://opendart.fss.or.kr/api/list.json?crtfc_key=',dart_api, '&corp_code=', corp_code, '&bgn_de=', bgn_date,'&end_de=', end_date,'&page_no=1&page_count=100') 시작일을 과거 30일로 수정하였으며, 기존 url에 &corp_code= 부분을 추가하였습니다. notice_data_ss = fromJSON(notice_url_ss) notice_data_ss = notice_data_ss[['list']] head(notice_data_ss) ## corp_code corp_name stock_code corp_cls ## 1 00126380 삼성전자 005930 Y ## 2 00126380 삼성전자 005930 Y ## 3 00126380 삼성전자 005930 Y ## 4 00126380 삼성전자 005930 Y ## 5 00126380 삼성전자 005930 Y ## 6 00126380 삼성전자 005930 Y ## report_nm ## 1 횡령ㆍ배임사실확인 ## 2 임원ㆍ주요주주특정증권등소유상황보고서 ## 3 최대주주등소유주식변동신고서 ## 4 기업설명회(IR)개최(안내공시) ## 5 연결재무제표기준영업(잠정)실적(공정공시) ## 6 임원ㆍ주요주주특정증권등소유상황보고서 ## rcept_no flr_nm rcept_dt rm ## 1 20210120800650 삼성전자 20210120 유 ## 2 20210111000382 안규리 20210111 ## 3 20210108800652 삼성전자 20210108 유 ## 4 20210108800573 삼성전자 20210108 유 ## 5 20210108800029 삼성전자 20210108 유 ## 6 20210107000257 국민연금공단 20210107 역시나 JSON 형태로 손쉽게 공시정보를 다운로드 받을 수 있습니다. 이 중 rcept_no는 공시번호에 해당하며, 해당 데이터를 이용해 공시에 해당하는 url에 접속을 할 수도 있습니다. notice_url_exam = notice_data_ss[1, 'rcept_no'] notice_dart_url = paste0( 'http://dart.fss.or.kr/dsaf001/main.do?rcpNo=',notice_url_exam) print(notice_dart_url) ## [1] "http://dart.fss.or.kr/dsaf001/main.do?rcpNo=20210120800650" dart 홈페이지의 공시에 해당하는 url과 첫번째 공시에 해당하는 공시번호를 합쳐주도록 합니다. 위 url에 접속하여 해당 공시를 좀 더 자세하게 확인할 수 있습니다. 그림 6.4: 공시 정보의 확인 6.3.4 사업보고서 주요 정보 API를 이용하여 사업보고서의 주요 정보 역시 다운로드 받을 수 있으며, 제공하는 목록은 다음과 같습니다. https://opendart.fss.or.kr/guide/main.do?apiGrpCd=DS002 이 중 예시로써 [배당에 관한 사항]을 다운로드 받도록 하며, 개발가이드 페이지는 다음과 같습니다. https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS002&apiId=2019005 url 생성에 필요한 요청 인자는 다음과 같습니다. 표 6.3: 배당에 관한 사항 주요 인자 키 명칭 설명 crtfc_key API 인증키 발급받은 인증키 corp_code 고유번호 공시대상회사의 고유번호(8자리) bsns_year 사업년도 사업연도(4자리) reprt_code 보고서 코드 1분기보고서 : 11013 반기보고서 : 11012 3분기보고서 : 11014 사업보고서 : 11011 이를 바탕으로 삼성전자의 2019년 사업보고서를 통해 배당에 관한 사항을 살펴보도록 하겠습니다. corp_code = '00126380' bsns_year = '2019' reprt_code = '11011' url_div = paste0('https://opendart.fss.or.kr/api/alotMatter.json?crtfc_key=', dart_api, '&corp_code=', corp_code, '&bsns_year=', bsns_year, '&reprt_code=', reprt_code ) API 인증키, 고유번호, 사업년도, 보고서 코드에 각각 해당하는 데이터를 입력하여 url 생성하도록 합니다. div_data_ss = fromJSON(url_div) div_data_ss = div_data_ss[['list']] head(div_data_ss) ## rcept_no corp_cls corp_code corp_name ## 1 20200330003851 Y 00126380 삼성전자 ## 2 20200330003851 Y 00126380 삼성전자 ## 3 20200330003851 Y 00126380 삼성전자 ## 4 20200330003851 Y 00126380 삼성전자 ## 5 20200330003851 Y 00126380 삼성전자 ## 6 20200330003851 Y 00126380 삼성전자 ## se thstrm frmtrm ## 1 주당액면가액(원) 100 100 ## 2 (연결)당기순이익(백만원) 21,505,054 43,890,877 ## 3 (별도)당기순이익(백만원) 15,353,323 32,815,127 ## 4 (연결)주당순이익(원) 3,166 6,461 ## 5 현금배당금총액(백만원) 9,619,243 9,619,243 ## 6 주식배당금총액(백만원) - - ## lwfr stock_knd ## 1 100 <NA> ## 2 41,344,569 <NA> ## 3 28,800,837 <NA> ## 4 5,997 <NA> ## 5 5,826,302 <NA> ## 6 - <NA> JSON 파일을 다운로드 받은 후 데이터를 확인해보면, 사업보고서 중 배당에 관한 사항만이 나타나 있습니다. 위 url의 alotMatter 부분을 각 사업보고서에 해당하는 값으로 변경해주면 다른 정보 역시 동일한 방법으로 수집이 가능합니다. 6.3.5 상장기업 재무정보 Open API에서는 상장기업의 재무정보 중 주요계정, 전체 재무제표, 원본파일을 제공하고 있습니다. 이 중 주요계정 및 전체 재무제표를 다운로드 받는법에 대해 알아보도록 하겠습니다. 6.3.5.1 단일회사 및 다중회사 주요계정 API를 통해 단일회사의 주요계정을, 혹은 한번에 여러 회사의 주요계정을 받을수 있습니다. 각각의 개발가이드는 다음과 같습니다. 단일회사 주요계정: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2019016 다중회사 주요계정: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2019017 먼저 단일회사(삼성전자)의 주요계정을 다운로드 받도록 하겠습니다. corp_code = '00126380' bsns_year = '2019' reprt_code = '11011' url_single = paste0( 'https://opendart.fss.or.kr/api/fnlttSinglAcnt.json?crtfc_key=', dart_api, '&corp_code=', corp_code, '&bsns_year=', bsns_year, '&reprt_code=', reprt_code ) url을 생성하는 방법이 기존 사업보고서 주요 정보 에서 살펴본 바와 매우 비슷하며, /api 뒷부분을 [fnlttSinglAcnt.json] 으로 변경하기만 하면 됩니다. fs_data_single = fromJSON(url_single) fs_data_single = fs_data_single[['list']] head(fs_data_single) ## rcept_no reprt_code bsns_year corp_code ## 1 20200330003851 11011 2019 00126380 ## 2 20200330003851 11011 2019 00126380 ## 3 20200330003851 11011 2019 00126380 ## 4 20200330003851 11011 2019 00126380 ## 5 20200330003851 11011 2019 00126380 ## 6 20200330003851 11011 2019 00126380 ## stock_code fs_div fs_nm sj_div sj_nm ## 1 005930 CFS 연결재무제표 BS 재무상태표 ## 2 005930 CFS 연결재무제표 BS 재무상태표 ## 3 005930 CFS 연결재무제표 BS 재무상태표 ## 4 005930 CFS 연결재무제표 BS 재무상태표 ## 5 005930 CFS 연결재무제표 BS 재무상태표 ## 6 005930 CFS 연결재무제표 BS 재무상태표 ## account_nm thstrm_nm thstrm_dt ## 1 유동자산 제 51 기 2019.12.31 현재 ## 2 비유동자산 제 51 기 2019.12.31 현재 ## 3 자산총계 제 51 기 2019.12.31 현재 ## 4 유동부채 제 51 기 2019.12.31 현재 ## 5 비유동부채 제 51 기 2019.12.31 현재 ## 6 부채총계 제 51 기 2019.12.31 현재 ## thstrm_amount frmtrm_nm frmtrm_dt ## 1 181,385,260,000,000 제 50 기 2018.12.31 현재 ## 2 171,179,237,000,000 제 50 기 2018.12.31 현재 ## 3 352,564,497,000,000 제 50 기 2018.12.31 현재 ## 4 63,782,764,000,000 제 50 기 2018.12.31 현재 ## 5 25,901,312,000,000 제 50 기 2018.12.31 현재 ## 6 89,684,076,000,000 제 50 기 2018.12.31 현재 ## frmtrm_amount bfefrmtrm_nm bfefrmtrm_dt ## 1 174,697,424,000,000 제 49 기 2017.12.31 현재 ## 2 164,659,820,000,000 제 49 기 2017.12.31 현재 ## 3 339,357,244,000,000 제 49 기 2017.12.31 현재 ## 4 69,081,510,000,000 제 49 기 2017.12.31 현재 ## 5 22,522,557,000,000 제 49 기 2017.12.31 현재 ## 6 91,604,067,000,000 제 49 기 2017.12.31 현재 ## bfefrmtrm_amount ord ## 1 146,982,464,000,000 1 ## 2 154,769,626,000,000 3 ## 3 301,752,090,000,000 5 ## 4 67,175,114,000,000 7 ## 5 20,085,548,000,000 9 ## 6 87,260,662,000,000 11 연결재무제표와 재무상태표에 해당하는 주요 내용이 수집되었으며, 각 열에 해당하는 내용은 페이지의 개발가이드의 [응답 결과]에서 확인할 수 있습니다. 그림 6.5: 단일회사 주요계정 응답 결과 이번에는 url을 수정하여 여러 회사의 주요계정을 한번에 받도록 하겠으며, 그 예로써 삼성전자, 셀트리온, KT의 데이터를 다운로드 받도록 합니다. corp_code = c('00126380,00413046,00190321') bsns_year = '2019' reprt_code = '11011' url_multiple = paste0( 'https://opendart.fss.or.kr/api/fnlttMultiAcnt.json?crtfc_key=', dart_api, '&corp_code=', corp_code, '&bsns_year=', bsns_year, '&reprt_code=', reprt_code ) 먼저 corp에 원하는 기업들의 고유번호를 나열해주며, url 중 [fnlttSinglAcnt]을 [fnlttMultiAcnt]로 수정합니다. fs_data_multiple = fromJSON(url_multiple) fs_data_multiple = fs_data_multiple[['list']] 3개 기업의 주요계정이 하나의 데이터 프레임으로 다운로드 됩니다. 마지막으로 각 회사별로 데이터를 나눠주도록 하겠습니다. fs_data_list = fs_data_multiple %>% split(f = .$corp_code) lapply(fs_data_list, head, 2) ## $`00126380` ## rcept_no reprt_code bsns_year corp_code ## 1 20200330003851 11011 2019 00126380 ## 2 20200330003851 11011 2019 00126380 ## stock_code fs_div fs_nm sj_div sj_nm ## 1 005930 CFS 연결재무제표 BS 재무상태표 ## 2 005930 CFS 연결재무제표 BS 재무상태표 ## account_nm thstrm_nm thstrm_dt ## 1 유동자산 제 51 기 2019.12.31 현재 ## 2 비유동자산 제 51 기 2019.12.31 현재 ## thstrm_amount frmtrm_nm frmtrm_dt ## 1 181,385,260,000,000 제 50 기 2018.12.31 현재 ## 2 171,179,237,000,000 제 50 기 2018.12.31 현재 ## frmtrm_amount bfefrmtrm_nm bfefrmtrm_dt ## 1 174,697,424,000,000 제 49 기 2017.12.31 현재 ## 2 164,659,820,000,000 제 49 기 2017.12.31 현재 ## bfefrmtrm_amount ord ## 1 146,982,464,000,000 1 ## 2 154,769,626,000,000 3 ## ## $`00190321` ## rcept_no reprt_code bsns_year corp_code ## 27 20200330004658 11011 2019 00190321 ## 28 20200330004658 11011 2019 00190321 ## stock_code fs_div fs_nm sj_div sj_nm ## 27 030200 CFS 연결재무제표 BS 재무상태표 ## 28 030200 CFS 연결재무제표 BS 재무상태표 ## account_nm thstrm_nm thstrm_dt ## 27 유동자산 제 38 기 2019.12.31 현재 ## 28 비유동자산 제 38 기 2019.12.31 현재 ## thstrm_amount frmtrm_nm frmtrm_dt ## 27 11,898,255,000,000 제 37 기 2018.12.31 현재 ## 28 22,163,037,000,000 제 37 기 2018.12.31 현재 ## frmtrm_amount bfefrmtrm_nm bfefrmtrm_dt ## 27 11,894,252,000,000 제 36 기 2017.12.31 현재 ## 28 20,294,578,000,000 제 36 기 2017.12.31 현재 ## bfefrmtrm_amount ord ## 27 9,672,412,000,000 1 ## 28 20,058,498,000,000 3 ## ## $`00413046` ## rcept_no reprt_code bsns_year corp_code ## 53 20200410002837 11011 2019 00413046 ## 54 20200410002837 11011 2019 00413046 ## stock_code fs_div fs_nm sj_div sj_nm ## 53 068270 CFS 연결재무제표 BS 재무상태표 ## 54 068270 CFS 연결재무제표 BS 재무상태표 ## account_nm thstrm_nm thstrm_dt ## 53 유동자산 제 29 기 2019.12.31 현재 ## 54 비유동자산 제 29 기 2019.12.31 현재 ## thstrm_amount frmtrm_nm frmtrm_dt ## 53 1,787,340,254,600 제 28 기 2018.12.31 현재 ## 54 2,106,351,351,846 제 28 기 2018.12.31 현재 ## frmtrm_amount bfefrmtrm_nm bfefrmtrm_dt ## 53 1,664,478,918,682 제 27 기 2017.12.31 현재 ## 54 1,876,147,755,272 제 27 기 2017.12.31 현재 ## bfefrmtrm_amount ord ## 53 1,614,033,788,024 1 ## 54 1,701,493,916,629 3 split() 함수 내 f 인자를 통해 corp_code, 즉 고유번호 단위로 각각의 리스트에 데이터가 저장됩니다. 6.3.6 단일회사 전체 재무제표 단일회사의 전체 재무제표 데이터 역시 다운로드 받을 수 있으며 개발가이드는 다음과 같습니다. https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2019020 예제로써 삼성전자의 2019년 사업보고서에 나와있는 전체 재무제표를 다운로드 받도록 하겠습니다. corp_code = '00126380' bsns_year = 2019 reprt_code = '11011' url_fs_all = paste0( 'https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json?crtfc_key=', dart_api, '&corp_code=', corp_code, '&bsns_year=', bsns_year, '&reprt_code=', reprt_code,'&fs_div=CFS' ) 역시나 앞선 예제들과 거의 동일화며, url의 api/ 뒷 부분을 [fnlttSinglAcntAll.json] 으로 변경해주도록 합니다. 연결재무제표와 일반재무제표를 구분하는 fs_div 인자는 연결재무제표를 의미하는 CFS로 선택해줍니다. fs_data_all = fromJSON(url_fs_all) fs_data_all = fs_data_all[['list']] head(fs_data_all) ## rcept_no reprt_code bsns_year corp_code sj_div ## 1 20200330003851 11011 2019 00126380 BS ## 2 20200330003851 11011 2019 00126380 BS ## 3 20200330003851 11011 2019 00126380 BS ## 4 20200330003851 11011 2019 00126380 BS ## 5 20200330003851 11011 2019 00126380 BS ## 6 20200330003851 11011 2019 00126380 BS ## sj_nm ## 1 재무상태표 ## 2 재무상태표 ## 3 재무상태표 ## 4 재무상태표 ## 5 재무상태표 ## 6 재무상태표 ## account_id ## 1 ifrs-full_CurrentAssets ## 2 ifrs-full_CashAndCashEquivalents ## 3 dart_ShortTermDepositsNotClassifiedAsCashEquivalents ## 4 -표준계정코드 미사용- ## 5 -표준계정코드 미사용- ## 6 ifrs-full_CurrentFinancialAssetsAtFairValueThroughProfitOrLossMandatorilyMeasuredAtFairValue ## account_nm account_detail ## 1 유동자산 - ## 2 현금및현금성자산 - ## 3 단기금융상품 - ## 4 단기매도가능금융자산 - ## 5 단기상각후원가금융자산 - ## 6 단기당기손익-공정가치금융자산 - ## thstrm_nm thstrm_amount frmtrm_nm frmtrm_amount ## 1 제 51 기 181385260000000 제 50 기 174697424000000 ## 2 제 51 기 26885999000000 제 50 기 30340505000000 ## 3 제 51 기 76252052000000 제 50 기 65893797000000 ## 4 제 51 기 제 50 기 ## 5 제 51 기 3914216000000 제 50 기 2703693000000 ## 6 제 51 기 1727436000000 제 50 기 2001948000000 ## bfefrmtrm_nm bfefrmtrm_amount ord thstrm_add_amount ## 1 제 49 기 146982464000000 1 <NA> ## 2 제 49 기 30545130000000 2 <NA> ## 3 제 49 기 49447696000000 3 <NA> ## 4 제 49 기 3191375000000 4 <NA> ## 5 제 49 기 5 <NA> ## 6 제 49 기 6 <NA> 총 210개의 재무제표 항목이 다운로드 됩니다. 이 중 thstrm_nm와 thstrm_amount는 당기(금년), frmtrm_nm과 frmtrm_amount는 전기, bfefrmtrm_nm과 bfefrmtrm_amount는 전전기를 의미합니다. 따라서 해당 열을 통해 최근 3년 재무제표 만을 선택할 수도 있습니다. yr_count = str_detect(colnames(fs_data_all), 'trm_amount') %>% sum() yr_name = seq(bsns_year, (bsns_year - yr_count + 1)) fs_data_all = fs_data_all[, c('corp_code', 'sj_nm', 'account_nm', 'account_detail')] %>% cbind(fs_data_all[, str_which(colnames(fs_data_all), 'trm_amount')]) colnames(fs_data_all)[str_which(colnames(fs_data_all), 'amount')] = yr_name head(fs_data_all) ## corp_code sj_nm account_nm ## 1 00126380 재무상태표 유동자산 ## 2 00126380 재무상태표 현금및현금성자산 ## 3 00126380 재무상태표 단기금융상품 ## 4 00126380 재무상태표 단기매도가능금융자산 ## 5 00126380 재무상태표 단기상각후원가금융자산 ## 6 00126380 재무상태표 단기당기손익-공정가치금융자산 ## account_detail 2019 2018 ## 1 - 181385260000000 174697424000000 ## 2 - 26885999000000 30340505000000 ## 3 - 76252052000000 65893797000000 ## 4 - ## 5 - 3914216000000 2703693000000 ## 6 - 1727436000000 2001948000000 ## 2017 ## 1 146982464000000 ## 2 30545130000000 ## 3 49447696000000 ## 4 3191375000000 ## 5 ## 6 str_detect() 함수를 이용해 열 이름에 trm_amount 들어간 갯수를 확인합니다. 이는 최근 3개년 데이터가 없는 경우도 고려하기 위함입니다. (일반적으로 3이 반환될 것이며, 재무데이터가 2년치 밖에 없는 경우 2가 반환될 것입니다.) 위에서 계산된 갯수를 이용해 열이름에 들어갈 년도를 생성합니다. corp_code(고유번호), sj_nm(재무제표명), account_nm(계정명), account_detail(계정상세) 및 연도별 금액에 해당하는 trm_amount가 포함된 열을 선택합니다. 연도별 데이터에 해당하는 열의 이름을 yr_name, 즉 각 연도로 변경합니다. 6.3.6.1 전 종목 전체 재무제표 데이터 수집하기 for loop 구문을 이용해 고유번호에 해당하는 corp_code 부분만 변경해주면 전 종목의 API를 통해 재무제표 데이터를 손쉽게 수집할 수도 있습니다. 단, 일부 종목(대부분 금융주)의 경우 API로 파일이 제공되지 않습니다. library(stringr) KOR_ticker = read.csv('data/KOR_ticker.csv', row.names = 1) corp_list = read.csv('data/corp_list.csv', row.names = 1) KOR_ticker$'종목코드' = str_pad(KOR_ticker$'종목코드', 6, side = c('left'), pad = '0') corp_list$'code' = str_pad(corp_list$'code', 8, side = c('left'), pad = '0') corp_list$'stock' = str_pad(corp_list$'stock', 6, side = c('left'), pad = '0') ticker_list = KOR_ticker %>% left_join(corp_list, by = c('종목코드' = 'stock')) %>% select('종목코드', '종목명', 'code') ifelse(dir.exists('data/dart_fs'), FALSE, dir.create('data/dart_fs')) 먼저 거래소에서 받은 티커 파일과 API를 통해 받은 고유번호 파일을 불러온 후, str_pad() 함수를 통해 0을 채워주며, 고유번호의 경우 8자리로 구성되어 있습니다. 그 후 dart_fs 폴더를 생성해 줍니다. bsns_year = 2019 reprt_code = '11011' for(i in 1 : nrow(ticker_list) ) { data_fs = c() name = ticker_list$code[i] # 오류 발생 시 이를 무시하고 다음 루프로 진행 tryCatch({ # url 생성 url = paste0('https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json?crtfc_key=', dart_api, '&corp_code=', name, '&bsns_year=', bsns_year, '&reprt_code=', reprt_code,'&fs_div=CFS' ) # JSON 다운로드 fs_data_all = fromJSON(url) fs_data_all = fs_data_all[['list']] # 만일 연결재무제표 없어서 NULL 반환시 # reprt_code를 OFS 즉 재무제표 다운로드 if (is.null(fs_data_all)) { url = paste0('https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json?crtfc_key=', dart_api, '&corp_code=', name, '&bsns_year=', bsns_year, '&reprt_code=', reprt_code,'&fs_div=OFS' ) fs_data_all = fromJSON(url) fs_data_all = fs_data_all[['list']] } # 데이터 선택 후 열이름을 연도로 변경 yr_count = str_detect(colnames(fs_data_all), 'trm_amount') %>% sum() yr_name = seq(bsns_year, (bsns_year - yr_count + 1)) fs_data_all = fs_data_all[, c('corp_code', 'sj_nm', 'account_nm', 'account_detail')] %>% cbind(fs_data_all[, str_which(colnames(fs_data_all), 'trm_amount')]) colnames(fs_data_all)[str_which(colnames(fs_data_all), 'amount')] = yr_name }, error = function(e) { # 오류 발생시 해당 종목명을 출력하고 다음 루프로 이동 data_fs <<- NA warning(paste0("Error in Ticker: ", name)) }) # 다운로드 받은 파일을 생성한 각각의 폴더 내 csv 파일로 저장 # 재무제표 저장 write.csv(fs_data_all, paste0('data/dart_fs/', ticker_list$종목코드[i], '_fs_dart.csv')) # 2초간 타임슬립 적용 Sys.sleep(2) } for loop 구문을 이용해 고유번호에 해당하는 값을 변경합니다. 일부 종목의 경우 연결재무제표가 아닌 재무제표를 업로드 하는 경우가 있으며, if (is.null(fs_data_all)) 부분을 통해 연결재무제표가 없을 경우 fs_div를 OFS로 변경하여 재무제표를 다운로드 받습니다. 이를 제외하고는 앞서 살펴본 예제와 동일합니다. 데이터 수집 및 정리를 해준 후, data 폴더의 dart_fs 폴더 내에 티커_fs_dart.csv 이름으로 저장해 줍니다. Open API 내에서는 2015년 이후 재무제표 데이터를 API 형태로 제공하고 있으므로 bsns_year 부분에도 for loop 구문을 이용하면 해당 데이터를 모두 수집할 수 있습니다. 그러나 간단한 퀀트 투자를 하기에는 최근 3년의 재무제표 데이터만 있어도 충분하며, 시간이 너무 오래 걸린다는 점, API 요청한도를 초과한다는 단점이 있으므로 본 책에서는 다루지 않도록 하겠습니다. https://finance.naver.com/item/fchart.nhn?code=005930↩︎ http://comp.fnguide.com/↩︎ 분모에 사용되는 재무데이터의 구체적인 항목과 발행주식수를 계산하는 방법의 차이로 인해 여러 업체에서 제공하는 가치지표와 다소 차이가 발생할 수 있습니다.↩︎ "]
]