1 서론

실패는 성공으로 가는 과정일 뿐이다. 중요한 건 실패 후에 어떻게 일어서느냐이다.
-젠슨 황(NVIDIA CEO)-

젠슨 황은 자신의 장점 중 하나로 낮은 기대치를 꼽습니다. 낮은 기대치만큼 회복 탄력성이 상승하기 때문입니다. 반대로 기대치가 높은 사람은 회복 탄력성도 낮은 편입니다. AI혁신의 중심에 선 그가 이처럼 회복 탄력성을 중요시하는 이유는 무엇일까요? 그는 “진정한 혁신은 위험을 감수하고 기꺼이 실패하는 것”이라고 말합니다. 실패하더라도 포기하지 않고, 높은 회복탄력성으로 다시 질문에 답을 찾으라고 말입니다.


본 리포트에서는 구조화된 데이터에서 현재 가장 뛰어난 성능을 발휘하는 XGBoost 모델을 사용하여 KOSPI200 예측 모델을 구축하고 검증합니다. KOSPI200과 유의미한 관계를 가지는 다양한 특징(Features)들을 선정하고 시계열 데이터를 적절하게 가공할 것입니다.(혹시 상관성이 떨어지더라도 XGBoost가 자동으로 제외시켜 줄 겁니다!) 총 27개의 범주에서 95개의 특징을 생성할 것이고 이를 통해 익월 KOSPI 수익률의 수준을 예측(Bull/Bear)하는 것을 목적으로 합니다. 젠슨 황의 조언대로 모델의 성능이 형편 없을 거지만 또 이것이 다음 스테이지를 위한 초석이 될 것입니다. 먼저 특징 공학(Feature Engineering)부터 시작하겠습니다.

2 Features

library(purrr)
package <- c("tidyverse", "quantmod", "readxl", "fastDummies", "httr", "jsonlite", 
             "PerformanceAnalytics", "caret", "xgboost", "doParallel", "irlba")
pack_map <- map(package, ~ library(.x, character.only = TRUE))

options(scipen = 999)

2.1 연월 및 더미변수

  • 시계열 데이터 구간: 2007-01 ~ 2025-06
    • 날짜 형식(2025-06-01) 대신 연(YEAR)과 월(MONTH)을 구분
    • 월 더미변수(Dummy Variables)를 추가하여 계절성(Seasonality) 포함
  • Feature Name
    1. YEAR
    2. MONTH
    3. MONTH_1 ~ MONTH_12
## 특징 테이블 생성
Feature_Table <- expand_grid(
  YEAR = c(2007:2025),
  MONTH = c(1:12)
) |> 
  filter(YEAR * 100 + MONTH <= 202506) |> 
  dummy_cols(select_columns = "MONTH", remove_selected_columns = F) |> # 월 더미 변수 생성
  mutate(across(-c(YEAR, MONTH), as.factor))

2.2 KOSPI200 지수

  • Source: Yahoo Finance [티커: ^KS200]
  • Feature Name
    1. KS200_R_EMA_20 - 20일 EMA(Exponential Moving Average, 지수이평)
    2. KS200_R_EMA_60 - 60일 EMA
    3. KS200_R_EMA_126 - 126일 EMA
    4. KS200_R_EMA_252 - 252일 EMA
    5. KS200_VOL_EMA_20 - 거래량의 20일 EMA
    6. KS200_RSI_20_EMA_20 - 20일 RSI(Relative Strength Index, 상대강도지수)의 20일 EMA
    7. KS200_RSI_20_EMA_20_F - 20일 RSI의 20일 EMA 범주화
      • \(\text{RSI} < 30\): 1(OverSold)
      • \(30 \le \text{RSI} \le 70\): 0(Neutral)
      • \(\text{RSI} > 70\): 2(OverBought)
    8. KS200_MACD_HIST_EMA_20_F - MACD HIST(MACD - SIGNAL)의 20일 EMA 범주화
      • \(\text{HIST_EMA} > 0\): 1(Upward)
      • \(\text{HIST_EMA} \le 0\): 0(Downward)
## KOSPI200 EMA 함수 정의
Func_KS200_EMA <- function(EMA_n = 20, OHLC = "Ad") {

  if (OHLC == "Ad") {xts_data = Ad(KS200)}
  else if (OHLC == "Vo") {xts_data = Vo(KS200)}
  else stop("OHLC must be 'Ad' or 'Vo'")
  
  EMA <- EMA(xts_data, n = EMA_n) |> 
    apply.monthly(last) |> 
    na.locf(fromLast = TRUE)
  
  EMA_tbl <- tibble(
    YEAR = year(index(EMA)),
    MONTH = month(index(EMA)),
    EMA = as.vector(coredata(EMA))
  )
  return(EMA_tbl)
}

## RSI_EMA 함수 정의
Func_RSI_EMA <- function(xts_data, RSI_n = 20, EMA_n = 20, U = 70, L = 30) {
  RSI_EMA <- RSI(xts_data, n = RSI_n) |> 
    EMA(n = EMA_n) |> 
    apply.monthly(last) |> 
    na.locf(fromLast = TRUE)
  
  RSI_EMA_F <- case_when(RSI_EMA > 70 ~ 2, # OverBought
                         RSI_EMA < 30 ~ 1, # OverSold
                         is.na(RSI_EMA) ~ NA,
                         .default = 0) # Neutral
  RSI_EMA_tbl <- tibble(
    YEAR = year(index(RSI_EMA)),
    MONTH = month(index(RSI_EMA)),
    EMA = as.vector(coredata(RSI_EMA)),
    EMA_F = as.factor(as.vector(coredata(RSI_EMA_F)))
  )
  return(RSI_EMA_tbl)
}

## 2.2.1 ~ 4 KS200_R_EMA_20 ~ 252
KS200 <- getSymbols("^KS200", auto.assign = FALSE, to = "2025-06-30") |> 
  na.omit()
EMA_period <- c(20, 60, 126, 252)
KS200_R_EMA_list <- lapply(EMA_period, Func_KS200_EMA)

