Post List

2019년 7월 28일 일요일

R 샤이니를 이용한 스탁 스크리너 만들기



먼저 샤이니 앱은 크게 3가지 파트로 구성되어 있습니다.

# library(shiny)

# Define UI for application 
ui = fluidPage(

    # Application title
    titlePanel("Title"),

    # Sidebar 
    sidebarLayout(
        sidebarPanel(
            ...
        ),

        # Show a plot of the generated distribution
        mainPanel(
           ...
        )
    )
)

# Define server logic
server = function(input, output) {
    ...
}

# Run the application 
shinyApp(ui, server)


1. ui

ui는 웹페이지에 보이는 출력 화면을 잡는 부분입니다. titlePanel은 제목 부분을 나타내며, sidebarLayout은 화면 구성을 잡아줍니다. 이 중 sidebarPanel은 좌측 혹은 상단의 메뉴를, mainPanel은 아래 server에서 생성된 결과물을 본문 부분에 어떻게 출력할지 정의합니다.

페이지를 만드는 함수들이기에 기존 R 문법과는 다소 다를 수 있지만, ui 부분에 사용되는 함수는 그리 많지 않아 샤이니 홈페이지의 예제 문법들을 보며 따라해도 충분히 원하는 화면을 만들 수 있습니다.

2. server

server는 페이지에 들어가는 내용들을 잡는 부분입니다. 기존 R 코딩과 거의 비슷하며, 결과물을 어떻게 render 할지만 차이가 있습니다.

ui에서 받아온 input을 통해 계산한 후 이를 output에 저장하면, ui에서는 이 output을 출력하는 형태입니다. 즉, ui 부분과 server 부분은 매우 유기적으로 연결되어 있습니다.

3. shinyApp() 함수를 통해 앱을 실행합니다.

샤이니 문법은 기존 R 문법과 다소 다른 부분도 있으며, html 기반으로 작동하므로 어렵다고 느낄 수 있지만, Rstudio에서 제공하는 각종 샘픔을 따라해보면 금방 이해하실 수 있습니다.

https://shiny.rstudio.com/gallery/

------------------------------


다음은 스탁 스크리너를 만드는 코드입니다.


pkg = c('httr', 'rvest', 'stringr', 'readr', 'shiny',
        'dplyr', 'shinycssloaders', 'DT')

new.pkg = pkg[!(pkg %in% installed.packages()[, "Package"])]
if (length(new.pkg)) 
  install.packages(new.pkg, dependencies = TRUE)
sapply(pkg, require, character.only = TRUE)

down_df = function() {
  # 최근 영업일 구하기
  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()
  
  KOR_ticker = merge(down_sector, down_ind,
                     by = intersect(names(down_sector),
                                    names(down_ind)),
                     all = FALSE
  )
  
  
  data = KOR_ticker %>%
    mutate(PER = parse_number(PER),
           PBR = parse_number(PBR),
           ROE = PBR / PER,
           ROE = round(ROE, 2),
           시총순위 = percent_rank(desc(`시가총액(원)`)),
           시총순위 = round(시총순위, 2)) %>%
    arrange(시총순위) %>%
    select(일자, 종목코드, 종목명, 종가, PER, PBR, 배당수익률, ROE, 시총순위)
}

먼저 사전에 필요한 내용들을 정리합니다. 필요한 패키지들을 pkg에 저장한 후, 이를 설치 및 불러오는 코드를 상단에 배치합니다. 그 후, 한국거래소의 티커 및 밸류 지표들을 다운로드 받아 클랜징하는 함수를 down_df()로 만듭니다. 해당 코드에 대한 자세한 설명은 퀀트 투자 쿡북을 참조하시기 바랍니다.

https://hyunyulhenry.github.io/quant_cookbook/section-17.html#section-18
(8월말 종이책으로도 출간됩니다.)


