본문 바로가기

Data Scraping/#Quant Portfolio

[퀀트] R을 활용한 퀀트 투자 포트폴리오 만들기 (5)

Chapter 5 금융 데이터 수집하기 (기본)

 

5.1 한국거래소의 산업별 현황 및 개별지표 크롤링

 

5.1.1 산업별 현황 크롤링

산업별 현황 페이지에서 OTP를 받고 이를 통해 데이터를 다운로드함

library(httr)
library(rvest)
library(readr)

# 항목을 제출할 url
gen_otp_url = 'http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx'

# f12 화면의 쿼리 내용을 리스트 형태로 입력
gen_otp_data = list(
	name = 'fileDown',
    filetype = 'csv', #기존 xls에서 변경
    url = 'MKD/03/0303/03030103/mkd03030103',
    tp_cd = 'ALL',
    date = '20190607',
    lang = 'ko',
    pagePath = '/contents/MKD/03/0303/03030103/MKD03030103.jsp')
    
# gen_otp_url에 쿼리를 전송한 것을 POST() 함수를 통해 받음
otp = POST(gen_otp_url, query = gen_otp_data) %>% 
	read_html() %>% #html 내용 읽기
    html_text() # html 파일 중 텍스트에 해당하는 부분만 추출하기 (OTP값만)
    

위 코드와 같이 개발자 탭의 Query String Parameter에 적힌 것들을 POST 방식으로 요청하여 gen_otp_url 주소에 쿼리로 발송함

# OTP를 제출할 url
down_url = 'http://file.krx.co.kr/download.jspx' 

# POST 방식으로 OTP를 url에 제출
down_sector = POST(down_url, query = list(code = otp),
					add_headers(referer = gen_otp_url)) %>% #referer 추가 
                    # OTP를 부여받고 제출하는 과정의 흔적을 남겨 로봇이 아님을 증명
	read_html() %>% # html 읽기
    html_text() %>% # text만 추출
    read_csv() # 요청 쿼리에서 csv 파일로 타입을 설정했으므로
    
    print(down_sector)
    

(결과) 산업별 현황 데이터 

A tibble: 2,243 x 7
    시장구분 종목코드 종목명_산업분류  	`현재가(종가)`

  1 코스피   030720   동원수산… 어업               8940
  2 코스피   007160   사조산업… 어업              54400
  3 코스피   006040   동원산업… 어업             246500
  4 코스피   280360   롯데제과… 음식료품         159500
  5 코스피   271560   오리온 음식료품          83300
  6 코스피   26490K   크라운제과… 음식료품           7930
  7 코스피   264900   크라운제과… 음식료품           9750
  8 코스피   248170   샘표식품… 음식료품          30100
  9 코스피   101530   해태제과식… 음식료품           9500
 10 코스피   145995   삼양사우… 음식료품          37300
  … with 2,233 more rows, and 2 more variables:
    전일대비 <dbl>, `시가총액(원)` <dbl>

위 데이터를 csv 파일로 저장

# data 폴더 생성
ifelse(dir.exists('data'), FALSE, dir.create('data'))
write.csv(down_sector, 'data/krx_sector.csv') # 파일 쓰기

 

5.1.2 개별종목 지표 크롤링

5.1.1의 과정과 유사, 요청하는 쿼리 값에만 차이가 있음

f12 개발자 탭에서 isu_cdnm, isu_cd, isu_nm, isu_srt_cd, fromdate을 제외한 항목을 쿼리로 전송

library(httr)
library(rvest)
library(readr)

gen_otp_url = 'http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx'

gen_otp_data = list(
	name = 'fileDown',
    filetype = 'csv',
    url = 'MKD/13/1302/13020401/mkd13020401',
    market_gubun = 'ALL',
    gubun = '1',
    schdate = '20190607',
    pagePath = '/contents/MKD/13/1302/13020401/MKD13020401.jsp')

otp = POST(gen_otp_url, query = gen_otp_data) %>%
	read_html() %>%
    html_text()
    