KS200_R_EMA <- purrr::reduce(KS200_R_EMA_list, .f = left_join, by = c("YEAR", "MONTH")) |> 
  `colnames<-`(c("YEAR", "MONTH", "KS200_R_EMA_20", "KS200_R_EMA_60", "KS200_R_EMA_126", "KS200_R_EMA_252"))

## 2.2.5 KS200_VOL_EMA_20
KS200_VOL_EMA_20 <- Func_KS200_EMA(OHLC = "Vo") |> 
  `colnames<-`(c("YEAR", "MONTH", "KS200_VOL_EMA_20"))

## 2.2.6 ~ 7 KS200_RSI_20_EMA_20
KS200_RSI_20_EMA_20 <- Func_RSI_EMA(Ad(KS200)) |> 
  `colnames<-`(c("YEAR", "MONTH", "KS200_RSI_20_EMA_20", "KS200_RSI_20_EMA_20_F"))

## 2.2.8 KS200_MACD_HIST_EMA_20_F
MACD <- MACD(Ad(KS200))
MACD_hist <- EMA(MACD$signal - MACD$macd, n = 20) |> 
  apply.monthly(last) |> 
  na.locf(fromLast = TRUE)

MACD_HIST_F <- as.factor(if_else(MACD_hist > 0, 1, 0))

KS200_MACD_HIST_EMA_20_F <- tibble(
  YEAR = year(index(MACD_hist)),
  MONTH = month(index(MACD_hist)),
  KS200_MACD_HIST_EMA_20_F = MACD_HIST_F
)

2.3 KOSPI200 변동성 지수

CBOE VIX처럼 공포지수로 코스피200 옵션 가격을 기반으로 향후 30일간의 증시 변동성에 대한 투자자들의 기대를 측정합니다.

  • Source: KRX [화면번호: 11012]
  • Feature Name
    1. VKOSPI_EMA_20_Lag_0
    2. VKOSPI_EMA_20_Lag_1
    3. VKOSPI_EMA_20_Lag_2
## EMA 임베딩 함수 정의의
Func_EMA_Embed <- function(tbl_data, EMA_n = 20, n_lag = 3) {
  tbl_data_xts <- as.xts(tbl_data[[2]], order.by = tbl_data[[1]])
  EMA_xts <- EMA(tbl_data_xts, n = EMA_n) |> 
    apply.monthly(last) |> 
    na.locf(fromLast = TRUE)
  
  EMA_tbl <- as_tibble(EMA_xts)
  
  Embed_tbl1 <- tibble(
    YEAR = year(index(EMA_xts)[-c(1:n_lag-1)]),
    MONTH = month(index(EMA_xts)[-c(1:n_lag-1)])
  )
  Embed_tbl2 <- as_tibble(embed(EMA_tbl$EMA, dimension = n_lag))
  Embed_tbl3 <- bind_cols(Embed_tbl1, Embed_tbl2)
  return(Embed_tbl3)
}

VKOSPI <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "KRX_V_KOSPI")

## 2.3.1 ~ 2 KS200_MACD_HIST_EMA_20
V_KOSPI_EMA_20 <- Func_EMA_Embed(VKOSPI) |> 
  `colnames<-`(c("YEAR", "MONTH", "VKOSPI_EMA_20_Lag_0", "VKOSPI_EMA_20_Lag_1", "VKOSPI_EMA_20_Lag_2"))

2.4 필라델피아 반도체 지수(PHLX Semiconductor Sector)

PHLX Semiconductor Sector는 미국에 상장된 주요 반도체 기업 30곳의 시가총액 가중지수로 글로벌 반도체 산업의 경기 사이클을 확인할 수 있습니다.

  • Source: Yahoo Finance [티커: ^SOX]
  • Feature Name
    1. SOX_RSI_20_EMA_20 - 20일 RSI의 20일 EMA
    2. SOX_RSI_20_EMA_20_F - 20일 RSI의 20일 EMA 범주화
      • \(\text{RSI} < 30\): 1(OverSold)
      • \(30 \le \text{RSI} \le 70\): 0(Neutral)
      • \(\text{RSI} > 70\): 2(OverBought)
SOX <- getSymbols("^SOX", auto.assign = FALSE, to = "2025-06-30") |> 
  na.omit()

## 2.4.1 ~ 2 SOX_RSI_20_EMA_20
SOX_RSI_20_EMA_20 <- Func_RSI_EMA(Ad(SOX)) |> 
   `colnames<-`(c("YEAR", "MONTH", "SOX_RSI_20_EMA_20", "SOX_RSI_20_EMA_20_F"))

2.5 원유

WTI 선물로 글로벌 원유 시장의 대표적인 벤치마크입니다.

  • Source: Yahoo Finance [티커: CL=F]
  • Feature Name
    1. OIL_RSI_20_EMA_20 - 20일 RSI의 20일 EMA
    2. OIL_RSI_20_EMA_20_F - 20일 RSI의 20일 EMA 범주화
      • \(\text{RSI} < 30\): 1(OverSold)
      • \(30 \le \text{RSI} \le 70\): 0(Neutral)
      • \(\text{RSI} > 70\): 2(OverBought)
OIL <- getSymbols("CL=F", auto.assign = FALSE, to = "2025-06-30") |> 
  na.omit()

## 2.5.1 ~ 2 OIL_RSI_20_EMA_20
OIL_RSI_20_EMA_20 <- Func_RSI_EMA(Ad(OIL)) |> 
  `colnames<-`(c("YEAR", "MONTH", "OIL_RSI_20_EMA_20", "OIL_RSI_20_EMA_20_F"))

2.6 달러인덱스

주요 6개 통화(EUR, JPY, GBP, CAD, CHF, SEK)의 가중 평균 환율로 USD의 상대적인 가치를 나타내는 지표입니다.

  • Source: Yahoo Finance [티커: DX-Y.NYB]
  • Feature Name
    1. UXD_RSI_20_EMA_20 - 20일 RSI의 20일 EMA
    2. UXD_RSI_20_EMA_20_F - 20일 RSI의 20일 EMA 범주화
      • \(\text{RSI} < 30\): 1(OverSold)
      • \(30 \le \text{RSI} \le 70\): 0(Neutral)
      • \(\text{RSI} > 70\): 2(OverBought)