# Define UI for app ----
ui = fluidPage(
  
  # App title ----
  titlePanel('Stock Screener'),
  
  # Sidebar to demonstrate various slider options ----
  sidebarPanel(
  
    # Input: Specification of range within  PER ----
    sliderInput("PER", "PER:",
                min = 0,
                max = 100,
                value = c(0,20),
                step = 0.01),
    
    # Input: Specification of range within PBR ----
    sliderInput("PBR", "PBR:",
                min = 0,
                max = 10,
                value = c(0, 2),
                step = 0.01),
    
    # Input: Specification of range within Div Yield ----
    sliderInput("DY", "배당수익률(%):",
                min = 0,
                max = 20,
                value = c(2, 5),
                step = 0.01),
    
    # Input: Specification of range within ROE ----
    sliderInput("ROE", "ROE:",
                min = 0,
                max = 2,
                value = c(0.1, 0.5),
                step = 0.01),
    
    # Input: Specification of range within Size ----
    sliderInput("SIZE", "SIZE(%):",
                min = 0,
                max = 1,
                value = c(0, 1),
                step = 0.01)
  ),
  
  mainPanel(
    dataTableOutput('screen_table') %>% withSpinner(color="#0dc5c1")
  )
)

다음은 ui를 통해 출력 화면을 잡습니다. 먼저 titlePanel을 통해 제목을 정해줍니다. sidebarPanel은 좌측의 스크리너 항목을 잡아주는 부분으로써, sliderInput을 통해 셀렉터 위젯을 설정해 줍니다.  해당 페이지에서는 슬라이더를 만들어 주었으며, 이 외에도 샤이니에서는 굉장히 많은 위젯을 제공하므로, 갤러리에서 원하는 위젯을 찾아 코드를 따오면 됩니다.

https://shiny.rstudio.com/gallery/widget-gallery.html



sliderInput("PER", "PER:",
                min = 0,
                max = 100,
                value = c(0,20),
                step = 0.01) 


sidebarPanel 내의 위 코드에서 맨앞의 PER는 input의 변수명을, PER:는 화면에 출력될 내용을, min은 슬라이더의 최소값, max는 슬라이더의 최대값, value는 초기에 어디에서 어디까지 보일지, step은 한 단위당 구간을 의미합니다.

즉 PER에 해당하는 슬라이더는 0에서 100까지 위치하며, 초기에는 0과 20을 슬라이더로 정합니다. 또한 범위는 0.01 단위로 선택이 가능합니다. 나머지 PBR, DY, ROE, SIZE 모두 동일하게 적용이 됩니다.

mainPanel(
    dataTableOutput('screen_table') %>% withSpinner(color="#0dc5c1")
  )

mainPanel은 output을 출력하는 부분입니다. 뒤의 server 부분에서 저장될 screen_table 라는 변수를 dataTable 형태로 출력하며, 출력이 되는 시간 동안 shinycssloaders 패키지의  withSpinner 함수를 통해 대기 화면을 보여줍니다.

이처럼 shiny의 ui 부분에서는 output을 어떠한 형태로 출력할지 정해주어야 하며, 테이블 형태는 크게 DT 패키지를 이용한 dataTable 형태, knitr과 kableextra 패키지를 이용한 html 형태의 테이블이 많이 사용됩니다. 그래프는 plotly 형태가 많이 사용됩니다.

# Define server logic to selected dataset ---- 
server = function(input, output) {  
  
  # Download Data ----
  data = down_df()
  
  # Filtered Table ----
  output$screen_table = DT::renderDataTable({
    data %>%
      filter(PER > input$PER[1] & PER < input$PER[2]) %>%
      filter(PBR > input$PBR[1] & PBR < input$PBR[2]) %>%
      filter(배당수익률 > input$DY[1] & 배당수익률 < input$DY[2]) %>%
      filter(ROE > input$ROE[1] & ROE < input$ROE[2]) %>%
      filter(시총순위 > input$SIZE[1] & 시총순위 < input$SIZE[2]) %>%
      DT::datatable(rownames= FALSE,
                    extensions = 'Buttons',
                    options = list(pageLength = 100,
                                   dom = 'Bfrtip',
                                   buttons = c('copy', 'csv', 'excel', 'pdf')
                                   ))
    })
}

