First backtests.
First: packages & data.
library(tidyverse)
library(lubridate)
library(xgboost)
library(keras)
load("data_ml.RData")
data_ml <- data_ml %>% arrange(date, stock_id)
dates <- levels(as.factor(data_ml$date))
Second: auxiliary variables.
depth <- c(6, 12, 24, 48, 96) # Looking back
label <- c("R1M_Usd", "R3M_Usd", "R6M_Usd", "R12M_Usd") # Label
label_num <- c(1, 3, 6, 12)
sep_oos <- as.Date("2009-01-01") # Starting point for backtest
ticks <- data_ml$stock_id %>% # List of all asset ids
as.factor() %>%
levels()
N <- length(ticks) # Max number of assets
dates <- data_ml$date %>% unique() # All dates
t_oos <- dates[dates > sep_oos] %>% # Out-of-sample dates
as.Date(origin = "1970-01-01")
Tt <- length(t_oos)
features <- colnames(data_ml[3:95]) # Keep the feature's column names (hard-coded, beware!)
Third: the predictive engines.
pred_fun <- function(train_data, test_data, features, label, j){
if(j == 1){
train_label <- train_data %>% select(label) %>% as.matrix() # Dependent variable
train_features <- train_data %>% select(features) %>% as.matrix() # Indep. variable
train_matrix <- xgb.DMatrix(data = train_features, label = train_label) # XGB format
fit <- train_matrix %>%
xgb.train(data = ., # Data source (pipe input)
eta = 0.2, # Learning rate
objective = "reg:linear", # Number of random trees
max_depth = 4, # Maximum depth of trees
nrounds = 80 # Number of trees used
)
xgb_test <- test_data %>% # Test sample => XGB format
select(features) %>%
as.matrix() %>%
xgb.DMatrix()
pred <- c() # Initialize output
pred$ret <- predict(fit, xgb_test) # Fill output with predicted returns
pred$names <- unique(test_data$stock_id)
return(pred) # Best predictions, equally-weighted
}
if(j == 2){
NN_train_features <- select(train_data, features) %>% as.matrix() # Matrix = important
NN_train_labels <- train_data %>% select(label) %>% as.matrix()
NN_test_features <- select(test_data, features) %>% as.matrix() # Matrix = important
model <- keras_model_sequential()
model %>% # This defines the structure of the network, i.e. how layers are organized
layer_dense(units = 16, activation = 'relu', input_shape = ncol(NN_train_features)) %>%
layer_dense(units = 8, activation = 'tanh') %>%
layer_dense(units = 1) # No activation means linear activation: f(x) = x.
model %>% compile( # Model specification
loss = 'mean_squared_error', # Loss function
optimizer = optimizer_rmsprop(), # Optimisation method (weight updating)
metrics = c('mean_absolute_error') # Output metric
)
fit_NN <- model %>%
fit(NN_train_features, # Training features
NN_train_labels, # Training labels
epochs = 10, batch_size = 512, verbose = 0) # Training parameters
pred <- c() # Initialize output
pred$ret <- predict(model, NN_test_features) # Fill output with predicted returns
pred$names <- unique(test_data$stock_id)
return(pred)
}
}
Fourth: backtesting loop! NOTE: it takes dozens of hours to run on CPUs.
pred <- array(NA, dim = c(Tt-1, length(depth), length(label), 2, N))
realized <- array(NA, dim = c(Tt-1,length(depth), length(label), 2, N))
for(t in 1:121){ #(Tt-1)){ # Stop before the last date: no fwd return!
if(t%%2==0){print(t_oos[t])} # Just checking the date status
for(k in 1:length(depth)){ # Estimation window
for(i in 1:length(label)){ # Label horizon
train_buffer <- label_num[i] # Training buffer size in months
label_used <- label[i]
ind_date = which(dates==t_oos[t])
train_data <- data_ml %>% filter(date <= dates[ind_date-train_buffer], # Rolling window with buffer
date >= dates[ind_date-train_buffer-depth[k]])
test_data <- data_ml %>% filter(date == t_oos[t]) # Common with past info!
for(j in 1:2){ # Predictor
pred_temp <- pred_fun(train_data, test_data, features, label_used, j)
ind2 <- match(pred_temp$names, ticks) %>% na.omit() # Index between all & test
pred[t,k,i,j,ind2] <- pred_temp$ret
real_tmp <- data_ml %>% filter(date == t_oos[t]) %>% select(label_used) %>% as.matrix()
realized[t,k,i,j, ind2] <- real_tmp
}
}
}
}
# save(pred, file = "pred.RData")
# save(realized, file = "realized.RData")
Fifth: analysis. Below we show how to compute the R^2 and Mariano-Dieblod statistics. An initial step is to clean and re-order the data.
library(reshape)
pred_df <- melt(pred)
realized_df <- melt(realized)
error_df <- cbind(pred_df[,1:5], realized_df[,6], realized_df[,6] - pred_df[,6])
colnames(error_df) <- c("Date", "Depth", "Label", "Method", "Stock", "Realized", "Error")
error_df$Depth[error_df$Depth == 5] <- 96
error_df$Depth[error_df$Depth == 4] <- 48
error_df$Depth[error_df$Depth == 3] <- 24
error_df$Depth[error_df$Depth == 2] <- 12
error_df$Depth[error_df$Depth == 1] <- 6
error_df$Label[error_df$Label == 4] <- 12
error_df$Label[error_df$Label == 3] <- 6
error_df$Label[error_df$Label == 2] <- 3
error_df$Method[error_df$Method == 1] <- "Boosted_trees"
error_df$Method[error_df$Method == 2] <- "Neural_network"
res <- error_df %>%
mutate(Error = Error / sqrt(Label),
abs_err = abs(Error),
sq_err = Error^2,
sq_realized = Realized^2) %>%
group_by(Depth, Label, Method) %>%
summarise(mae = mean(abs_err, na.rm = T),
r2 = 1-sum(sq_err, na.rm = T)/sum(sq_realized, na.rm = T)) %>%
ungroup() %>%
mutate(Label = as.factor(Label),
Label = recode_factor(Label,
"1"= "1 month label",
"3"= "3 month label",
"6"= "6 month label",
"12"= "12 month label",
.ordered = T))
Then, the R^2.
res <- error_df %>%
group_by(Depth, Label, Method) %>%
summarise(R2 = 1-mean(Error^2, na.rm = T)/mean(Realized^2, na.rm = T)) %>%
ungroup() %>%
mutate(Depth = as.factor(Depth),
Method = as.factor(Method))
res %>%
ggplot(aes(x = Depth, y = R2, fill = Method)) + geom_col(position = "dodge") +
facet_grid(. ~Label, scales = "free") +
ylab("Out-of-sample R^2") + xlab("Sample depth (months)") +
scale_fill_manual(values = c("#133E7E","#0FD581"),
name = "Method",
labels = c("Boosted trees", "Neural networks")) +
theme(legend.position = "top")
And finally, the MD series.
library(viridis)
error_df %>%
spread(key = Method, value = Error) %>%
group_by(Date, Depth, Label) %>%
summarise(MD = mean(Boosted_trees^2-Neural_network^2, na.rm=T) / sd(Boosted_trees^2-Neural_network^2, na.rm=T)) %>%
ggplot(aes(x=t_oos[Date], y = MD, color = as.factor(Depth))) + geom_line() + facet_grid(Label ~.) +
ylab("Diebold-Mariano statistic") + xlab("Date") + scale_color_viridis(option="plasma", discrete=TRUE) +
labs(color = "Depth (months)")
LS0tCnRpdGxlOiBCYXNlbGluZSByZXN1bHRzCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCiMgRmlyc3QgYmFja3Rlc3RzLgoKKipGaXJzdCoqOiBwYWNrYWdlcyAmIGRhdGEuCgpgYGB7ciwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0V9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGx1YnJpZGF0ZSkKbGlicmFyeSh4Z2Jvb3N0KQpsaWJyYXJ5KGtlcmFzKQoKbG9hZCgiZGF0YV9tbC5SRGF0YSIpCmRhdGFfbWwgPC0gZGF0YV9tbCAlPiUgYXJyYW5nZShkYXRlLCBzdG9ja19pZCkKZGF0ZXMgPC0gbGV2ZWxzKGFzLmZhY3RvcihkYXRhX21sJGRhdGUpKQpgYGAKCioqU2Vjb25kKio6IGF1eGlsaWFyeSB2YXJpYWJsZXMuCgpgYGB7cn0KZGVwdGggPC0gYyg2LCAxMiwgMjQsIDQ4LCA5NikgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBMb29raW5nIGJhY2sKbGFiZWwgPC0gYygiUjFNX1VzZCIsICJSM01fVXNkIiwgIlI2TV9Vc2QiLCAiUjEyTV9Vc2QiKSAgICAgIyBMYWJlbApsYWJlbF9udW0gPC0gYygxLCAzLCA2LCAxMikKc2VwX29vcyA8LSBhcy5EYXRlKCIyMDA5LTAxLTAxIikgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBTdGFydGluZyBwb2ludCBmb3IgYmFja3Rlc3QKdGlja3MgPC0gZGF0YV9tbCRzdG9ja19pZCAlPiUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBMaXN0IG9mIGFsbCBhc3NldCBpZHMKICAgIGFzLmZhY3RvcigpICU+JQogICAgbGV2ZWxzKCkKTiA8LSBsZW5ndGgodGlja3MpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBNYXggbnVtYmVyIG9mIGFzc2V0cwpkYXRlcyA8LSBkYXRhX21sJGRhdGUgJT4lICB1bmlxdWUoKSAgICAgICAgICAgICAgICAgICAgICAgICAjIEFsbCBkYXRlcwp0X29vcyA8LSBkYXRlc1tkYXRlcyA+IHNlcF9vb3NdICU+JSAgICAgICAgICAgICAgICAgICAgICAgICAjIE91dC1vZi1zYW1wbGUgZGF0ZXMKICAgIGFzLkRhdGUob3JpZ2luID0gIjE5NzAtMDEtMDEiKSAgICAgICAgICAgCiAgICAKVHQgPC0gbGVuZ3RoKHRfb29zKSAgICAgICAgICAgICAgICAgICAgICAKZmVhdHVyZXMgPC0gY29sbmFtZXMoZGF0YV9tbFszOjk1XSkgIyBLZWVwIHRoZSBmZWF0dXJlJ3MgY29sdW1uIG5hbWVzIChoYXJkLWNvZGVkLCBiZXdhcmUhKQpgYGAKCioqVGhpcmQqKjogdGhlIHByZWRpY3RpdmUgZW5naW5lcy4KCmBgYHtyfQpwcmVkX2Z1biA8LSBmdW5jdGlvbih0cmFpbl9kYXRhLCB0ZXN0X2RhdGEsIGZlYXR1cmVzLCBsYWJlbCwgail7IAogICAgaWYoaiA9PSAxKXsKICAgICAgICB0cmFpbl9sYWJlbCA8LSB0cmFpbl9kYXRhICU+JSBzZWxlY3QobGFiZWwpICU+JSBhcy5tYXRyaXgoKSAgICAgICAgICAgICAgICMgRGVwZW5kZW50IHZhcmlhYmxlCiAgICAgICAgdHJhaW5fZmVhdHVyZXMgPC0gdHJhaW5fZGF0YSAlPiUgc2VsZWN0KGZlYXR1cmVzKSAlPiUgYXMubWF0cml4KCkgICAgICAgICAjIEluZGVwLiB2YXJpYWJsZQogICAgICAgIHRyYWluX21hdHJpeCA8LSB4Z2IuRE1hdHJpeChkYXRhID0gdHJhaW5fZmVhdHVyZXMsIGxhYmVsID0gdHJhaW5fbGFiZWwpICAgIyBYR0IgZm9ybWF0CiAgICAgICAgZml0IDwtIHRyYWluX21hdHJpeCAlPiUgCiAgICAgICAgICAgIHhnYi50cmFpbihkYXRhID0gLiwgICAgICAgICAgICAgICAgICAgICAgICMgRGF0YSBzb3VyY2UgKHBpcGUgaW5wdXQpCiAgICAgICAgICAgICAgICAgICAgICBldGEgPSAwLjIsICAgICAgICAgICAgICAgICAgICAgICMgTGVhcm5pbmcgcmF0ZQogICAgICAgICAgICAgICAgICAgICAgb2JqZWN0aXZlID0gInJlZzpsaW5lYXIiLCAgICAgICAjIE51bWJlciBvZiByYW5kb20gdHJlZXMKICAgICAgICAgICAgICAgICAgICAgIG1heF9kZXB0aCA9IDQsICAgICAgICAgICAgICAgICAgIyBNYXhpbXVtIGRlcHRoIG9mIHRyZWVzCiAgICAgICAgICAgICAgICAgICAgICBucm91bmRzID0gODAgICAgICAgICAgICAgICAgICAgICMgTnVtYmVyIG9mIHRyZWVzIHVzZWQKICAgICAgICAgICAgKQogICAgICAgIHhnYl90ZXN0IDwtIHRlc3RfZGF0YSAlPiUgICAgICAgICAgICAgICAgICAgICAjIFRlc3Qgc2FtcGxlID0+IFhHQiBmb3JtYXQKICAgICAgICAgICAgc2VsZWN0KGZlYXR1cmVzKSAlPiUgCiAgICAgICAgICAgIGFzLm1hdHJpeCgpICU+JQogICAgICAgICAgICB4Z2IuRE1hdHJpeCgpCiAgICAgICAgcHJlZCA8LSBjKCkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgSW5pdGlhbGl6ZSBvdXRwdXQKICAgICAgICBwcmVkJHJldCA8LSBwcmVkaWN0KGZpdCwgeGdiX3Rlc3QpICAgICAgICAgICAgIyBGaWxsIG91dHB1dCB3aXRoIHByZWRpY3RlZCByZXR1cm5zCiAgICAgICAgcHJlZCRuYW1lcyA8LSB1bmlxdWUodGVzdF9kYXRhJHN0b2NrX2lkKQogICAgICAgIHJldHVybihwcmVkKSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIEJlc3QgcHJlZGljdGlvbnMsIGVxdWFsbHktd2VpZ2h0ZWQKICAgIH0KICAgIGlmKGogPT0gMil7CiAgICAgICAgTk5fdHJhaW5fZmVhdHVyZXMgPC0gc2VsZWN0KHRyYWluX2RhdGEsIGZlYXR1cmVzKSAlPiUgYXMubWF0cml4KCkgICMgTWF0cml4ID0gaW1wb3J0YW50CiAgICAgICAgTk5fdHJhaW5fbGFiZWxzIDwtIHRyYWluX2RhdGEgJT4lIHNlbGVjdChsYWJlbCkgJT4lIGFzLm1hdHJpeCgpCiAgICAgICAgTk5fdGVzdF9mZWF0dXJlcyA8LSBzZWxlY3QodGVzdF9kYXRhLCBmZWF0dXJlcykgJT4lIGFzLm1hdHJpeCgpICAgICMgTWF0cml4ID0gaW1wb3J0YW50CiAgICAgICAgbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpCiAgICAgICAgbW9kZWwgJT4lICAgIyBUaGlzIGRlZmluZXMgdGhlIHN0cnVjdHVyZSBvZiB0aGUgbmV0d29yaywgaS5lLiBob3cgbGF5ZXJzIGFyZSBvcmdhbml6ZWQKICAgICAgICAgICAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxNiwgYWN0aXZhdGlvbiA9ICdyZWx1JywgaW5wdXRfc2hhcGUgPSBuY29sKE5OX3RyYWluX2ZlYXR1cmVzKSkgJT4lCiAgICAgICAgICAgIGxheWVyX2RlbnNlKHVuaXRzID0gOCwgYWN0aXZhdGlvbiA9ICd0YW5oJykgJT4lCiAgICAgICAgICAgIGxheWVyX2RlbnNlKHVuaXRzID0gMSkgIyBObyBhY3RpdmF0aW9uIG1lYW5zIGxpbmVhciBhY3RpdmF0aW9uOiBmKHgpID0geC4KICAgICAgICBtb2RlbCAlPiUgY29tcGlsZSggICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgTW9kZWwgc3BlY2lmaWNhdGlvbgogICAgICAgICAgICBsb3NzID0gJ21lYW5fc3F1YXJlZF9lcnJvcicsICAgICAgICAgICAgICAgIyBMb3NzIGZ1bmN0aW9uCiAgICAgICAgICAgIG9wdGltaXplciA9IG9wdGltaXplcl9ybXNwcm9wKCksICAgICAgICAgICAjIE9wdGltaXNhdGlvbiBtZXRob2QgKHdlaWdodCB1cGRhdGluZykKICAgICAgICAgICAgbWV0cmljcyA9IGMoJ21lYW5fYWJzb2x1dGVfZXJyb3InKSAgICAgICAgICMgT3V0cHV0IG1ldHJpYwogICAgICAgICkKICAgICAgICBmaXRfTk4gPC0gbW9kZWwgJT4lIAogICAgICAgICAgICBmaXQoTk5fdHJhaW5fZmVhdHVyZXMsICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBUcmFpbmluZyBmZWF0dXJlcwogICAgICAgICAgICAgICAgTk5fdHJhaW5fbGFiZWxzLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBUcmFpbmluZyBsYWJlbHMKICAgICAgICAgICAgICAgIGVwb2NocyA9IDEwLCBiYXRjaF9zaXplID0gNTEyLCB2ZXJib3NlID0gMCkgICAgICAgICAgICAgICMgVHJhaW5pbmcgcGFyYW1ldGVycwogICAgICAgIHByZWQgPC0gYygpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIEluaXRpYWxpemUgb3V0cHV0CiAgICAgICAgcHJlZCRyZXQgPC0gcHJlZGljdChtb2RlbCwgTk5fdGVzdF9mZWF0dXJlcykgICMgRmlsbCBvdXRwdXQgd2l0aCBwcmVkaWN0ZWQgcmV0dXJucwogICAgICAgIHByZWQkbmFtZXMgPC0gdW5pcXVlKHRlc3RfZGF0YSRzdG9ja19pZCkKICAgICAgICByZXR1cm4ocHJlZCkKICAgIH0KfQpgYGAKCgoqKkZvdXJ0aCoqOiBiYWNrdGVzdGluZyBsb29wISAqKk5PVEUqKjogaXQgdGFrZXMgKmRvemVucyogb2YgaG91cnMgdG8gcnVuIG9uIENQVXMuCgoKYGBge3J9CnByZWQgPC0gYXJyYXkoTkEsIGRpbSA9IGMoVHQtMSwgbGVuZ3RoKGRlcHRoKSwgbGVuZ3RoKGxhYmVsKSwgMiwgTikpCnJlYWxpemVkIDwtIGFycmF5KE5BLCBkaW0gPSBjKFR0LTEsbGVuZ3RoKGRlcHRoKSwgbGVuZ3RoKGxhYmVsKSwgMiwgTikpCgpmb3IodCBpbiAxOjEyMSl7ICMoVHQtMSkpeyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgU3RvcCBiZWZvcmUgdGhlIGxhc3QgZGF0ZTogbm8gZndkIHJldHVybiEKICAgIGlmKHQlJTI9PTApe3ByaW50KHRfb29zW3RdKX0gICAgICAgICAgICAgICAgICAgICAgICAjIEp1c3QgY2hlY2tpbmcgdGhlIGRhdGUgc3RhdHVzCiAgICBmb3IoayBpbiAxOmxlbmd0aChkZXB0aCkpeyAgICAgICAgICAgICAgICAgICAgICAgICAgIyBFc3RpbWF0aW9uIHdpbmRvdwogICAgICAgIGZvcihpIGluIDE6bGVuZ3RoKGxhYmVsKSl7ICAgICAgICAgICAgICAgICAgICAgICMgTGFiZWwgaG9yaXpvbgogICAgICAgICAgICB0cmFpbl9idWZmZXIgPC0gbGFiZWxfbnVtW2ldICAgICAgICAgICAgICAgICMgVHJhaW5pbmcgYnVmZmVyIHNpemUgaW4gbW9udGhzCiAgICAgICAgICAgIGxhYmVsX3VzZWQgPC0gbGFiZWxbaV0KICAgICAgICAgICAgaW5kX2RhdGUgPSB3aGljaChkYXRlcz09dF9vb3NbdF0pCiAgICAgICAgICAgIHRyYWluX2RhdGEgPC0gZGF0YV9tbCAlPiUgZmlsdGVyKGRhdGUgPD0gZGF0ZXNbaW5kX2RhdGUtdHJhaW5fYnVmZmVyXSwgICAjIFJvbGxpbmcgd2luZG93IHdpdGggYnVmZmVyCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGUgPj0gZGF0ZXNbaW5kX2RhdGUtdHJhaW5fYnVmZmVyLWRlcHRoW2tdXSkgICAgCiAgICAgICAgICAgIHRlc3RfZGF0YSA8LSBkYXRhX21sICU+JSBmaWx0ZXIoZGF0ZSA9PSB0X29vc1t0XSkgICAgICAgICAgIyBDb21tb24gd2l0aCBwYXN0IGluZm8hIAogICAgICAgICAgICBmb3IoaiBpbiAxOjIpeyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgUHJlZGljdG9yCiAgICAgICAgICAgICAgICBwcmVkX3RlbXAgPC0gcHJlZF9mdW4odHJhaW5fZGF0YSwgdGVzdF9kYXRhLCBmZWF0dXJlcywgbGFiZWxfdXNlZCwgaikKICAgICAgICAgICAgICAgIGluZDIgPC0gbWF0Y2gocHJlZF90ZW1wJG5hbWVzLCB0aWNrcykgJT4lIG5hLm9taXQoKSAgICAjIEluZGV4IGJldHdlZW4gYWxsICYgdGVzdAogICAgICAgICAgICAgICAgcHJlZFt0LGssaSxqLGluZDJdIDwtIHByZWRfdGVtcCRyZXQgIAogICAgICAgICAgICAgICAgcmVhbF90bXAgPC0gZGF0YV9tbCAlPiUgZmlsdGVyKGRhdGUgPT0gdF9vb3NbdF0pICU+JSBzZWxlY3QobGFiZWxfdXNlZCkgJT4lIGFzLm1hdHJpeCgpCiAgICAgICAgICAgICAgICByZWFsaXplZFt0LGssaSxqLCBpbmQyXSA8LSByZWFsX3RtcCAKICAgICAgICAgICAgfQogICAgICAgIH0KICAgIH0KfQoKIyBzYXZlKHByZWQsIGZpbGUgPSAicHJlZC5SRGF0YSIpCiMgc2F2ZShyZWFsaXplZCwgZmlsZSA9ICJyZWFsaXplZC5SRGF0YSIpCmBgYAoKCioqRmlmdGgqKjogYW5hbHlzaXMuIEJlbG93IHdlIHNob3cgaG93IHRvIGNvbXB1dGUgdGhlIFJeMiBhbmQgTWFyaWFuby1EaWVibG9kIHN0YXRpc3RpY3MuCkFuIGluaXRpYWwgc3RlcCBpcyB0byBjbGVhbiBhbmQgcmUtb3JkZXIgdGhlIGRhdGEuCgpgYGB7cn0KbGlicmFyeShyZXNoYXBlKQpwcmVkX2RmIDwtIG1lbHQocHJlZCkKcmVhbGl6ZWRfZGYgPC0gbWVsdChyZWFsaXplZCkKZXJyb3JfZGYgPC0gY2JpbmQocHJlZF9kZlssMTo1XSwgcmVhbGl6ZWRfZGZbLDZdLCByZWFsaXplZF9kZlssNl0gLSBwcmVkX2RmWyw2XSkKY29sbmFtZXMoZXJyb3JfZGYpIDwtIGMoIkRhdGUiLCAiRGVwdGgiLCAiTGFiZWwiLCAiTWV0aG9kIiwgIlN0b2NrIiwgIlJlYWxpemVkIiwgIkVycm9yIikKZXJyb3JfZGYkRGVwdGhbZXJyb3JfZGYkRGVwdGggPT0gNV0gPC0gOTYKZXJyb3JfZGYkRGVwdGhbZXJyb3JfZGYkRGVwdGggPT0gNF0gPC0gNDgKZXJyb3JfZGYkRGVwdGhbZXJyb3JfZGYkRGVwdGggPT0gM10gPC0gMjQKZXJyb3JfZGYkRGVwdGhbZXJyb3JfZGYkRGVwdGggPT0gMl0gPC0gMTIKZXJyb3JfZGYkRGVwdGhbZXJyb3JfZGYkRGVwdGggPT0gMV0gPC0gNgoKZXJyb3JfZGYkTGFiZWxbZXJyb3JfZGYkTGFiZWwgPT0gNF0gPC0gMTIKZXJyb3JfZGYkTGFiZWxbZXJyb3JfZGYkTGFiZWwgPT0gM10gPC0gNgplcnJvcl9kZiRMYWJlbFtlcnJvcl9kZiRMYWJlbCA9PSAyXSA8LSAzCgplcnJvcl9kZiRNZXRob2RbZXJyb3JfZGYkTWV0aG9kID09IDFdIDwtICJCb29zdGVkX3RyZWVzIgplcnJvcl9kZiRNZXRob2RbZXJyb3JfZGYkTWV0aG9kID09IDJdIDwtICJOZXVyYWxfbmV0d29yayIKCnJlcyA8LSBlcnJvcl9kZiAlPiUgCiAgICBtdXRhdGUoRXJyb3IgPSBFcnJvciAvIHNxcnQoTGFiZWwpLAogICAgICAgICAgIGFic19lcnIgPSBhYnMoRXJyb3IpLAogICAgICAgICAgIHNxX2VyciA9IEVycm9yXjIsCiAgICAgICAgICAgc3FfcmVhbGl6ZWQgPSBSZWFsaXplZF4yKSAlPiUKICAgIGdyb3VwX2J5KERlcHRoLCBMYWJlbCwgTWV0aG9kKSAlPiUKICAgIHN1bW1hcmlzZShtYWUgPSBtZWFuKGFic19lcnIsIG5hLnJtID0gVCksCiAgICAgICAgICAgICAgcjIgPSAxLXN1bShzcV9lcnIsIG5hLnJtID0gVCkvc3VtKHNxX3JlYWxpemVkLCBuYS5ybSA9IFQpKSAlPiUKICAgIHVuZ3JvdXAoKSAlPiUKICAgIG11dGF0ZShMYWJlbCA9IGFzLmZhY3RvcihMYWJlbCksCiAgICAgICAgICAgTGFiZWwgPSByZWNvZGVfZmFjdG9yKExhYmVsLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiMSI9ICIxIG1vbnRoIGxhYmVsIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjMiPSAiMyBtb250aCBsYWJlbCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICI2Ij0gIjYgbW9udGggbGFiZWwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiMTIiPSAiMTIgbW9udGggbGFiZWwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAub3JkZXJlZCA9IFQpKSAKYGBgCgpUaGVuLCB0aGUgUl4yLgoKYGBge3J9CnJlcyA8LSBlcnJvcl9kZiAlPiUKICAgIGdyb3VwX2J5KERlcHRoLCBMYWJlbCwgTWV0aG9kKSAlPiUKICAgIHN1bW1hcmlzZShSMiA9IDEtbWVhbihFcnJvcl4yLCBuYS5ybSA9IFQpL21lYW4oUmVhbGl6ZWReMiwgbmEucm0gPSBUKSkgJT4lCiAgICB1bmdyb3VwKCkgJT4lCiAgICBtdXRhdGUoRGVwdGggPSBhcy5mYWN0b3IoRGVwdGgpLAogICAgICAgICAgIE1ldGhvZCA9IGFzLmZhY3RvcihNZXRob2QpKSAKcmVzICU+JQogICAgZ2dwbG90KGFlcyh4ID0gRGVwdGgsIHkgPSBSMiwgZmlsbCA9IE1ldGhvZCkpICsgZ2VvbV9jb2wocG9zaXRpb24gPSAiZG9kZ2UiKSArIAogICAgZmFjZXRfZ3JpZCguIH5MYWJlbCwgc2NhbGVzID0gImZyZWUiKSArCiAgICB5bGFiKCJPdXQtb2Ytc2FtcGxlIFJeMiIpICsgeGxhYigiU2FtcGxlIGRlcHRoIChtb250aHMpIikgKyAKICAgIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoIiMxMzNFN0UiLCIjMEZENTgxIiksIAogICAgICAgICAgICAgICAgICAgICAgbmFtZSA9ICJNZXRob2QiLCAKICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIkJvb3N0ZWQgdHJlZXMiLCAiTmV1cmFsIG5ldHdvcmtzIikpICsKICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJ0b3AiKSAKYGBgCgpBbmQgZmluYWxseSwgdGhlIE1EIHNlcmllcy4KCmBgYHtyfQpsaWJyYXJ5KHZpcmlkaXMpCmVycm9yX2RmICU+JQogICAgc3ByZWFkKGtleSA9IE1ldGhvZCwgdmFsdWUgPSBFcnJvcikgJT4lCiAgICBncm91cF9ieShEYXRlLCBEZXB0aCwgTGFiZWwpICU+JQogICAgc3VtbWFyaXNlKE1EID0gbWVhbihCb29zdGVkX3RyZWVzXjItTmV1cmFsX25ldHdvcmteMiwgbmEucm09VCkgLyBzZChCb29zdGVkX3RyZWVzXjItTmV1cmFsX25ldHdvcmteMiwgbmEucm09VCkpICU+JQogICAgZ2dwbG90KGFlcyh4PXRfb29zW0RhdGVdLCB5ID0gTUQsIGNvbG9yID0gYXMuZmFjdG9yKERlcHRoKSkpICsgZ2VvbV9saW5lKCkgKyBmYWNldF9ncmlkKExhYmVsIH4uKSArCiAgICB5bGFiKCJEaWVib2xkLU1hcmlhbm8gc3RhdGlzdGljIikgKyB4bGFiKCJEYXRlIikgKyBzY2FsZV9jb2xvcl92aXJpZGlzKG9wdGlvbj0icGxhc21hIiwgZGlzY3JldGU9VFJVRSkgKwogICAgbGFicyhjb2xvciA9ICJEZXB0aCAobW9udGhzKSIpCmBgYA==