UXD <- getSymbols("DX-Y.NYB", auto.assign = FALSE, to = "2025-06-30") |> 
  na.omit()

## 2.6.1 ~ 2 UXD_RSI_20_EMA_20
UXD_RSI_20_EMA_20 <- Func_RSI_EMA(Ad(UXD)) |> 
  `colnames<-`(c("YEAR", "MONTH", "UXD_RSI_20_EMA_20", "UXD_RSI_20_EMA_20_F"))

2.7 엔화

대표적인 안전자산 통화로 위험선호 및 회피 심리를 판단할 수 있습니다.

  • Source: Yahoo Finance [티커: JPY=X]
  • Feature Name
    1. YEN_RSI_20_EMA_20 - 20일 RSI의 20일 EMA
    2. YEN_RSI_20_EMA_20_F - 20일 RSI의 20일 EMA 범주화
      • \(\text{RSI} < 30\): 1(OverSold)
      • \(30 \le \text{RSI} \le 70\): 0(Neutral)
      • \(\text{RSI} > 70\): 2(OverBought)
YEN <- getSymbols("JPY=X", auto.assign = FALSE, to = "2025-06-30") |> 
  na.omit()

## 2.7.1 ~ 2 YEN_RSI_20_EMA_20
YEN_RSI_20_EMA_20 <- Func_RSI_EMA(Ad(YEN)) |> 
  `colnames<-`(c("YEAR", "MONTH", "YEN_RSI_20_EMA_20", "YEN_RSI_20_EMA_20_F"))

2.8 위안화

한국 수출국 1위인 중국의 경제 및 정책 방향, 외환시장 안정성 등을 반영합니다.

  • Source: Yahoo Finance [티커: CNY=X]
  • Feature Name
    1. YUAN_RSI_20_EMA_20 - 20일 RSI의 20일 EMA
    2. YUAN_RSI_20_EMA_20_F - 20일 RSI의 20일 EMA 범주화
      • \(\text{RSI} < 30\): 1(OverSold)
      • \(30 \le \text{RSI} \le 70\): 0(Neutral)
      • \(\text{RSI} > 70\): 2(OverBought)
YUAN <- getSymbols("CNY=X", auto.assign = FALSE, to = "2025-06-30") |> 
  na.omit()

## 2.8.1 ~ 2 YUAN_RSI_20_EMA_20
YUAN_RSI_20_EMA_20 <- Func_RSI_EMA(Ad(YUAN)) |> 
  `colnames<-`(c("YEAR", "MONTH", "YUAN_RSI_20_EMA_20", "YUAN_RSI_20_EMA_20_F"))

2.9 CDS 한국 5Y

CDS(Credit Default Swap) Premium을 통해 한국 신용위험 수준을 파악할 수 있습니다.

CDS_5Y <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "INVESTING_CDS_5Y")

## 2.9.1 ~ 2 CDS_5Y_EMA_20
CDS_5Y_EMA_20 <- Func_EMA_Embed(CDS_5Y) |> 
  `colnames<-`(c("YEAR", "MONTH", "CDS_5Y_EMA_20_Lag_0", "CDS_5Y_EMA_20_Lag_1", "CDS_5Y_EMA_20_Lag_2"))

2.10 KOSPI200 옵션/선물 미결제약정

미결제약정(Open Interest)은 특정 시점에 청산되지 않고 남아있는 계약 수로 증가하면 새로운 자금 유입 가능성을 의미하며 추세 지속 신호를 나타냅니다. 반대로 감소하면 추세 악화 및 전환 가능성을 의미합니다.

  • Source: KRX [화면번호: 45002]
  • Feature Name
    1. OPTION_OI_EMA_20_Lag_0
    2. OPTION_OI_EMA_20_Lag_1
    3. OPTION_OI_EMA_20_Lag_2
    4. FUTURE_OI_EMA_20_Lag_0
    5. FUTURE_OI_EMA_20_Lag_1
    6. FUTURE_OI_EMA_20_Lag_2
OI <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "KRX_OI")

## 2.10.1 ~ 6 OPTION_OI_EMA_20/FUTURE_OI_EMA_20
OPTION_OI_EMA_20 <- Func_EMA_Embed(OI[-2]) |> 
  `colnames<-`(c("YEAR", "MONTH", "OPTION_OI_EMA_20_Lag_0", "OPTION_OI_EMA_20_Lag_1", "OPTION_OI_EMA_20_Lag_2"))
FUTURE_OI_EMA_20 <- Func_EMA_Embed(OI[-3]) |> 
  `colnames<-`(c("YEAR", "MONTH", "FUTURE_OI_EMA_20_Lag_0", "FUTURE_OI_EMA_20_Lag_1", "FUTURE_OI_EMA_20_Lag_2"))

2.11 ETF 자산총액(국내주식)

  • Source: KRX [화면번호: 43002]
  • Transform: log
  • Feature Name
    1. ETF_TotAmt_EMA_20_log_Lag_0
    2. ETF_TotAmt_EMA_20_log_Lag_1
    3. ETF_TotAmt_EMA_20_log_Lag_2
ETF <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "KRX_ETF_Amt")

## 2.11.1 ~ 3 ETF_TotAmt_EMA_20_log
ETF_TotAmt_EMA_20_Log <- ETF |> 
  mutate(ETF_Amt = log(ETF_Amt)) |> 
  Func_EMA_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "ETF_TotAmt_EMA_20_Log_Lag_0", 
                 "ETF_TotAmt_EMA_20_Log_Lag_1", 
                 "ETF_TotAmt_EMA_20_Log_Lag_2"))

2.12 KOSPI 외국인 보유비중

  • Source: KRX [화면번호: 12022]
  • Feature Name
    1. FOREIGN_RATIO_Lag_0
    2. FOREIGN_RATIO_Lag_1
    3. FOREIGN_RATIO_Lag_2
# 기본 임베딩 함수 정의
Func_Embed <- function(tbl_data, n_dim = 3, n_lag = 0) {
  
  Embed_tbl1 <- tibble(
    YEAR = year(tbl_data$Date + months(n_lag)),
    MONTH = month(tbl_data$Date + months(n_lag))
  ) |>
    filter(YEAR > 2006)
  
  Embed_tbl2 <- as_tibble(embed(tbl_data[[2]], dimension = n_dim))
  Embed_tbl3 <- bind_cols(Embed_tbl1, Embed_tbl2)
  return(Embed_tbl3)
  
}