server 부분은 출력될 내용을 잡는 부분이며 매우 간단합니다. 먼저 사전에 정의된 down_df() 함수를 통해 거래소 데이터를 크롤링 및 클랜징 합니다. 그 후, filter() 함수를 이용하여 input에서 정의된 값에 해당하는 데이터를 필터링 합니다. input$PER[1]은 슬라이더 부분의 좌측 값을, input$PER[2]는 슬라이더 부분의 우측 값을 의미합니다.

그 후 해당 페이들을 datatable() 함수를 이용해 데이터테이블 형태로 변경합니다. rownames = FALSE를 통해 행이름을 삭제하며, extensions를 통해 추가적인 버튼을 생성합니다. pageLength는 한 화면에 몇개의 행을 보일지 정의하며 dom은 버튼의 위치를, buttons는 어떠한 버튼을 보여줄지 정의합니다.

위의 결과를 renderDataTable()을 통해 데이터 테이블 형태로 렌더링 한 후, output의 screen_table에 저장합니다. ui의 mainPanel 부분에서 dataTableOutput('screen_table')는 이 결과물을 받아 출력하는 것입니다.

# Create Shiny app ----
shinyApp(ui, server)

마지막으로 shinyApp 함수를 통해 ui와 server를 받아 웹페이지를 출력합니다.




좌측의 슬라이더 값에는 우리가 사전에 정의했던 값들이 들어 가있으며, 우측의 메인 화면에는 슬라이더 인풋의 조건에 해당하는 종목들이 출력되었습니다.

이제 슬라이더를 변경해보도록 하겠습니다.


슬라이더 인풋을 조금씩 변경할때마다 조건에 해당하는 테이블이 출력됩니다. 저 PER, 저 PBR, 고배당, 고ROE, 소형주에 해당하는 종목을 찾아보면 다음과 같이 출력이 됩니다.

이번에는 상단의 Excel 버튼을 눌러보도록 하겠습니다.


테이블 결과가 그대로 엑셀에 저장되었습니다.


그렇다면 이렇게 만들어진 샤이니 앱을 어떻게 배포할까요?
먼저 Rstudio에서 제공하는 shinyapps.io에 계정을 만든 후 배포할 수 있습니다.



위 방법을 이용하면 웹주소가 생성되어 언제 어디서든 R을 이용하지 않고도 인터넷이나 모바일을 통해 접속이 가능합니다. 그러나 무료 계정의 경우 월에 제공하는 트래픽 수가 지나치게 작아 여러사람이 이용하기는 어려움이 있습니다. 이와 비슷한 방법으로 AWS와 같은 가상서버를 구매한 후 샤이니를 올리는 방법도 있습니다. (이는 나중에 기회가 되면 다루도록 하겠습니다.)

github를 이용하는 법도 있습니다. 먼저 github 저장소를 만든 후, 위 코드를 app.R로 저장하여 업로드 합니다. 위 코드의 경우 아래의 깃허브 저장소에 올려져 있습니다.

https://github.com/hyunyulhenry/stock_screener

그 후 R내에서 shiny::runGitHub('저장소 이름', '계정명') 을 입력하면 해당 샤이니가 실행됩니다. 


댓글 1개:

  1. 안녕하세요 글 잘 봤습니다. 혹시 R 샤이니 앱 만드실때 한글 인코딩 문제, disconnected server오류는 없으셨나요? Henry님과 코드가 크게 다르지 않은데(다른건 파일 업로드, 다운로드 하는 것정도) 로컬에서는 잘 실행되던게 링크를 통해 접속하면 disconnected server오류가 뜨더라구요. 혹시 프로그래밍 할 때 이런 오류가 떴었는지 궁금합니다.

    답글삭제