down_url = 'http://file.krx.co.kr/download.jspx'
down_ind = POST(down_url, query = list(code = otp),
				add_headers(referer = gen_otp_url)) %>%
    read_html() %>%
    html_text() %>%
    read_csv()
    
print(down_ind)

(결과) 개별종목 지표 데이터

    A tibble: 2,204 x 13
  
    일자       종목코드 종목명_관리여부     종가   EPS   PER  

  1 2019-06-07 000250   삼천당제약… -        39650 409   96.94
  2 2019-06-07 000440   중앙에너비… -         6880 958   7.18 
  3 2019-06-07 001000   신라섬유… -         2225 7     317.…
  4 2019-06-07 001540   안국약품… -        11350 1,154 9.84 
  5 2019-06-07 001810   무림SP -         2795 505   5.53 
  6 2019-06-07 001840   이화공영… -         5290 24    220.…
  7 2019-06-07 002230   피에스텍… -         4275 -     -    
  8 2019-06-07 002290   삼일기업공… -         3185 250   12.74
  9 2019-06-07 002680   한탑   -         2435 -     -    
  10 2019-06-07 002800   신신제약… -         7050 191   36.91
 
  … with 2,194 more rows, and 6 more variables:
    BPS <chr>, PBR <chr>, 주당배당금 <dbl>,
    배당수익률 <dbl>, `게시물 일련번호` <dbl>,
    총카운트 <dbl>

csv 파일로 쓰기

write.csv(down_ind, 'data/krx_ind.csv')

 

5.1.3 최근 영업일 기준 데이터 받기

위 코드에서 date와 schdate 항목이 원하는 날짜로 자동 반영되게 만들고 싶음

네이버 금융의 증시자금동향 페이지에 탑재된 자동 업데이트 기능을 크롤링해 쿼리 항목에 사용할 것임

이때, 크롤링해 올 대상이 소수이므로 HTML 구조로 가져와 분해 후 추출하는 것보다, XML 중 태그나 속성을 찾기 쉽도록 만든 주소인 Xpath를 이용하는 것이 효율적 (Copy Xpath를 통해 복사해올 수 있음)

library(httr)
library(rvest)
library(stringr)

# 페이지 url 저장
url = 'https://finance.naver.com/sise/sise_deposit.nhn' 

# GET 방식으로 해당 페이지의 내용을 받음
biz_day = GET(url) %>%
	read_html(encoding = 'EUC-KR') %>% # html 내용을 읽어와 인코딩 지정
    html_nodes(xpath = '//*[@id="type_1"]/div/ul[2]/li/span') %>% # Xpath를 통해 원하는 지점의 데이터 추출
    html_text() %>% # 텍스트만 추출
    str_match(('[0-9]+.[0-9]+.[0-9]+') ) %>% # 정규표현식을 활용해 '숫자.숫자.숫자' 형식으로 추출
    str_replace_all('\\.', '') # 마침표 제거
    
print(biz_day)

(결과) yyyymmdd 형태의 날짜 

[1] "20200527"

이 결과를 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://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx'
gen_otp_data = list(
	name = 'fileDown',
    filetype = 'csv',
    url = 'MKD/03/0303/03030103/mkd03030103',
    tp_cd = 'ALL',
    date = biz_day # 최근영업일로 변경
    lang = 'ko',
    pagePath = '/contents/MKD/03/0303/03030103/MKD03030103.jsp')
    
 otp = POST(gen_otp_url, query = gen_otp_data) %>%
 	read_html() %>%
    html_text()
    
# 산업별 현황 데이터 다운로드
down_url = 'http://file.krx.co.kr/download.jspx'
down_sector = POST(down_url, query = list(code = otp),
					add_headers(referer = gen_otp_url)) %>%
	read_html() %>%
    html_text() %>%
    read_csv()
    
ifelse(dir.exists('data'), FALSE, dir.create('data'))
write.csv(down_sector, 'data/krx_sector.csv')