## 2.12.1 ~ 3 FOREIGN_RATIO
FOREIGN_RATIO <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "KRX_Monthly_Data") |> 
  select(Date, FOREIGN_RATIO) |> 
  Func_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "FOREIGN_RATIO_Lag_0", 
                 "FOREIGN_RATIO_Lag_1", 
                 "FOREIGN_RATIO_Lag_2"))

2.13 미국 ISM 제조업지수

## 2.13.1 ~ 3 US_PMI
US_PMI <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "KRX_Monthly_Data") |> 
  select(Date, US_PMI) |> 
  Func_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "US_PMI_Lag_0", 
                 "US_PMI_Lag_1", 
                 "US_PMI_Lag_2"))

2.14 중국 제조업지수

## 2.14.1 ~ 3 CN_PMI
CN_PMI <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "KRX_Monthly_Data") |> 
  select(Date, CN_PMI) |> 
  Func_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "CN_PMI_Lag_0", 
                 "CN_PMI_Lag_1", 
                 "CN_PMI_Lag_2"))

2.15 KB주택종합 매매가격지수(서울)

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y062 [주택매매가격지수(KB)]
    • 주기: M
    • 통계항목 코드: P63AD [총지수(서울), 2020.01 = 100]
  • Feature Name
    1. KB_HouseSalePrice_Lag_0
    2. KB_HouseSalePrice_Lag_1
    3. KB_HouseSalePrice_Lag_2
## BOK API KEY
BOK_api <- "N3UTV277ZVHGVL02Z352"

## BOK API 함수 정의
Func_BOK_API <- function(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, BOK_code3 = "?", TRANS = "N") {
  
# BOK_code <- "403Y001"
# BOK_freq <- "M"
# BOK_start <- "200510"
# BOK_end <- "202505"
# BOK_code2 <- "*AA"
# BOK_code3 = "?"
# TRANS <- "YOY"
  
  BOK_interval <- time_length(interval(ym(BOK_start), ym(BOK_end)), unit = "months") + 1
  BOK_url <- paste("https://ecos.bok.or.kr/api/StatisticSearch", 
                 BOK_api, 
                 "json/kr/1", 
                 BOK_interval, 
                 BOK_code, 
                 BOK_freq, 
                 BOK_start,
                 BOK_end, 
                 BOK_code2, 
                 BOK_code3, 
                 sep = "/") |> 
    GET() |> 
    content(as = "text", encoding = "UTF-8") |> 
    fromJSON()
  
  BOK_res <- BOK_url$StatisticSearch$row |> 
    mutate(
      DATA_VALUE = as.numeric(DATA_VALUE)
      ) |> 
    mutate(
      DATA_VALUE = case_when(
        TRANS == "YOY" ~ ((DATA_VALUE - lag(DATA_VALUE, n = 12, default = 0)) / 
                            lag(DATA_VALUE, n = 12, default = 0)),
        TRANS == "MOM" ~ ((DATA_VALUE - lag(DATA_VALUE, n = 1, default = 0)) / 
                            lag(DATA_VALUE, n = 1, default = 0)),
        .default = DATA_VALUE
      )
    ) |> 
    select(TIME, DATA_VALUE) |> 
    filter(DATA_VALUE != Inf)
  
  return(BOK_res)
}

## BOK 임베딩 함수 정의
Func_BOK_Embed <- function(tbl_data, n_dim = 3, n_lag = 0) {
  Embed_tbl1 <- tibble(
    YEAR = year(ym(tbl_data$TIME) + months(n_lag)),
    MONTH = month(ym(tbl_data$TIME) + months(n_lag))
  ) |> 
    filter(YEAR > 2006)
  Embed_tbl2 <- as_tibble(embed(tbl_data[[2]], dimension = n_dim))
  Embed_tbl3 <- bind_cols(Embed_tbl1, Embed_tbl2)
  return(Embed_tbl3)
}

## BOK API 설정
BOK_code <- "901Y062"
BOK_freq <- "M"
BOK_start <- "200611"
BOK_end <- "202506"
BOK_code2 <- "P63AD"

## 2.15.1 ~ 3 KB_HouseSalePrice
KB_HouseSalePrice <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2) |> 
  Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "KB_HouseSalePrice_Lag_0", 
                 "KB_HouseSalePrice_Lag_1", 
                 "KB_HouseSalePrice_Lag_2"))

2.16 선행지수 순환변동치

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y067 [경기종합지수]
    • 주기: M
    • 통계항목 코드: I16E [선행지수 순환변동치, 2020.01 = 100]
    • Lag = 1
  • Feature Name
    1. LCI_Lag_1
    2. LCI_Lag_2
    3. LCI_Lag_3
## BOK API 설정
BOK_code <- "901Y067"
BOK_freq <- "M"
BOK_start <- "200610"
BOK_end <- "202505"
BOK_code2 <- "I16E"

## 2.16.1 ~ 3 LCI
LCI <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2) |> 
  Func_BOK_Embed(n_lag = 1) |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "LCI_Lag_1", 
                 "LCI_Lag_2", 
                 "LCI_Lag_3"))

2.17 수출금액지수

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 403Y001 [수출금액지수]
    • 주기: M
    • 통계항목 코드: *AA [총지수, 2020.01 = 100]
    • Transform: 전년동기대비증감률(YOY)
    • Lag: 1
  • Feature Name
    1. ExAmtIdx_YOY_Lag_1
    2. ExAmtIdx_YOY_Lag_2
    3. ExAmtIdx_YOY_Lag_3
## BOK API 설정
BOK_code <- "403Y001"
BOK_freq <- "M"
BOK_start <- "200510"
BOK_end <- "202505"
BOK_code2 <- "*AA"

## 2.17.1 ~ 3 ExAmtIdx_YOY
ExAmtIdx_YOY <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, TRANS = "YOY") |>  
  Func_BOK_Embed(n_lag = 1) |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "ExAmtIdx_YOY_Lag_1", 
                 "ExAmtIdx_YOY_Lag_2", 
                 "ExAmtIdx_YOY_Lag_3"))

