}

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [R/Python] 일별 주가 데이터 크롤링, 수집한 데이터 나눠서 연산하기
    Project/연구 2020. 10. 2. 17:09
    반응형

    0. 들어가는 글

    코로나19로 인해 귀향을 하지 않으니 서울에서 긴 연휴를 맞게 되었다.

    5일의 연휴동안 먹고 자고 하기에도 부족한 시간이지만(?) 기왕이면 생산성있게 보내고자 그 동안 미뤄뒀던 일을 처리하기로 했다. 이름하야 주식 자동거래 알고리즘 짜기(!).

    그동안 틈틈히 프로그램 동산님의 유튜브 영상을 보면서 자동거래를 할 수 있는 프로그램은 짜두긴 했다. 생각보다 오래 걸리기는 했는데, 아직 이걸로는 부족하다. 왜냐면 현재까지 짜둔 프로그램은 매수한다/매도한다만 가능하고, 아주 간단한 조건만 넣어둔 상태이다. 말하자면 '무엇을', '얼마에' 사고 파는 조건은 아직 들어가있지 않다.

    기왕이면 이 조건을 파이썬 내에 구현시키면 참 좋으련만, 아직 데이터를 분석이나 머신러닝을 적용하는 데에는 R이 더 익숙하다. 마침 이미 구현한 파이썬 프로그램은 저장된 종목코드를 불러오는 기능은 있으니, 내가 할 일은 적당한 종목과 적당한 가격을 잘 구하면 되겠다.

    요컨대 R로 주식 데이터를 분석해서 "살만한 종목"코드를 저장해두면 파이썬이 구매하는 방식을 생각해둔 상태이다.

    그러기 위해서 먼저 해두어야 할 게 있다.

     

    1. 데이터 수집

    데이터를 수집하는 방식은 다양할텐데, 나는 웹크롤링을 이용해서 데이터를 수집해둔 상태이다.

    주식거래에서 발생하는 데이터 중 가장 작은 단위는 틱(Tick)봉인데, 거래일에 발생하는 모든 거래에 대해 기록된 데이터이다. 이걸 증권사가 제공하는 오픈API를 통해서 받을 수는 있는데, 문제는 하루에 발생하는 틱데이터가 어마어마한데 이걸 일일히 받기에 굉장히 오래걸린다. 단 시간 내에 너무 많은 요청을 하면 증권사에서 접속을 차단하기 때문에. 그래도 데이터는 가장 작은 단위부터 수집해두면 좋긴 해서 이건 방법을 추후에 찾아보려고 한다.

    아무튼 현재 수집 중인 데이터는 매분마다 발생하는 데이터를 크롤링한건데, 웹사이트에 올려둔 데이터를 증권거래가 종료된 이후에 수집하고 있다. 웹사이트마다 정책은 차이가 있지만, 보통은 매일 데이터를 갱신해두므로 매일 쌓아두고 있다. 사실 이걸로도 용량이 적지 않은데, 하루에 쌓이는 데이터가 보통 38MB 정도 된다. 지난 3달동안 쌓아뒀으니 현재까지 2.4GB정도 된다. 이걸 어떻게 쓸지는 아직 고민해두지 못했는데, 우선은 계속 쌓아둔 뒤에 계속 고민해볼 생각이다.

    아무튼 연휴 동안에는 일별 데이터를 이용해서 알고리즘을 짜보려고 한다.

    일별 데이터는 수정주가가 반영된 데이터를 불러 올 생각인데, 블로그 Henry's Quantopia를 참고해서 만들었다. 기본적으로 네이버 주식차트의 원데이터를 불러오는 방식이다.

    packages <- c("rvest", "httr", "parallel", "foreach","doParallel", "data.table", "readxl", "dplyr", "foreach")
    pacman::p_load("rvest", "httr", "parallel", "foreach","doParallel", "data.table", "readxl", "dplyr", "foreach", "doSNOW")
    
    numCores    <- detectCores() - 1
    cl          <- makeCluster(numCores)
    registerDoParallel(cl)
    registerDoSNOW(cl)
    
    
    path <- "D:/R/Data"
    days <- 600
    
    ticker_name <- paste0(path, "/", "KOR_ticker.txt")
    ticker      <- fread(ticker_name, encoding = "UTF-8", colClasses = "character")
    code_list   <- ticker$code
    
    iterations <- length(code_list)
    pb         <- txtProgressBar(max = iterations, style = 3)
    progress   <- function(n) {setTxtProgressBar(pb, n)}
    opts       <- list(progress = progress)
    
    
    daily_prc_collector <- function(code, days){
       url <- paste0("https://fchart.stock.naver.com/sise.nhn?symbol=", code,
                     "&timeframe=day&count=", days,"&requestType=0")
       html_url <- GET(url)
       df <- read_html(html_url, encoding = "EUC-KR") %>%
          html_nodes(., "item") %>%
          html_attr("data") %>%
          strsplit("\\|") %>%
          lapply(., function(x){
             x %>% t() %>% data.table}) %>%
          rbindlist(.)
       names(df) <- c("date", "init.prc", "high.prc", "low.prc", "end.prc", "quant")
       df$code <- code
       Sys.sleep(0.5)
       return(df)
    }
    
    daily_data_list <- foreach(     j        = 1:iterations,
                                    .packages      = packages,
                                    .errorhandling = "pass",
                                    .combine       = rbind,
                                    .options.snow  = opts) %dopar% {
                                       code <- code_list[j]
                                       df <- daily_prc_collector(code, days)
                                       return(df)
                                    }
    
    daily_df_name <- paste0(path, "/", "daily_price.txt")
    fwrite(daily_data_list, daily_df_name)
    

    티스토리의 코드블럭에 R이 없어서 Python을 읽는 방식으로 했는데, 가독성이 많이 떨어지긴 한다.

    아무튼 기본적인 작동 방식은 네이버 차트의 원데이터에 접근해서 해당 데이터를 긁어온 뒤에 잘 정리해서 저장하는 방식이고, 이걸 모든 종목에 대해서 for문으로 작동시켰다.

    for문을 대신해서 apply계열의 함수를 쓰면 더 빠르기는 하다. 하지만 더 빠른건 병렬컴퓨팅을 해둔 foreach문이다(!).

    daily_prc_collector()라는 함수를 foreach문 안에서 돌린건데, 병렬연산을 위해 %dopar%로 연결해두었다. R에서 하는 병렬연산은 다른 포스트가 많으니 구글링을 하시라. 기회가 되면 나도 잘 정리해서 쓸 생각이다.

     

    2. 데이터별로 연산하기

    대표적으로 많이 쓰이는 볼린저밴드를 이용한 알고리즘을 짜볼 생각인데, 통상적인 볼린저밴드는 20일 이동평균선을 기준으로 위 아래로 2 * 표준편차를 범위로 본다. 한 마디로 말해서 과거 20일간 주가의 변동성을 표현한건데 최근에 급등한 종목이 아니라면 대체로 이 범위 안에 있다. 하지만 만약 오늘 급등했다면 당연히 이 범위를 넘어선 곳에서 거래가 이루어진다는 것인데, 이걸 가지고 짜볼 생각이다.

    볼린저밴드를 구하려면, 다행히도 제공하는 패키지가 있기는 하다. 하지만 나중을 생각해서 데이터를 쪼개서 연산하는 방법을 만들어 둘 필요가 있겠다. 이동평균선을 구하거나, 거래량을 가중치로 쓴 이동평균선을 구하거나, 거래량을 가중치로 쓴 표준편차를 구하거나, 또는 중앙값을 구하거나 최빈값을 구하고 싶을 수도 있는데 그 때 마다 적용되는 패키지를 찾기는 조금 번거롭다. 물론 지금 언급한 통계량들은 TTR 패키지를 이용해 가능하긴 하다. 하지만 분석에 자유롭게 적용시키는 게 R을 공부하는 이유이기도 하니 패키지없이 우선 구현을 해두는 게 좋겠다 생각이 든다.

    기본적인 프로그램 구상을 해보면

    ① 내가 원하는 크기만큼의 샘플을 뽑아서

    ② 원하는 함수를

    ③ ①에서 뽑은 각각의 데이터에 대해 적용하면 된다.

    우선 손으로 직접 한다고 생각해보고, 그 뒤에 컴퓨터가 알아먹을 수 있도록 코딩을 하면 된다.

    예를 들어서 표본크기가 100인 데이터가 있고, 10일 이동평균선을 구하는 과정을 아래와 같이 하면 된다.

     

    1번째 데이터부터 10번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.
    2번째 데이터부터 11번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.
    3번째 데이터부터 12번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.
    ........
    91번째 데이터부터 100번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.

     

    이 중에 변하는 부분은 데이터의 시작인 1,2,3,4,5, ... 91과 데이터의 끝인 10, 11, 12, ... 100이다.

    이 중 데이터의 끝은 데이터의 시작과 뽑고자 하는 데이터의 크기를 알면 바로 정할 수 있다. 처음 데이터부터 10개를 뽑기로 했으니까, "첫 데이터의 순서 - 뽑으려는 데이터의 크기 + 1"을 해주면 된다. 그러므로 작업 과정은 다음과 같이 된다.

     

    1번째 데이터부터 1 + 10 - 1번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.
    2번째 데이터부터 2 + 10 - 1번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.
    3번째 데이터부터 3 + 10 - 1번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.
    ........
    91번째 데이터부터 91 + 10 - 1번째 데이터를 뽑아서 평균을 낸 뒤에 값을 저장한다.

     

    이 과정에서 데이터의 시작에 따라서 연산을 계속 반복할 것이니까, 이 부분을 for문에 들어갈 변수로 기입해두면 둔다. 다만 데이터의 크기는 미리 정해두면 되겠다.

    뒷부분은 그대로 반복되긴 하는데, 원래 목적대로 평균이 아닌 다른 함수도 적용하려고 한다. 즉 실제로 하려는 작업은 아래와 같을 거다.

     

    1번째 데이터부터 1 + 10 - 1번째 데이터를 뽑아서 평균/표준편차/기타작업을 낸 뒤에 값을 저장한다.
    2번째 데이터부터 2 + 10 - 1번째 데이터를 뽑아서 평균/표준편차/기타작업을 낸 뒤에 값을 저장한다.
    3번째 데이터부터 3 + 10 - 1번째 데이터를 뽑아서 평균/표준편차/기타작업을 낸 뒤에 값을 저장한다.
    ........
    91번째 데이터부터 91 + 10 - 1번째 데이터를 뽑아서 평균/표준편차/기타작업을 낸 뒤에 값을 저장한다.

     

    평균/표준편차/기타작업 등은 뽑을 데이터의 크기처럼 미리 정해두고 갈거다.

    미리 정해둔 값은 빼고, 반복적으로 바꾸면서 계산할 값은 결국 1,2,3, ... 91이다.

    여기서 고민해둬야 할 부분은 91번째가 끝이라는 것도 바로 알려주면 좋을텐데, 원래 표본크기에서 뽑으려는 표본크기와 연동해보면 답이 나온다. 가장 끝에서부터 "뽑으려는 표본크기"번째 데이터가 가장 마지막이다.

    따라서 "원래 표본크기 - 뽑으려는 데이터 크기 + 1"을 하면 된다.

    내용을 정리해서, 아래와 같은 함수를 이용하면 부분표본별로 연산할 수 있다.

    process_by_subset <- function(df,n, fun){
          df <- as_tibble(df)
          smpl_size <- dim(df)[1]
          iter <- smpl_size - n + 1
          bb <- foreach(i = 1:iter,
                        .packages = packages) %dopar% {
                              end <- i + n - 1
                              df %>% slice(i:end) %>% sapply(., FUN = fun, na.rm = TRUE)
                        } %>% 
                do.call(rbind, .) %>% 
                rbind(., matrix(0, nrow = smpl_size - dim(.)[1], ncol = dim(.)[2])) %>% 
                as_tibble()
          return(bb)
          }

    인자로 들어가는 df는 데이터를 말하고, n은 뽑으려는 샘플크기, fun은 적용하고자 하는 함수를 말한다.

    주의할 점은 sapply()함수 안에 들어가는 값이므로, sapply()를 이용해 작동하는 함수를 넣어야 한다.

    반응형

    댓글

Designed by Tistory.