# 개별종목 지표 OTP 발급
gen_otp_url = 'http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx'
gen_otp_data = list(
	name = 'fileDown',
  filetype = 'csv',
  url = "MKD/13/1302/13020401/mkd13020401",
  market_gubun = 'ALL',
  gubun = '1',
  schdate = biz_day, # 최근영업일로 변경
  pagePath = "/contents/MKD/13/1302/13020401/MKD13020401.jsp")

otp = POST(gen_otp_url, query = gen_otp_data) %>%
	read_html() %>%
    html_text()
    
# 개별종목 지표 데이터 다운로드
down_url = 'http://file.krx.co.kr/download.jspx'
down_ind = POST(down_url, query = list(code = otp),
				add_headers(referer = gen_otp_url)) %>%
    read_html() %>%
    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,
						stringAsFactors = FASLE)
down_ind = read.csv('data/krx_ind.csv', row.names = 1,
					stringAsFactors = FALSE)

# 중복되는 열 이름 추출
intersect(names(down_sector), names(down_ind))

(결과)

[1] "종목코드" "종목명"

두 데이터에 중복되지 않는 종목만 추출 

stdiff(down_sector[, '종목명'], down_ind[, '종목명']) 

(결과) 선박, 광물 펀드 및 해외 종목 등 일반적이지 않은 종목 -->  제외

  [1] "엘브이엠씨홀딩스"   "한국패러랠"        
  [3] "한국ANKOR유전"      "맵스리얼티1"       
  [5] "맥쿼리인프라"       "하나니켈2호"       
  [7] "하나니켈1호"        "베트남개발1"       
  [9] "NH프라임리츠"       "롯데리츠"          
 [11] "신한알파리츠"       "이리츠코크렙"      
 [13] "모두투어리츠"       "하이골드12호"      
 [15] "하이골드8호"        "바다로19호"        
 [17] "하이골드3호"        "케이탑리츠"        
 [19] "에이리츠"           "동북아13호"        
 [21] "동북아12호"         "컬러레이"          
 [23] "JTC"                "뉴프라이드"        
 [25] "윙입푸드"           "글로벌에스엠"      
 [27] "크리스탈신소재"     "씨케이에이치"      
 [29] "차이나그레이트"     "골든센츄리"        
 [31] "오가닉티코스메틱"   "GRT"               
 [33] "로스웰"             "헝셩그룹"          
 [35] "이스트아시아홀딩스" "에스앤씨엔진그룹"  
 [37] "SNK"                "SBI핀테크솔루션즈" 
 [39] "잉글우드랩"         "코오롱티슈진"      
 [41] "엑세스바이오"
# by 뒤의 기준(공통으로 존재하는 종목코드, 종목명)으로 merge
KOR_ticker = merge(down_sector, down_ind,
					by = intersect(names(down_sector),
                    				names(down_ind)),
                    all = FALSE # 교집합을 반환 (공통으로 존재하는 항목)
                    )
                    
# 내림차순 정렬
KOR_ticker = KOR_ticker[order(-KOR_ticker['시가총액.원.']), ]
print(head(KOR_ticker))

(결과) 

      종목코드           종목명 시장구분 산업분류
 332    005930         삼성전자   코스피 전기전자
 45     000660       SK하이닉스   코스피 전기전자
 333    005935       삼성전자우   코스피 전기전자
 1938   207940 삼성바이오로직스   코스피   의약품
 852    035420            NAVER   코스피 서비스업
 1082   051910           LG화학   코스피     화학
 
      현재가.종가. 전일대비 시가총액.원.       일자
 332         52100    -2500    3.110e+14 2020-03-11
 45          85500    -3600    6.224e+13 2020-03-11
 333         44350    -1150    3.650e+13 2020-03-11
 1938       484000   -12000    3.202e+13 2020-03-11
 852        170000    -2000    2.802e+13 2020-03-11
 1082       365000    -8500    2.577e+13 2020-03-11
 
 		관리여부   종가    EPS   PER     BPS  PBR
 332         -  52100  6,461  8.06  35,342 1.47
 45          -  85500 22,255  3.84  64,348 1.33
 333         -  44350      -     -       -    -
 1938        - 484000  3,387 142.9  62,805 7.71
 852         - 170000  4,437 38.31  31,795 5.35
 1082        - 365000 19,217 18.99 218,227 1.67
 
      주당배당금 배당수익률 게시물..일련번호 총카운트
 332        1416       2.72             1518       NA
 45         1500       1.75             1471       NA
 333        1417       3.20             1851       NA
 1938          0       0.00             1650       NA
 852         314       0.18             1591       NA
 1082       6000       1.64             1603       NA