2.18 소비자물가지수

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y009 [소비자물가지수]
    • 주기: M
    • 통계항목 코드: 0 [총지수, 2020.01 = 100]
    • Transform: 전년동기대비증감률(YOY)
  • Feature Name
    1. CPI_YOY_Lag_0
    2. CPI_YOY_Lag_1
    3. CPI_YOY_Lag_2
## BOK API 설정
BOK_code <- "901Y009"
BOK_freq <- "M"
BOK_start <- "200511"
BOK_end <- "202506"
BOK_code2 <- "0"

## 2.18.1 ~ 3 CPI_YOY
CPI_YOY <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, TRANS = "YOY") |>  
  Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "CPI_YOY_Lag_0", 
                 "CPI_YOY_Lag_1", 
                 "CPI_YOY_Lag_2"))

2.19 반도체 수출입 실적

  • Source: 관세청 수출입무역통계 [HS코드: 8542 전자집적회로]
  • Feature Name
    1. SemiAmt_YOY_Lag_0
    2. SemiAmt_YOY_Lag_1
    3. SemiAmt_YOY_Lag_2
## 2.19.1 ~ 3 SemiAmt_YOY
SEMI_ExAmt <- readxl::read_excel(path = "Feature_Engineering.xlsx", sheet = "CUSTOMS_SEMI_ExAmt") |> 
  select(Date, SEMI_ExAmt) |> 
  mutate(
    YOY = ((SEMI_ExAmt - lag(SEMI_ExAmt, n = 12, default = 0)) / lag(SEMI_ExAmt, n = 12, default = 0))) |> 
  select(Date, YOY) |> 
  filter(YOY != Inf) |> 
  Func_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "SemiAmt_YOY_Lag_0", 
                 "SemiAmt_YOY_Lag_1", 
                 "SemiAmt_YOY_Lag_2"))

2.20 투자자 예탁금

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y056 [증시주변자금동향]
    • 주기: M
    • 통계항목 코드: S23A [투자자 예탁금]
    • Transform: 전기대비증감률(MOM)
  • Feature Name
    1. DEPOSIT_MOM_Lag_0
    2. DEPOSIT_MOM_Lag_1
    3. DEPOSIT_MOM_Lag_2
## BOK API 설정
BOK_code <- "901Y056"
BOK_freq <- "M"
BOK_start <- "200610"
BOK_end <- "202506"
BOK_code2 <- "S23A"

## 2.20.1 ~ 3 DEPOSIT_MOM
DEPOSIT_MOM <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, TRANS = "MOM") |>  
  Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "DEPOSIT_MOM_Lag_0", 
                 "DEPOSIT_MOM_Lag_1", 
                 "DEPOSIT_MOM_Lag_2"))

2.21 RP

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y056 [증시주변자금동향]
    • 주기: M
    • 통계항목 코드: S23C [RP]
    • Transform: 전기대비증감률(MOM)
  • Feature Name
    1. RP_MOM_Lag_0
    2. RP_MOM_Lag_1
    3. RP_MOM_Lag_2
## BOK API 설정
BOK_code <- "901Y056"
BOK_freq <- "M"
BOK_start <- "200610"
BOK_end <- "202506"
BOK_code2 <- "S23C"

## 2.21.1 ~ 3 RP_MOM
RP_MOM <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, TRANS = "MOM") |>
  Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "RP_MOM_Lag_0", 
                 "RP_MOM_Lag_1", 
                 "RP_MOM_Lag_2"))

2.22 M2

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 101Y004 [M2 상품별 구성내역(평잔, 원계열)]
    • 주기: M
    • 통계항목 코드: BBHA00 [M2(평잔, 원계열)]
    • Lag: 2
    • Transform: 전년동기대비증감률(YOY)
  • Feature Name
    1. M2_YOY_Lag_2
    2. M2_YOY_Lag_3
    3. M2_YOY_Lag_4
## BOK API 설정
BOK_code <- "101Y004"
BOK_freq <- "M"
BOK_start <- "200509"
BOK_end <- "202504"
BOK_code2 <- "BBHA00"

## 2.22.1 ~ 3 M2_YOY
M2_YOY <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, TRANS = "YOY") |>
  Func_BOK_Embed(n_lag = 2) |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "M2_YOY_Lag_2", 
                 "M2_YOY_Lag_3", 
                 "M2_YOY_Lag_4"))

2.23 고용률

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y027 [경제활동인구]
    • 주기: M
    • 통계항목 코드: I61E [고용률]
    • 통계항목 코드2: I28A [원계열]
    • Lag: 1
    • Transform: 전년동기대비증감률(YOY)
  • Feature Name
    1. EmpRatio_YOY_Lag_1
    2. EmpRatio_YOY_Lag_2
    3. EmpRatio_YOY_Lag_3
## BOK API 설정
BOK_code <- "901Y027"
BOK_freq <- "M"
BOK_start <- "200510"
BOK_end <- "202505"
BOK_code2 <- "I61E"
BOK_code3 <- "I28A"

## 2.23.1 ~ 3 EmpRatio_YOY
EmpRatio_YOY <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, BOK_code3, TRANS = "YOY") |>
  Func_BOK_Embed(n_lag = 1) |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "EmpRatio_YOY_Lag_1", 
                 "EmpRatio_YOY_Lag_2", 
                 "EmpRatio_YOY_Lag_3"))

2.24 실업률

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 901Y027 [경제활동인구]
    • 주기: M
    • 통계항목 코드: I61BC [실업률]
    • 통계항목 코드2: I28A [원계열]
    • Lag: 1
    • Transform: 전년동기대비증감률(YOY)
  • Feature Name
    1. UnEmpRatio_YOY_Lag_1
    2. UnEmpRatio_YOY_Lag_2
    3. UnEmpRatio_YOY_Lag_3
## BOK API 설정
BOK_code <- "901Y027"
BOK_freq <- "M"
BOK_start <- "200510"
BOK_end <- "202505"
BOK_code2 <- "I61BC"
BOK_code3 <- "I28A"

