대부분의 금융 관련 머신러닝 논문은 고정 기간 기법으로 관측값을 레이블한다. 그렇지만 이러한 기법은 그 인지도에도 불구하고 피해야 한다고 프라도 교수는 말한다. \(\tau\)는 사전에 정의된 상수 임계값을 의미한다.
\[ y_t = \left\{ \begin{array}{11} 1 & \text{if } \,r_{t_i, t_i+h} > \tau \\ 0 & \text{if } \,\left|r_{t_i, t_i+h}\right| \le \tau \\ -1 & \text{if } \,r_{t_i, t_i+h} < \tau \end{array} \right. \\ \\ r_{t_i, t_i+h} = \frac{P_{t+h}}{P_t}-1 \]
변동성이 높으면 더 높은 수익률을 요구하고, 낮으면 더 낮은 수익률에 반응하도록 수익률 임계값 \(\tau\)를 조정하는 것이 목적이다.
get_daily_volatility()
삼중 배리어(Tripple-Barrier) 기법의 기본적인 아이디어는 세 가지 배리어 중 최초로 도달한 배리어에 따라 관측값을 레이블하는 것이다. 세 가지 배리어는 2개의 수평 배리어(horizontal barrier)와 1개의 수직 배리어(vertical barrier)으로 구성된다.
이처럼 삼중 배리어 기법은 어떤 배리어에 먼저 도달하느냐에 따라 lable이 결정된다. 즉, 경로 의존적(path-dependent)임을 알 수 있다!!
알고리즘이 배팅의 방향(long vs. short)과 크기를 학습할 수 있도록 레이블하는 방법을 다룬다. 보통 포지션의 부호를 설정하는 기저 모델이 없을 경우 이익/손실 배리어를 구분할 필요가 없이 양방향 대칭을 가정하여 학습해야 한다. 교재에서 다루는 병렬 처리 기법에 대한 부분은 생략한다.
get_events()
: 첫 번째 배리어가 도달하는 시간을
측정한다.
price
: 가격 시계열 데이터tEvents
: 삼중 배리어 시드가 될 타임스탬프 값을 가진
인덱스로 이전에 다룬 표본 추출 절차에 의해 선택된 타임스탬프다.ptSl
: 음이 아닌 실수로 두 배리어의 너비 의미(0일 경우
각 수평배리어 비활성화)trgt
: 수익률의 절대값으로 표현한 목표 시계열
데이터minRet
: 삼중 배리어 검색을 진행할 때 필요한 최소 목표
수익률t1
: 수직 배리어의 타임 스탬프(무시하려면 FALSE
전달)getEvents <- function(price,
tEvents,
ptSl = c(1,1),
trgt,
minRet = 0.005,
t1 = FALSE) {
# 1. 목표 구하기
trgt <- trgt[tEvents]
# 2. t1구하기(최대 보유 기간)
if (isFALSE(t1)) t1 <- rep(NA, length(trgt))
# 3. t1에 손절을 적용해 이벤트 객체를 생성
side <- rep(1, length(trgt))
events <- tibble(
t0 = index(trgt),
t1 = t1,
trgt = trgt,
side = side
)
events <- events |>
filter(trgt > minRet & !is.na(trgt))
df0 <- apply_pt_sl_on_t1(
price = price,
events = events,
ptSl = ptSl
) |>
unnest(cols = c("pt", "sl"))
df0 <- df0 |>
rowwise() |>
mutate(df0_min = min(c_across(everything()), na.rm = TRUE)) |>
ungroup()
events$t1 <- df0$df0_min
return(events)
}
apply_pt_sl_on_t1()
: pt/sl/t1 중 어떤 배리어가 먼저
터치되었는지 계산하여 그 시점의 타임스탬프를
반환한다.apply_pt_sl_on_t1 <- function(price,
events,
ptSl) {
# t1(이벤트 끝)이전에 발생하면 이익/손절 실현을 적용한다.
events <- events |>
mutate(
t1 = if_else(is.na(t1), last(index(price)), t1),
pt = if (ptSl[1] > 0) ptSl[1] * coredata(trgt) else NA_real_,
sl = if (ptSl[2] > 0) -ptSl[2] * coredata(trgt) else NA_real_
)
res <- events %>%
rowwise() %>%
mutate(
px_path = list(window(price, start = t0, end = t1)),
ret_path = list({
base_px <- as.numeric(price[t0])
r <- (coredata(px_path) / base_px - 1) * side
}),
date_path = list({
index(px_path)
}),
sl_date = list({
which_hit <- which(ret_path < sl)
if (length(which_hit) > 0) date_path[which_hit[1]] else NA
}),
pt_date = list({
which_hit <- which(ret_path > pt)
if (length(which_hit) > 0) date_path[which_hit[1]] else NA
})
) |>
ungroup() |>
select(t1, pt = pt_date, sl = sl_date)
return(res)
}
add_vertical_barrier()
: tEvents
의 각
인덱스에 대해 그 다음 가격 바 또는 num_days
며칠 이후의
타임스탬프를 반환add_vertical_barrier <- function(price, tEvents, num_days = 5) {
tEvents <- index(price[tEvents])
price_idx <- index(price)
t1_vec <- sapply(tEvents, function(t0) {
target_day <- t0 + days(num_days)
t1_idx <- which(price_idx >= target_day)[1]
price_idx[t1_idx]
})
return(as.POSIXct(t1_vec, tz = "UTC"))
}
get_bins()
: 수익률과 방향 라벨 생성get_bins <- function(price, events) {
out <- events |>
rowwise() |>
mutate(
ret = coredata(price[t1]) / coredata(price[t0]) - 1,
bin = sign(ret)) |>
ungroup() |>
select(t0, ret, bin)
return(out)
}
drop_labels <- function(labels, minPtc = 0.05) {
repeat {
props <- prop.table(table(labels$bin))
if (min(props) > 0.05 || length(props) < 3 ) break
rare <- names(props)[which.min(props)]
labels <- labels |>
filter(bin != rare)
}
return(labels)
}
library(tidyverse)
library(quantmod)
library(PerformanceAnalytics)
url <- "https://raw.githubusercontent.com/firmai/research/master/Advances%20in%20Financial%20Machine%20Learning/Labelling/sample_dollar_bars.csv"
data <- read.csv(url, stringsAsFactors = FALSE)
data <- data |>
mutate(date_time = as_datetime(date_time, tz = "UTC"))
data_zoo <- zoo(data$close, order.by = data$date_time)
plot(data_zoo, main = "E-mini S&P500", xlab = "Date", ylab = "Close")
threshold
는 일일 변동성의 평균을 사용한다.# 1. 일일 변동성
vol <- get_daily_volatility(data_zoo, span = 50)
# 2. 이벤트 생성
get_t_events <- function(gRaw, h) {
# gRaw: 벡터 또는 시계열 (숫자형)
# h: threshold (양수)
# diff 계산
delta <- diff(gRaw)
tEvents <- c()
sPos <- 0
sNeg <- 0
# loop 시작 (R의 diff는 길이가 n-1이므로 index를 2부터)
for (i in 1:length(delta)) {
sPos <- max(0, sPos + delta[i])
sNeg <- min(0, sNeg + delta[i])
if (sNeg < -h) {
sNeg <- 0
tEvents <- c(tEvents, i + 1) # index 보정 (diff는 1개 짧음)
} else if (sPos > h) {
sPos <- 0
tEvents <- c(tEvents, i + 1)
}
}
return(tEvents) # 시계열 인덱스가 있다면 index 벡터로 바꿔도 됨
}
log_ret <- diff(log(data_zoo))
tEvents <- get_t_events(log_ret, h = mean(vol, na.rm = TRUE)) + 1
length(tEvents)
## [1] 4809
num_days = 1
## [1] "2011-08-02 09:07:37 UTC" "2011-08-02 10:52:48 UTC"
## [3] "2011-08-02 12:27:07 UTC" "2011-08-02 13:13:13 UTC"
## [5] "2011-08-02 13:32:02 UTC" "2011-08-02 13:37:28 UTC"
tripple_barrier_events <- getEvents(price = data_zoo,
tEvents = tEvents,
ptSl = c(1,1),
trgt = vol,
minRet = mean(vol, na.rm = T),
t1 = t1)
head(tripple_barrier_events)
## # A tibble: 6 × 4
## t0 t1 trgt side
## <dttm> <dttm> <zoo> <dbl>
## 1 2011-08-01 19:47:28 2011-08-02 06:46:46 0.002159646 1
## 2 2011-08-01 19:53:37 2011-08-02 06:46:46 0.002157316 1
## 3 2011-08-02 12:47:28 2011-08-02 13:44:01 0.002057912 1
## 4 2011-08-02 13:13:13 2011-08-02 13:44:01 0.002062822 1
## 5 2011-08-03 18:00:05 2011-08-03 18:36:01 0.002075666 1
## 6 2011-08-03 18:15:08 2011-08-03 18:26:42 0.002079140 1
## # A tibble: 6 × 3
## t0 ret bin
## <dttm> <dbl> <dbl>
## 1 2011-08-01 19:47:28 -0.00604 -1
## 2 2011-08-01 19:53:37 -0.00507 -1
## 3 2011-08-02 12:47:28 0.00354 1
## 4 2011-08-02 13:13:13 0.00275 1
## 5 2011-08-03 18:00:05 -0.00320 -1
## 6 2011-08-03 18:15:08 -0.00399 -1
##
## -1 0 1
## 1300 6 1224
베팅의 크기를 알고 있다고 가정해보자. 이 경우에는 베팅의 크기가 중요한데 이는 얼마를 집행할 것인지를 결정하는 문제이다. 물론 베팅이 없을 가능성도 존재한다. 지금까지는 베팅의 방향과 크기 모두를 머신러닝 알고리즘에 의존해 한번에 라벨링을 시도했다면 앞으로는 베팅의 방향을 이미 알고 있다는 전제 하에 베팅의 크기에 대한 학습을 시도하려고 한다. 프라도 교수는 이러한 방식을 메타 레이블링(meta-labeling)이라고 부르는데 1차 외생 모델을 어떻게 사용할지 학습하는 부수적인 머신러닝 모델을 구축하고자 하기 때문이다.
메타 레이블링을 기존 함수들에 적용하기 위해 약간의 수정이 필요하다.
getEvents_meta()
side
인자가 추가되었다.getEvents_meta <- function(price,
tEvents,
ptSl = c(1, 1),
trgt,
minRet = 0.005,
t1 = FALSE,
side = NULL) {
# 1. 목표 구하기
trgt <- trgt[tEvents]
# 2. t1구하기(최대 보유 기간)
if (isFALSE(t1)) t1 <- rep(NA, length(trgt))
# 3. t1에 손절을 적용해 이벤트 객체를 생성
if (is.null(side)) {
side <- rep(1, length(trgt))
ptSl <- c(ptSl[1], ptSl[1]) # 대칭
} else {
side <- side[tEvents]
}
events <- tibble(
t0 = index(trgt),
t1 = t1,
trgt = trgt,
side = side
)
events <- events |>
filter(trgt > minRet & !is.na(trgt))
df0 <- apply_pt_sl_on_t1(
price = price,
events = events,
ptSl = ptSl
) |>
unnest(cols = c("pt", "sl"))
df0 <- df0 |>
rowwise() |>
mutate(df0_min = min(c_across(everything()), na.rm = TRUE)) |>
ungroup()
events$t1 <- df0$df0_min
return(events)
}
get_bins_meta()
get_bins_meta <- function(price, events) {
out <- events |>
rowwise() |>
mutate(
ret = coredata(price[t1]) / coredata(price[t0]) - 1,
bin = if_else(side * ret <= 0, 0, 1)) |>
ungroup() |>
select(t0, ret, bin)
return(out)
}
get_bins()
와 다르게 get_bins_meta()
의 출력
값은 \(\{0, 1\}\)이다. 예측 레이블이
1이라면 2차 모델의 확률을 사용해 베팅의 크기를 알아낼
수 있다.메타 레이블링 전략의 출력값은 이진 변수이기에 1종 오류와 2종 오류 간의 트레이트 오프로 생각할 수 있다. 정밀도는 ’모델의 예측값이 얼마나 정확하게 예측됐는가를 나타내는 지표’이고 재현도는 ’실제값 중에서 모델이 얼마나 정확하게 예측했는가를 나타내는 지표’이다. 특히 재현율은 모델의 검증력(power)과 유사하다.
분류기 전체의 효율성을 극대화시키는 정밀도와 재현율의 조합이 존재하는데 F1-score는 분류기의 효율성을 측정하는데 정밀도와 재현율 사이의 조화 평균을 사용해 측정한다. 그리고 메타 레이블링은 더 높은 F1-score를 원할 경우 더욱 유용하다.
\[ \text{Precision(정밀도)} = \frac{TP}{TP + FP} \\ \text{Recall(재현율)} = \frac{TP}{TP + FN} \\ \]
2차 머신러닝 알고리즘의 역할은 1차(외생) 모델에 의해 결정된 양성이 참인지 거짓인지를 판단하는 것이다. 즉 베팅 기회를 측정하는 것이 목적이 아니라 제안된 베팅을 실행할 것인지, 실행하지 않을 것인지를 결정하는 것이 목적이다. 메타 레이블링이 강력한 이유는 다음과 같다.
ptS=[1,2]
와 t1
에 대해 메타 레이블링을
만들어라. 여기서 numDays=1
이다. 코드 3.1에서 계산된 일별
표준 편차를 trgt
로 사용하라.[-1,1]
을 결정했으므로 정해야 할 사항은 거래할 것인지 말
것인지를 판단하는 것이다.data <- tibble(
date = index(data_zoo),
close = coredata(data_zoo)
)
data <- data |>
mutate(
fast_mavg = TTR::SMA(close, n = 20),
slow_mavg = TTR::SMA(close, n = 50),
side = dplyr::lag(if_else(fast_mavg >= slow_mavg, 1, -1))
) |>
na.omit()
head(data)
## # A tibble: 6 × 5
## date close fast_mavg slow_mavg side
## <dttm> <dbl> <dbl> <dbl> <dbl>
## 1 2011-08-01 19:47:28 1284. 1276. 1284. -1
## 2 2011-08-01 19:53:37 1282. 1277. 1284. -1
## 3 2011-08-01 19:58:00 1282. 1277. 1283. -1
## 4 2011-08-01 20:00:00 1282. 1277. 1283. -1
## 5 2011-08-01 20:10:06 1282. 1278. 1283. -1
## 6 2011-08-01 23:41:59 1281 1278. 1282. -1
##
## -1 1
## 4747 5203
# 일일 변동성 계산
daily_vol <- get_daily_volatility(data_zoo, span = 50)
# CUSUM필터로 이벤트 생성
log_ret <- diff(log(data$close))
tEvents <- get_t_events(log_ret, h = mean(daily_vol, na.rm = T)) + 1
# 수직 배리어 생성
t1 <- add_vertical_barrier(price = data_zoo,
tEvents = tEvents,
num_days = 1)
ptSl <- c(1, 2)
# 삼중 배리어 기법 적용
triple_barrier_events <- getEvents_meta(price = data_zoo,
tEvents = tEvents,
ptSl = ptSl,
trgt = daily_vol,
minRet = mean(daily_vol, na.rm = T),
t1 = t1,
side = data$side)
# 메타 라벨링
labels <- get_bins_meta(data_zoo, events = triple_barrier_events)
head(labels)
## # A tibble: 6 × 3
## t0 ret bin
## <dttm> <dbl> <dbl>
## 1 2011-08-01 19:53:37 -0.00507 1
## 2 2011-08-03 17:43:21 -0.00319 1
## 3 2011-08-03 17:48:20 -0.00399 1
## 4 2011-08-03 18:26:42 0.00420 0
## 5 2011-08-03 18:36:01 0.00541 0
## 6 2011-08-03 19:15:17 0.00500 0
##
## 0 1
## 810 1641
roll_ret <- function(close, n) {
TTR::ROC(close, n = n, type = "discrete")
}
roll_sd <- function(ret, width) {
rollapply(ret, width = width, FUN = sd, na.pad = TRUE, align = "right")
}
func_acf <- function(ret, lag) {
ret <- na.omit(ret)
acf(ret, plot = F, lag.max = lag)$acf[lag + 1]
}
roll_acf <- function(ret, lag) {
rollapply(ret, width = 50, FUN = func_acf, lag = lag, na.pad = T)
}
feature_data <- data |>
mutate(log_ret = log(close) - log(dplyr::lag(close)),
mon1 = roll_ret(close, 1),
mon2 = roll_ret(close, 2),
mon3 = roll_ret(close, 3),
mon4 = roll_ret(close, 4),
mon5 = roll_ret(close, 5),
volatility_50 = roll_sd(log_ret, 50),
volatility_31 = roll_sd(log_ret, 31),
volatility_15 = roll_sd(log_ret, 15),
autocorr_1 = roll_acf(log_ret, lag = 1),
autocorr_2 = roll_acf(log_ret, lag = 2),
autocorr_3 = roll_acf(log_ret, lag = 3),
autocorr_4 = roll_acf(log_ret, lag = 4),
autocorr_5 = roll_acf(log_ret, lag = 5),
log_t1 = dplyr::lag(log_ret, 1),
log_t2 = dplyr::lag(log_ret, 2),
log_t3 = dplyr::lag(log_ret, 3),
log_t4 = dplyr::lag(log_ret, 4),
log_t5 = dplyr::lag(log_ret, 5)
) |>
select(-c(close, fast_mavg, slow_mavg))
x <- feature_data[which(feature_data$date %in% labels$t0), ]
y <- labels$bin
library(caret)
library(rsample)
library(MLmetrics)
# Split data into training, validation and test sets
data_split <- initial_time_split(x, prop = 0.85)
x_train <- training(data_split)
x_validation <- testing(data_split)
y_train <- y[data_split$in_id]
y_validation <- y[data_split$out_id]
train_df <- tibble(bin = y_train) |>
bind_cols(x_train) |>
mutate(bin = factor(if_else(bin == 1, "Y", "N"))) |>
select(-date) |>
na.omit()
valid_df <- tibble(bin = y_validation) |>
bind_cols(x_validation) |>
mutate(bin = factor(if_else(bin == 1, "Y", "N"))) |>
select(-date) |>
na.omit()
grid_rf <- expand.grid(
mtry = c(10, 25, 50, 100)
)
ctrl <- trainControl(
method = "timeslice",
initialWindow = 60,
horizon = 3,
fixedWindow = T,
classProbs = T,
summaryFunction = prSummary
)
set.seed(1995)
m_rf <- caret::train(bin ~ .,
data = train_df,
method = "rf",
trControl = ctrl,
tuneGrid = grid_rf,
metric = "F")
m_rf
## Random Forest
##
## 2082 samples
## 20 predictor
## 2 classes: 'N', 'Y'
##
## No pre-processing
## Resampling: Rolling Forecasting Origin Resampling (3 held-out with a fixed window)
## Summary of sample sizes: 60, 60, 60, 60, 60, 60, ...
## Resampling results across tuning parameters:
##
## mtry AUC Precision Recall F
## 10 0.2430556 0.4729272 0.3727665 0.7543676
## 25 0.2388285 0.4677356 0.3898644 0.7510624
## 50 0.2410427 0.4577909 0.3801602 0.7484111
## 100 0.2401872 0.4579365 0.3867837 0.7438420
##
## F was used to select the optimal model using the largest value.
## The final value used for the model was mtry = 10.
library(pROC)
p_valid <- predict(m_rf, newdata = valid_df, type = "prob")[, "Y"]
roc_obj <- pROC::roc(valid_df$bin, p_valid)
plot(roc_obj, print.auc = T, lwd = 2, legacy.axes = T, grid = T)
vi <- caret::varImp(m_rf, scale = F)$importance
vi <- vi |>
arrange(desc(Overall))
ggplot(vi, aes(x = reorder(rownames(vi), Overall), y = Overall)) +
geom_col(fill = "darkblue") +
coord_flip() +
theme_bw() +
labs(title = "Feature Importance (Gini, RF)", x = NULL, y = "Importance")