스팩, 우선주 종목 제외

library(stringr)

# 종목명에 '스팩'이 들어가는 종목을 찾음
KOR_ticker[grepl('스팩', KOR_ticker[, '종목명']), '종목명']

# str_sub() 함수로 종목코드 끝이 0이 아닌 우선주 종목을 찾음
KOR_ticker[str_sub(KOR_ticker[, '종목코드'], -1, -1) != 0,  '종목명']

(스팩 종목만 찾은 결과)

  [1] "엔에이치스팩14호"    "하나금융11호스팩"   
  [3] "케이비제18호스팩"    "엔에이치스팩12호"   
  [5] "삼성스팩2호"         "한화에스비아이스팩" 
  [7] "미래에셋대우스팩3호" "신한제4호스팩"      
  [9] "유안타제5호스팩"     "SK6호스팩"          
 [11] "케이비17호스팩"      "대신밸런스제7호스팩"
 [13] "SK4호스팩"           "한국제7호스팩"      
 [15] "대신밸런스제6호스팩" "신한제6호스팩"      
 [17] "IBKS제11호스팩"      "동부스팩5호"        
 [19] "상상인이안1호스팩"   "하나머스트제6호스팩"
 [21] "유안타제4호스팩"     "삼성머스트스팩3호"  
 [23] "DB금융스팩7호"       "한국제6호스팩"      
 [25] "하이제4호스팩"       "하나금융9호스팩"    
 [27] "하나금융14호스팩"    "유안타제3호스팩"    
 [29] "IBKS제10호스팩"      "미래에셋대우스팩4호"
 [31] "SK5호스팩"           "케이비제19호스팩"   
 [33] "신한제5호스팩"       "신영스팩6호"        
 [35] "교보7호스팩"         "유진스팩5호"        
 [37] "상상인이안제2호스팩" "교보8호스팩"        
 [39] "IBKS제7호스팩"       "키움제5호스팩"      
 [41] "한화에이스스팩4호"   "신영스팩5호"        
 [43] "유진스팩4호"         "한국제8호스팩"      
 [45] "엔에이치스팩13호"    "IBKS제12호스팩"