## 2.24.1 ~ 3 UnEmpRatio_YOY
UnEmpRatio_YOY <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2, BOK_code3, TRANS = "YOY") |>
  Func_BOK_Embed(n_lag = 1) |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "UnEmpRatio_YOY_Lag_1", 
                 "UnEmpRatio_YOY_Lag_2", 
                 "UnEmpRatio_YOY_Lag_3"))

2.25 기준금리

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 722Y001 [한국은행 기준금리 및 여수신금리]
    • 주기: M
    • 통계항목 코드: 0101000 [한국은행 기준금리]
  • Feature Name
    1. BaseRate_Lag_0
    2. BaseRate_Lag_1
    3. BaseRate_Lag_2
## BOK API 설정
BOK_code <- "722Y001"
BOK_freq <- "M"
BOK_start <- "200611"
BOK_end <- "202506"
BOK_code2 <- "0101000"

## 2.25.1 ~ 3 BaseRate
BaseRate <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2) |> 
    Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "BaseRate_Lag_0", 
                 "BaseRate_Lag_1", 
                 "BaseRate_Lag_2"))

2.26 국채 스프레드(10Y-3Y)

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 721Y001 [시장금리(월)]
    • 주기: M
    • 통계항목 코드: 5050000 [국고채(10년)] / 5020000 [국고채(3년)]
  • Feature Name
    1. TB_Sp_Lag_0
    2. TB_Sp_Lag_1
    3. TB_Sp_Lag_2
## BOK API 설정
## 국채10년
BOK_code <- "721Y001"
BOK_freq <- "M"
BOK_start <- "200611"
BOK_end <- "202506"
BOK_code2 <- "5050000"

T_10 <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2)

## 국채3년
BOK_code2 <- "5020000"

T_3 <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2)

## 2.26.1 ~ 3 TB_Sp
TB_Sp <- T_10 |> 
  mutate(SPREAD = DATA_VALUE - T_3$DATA_VALUE) |> 
  select(TIME, SPREAD) |> 
  Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "TB_Sp_Lag_0", 
                 "TB_Sp_Lag_1", 
                 "TB_Sp_Lag_2"))

2.27 회사채 스프레드(회사채10Y-국채3Y)

  • Source: 한국은행(BOK) OpenAPI
    • 통계표 코드: 721Y001 [시장금리(월)]
    • 주기: M
    • 통계항목 코드: 7030000 [회사채(10년, BBB-)] / 5020000 [국고채(3년)]
  • Feature Name
    1. Corp_Sp_Lag_0
    2. Corp_Sp_Lag_1
    3. Corp_Sp_Lag_2
## BOK API 설정
BOK_code <- "721Y001"
BOK_freq <- "M"
BOK_start <- "200611"
BOK_end <- "202506"
BOK_code2 <- "7030000"

CORP_BBB_3 <- Func_BOK_API(BOK_code, BOK_freq, BOK_start, BOK_end, BOK_code2)

## 2.27.1 ~ 3 CORP_Sp
CORP_Sp <- CORP_BBB_3 |> 
  mutate(SPREAD = DATA_VALUE - T_3$DATA_VALUE) |> 
  select(TIME, SPREAD) |> 
  Func_BOK_Embed() |> 
  `colnames<-`(c("YEAR", 
                 "MONTH", 
                 "CORP_Sp_Lag_0", 
                 "CORP_Sp_Lag_1", 
                 "CORP_Sp_Lag_2"))

3 Dataset

3.1 Feature Table

27개의 특징 범주를 정의했습니다. 이제 특징 테이블을 통합할 수 있습니다!

Feature_Table <- 
  list(
    Feature_Table,
    KS200_R_EMA,
    KS200_VOL_EMA_20,
    KS200_RSI_20_EMA_20,
    KS200_MACD_HIST_EMA_20_F,
    V_KOSPI_EMA_20,
    SOX_RSI_20_EMA_20,
    OIL_RSI_20_EMA_20,
    UXD_RSI_20_EMA_20,
    YEN_RSI_20_EMA_20,
    YUAN_RSI_20_EMA_20,
    CDS_5Y_EMA_20,
    OPTION_OI_EMA_20,
    FUTURE_OI_EMA_20,
    ETF_TotAmt_EMA_20_Log,
    FOREIGN_RATIO,
    US_PMI,
    CN_PMI,
    KB_HouseSalePrice,
    LCI,
    ExAmtIdx_YOY,
    CPI_YOY,
    SEMI_ExAmt,
    DEPOSIT_MOM,
    RP_MOM,
    M2_YOY,
    EmpRatio_YOY,
    UnEmpRatio_YOY,
    BaseRate,
    TB_Sp,
    CORP_Sp
    ) |>
  purrr::reduce(left_join, by = c("YEAR", "MONTH")) |> 
  na.locf(fromLast = TRUE)

샘플 222개, 특징 95개인 테이블이 완성됐습니다.

library(Matrix)
Feature_Table_matrix <- sparse.model.matrix(~ ., data = Feature_Table)

XGBoost는 단순히 데이터 프레임 형식이 아닌 행렬 형식으로 사용해야 하며 이를 위해 Matrix::sparse.model.matrix()함수를 사용합니다.(하지만 caret::train()를 사용할 경우 내부에서 자동으로 처리되므로 별도의 전처리가 필요하지 않습니다.)

3.2 Target Lable

  • 특징 테이블을 사용하여 예측할 범주 집합이다. 익월 KOSPI 수익률의 수준을 의미한다.
    • \(\text{익월 KOSPI200 수익률} > 0\): BULL(1)
    • \(\text{익월 KOSPI200 수익률} <= 0\): BEAR(0)
KS200 <- getSymbols("^KS200", auto.assign = FALSE, from = "2007-02-01", to = "2025-07-20") |> 
  Ad() |> 
  na.omit() |> 
  apply.monthly(monthlyReturn, type = "log")

KS200_TARGET <- tibble(
  YEAR = year(add_with_rollback(index(KS200), -months(1))),
  MONTH = month(add_with_rollback(index(KS200), -months(1))),
  TARGET_LABLE = as.factor(if_else(as.vector(coredata(KS200)) > 0, 1, 0))
)

KS200_TARGET_Lable <- KS200_TARGET$TARGET_LABLE

4 Modeling

