Post List

2019년 8월 24일 토요일

R을 이용한 스마트베타 지수 크롤링 & 샤이니를 통한 페이지 만들기




'R을 이용한 퀀트 투자 포트폴리오 만들기'가 출간 되었습니다.
많은 관심 부탁드리겠습니다.

↓ 구매링크 
http://www.yes24.com/Product/Goods/78125551?scode=032&OzSrank=1



국내에 수많은 스마트베타 관련 지수 중 개인적으로 가장 좋아하는 지수는 와이즈인덱스에서 제공하는 Wise 스마트베타 Quality, Value, Momentum, LowVol 지수입니다. 이는 한화자산운용의 Arirang 스마트베타 ETF의 기초지수 이기도 합니다.


해당 지수가 웹에서 제공되고 있기는 하지만 최대 3년까지 밖에 조회가 되지 않습니다. 그러나 웹구조만 잘 이해하고 뜯어보면 시작부터의 모든 데이터를 크롤링할 수 있습니다.



1. 웹페이지에 접속하여 개발자도구화면을 엽니다.
2. 일자 부분을 클릭하여 [최근 3년]을 선택한 후 확인을 누릅니다.
3. 결과보기 부분을 누릅니다. (화면의 스크롤창이 생기지 않아 해당 버튼이 보이지 않습니다. [Tab] 버튼을 눌러 이동시킨 후 클릭합니다.

Network 탭에는 총 2가지 항목이 생성됩니다. 먼저 첫번째 항목인 ChartData?endDT=2019-08-23&fromDT=2016-08-24&..... 을 클릭하여 Request URL에 접속합니다.

http://www.wiseindex.com/DataCenter/ChartData?endDT=2019-08-23&fromDT=2016-08-24&index_ids=WSI0601&isEnd=1&term=1


Date는 뭔가 날짜를 나타내는 항목으로 보이며, VAL1은 수익률을 나타내는 항목으로 보입니다. 즉 해당 데이터들은 그래프를 나타내는데 사용되는 데이터들입니다. 그러나 지수값이 아닌 수익률 값으로 표현되어 그리 좋은 데이터는 아닙니다.

두번째 항목인 GridData?currentPage=1&endDT=2019-08-23&... 를 클릭하여 Request URL에 접속합니다.

http://www.wiseindex.com/DataCenter/GridData?currentPage=1&endDT=2019-08-23&fromDT=2016-08-24&index_ids=WSI0601&isEnd=1&itemType=1&perPage=20&term=1




이는 하단의 Index Value 테이블과 매칭되는 데이터입니다. Date는 날짜를, VAL1은 지수값을, VAL2는 시가총액을, VAL3은 거래대금을 의미합니다. 따라서 지수값 자체가 표시된 해당 데이터를 크롤링 하는것이 훨씬 좋습니다.

위의 주소는 총 3년에 해당하는 데이터만이 있으며, 페이지에서도 3년치 데이터밖에 선택할 수 없습니다. 그러나 URL 주소를 조금만 수정하면 훨씬 긴 기간의 데이터를 선택할 수 있습니다. URL 중 수정해야할 항목은 다음과 같습니다.

endDT : 종료일자
fromDT : 시작일자
perPage = 출력갯수

이를 고려하여 수정된 url은 다음과 같습니다.

http://www.wiseindex.com/DataCenter/GridData?currentPage=1&endDT=2019-08-09&fromDT=2000-01-01&index_ids=WSI0601&isEnd=1&itemType=1&perPage=10000&term=1


시작을 2000년 1월 1일로 변경하며, 데이터를 최대한 넉넉하게 10000으로 잡아줍니다. 해당 url을 접속하면 지수 산출 이후 모든 값이 표시됩니다. 그러나 Date가 15652740000와 같은 익숙하지 않은 모습으로 표시되어 있습니다.

이제부터 본격적으로 크롤링을 통해 데이터를 클렌징 해보겠습니다.


library(jsonlite)
library(dplyr)
library(stringr)
library(lubridate)
library(magrittr)
library(timetk)
library(PerformanceAnalytics)
library(quantmod)

url = 'http://www.wiseindex.com/DataCenter/GridData?currentPage=1&endDT=2019-08-09&fromDT=2000-01-01&index_ids=WSI0601&isEnd=1&itemType=1&perPage=10000&term=1'

down = fromJSON(url)
head(down)

  ROW_IDX TOT_ROW                TRD_DT IDX1_VAL1 IDX1_VAL2 IDX1_VAL3
1       1    4489 /Date(1565276400000)/   5812.28 339538918  928281.6
2       2    4489 /Date(1565190000000)/   5768.94 337007243 1453117.2
3       3    4489 /Date(1565103600000)/   5702.72 333138712 1193662.1
4       4    4489 /Date(1565017200000)/   5738.02 335200868 1575052.4
5       5    4489 /Date(1564930800000)/   5852.63 341895953 1389064.1
6       6    4489 /Date(1564671600000)/   5957.01 347993864 1429756.4

JSON 형식의 데이터는 fromJSON() 함수를 통해 쉽게 테이블형태로 읽어올 수 있습니다. 이 중 날짜에 해당하는 TRD_DT와 지수값에 해당하는 IDX1_VAL1를 추출합니다.


data = down %>%
  select('TRD_DT', 'IDX1_VAL1') %>%
  mutate(TRD_DT = str_match_all(TRD_DT, '[0-9]+'),
         TRD_DT = as.numeric(TRD_DT))

head(data)
         TRD_DT IDX1_VAL1
1 1565276400000   5812.28
2 1565190000000   5768.94
3 1565103600000   5702.72
4 1565017200000   5738.02
5 1564930800000   5852.63
6 1564671600000   5957.01

먼저 select() 함수를 이용해 두개 열을 선택해준 후, mutate() 함수를 통해 클렌징을 해줍니다. 먼저 str_match_all() 함수에서 정규표현식을 이용해 TRD_DT 열 중 숫자에 해당하는 부분만 추출하며, 이를 numeric 형태로 변경합니다.

그렇다면 과연 1565276400000 숫자가 왜 date를 의미할까요? 이를 POSIXct 형태이며, 의미를 알아보기 위해 분해해보도록 하겠습니다.

gap = data[1,1] - data[2,1]
gap
[1] 86400000

gap / 24 # 시간
[1] 3600000

gap / 24 / 60 # 분
[1] 60000

gap / 24 / 60 / 60 # 초
[1] 1000

1. 먼저 첫번째 행의 데이터와 두번째 행의 데이터를 빼보면 86400000 값이 나옵니다. 지수는 일별로 나오니, 직관적으로 이 숫자가 하루에 해당하는 숫자임을 알수 있습니다.
2. 이를 '시간'에 해당하는 24로 나누면 3600000 값이 나옵니다.
3. 이를 다시 '분'에 해당하는 60으로 나누면 60000 값이 나옵니다.
4. 다시 다시 '초'에 해당하는 60으로 나누면 1000이 나옵니다.

즉, 86400000 숫자는 천분의 1초인 밀리세컨드의 하루에 해당하는 값입니다. 또한 POSIXct 데이터는 일반적으로 1970년 1월 1일을 시작으로 하며(이는 정의하는 곳 마다 다릅니다), 첫번째 행값인 1565276400000는 1970년 1월 1일부터 1565276400000 밀리세컨드가 지난 날짜를 의미합니다. 한번 해당 값을 날짜로 바꿔보도록 하겠습니다.


test_date = data[1,1]
test_date
[1] 1565276400000
 
test_date %>% 
  divide_by(1000) %>%
  as.POSIXct(origin = '1970-01-01') %>%
  as.character() %>%
  as.Date()
[1] "2019-08-09"

1. 일반적으로 R내에서 POSIXct 형식은 초까만 인식하므로, divide_by() 함수를 통해 1000으로 나누어 줍니다.
2. as.POSIXct() 함수를 통해 날짜 형태로 변경해주며, 시작점은 1970년 1월 1일로 입력합니다.
3. as.character()와 as.Date() 함수를 통해 데이트 형태로 변경합니다.

물론 as.POSIXct() 함수 뒤 바로 as.Date() 함수를 이용해도 되지만, KST와 UCT 간의 시차 차이로 하루씩 날짜가 밀릴 수도 있으므로, 캐릭터 형태로 변경 후 데이트 형태로 변경하는 것이 안전합니다.

위 과정을 통해 POSIXct 값을 날짜로 변경하는 법을 알아보았습니다. 이를 함수 형태로 적용하여 TRD_DT에 해당 클렌징 작업을 실시합니다.


posix_to_date = function(df){
 df %>% 
  divide_by(1000) %>%
  as.POSIXct(., origin = '1970-01-01') %>%
  as.character() %>%
  as.Date()
}

data = data %>%
  mutate(TRD_DT = posix_to_date(TRD_DT))

head(data)
      TRD_DT IDX1_VAL1
1 2019-08-09   5812.28
2 2019-08-08   5768.94
3 2019-08-07   5702.72
4 2019-08-06   5738.02
5 2019-08-05   5852.63
6 2019-08-02   5957.01

data = tk_xts(data, date_var = TRD_DT)
plot(data)




먼저 posix_to_date() 함수를 만든 후, 이를 data의 TRD_DT 열에 적용해줍니다. 결과를 확인해보면 우리에게 익숙한 date 형태로 변경되었습니다.

tk_xts() 함수를 이용해 xts 형태로 만들어주두록 합니다.

해당 과정을 4개 지수 모두에 적용할 수도 있으며, fromDT와 endDT, perPage 값만 수정하면 처음부터 오늘까지의 데이터를 자동으로 가져올 수 있습니다.

먼저 4개 지수의 티커는 다음과 같습니다.

퀄리티: WSI0601
밸류: WSI0602
모멘텀: WSI0603
로우볼: WSI0604

이를 고려한 변수는 다음가 같습니다.

ticker = c('WSI0601', 'WSI0602', 'WSI0603', 'WSI0604')

DT_start = '2001-06-15' %>% as.Date()
DT_end = today()
DT_length = interval(DT_start, DT_end) / days() + 1

티커에는 지수의 티커를 입력합니다.
DT_start는 지수의 시작값인 2001년 6월 15일을 입력합니다.
DT_end에는 today()를 통해 오늘 날짜를 입력합니다.
DT_length() 에는 시작일과 종료일의 차이를 interval() 함수를 통해 계산합니다.

index_list = list()

for (tick in ticker) {
  url = str_c('http://www.wiseindex.com/DataCenter/GridData?currentPage=1&endDT=',DT_end,'&fromDT=',DT_start,'&index_ids=',tick,'&isEnd=1&itemType=1&perPage=',DT_length,'&term=1')
  
  down = fromJSON(url) %>%
    select('TRD_DT', 'IDX1_VAL1') %>%
    mutate(TRD_DT = str_match_all(TRD_DT, '[0-9]+'),
           TRD_DT = as.numeric(TRD_DT),
           TRD_DT = posix_to_date(TRD_DT))
    
  index_list[[tick]] = tk_xts(down, date_var = TRD_DT)
  
  Sys.sleep(1)
   
}

index_list = do.call(cbind, index_list)
head(index_list)
           IDX1_VAL1 IDX1_VAL1.1 IDX1_VAL1.2 IDX1_VAL1.3
2001-06-15   1000.00     1000.00     1000.00     1000.00
2001-06-18    993.64      994.97      993.30      992.98
2001-06-19    993.45      990.53      990.58      988.40
2001-06-20    981.32      969.01      972.51      971.04
2001-06-21    984.44      969.60      977.13      974.00
2001-06-22    994.70      977.23      987.43      981.58

tail(index_list)
           IDX1_VAL1 IDX1_VAL1.1 IDX1_VAL1.2 IDX1_VAL1.3
2019-08-16   5733.32     6672.15     7854.46     7719.23
2019-08-19   5786.91     6827.36     7953.38     7828.81
2019-08-20   5820.50     6855.78     7975.84     7855.61
2019-08-21   5825.33     6896.77     7995.75     7886.54
2019-08-22   5772.74     6859.46     7946.59     7861.81
2019-08-23   5742.52     6824.45     7906.79     7815.98

for loop 내에서 위 변수들을 응용해 4개 지수의 모든 값을 다운로드 합니다. 결과를 확인해보면 시작인 2001년 6월 15일부터 현재까지 지수가 다운로드 됩니다.


KOSPI = getSymbols('^KS11', from = '2001-06-15', auto.assign = FALSE)
KOSPI = Ad(KOSPI)

index_list = cbind(index_list, KOSPI) %>% na.omit()
names(index_list) =
  c('퀄리티', '밸류', '모멘텀', '로우볼', '코스피')


마지막으로 벤치마크인 KOSPI 지수를 getSymbols() 함수를 통해 다운로드 받습니다. 그 후 스마트베타 지수와 코스피 지수를 합친 후, names()를 통해 열 이름을 입력합니다.

index_list %>% Return.calculate() %>%
  charts.PerformanceSummary(main = 'Smartbeta Index')


스마트베타 지수와 코스피 지수가 제대로 들어왔습니다.


index_list['2018::'] %>% Return.calculate() %>%
  charts.PerformanceSummary(main = 'Smartbeta Index')


xts형식은 내부에 일자를 입력하여 간단하게 시작일과 종료일 기준으로 데이터를 선택할 수 있습니다. index_list['2018::']를 통해 2018년 이후 데이터를 살펴보면 위와 같습니다.

===================

이번에는 shiny에서 위 과정을 적용하여 페이지를 만들어 보도록 하겠습니다.


library(jsonlite)
library(dplyr)
library(stringr)
library(lubridate)
library(magrittr)
library(timetk)
library(PerformanceAnalytics)
library(quantmod)
library(shiny)
library(plotly)
library(tidyr)
library(DT)

# Define posix to date function (for mutate date) 
posix_to_date = function(df){
    df %>% 
        divide_by(1000) %>%
        as.POSIXct(., origin = '1970-01-01') %>%
        as.character() %>%
        as.Date()
}

# Define Download Data function
down_index = function() {
    
    # smartbeta index ticker
    ticker = c('WSI0601', 'WSI0602', 'WSI0603', 'WSI0604')
    
    # set date
    DT_start = '2001-06-15' %>% as.Date()
    DT_end = today()
    DT_length = interval(DT_start, DT_end) / days() + 1
    
    index_list = list()
    
    # download data
    for (tick in ticker) {
        
        url = str_c('http://www.wiseindex.com/DataCenter/GridData?currentPage=1&endDT=',DT_end,'&fromDT=',DT_start,'&index_ids=',tick,'&isEnd=1&itemType=1&perPage=',
                    DT_length,'&term=1')
        
        down = fromJSON(url) %>%
            select('TRD_DT', 'IDX1_VAL1') %>%
            mutate(TRD_DT = str_match_all(TRD_DT, '[0-9]+'),
                   TRD_DT = as.numeric(TRD_DT),
                   TRD_DT = posix_to_date(TRD_DT))
        
        index_list[[tick]] = tk_xts(down, date_var = TRD_DT)
        
        Sys.sleep(1)
        
    }
    
    index_list = do.call(cbind, index_list)
    
    # Download KOSPI index from yahoo
    KOSPI = getSymbols('^KS11', from = '2001-06-15', auto.assign = FALSE)
    KOSPI = Ad(KOSPI)
    
    # Bind data
    index_list = cbind(index_list, KOSPI) %>% na.omit()
    names(index_list) =
        c('퀄리티', '밸류', '모멘텀', '로우볼', '코스피')
    
    return(index_list)
}

먼저 패키지를 불러오며 데이터 크롤링 및 클렌징 하는 과정을 down_index() 함수로 만들어 줍니다.


# Define UI for application that draws a histogram
ui = fluidPage(

    # Application title
    titlePanel("Smartbeta Index List"),

    # Show a plot of the generated distribution
    mainPanel(
        sliderInput("range",
                    "Dates:",
                    min = as.Date('2001-06-18'),
                    max = Sys.Date(),
                    value = c(Sys.Date() - years(3), Sys.Date()),
                    step = 1,
                    timeFormat="%Y-%m-%d",
                    width = '100%'),

        br(),
        br(),
        plotlyOutput("chart"),
        br(),
        DT::dataTableOutput("table"),
        br(),
        fluidRow(
            column(1, offset = 10,
                   downloadButton("downloadData", "Download Data")
            ))
        )
    )


ui는 웹페이지에 보이는 내용을 정의합니다.
먼저 sliderInput을 통해 일자를 선택할 수 있는 바를 생성합니다. min 즉 시작일은 지수의 시작일인 2001년 6월 18일, max 즉 종료일은 오늘 날짜를 입력하며, default 값인 value는 오늘부터 3년전 값을 입력합니다. 결과적으로 페이지에는 아래와 같은 모습으로 나타납니다.



나중에 server에서 작성될 차트와 테이블을 각각 plotlyOutput() 함수와 dataTableOutput() 함수를 통해 출력해주며, downloadButton() 함수를 통해 지수 데이터를 다운로드 하는 버튼을 만들어 줍니다.


# Define server logic required to draw a return chart
server = function(input, output) {

    # download data
    df = down_index() %>%
        Return.calculate() %>% na.omit()
    
    # plot graph
    output$chart = renderPlotly({
        
        # select period
        df_plotly = df[paste0(input$range[1],"::",input$range[2])]
        df_plotly = cumprod(1+df_plotly) - 1
        
        # draw chart
        df_plotly %>% 
            fortify.zoo() %>%
            gather(key, value, -c('Index')) %>%
            mutate(key = factor(key, levels = unique(key))) %>%
            plot_ly(x = ~Index, y = ~ value, color = ~key) %>%
            add_lines() %>%
            layout(xaxis = list(title = "",
                                type = 'date',
                                tickformat = '%y-%m-%d'),
                   yaxis = list(title = "",
                                tickformat = '%'),
                   legend = list(orientation = 'h',
                                 xanchor = "center",
                                 x = 0.5))
    })
    
    # raw data table
    output$table = DT::renderDataTable({
        
        data.frame(round(df,4)) %>%
            cbind('Date' = rownames(.), .) %>%
            `rownames<->%
            arrange(desc(Date)) %>%
            datatable(options = list(pageLength = 50))
      })
    
    # download button
    output$downloadData = downloadHandler(
        filename = function() {
            paste("ret_data", ".csv", sep="")
            },
        content = function(file) {
            write.csv(data.frame(round(df,4)), file)
            }
        )
      
}

server 부분은 웹페이지에 출력될 데이터를 가공하는 영역입니다.

먼저 df에는 사전에 정의한 down_index() 함수를 통해 지수 데이터를 다운로드 받고, Return.calculate() 함수를 통해 수익률을 계산합니다.

output$chart는 plotly 형태의 그래프를 그려줍니다. 먼저 app에서 sliderInput에서 입렫받은 input값(시작일, 종료일)을 통해 xts 형태인 df의 날짜를 선택합니다.

df[paste0(input$range[1],"::",input$range[2])]

그 후, cumprod() 함수를 통해 해당일자의 누적수익률을 계산해 줍니다. 마지막으로 plot_ly() 함수를 통해 그래프로 나타내 줍니다.

output$table은 DataTable형태의 테이블을 만들어줍니다.
output$downloadData은 위의 데이터를 csv 형태로 다운로드 받게합니다.


# Run the application 
shinyApp(ui = ui, server = server)

마지막으로 shinyApp() 함수를 통해 샤이니를 실행합니다.




실행된 앱을 살펴보면, 최근 3년 기준 지수의 누적수익률이 그래프로 나타나며, 하단에는 모든 raw data가 표시됩니다.



상단의 슬라이드를 움직이면, 해당 기간의 그래프로 변경되어 표시됩니다.


테이블 맨 하단의 Download Data 버튼을 누르면 'red_data.csv' 파일이 다운로드 되며, 해당 파일을 확인하면 스마트베타 지수 및 코스피 지수의 전체 데이터가 다운로드 됩니다.

댓글 없음:

댓글 쓰기