(우선주만 찾은 결과)

   [1] "삼성전자우"         "현대차2우B"        
   [3] "LG생활건강우"       "현대차우"          
   [5] "LG화학우"           "아모레퍼시픽우"    
   [7] "미래에셋대우2우B"   "삼성화재우"        
   [9] "LG전자우"           "신영증권우"        
  [11] "아모레G3우(전환)"   "한화3우B"          
  [13] "삼성SDI우"          "CJ4우(전환)"       
  [15] "한국금융지주우"     "대신증권우"        
  [17] "두산우"             "삼성전기우"        
  [19] "LG우"               "NH투자증권우"      
  [21] "아모레G우"          "S-Oil우"           
  [23] "현대차3우B"         "CJ제일제당 우"     
  [25] "대림산업우"         "삼성물산우B"       
  [27] "SK우"               "CJ우"              
  [29] "SK이노베이션우"     "금호석유우"        
  [31] "대신증권2우B"       "두산솔루스1우"     
  [33] "GS우"               "대교우B"           
  [35] "미래에셋대우우"     "부국증권우"        
  [37] "롯데지주우"         "코오롱인더우"      
  [39] "유한양행우"         "두산퓨얼셀1우"     
  [41] "두산2우B"           "유화증권우"        
  [43] "롯데칠성우"         "SK케미칼우"        
  [45] "호텔신라우"         "두산솔루스2우B"    
  [47] "신풍제약우"         "유안타증권우"      
  [49] "BYC우"              "한진칼우"          
  [51] "두산퓨얼셀2우B"     "LG하우시스우"      
  [53] "대한항공우"         "세방우"            
  [55] "SK디스커버리우"     "남양유업우"        
  [57] "대덕전자1우"        "태영건설우"        
  [59] "하이트진로2우B"     "대상우"            
  [61] "넥센타이어1우B"     "쌍용양회우"        
  [63] "녹십자홀딩스2우"    "한화우"            
  [65] "NPC우"              "한화솔루션우"      
  [67] "삼양홀딩스우"       "넥센우"            
  [69] "태양금속우"         "SK증권우"          
  [71] "현대건설우"         "코리아써우"        
  [73] "삼양사우"           "남선알미우"        
  [75] "서울식품우"         "덕성우"            
  [77] "코오롱우"           "SK네트웍스우"      
  [79] "계양전기우"         "금호산업우"        
  [81] "일양약품우"         "한화투자증권우"    
  [83] "유유제약1우"        "대한제당우"        
  [85] "루트로닉3우C"       "동원시스템즈우"    
  [87] "성문전자우"         "깨끗한나라우"      
  [89] "크라운해태홀딩스우" "CJ씨푸드1우"       
  [91] "대원전선우"         "하이트진로홀딩스우"
  [93] "노루페인트우"       "삼성중공우"        
  [95] "현대비앤지스틸우"   "대호피앤씨우"      
  [97] "성신양회우"         "크라운제과우"      
  [99] "코오롱글로벌우"     "대상홀딩스우"      
 [101] "소프트센우"         "금강공업우"        
 [103] "JW중외제약우"       "DB하이텍1우"       
 [105] "동부건설우"         "진흥기업우B"       
 [107] "흥국화재우"         "진흥기업2우B"      
 [109] "한양증권우"         "코리아써키트2우B"  
 [111] "동양우"             "JW중외제약2우B"    
 [113] "동부제철우"         "신원우"            
 [115] "동양2우B"           "노루홀딩스우"      
 [117] "유유제약2우B"       "흥국화재2우B"      
 [119] "대한제당3우B"       "동양3우

 

5.2 WICS 기준 섹터정보 크롤링 

주식의 섹터를 나누는 기준인 GICS, 와이즈인덱스에서 이와 비슷한 WICS 산업분류를 크롤링해올 수 있음

섹터의 구성종목을 확인한 후 개발자도구 탭에서 데이터 전송과정을 확인

http://www.wiseindex.com/Index/GetIndexComponets2ceil_yn=0&dt=202000826&sec_cd=G10각각 데이터 요청 url, 실링 여부 (0은 비실링), 조회일자, 섹터코드로 구성됨

위 페이지 주소를 열어보면 JSON 형식으로 저장되어 단순한 문법과 작은 용량의 데이터로 빠르게 데이터 교환 가능하며 jsonlite 패키지의 fromJSON() 함수로 크롤링 가능

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

for loop 구문으로 섹터 번호만 변경해주면 모든 섹터의 구성 종목을 얻을 수 있음

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=', bizday, '&sec_cd =', i)
        
    data = fromJSON(url)
    data = data$list
    
    data_sector[[i]] = data
    
    Sys.sleep(1)
}


data_sector = do.call(rbind, data_sector)

write.csv(data_sector, 'data/KOR_sector.csv')

 


 

본 게시글은 다음 강의를 참고하여 공부한 내용을 기록한 것입니다.

 

https://www.fastcampus.co.kr/courses/202382/clips/

 


 

I'm a Senior Student in Data Science ! 

데이터 사이언스를 공부하고 있는 4학년 학부생의 TIL 블로그입니다. 게시글이 도움 되셨다면 구독과 좋아요 :)