4.1 XGBoost

  • XGBoost(eXtreme Gradient Boosting)은 최신 그라디언트 부스팅 기법을 사용하며 현재 전통적인 분류와 수치 예측을 포함한 모든 분야에서 매우 우수한 성능을 발휘합니다. 대신 하이퍼파라미터의 철저한 튜닝이 필요하며 이로 인해 다른 모델에 비해 사용이 어려운 경향이 있지만 뛰어난 R의 함수들로 극복이 가능합니다.
  • xgboost::xgboost()를 통해 알고리즘 구현이 가능합니다.
  • 주요 파라미터
    1. \(\text{objective}\)
      • “binary:logistic”: 이진분류
      • “multi:softprob”: 범주형 결과
      • “reg:squarederror”: 회귀
      • “count:poisson”: 개수 데이터
    2. \(\text{max_depth}\)
      • \(0~\le~x~<~\infty\)
      • 모든 트리의 최대 깊이로 값이 클수록 더 구체적인 패턴을 찾을 수 있지만 과적합의 위험이 있음.
    3. \(\text{eta}\)
      • \(0~\le~x~\le~1\)
      • 학습률로 낮은 값은 과적합을 제한하지만, 훈련 시간이 증가시킴.
    4. \(\text{gamma}\)
      • \(0~\le~x~\le~1\)
      • 알고리즘이 계속 분할할지를 결정하며 낮은 값은 더 구체적인 패턴을 찾을 수 있지만 과적합의 위험이 있음.
    5. \(\text{colsample_bytree}\)
      • \(0~\le~x~\le~1\)
      • 각 트리에 무작위로 선택되는 특징의 비율
    6. \(\text{min_child_weight}\)
      • \(0~\le~x~<~\infty\)
      • 분할의 위해 필요한 최소한의 예제 수
    7. \(\text{subsample}\)
      • \(0~\le~x~\le~1\)
      • 각 반복에서 무작위로 선택되는 예시의 비율

4.2 모델 훈련 및 튜닝

4.2.1 훈련 및 테스트 데이터

  • 전체 데이터(KOSPI200_ModelData): 2007.01 ~ 2025.06
    • 훈련 데이터: 2007.01 ~ 2020.12
    • 테스트 데이터: 2021.01 ~ 2025.06
## 모델 훈련/테스트 데이터 분리
KOSPI200_ModelData <- left_join(Feature_Table, KS200_TARGET, by = c("YEAR", "MONTH"))
KOSPI200_Train <- KOSPI200_ModelData |> 
  filter(YEAR * 100 + MONTH <= 202012)
KOSPI200_Test <- KOSPI200_ModelData |> 
  filter(YEAR * 100 + MONTH >= 202101)

4.2.2 하이퍼 파라미터 튜닝

  • caret패키지의 train(), trainControl()함수는 뛰어난 튜닝 기능을 제공합니다.
  • 시계열 데이터 모델링이기 때문에 일반적인 CV(Cross-Validation) 방식 대신 timeslice를 사용합니다.
    • initialWindow(훈련 윈도우): 60 -> 60기간 데이터를 훈련에 사용
    • horizon(예측 기간): 3 -> 3기간 예측
    • fixedWindow: 훈련 윈도우가 증가하지 않고 매번 60기간으로 고정
  • doParallel::registerDoParallel()함수는 아주 간단하게 모델 훈련의 병렬 처리를 가능하게 해줍니다.(대신 컴퓨터의 코어 개수를 알아야 합니다!)
  • 분류기의 성능을 파악하기 위해서 Kappa통계량을 사용합니다.
    • Kappa통계량은 우연히 정확한 예측을 할 가능성을 설명함으로써 정확도(Accuracy)를 조정하는 뛰어난 metric입니다.
    • 0부터 1까지의 범위를 가지며, 더 높은 값은 모델의 예측과 실제 값 간의 강한 일치를 의미합니다.
    • vcd::Kappa()
## 튜닝 그리드 생성
grid_xgb <- expand.grid(
  max_depth = c(1, 2, 3),
  eta = c(0.05, 0.1, 0.3),
  gamma = c(0, 1),
  colsample_bytree = c(0.6, 0.8),
  min_child_weight = 1,
  subsample = c(0.5, 0.75, 1),
  nrounds = c(50, 100, 150)
)

## 훈련 제어 객체 생성
ctrl <- trainControl(
  method = "timeslice", 
  initialWindow = 60, 
  horizon = 3, 
  fixedWindow = T
)

## 랜덤 시드 설정
set.seed(1995)

## 병렬처리를 위한 코어 설정
registerDoParallel(cores = 12)

## 모델 튜닝
m_xgb <- caret::train(TARGET_LABLE ~ ., 
                      data = KOSPI200_Train, 
                      method = "xgbTree", 
                      trControl = ctrl, 
                      tuneGrid = grid_xgb,
                      metric = "Kappa",
                      verbosity = 0)

4.2.3 모델 성능 평가

## 혼동 행렬
pred <- predict(m_xgb, newdata = KOSPI200_Test)
actual <- KOSPI200_Test$TARGET_LABLE

confusionMatrix(pred, actual)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 11 15
##          1 17 11
##                                           
##                Accuracy : 0.4074          
##                  95% CI : (0.2757, 0.5497)
##     No Information Rate : 0.5185          
##     P-Value [Acc > NIR] : 0.9618          
##                                           
##                   Kappa : -0.1836         
##                                           
##  Mcnemar's Test P-Value : 0.8597          
##                                           
##             Sensitivity : 0.3929          
##             Specificity : 0.4231          
##          Pos Pred Value : 0.4231          
##          Neg Pred Value : 0.3929          
##              Prevalence : 0.5185          
##          Detection Rate : 0.2037          
##    Detection Prevalence : 0.4815          
##       Balanced Accuracy : 0.4080          
##                                           
##        'Positive' Class : 0               
## 

\(\text{Kappa}: -0.1836\)으로 모델은 굉장히 형편없습니다. kappa통계량이 음수라는 것은 그냥 무작위 보다 못하다는 것으로 그냥 다 0으로 예측한게 더 뛰어나다는 겁니다. 그럼 이렇게 형편없는 모델이 나온 이유는 무엇이고 어떻게 하면 성능이 향상될 수 있는지 알아보도록 하겠습니다.

4.2.4 모델 성능 개선

1. 모델이 형편없는 이유

1) 너무 많은 예측 변수

데이터셋은 27개의 범주에서 95개의 예측변수로 구성되어 있습니다. 머신러닝 세계에서 95개의 예측 변수가 많다고 한다면… 아닙니다! 특히 XGBoost는 이보다 훨씬 많은 예측 변수가 있다고 하더라도 간단히 처리할 수 있으며 관련이 떨어지는 변수들은 자동으로 제외하고 모델링을 실시합니다.

2) 너무 적은 데이터

데이터셋의 기간은 2007.01 ~ 2025.06으로 월간 데이터 기준으로 총 222개의 샘플로 구성되어 있습니다. 만약 이를 일일 기준으로 처리한다고 해도 2,664개 데이터 포인트에 불과합니다. 이는 일반적인 머신러닝 데이터셋에 비해 턱없이 부족한 수준입니다. 오버샘플링, SMOTE 등의 기법을 통해 인위적으로 샘플의 수를 증가시킬 수 있지만 각각은 치명적인 단점들이 존재하고 무엇보다 자기상관성이 강한 금융 시계열과는 적합하지 않은 방식입니다.

3) 정교한 튜닝의 부재

XGBoost의 주요 하이퍼 파라미터의 그리드인 grid_xgb는 총 7개의 하이퍼 파라미터의 324개의 경우의 수로 구성되어 있습니다. 이보다 더 많은 수를 고려해야 했을까요? 1000개? 2000개? 아쉽지만 그리드 수를 증가시켜도 성능 개선의 여지가 제한적입니다. 간단한 324개의 경우의 수로도 Kappa 통계량이 음수가 나왔기 때문에 빛 좋은 개살구가 될 뿐입니다.

4) 금융 시계열 데이터의 특징

데이터 자체의 문제일 수도 있습니다. 우선 금융 시계열 데이터는 유의미한 신호 대비 노이즈가 너무 많습니다. 주가 수익률 움직임은 대부분 무작위이며 예측 가능한 패턴을 찾기란 너무 힘듭니다. 노이즈를 제거한 stationary 데이터를 사용한다고 해도 예측은 제한적일 것입니다. 어제의 패턴이 오늘과 다르고 내일은 이전과는 완전히 다른 새로운 패턴이 등장할 수 있기 때문입니다. 저명한 퀀트 트레이더들과 금융공학자들이 대부분 실패한 이유가 있을 것입니다.

2. PCA를 통한 차원 축소

마지막 지푸라기라도 잡는 심정으로 95개의 예측변수를 20개의 PCA(Principal Components Analysis) 변수로 바꿔서 다시 모델링을 시도해보겠습니다. irlba::prcomp_irlba()함수를 사용합니다.

## PCA 훈련 및 테스트 데이터 생성
ks200_pca <- KOSPI200_Train |> 
  select(-any_of(starts_with(c("YEAR", "MONTH", "TARGET_LABLE")))) |> 
  mutate(across(everything(), as.numeric)) |> 
  prcomp_irlba(n = 20, center = T, scale. = T)

KOSPI200_Train_pca <- bind_cols(KOSPI200_Train[ , c(1:14, 96)], ks200_pca$x)

KOSPI200_Test_num <- KOSPI200_Test |> 
  select(-any_of(starts_with(c("YEAR", "MONTH", "TARGET_LABLE")))) |> 
  mutate(across(everything(), as.numeric))

KOSPI200_Test_pca <- bind_cols(KOSPI200_Test[ , c(1:14, 96)], predict(ks200_pca, KOSPI200_Test_num))

## 모델 훈련
m_xgb_pca <- caret::train(TARGET_LABLE ~ ., 
                      data = KOSPI200_Train_pca, 
                      method = "xgbTree", 
                      trControl = ctrl, 
                      tuneGrid = grid_xgb,
                      metric = "Kappa",
                      verbosity = 0)
## 혼동 행렬
pred <- predict(m_xgb_pca, newdata = KOSPI200_Test_pca)
actual <- KOSPI200_Test$TARGET_LABLE
confusionMatrix(pred, actual)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  3  1
##          1 25 25
##                                           
##                Accuracy : 0.5185          
##                  95% CI : (0.3784, 0.6566)
##     No Information Rate : 0.5185          
##     P-Value [Acc > NIR] : 0.5547          
##                                           
##                   Kappa : 0.0665          
##                                           
##  Mcnemar's Test P-Value : 0.000006462     
##                                           
##             Sensitivity : 0.10714         
##             Specificity : 0.96154         
##          Pos Pred Value : 0.75000         
##          Neg Pred Value : 0.50000         
##              Prevalence : 0.51852         
##          Detection Rate : 0.05556         
##    Detection Prevalence : 0.07407         
##       Balanced Accuracy : 0.53434         
##                                           
##        'Positive' Class : 0               
## 

\(\text{Kappa}: 0.0665\)으로 모델은 이전 -0.1836보다 훨씬 개선됐습니다!(무려 양수로 전환) 하지만 0.0665라는 수치는 여전히 매우 작은 수치입니다.

5 결론

이번 분석을 통해 XGBoost를 활용한 예측 모델은 랜덤 모델보다 낮은 성능을 보인다는 점을 확인했습니다. 필자의 머신러닝에 대한 이해와 특징 공학의 전문성 부족이 주요 원인일 수 있지만 동시에 저명한 퀀트 트레이더들이 실패했던 것과 같은 이유일 수도 있습니다.


그렇다면 머신러닝으로는 복잡한 금융 시장의 향방을 예측할 수 없을까요? 필자가 최근 머신러닝과 관련된 공부를 처음 시작하면서 느꼈던 것은 가능성은 충분히 있다는 점입니다. 머신러닝은 아직도 진화 중이며, 특히 시계열 데이터에 특화된 딥러닝 모델(LSTM, Transformer)은 여전히 많은 가능성을 가지고 있습니다. 기꺼이 실패하라는 젠슨 황의 조언대로 실패하더라도 밀고 나아간다면 결국 답을 찾을 수 있을 것입니다.