If any issues, questions or suggestions feel free to reach me out via e-mail or Linkedin. You can also visit my Github.

if(!require(pacman)) install.packages('pacman')
pacman::p_load(dplyr, tidyr, ggplot2, pROC, broom, gridExtra, ggpubr, car, stringr)

In this post we fit logistic regression model in order to predict increase or decrease of stock market indices based on technical analysis indicators. Our goal will be selection of the most informative technical indicators for making investment decisions for 1-day ahead. In the blog post Technical Indicators - Data Preparation we downloaded data, calculated technical indicators and spllited the data into train set and test set.

train_set <- dget('technical_indicators_train_set.txt')
train_set <- lapply(train_set, function(x) lapply(x, function(y) if(is.character(y)) as.factor(y) else y))
train_set <- lapply(train_set, as.data.frame)
test_set <- dget('technical_indicators_test_set.txt')
test_set <- lapply(test_set, function(x) lapply(x, function(y) if(is.character(y)) as.factor(y) else y))
test_set <- lapply(test_set, as.data.frame)

In the below loop we do the following steps for stock indices in the dataset:
- fit logistic regression model using all predictors
- apply two-directional stepwise algorithm to select model with minimum AIC
- predict probabilities on the test set
- if predicted probability is higher than \(0.5\) then we assume Increase (i.e. higher probability of increase of stock index), otherwise Decrease
- create missclasification table
- calculate accuracy, specificity and sensititivity
- calculate ROC.

tickers <- names(train_set)

logistic_models <- list()
logistic_predictions <- list()
misclass_tables <- list()
evaluation_metrics <- list()
evaluation_roc_train <- list()
evaluation_roc_test <- list()

for (i in 1:length(tickers)){
  initial_model <- glm(formula = Class_Returns ~., family = binomial, data = train_set[[i]])
  step_model <- step(initial_model, direction = 'both', trace = 0)
  logistic_models[[tickers[i]]] <- step_model
  
  prediction <- data.frame(Probability = predict(step_model, test_set[[i]], type = 'response')) %>%
    mutate(Class_Returns = if_else(Probability > 0.5, 'Increase', 'Deacrease') %>%
             factor(levels = c('Deacrease', 'Increase')))
  logistic_predictions[[tickers[i]]] <- prediction
  
  misclass_table <- table(Predicted = prediction$Class_Returns
                          ,Actual = test_set[[i]]$Class_Returns)
  misclass_tables[[tickers[i]]] <- misclass_table
  
  accuracy <- round(100*sum(diag(misclass_table)) / sum(misclass_table), 2)
  sensitivity <- round(100*misclass_table[2,2] / (misclass_table[2,2] + misclass_table[1,2]), 2)
  specificity <- round(100*misclass_table[1,1] / (misclass_table[1,1] + misclass_table[2,1]), 2)
  evaluation_metrics[[tickers[i]]] <- c(accuracy = accuracy
                                        ,sensitivity = sensitivity
                                        ,specificity = specificity)
  
  roc_train <- roc(train_set[[i]]$Class_Returns, step_model$fitted.values)
  evaluation_roc_train[[tickers[i]]] <- roc_train
  
  roc_test <- roc(test_set[[i]]$Class_Returns, prediction$Probability)
  evaluation_roc_test[[tickers[i]]] <- roc_test
}

The most informative variables in our study are Parkinson_Volatility which appeared in 4 models and position of EMA20 relative to close prices which appeared in 3 models,

lapply(seq_along(logistic_models)
       ,function(x) names(logistic_models[[x]]$coefficients[-1])) %>%
  unlist() %>%
  table() %>%
  as.data.frame() %>%
  ggplot() +
  geom_bar(aes(x = Freq, y = reorder(., Freq)), stat = 'identity'
           ,fill = 'gray', color = 'black') +
  theme_bw() +
  labs(x = '', y = '', title = 'How often indicators appeared in logistic models?')

Overall accuracy in our models is about \(46 \%\), sensitivity about \(18 \%\) and specificity \(78 \%\) which means the logistic models perform well in predicting decreases of stock indices and perform poorly in predicting increases of stock indices.

as.data.frame(evaluation_metrics) %>%
  t() %>%
  as.data.frame() %>%
  pivot_longer(cols = 1:3, names_to = 'Metric', values_to = 'Value') %>%
  ggplot() +
  geom_boxplot(aes(x = Metric, y = Value, fill = Metric)) +
  facet_wrap(vars(Metric), scales = 'free') +
  theme_bw() +
  theme(legend.position = 'none', axis.text.x = element_blank(), axis.ticks.x = element_blank()) +
  labs(x = '', y = '')

NA

In all cases AUC is about \(50 \%\) which indicates that logistic models wouldn’t outperform guessing by change if stock indices increase either decrease.

for (i in 1:7) {
  
  if (!(i %% 2 == 0)) par(mfrow = c(1,2))
  
  plot(x = 1 - evaluation_roc_test[[i]]$specificities
     ,y = evaluation_roc_test[[i]]$sensitivities
     ,type = 'l'
     ,lwd = 2
     ,col = 'orange'
     ,xlab = ''
     ,ylab = '')
par(new = TRUE)
plot(x = 1 - evaluation_roc_train[[i]]$specificities
     ,y = evaluation_roc_train[[i]]$sensitivities
     ,type = 'l'
     ,lwd = 2
     ,col = 'cyan'
     ,xlab = 'FPR'
     ,ylab = 'TPR'
     ,main = names(evaluation_roc_train)[[i]])
abline(a = 0
       ,b = 1)
legend('bottomright'
       ,legend = c(paste0('Train ROC, AUC = ', round(auc(evaluation_roc_train[[i]])[1], 4))
                   ,paste0('Test ROC, AUC = ', round(auc(evaluation_roc_test[[i]])[1], 4)))
       ,col = c('orange'
                ,'cyan')
       ,lty = 1
       ,lwd = 2)
  
}

NA

Let’s do some diagnostics. Below we plot standardized residuals from our models. Standardized residuals with abolute value freater tghan 3 would indicate influential outliers in the continuous predictors. This ain’t the case in our models.

Variance inflation factor (VIF) above 10 would indicate intercorrelation among predictors. This also ain’t the case in our models.

lapply(logistic_models, function(x) tryCatch(
  expr = {vif(x)}
  ,error = function(e){'No predictors in the model'}
))
$WIG20
             Stoch_K Parkinson_Volatility 
            1.001808             1.001808 

$mWIG40
                 RSI              Stoch_D Parkinson_Volatility 
            3.682298             3.679802             1.044789 

$sWIG80
               EMA15               EMA200                  RSI Parkinson_Volatility 
            3.323025             1.245302             3.456034             1.035835 

$`^SPX`
[1] "No predictors in the model"

$`^DAX`
    Close_Volatility Parkinson_Volatility 
            6.213605             6.213605 

$`^NDQ`
[1] "No predictors in the model"

$EEM.US
          EMA200              ADX          Stoch_K Close_Volatility 
        1.314077         1.012254         1.137232         1.178884 

In most cases only MACD Histogram is in linear relationship to the logit of the outcome in the traning set. In further research we will try polynomial transformation to technical indicators like Stoch, ADX, RSI.

for (i in 1:7) {
  
  if (!((logistic_models[[i]]$coefficients[-1] %>%
         names() %>%
         str_detect('EMA.+', negate = TRUE) %>%
         sum()) == 0)){
    
  continuous_predictors <- train_set[[i]] %>%
    select_if(is.numeric) %>%
    mutate(prob = predict(logistic_models[[i]], train_set[[i]], type = 'response')) %>%
    mutate(logit = log(prob / (1 - prob))) %>%
    select(-prob) %>%
    pivot_longer(ADX:Parkinson_Volatility, names_to = 'Indicator', values_to = 'Value')
  
  linearity_plots <- ggplot(continuous_predictors, aes(logit, Value)) +
    geom_point(size = 0.5) +
    geom_smooth(method = 'loess') + 
    facet_wrap(vars(Indicator), scales = 'free') +
    labs(title = names(train_set)[i])
  
  print(linearity_plots)
  
  }
  
}

LS0tDQp0aXRsZTogIlRlY2huaWNhbCBJbmRpY2F0b3JzIC0gTG9naXN0aWMgUmVncmVzc2lvbiINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCklmIGFueSBpc3N1ZXMsIHF1ZXN0aW9ucyBvciBzdWdnZXN0aW9ucyBmZWVsIGZyZWUgdG8gcmVhY2ggbWUgb3V0IHZpYSBlLW1haWwgPHdpZWN6eW5za2lwYXdlbEBnbWFpbC5jb20+IG9yIFtMaW5rZWRpbl0oaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2luL3Bhd2VsLXdpZWN6eW5za2kvKS4gWW91IGNhbiBhbHNvIHZpc2l0IG15IFtHaXRodWJdKGh0dHBzOi8vZ2l0aHViLmNvbS9wYXdlbC13aWVjenluc2tpKS4NCg0KYGBge3IgbGlicmFyaWVzLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KaWYoIXJlcXVpcmUocGFjbWFuKSkgaW5zdGFsbC5wYWNrYWdlcygncGFjbWFuJykNCnBhY21hbjo6cF9sb2FkKGRwbHlyLCB0aWR5ciwgZ2dwbG90MiwgcFJPQywgYnJvb20sIGdyaWRFeHRyYSwgZ2dwdWJyLCBjYXIsIHN0cmluZ3IpDQpgYGANCg0KSW4gdGhpcyBwb3N0IHdlIGZpdCBsb2dpc3RpYyByZWdyZXNzaW9uIG1vZGVsIGluIG9yZGVyIHRvIHByZWRpY3QgaW5jcmVhc2Ugb3IgZGVjcmVhc2Ugb2Ygc3RvY2sgbWFya2V0IGluZGljZXMgYmFzZWQgb24gdGVjaG5pY2FsIGFuYWx5c2lzIGluZGljYXRvcnMuIE91ciBnb2FsIHdpbGwgYmUgc2VsZWN0aW9uIG9mIHRoZSBtb3N0IGluZm9ybWF0aXZlIHRlY2huaWNhbCBpbmRpY2F0b3JzIGZvciBtYWtpbmcgaW52ZXN0bWVudCBkZWNpc2lvbnMgZm9yIDEtZGF5IGFoZWFkLiBJbiB0aGUgYmxvZyBwb3N0IFtUZWNobmljYWwgSW5kaWNhdG9ycyAtIERhdGEgUHJlcGFyYXRpb25dKGh0dHBzOi8vcnB1YnMuY29tL3Bhd2VsLXdpZWN6eW5za2kvODcxMzkzKSB3ZSBkb3dubG9hZGVkIGRhdGEsIGNhbGN1bGF0ZWQgdGVjaG5pY2FsIGluZGljYXRvcnMgYW5kIHNwbGxpdGVkIHRoZSBkYXRhIGludG8gdHJhaW4gc2V0IGFuZCB0ZXN0IHNldC4NCg0KYGBge3IgbG9hZF9kYXRhfQ0KdHJhaW5fc2V0IDwtIGRnZXQoJ3RlY2huaWNhbF9pbmRpY2F0b3JzX3RyYWluX3NldC50eHQnKQ0KdHJhaW5fc2V0IDwtIGxhcHBseSh0cmFpbl9zZXQsIGZ1bmN0aW9uKHgpIGxhcHBseSh4LCBmdW5jdGlvbih5KSBpZihpcy5jaGFyYWN0ZXIoeSkpIGFzLmZhY3Rvcih5KSBlbHNlIHkpKQ0KdHJhaW5fc2V0IDwtIGxhcHBseSh0cmFpbl9zZXQsIGFzLmRhdGEuZnJhbWUpDQp0ZXN0X3NldCA8LSBkZ2V0KCd0ZWNobmljYWxfaW5kaWNhdG9yc190ZXN0X3NldC50eHQnKQ0KdGVzdF9zZXQgPC0gbGFwcGx5KHRlc3Rfc2V0LCBmdW5jdGlvbih4KSBsYXBwbHkoeCwgZnVuY3Rpb24oeSkgaWYoaXMuY2hhcmFjdGVyKHkpKSBhcy5mYWN0b3IoeSkgZWxzZSB5KSkNCnRlc3Rfc2V0IDwtIGxhcHBseSh0ZXN0X3NldCwgYXMuZGF0YS5mcmFtZSkNCmBgYA0KDQpJbiB0aGUgYmVsb3cgbG9vcCB3ZSBkbyB0aGUgZm9sbG93aW5nIHN0ZXBzIGZvciBzdG9jayBpbmRpY2VzIGluIHRoZSBkYXRhc2V0OiBcDQogLSBmaXQgbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbCB1c2luZyBhbGwgcHJlZGljdG9ycyBcDQogLSBhcHBseSB0d28tZGlyZWN0aW9uYWwgc3RlcHdpc2UgYWxnb3JpdGhtIHRvIHNlbGVjdCBtb2RlbCB3aXRoIG1pbmltdW0gQUlDIFwNCiAtIHByZWRpY3QgcHJvYmFiaWxpdGllcyBvbiB0aGUgdGVzdCBzZXQgXA0KIC0gaWYgcHJlZGljdGVkIHByb2JhYmlsaXR5IGlzIGhpZ2hlciB0aGFuICQwLjUkIHRoZW4gd2UgYXNzdW1lICpJbmNyZWFzZSogKGkuZS4gaGlnaGVyIHByb2JhYmlsaXR5IG9mIGluY3JlYXNlIG9mIHN0b2NrIGluZGV4KSwgb3RoZXJ3aXNlICpEZWNyZWFzZSogXA0KIC0gY3JlYXRlIG1pc3NjbGFzaWZpY2F0aW9uIHRhYmxlIFwNCiAtIGNhbGN1bGF0ZSBhY2N1cmFjeSwgc3BlY2lmaWNpdHkgYW5kIHNlbnNpdGl0aXZpdHkgXA0KIC0gY2FsY3VsYXRlIFJPQy4NCmBgYHtyIGJ1aWxkX21vZGVsc19wcmVkaWN0X2FuZF9ldmFsdWF0ZSwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCnRpY2tlcnMgPC0gbmFtZXModHJhaW5fc2V0KQ0KDQpsb2dpc3RpY19tb2RlbHMgPC0gbGlzdCgpDQpsb2dpc3RpY19wcmVkaWN0aW9ucyA8LSBsaXN0KCkNCm1pc2NsYXNzX3RhYmxlcyA8LSBsaXN0KCkNCmV2YWx1YXRpb25fbWV0cmljcyA8LSBsaXN0KCkNCmV2YWx1YXRpb25fcm9jX3RyYWluIDwtIGxpc3QoKQ0KZXZhbHVhdGlvbl9yb2NfdGVzdCA8LSBsaXN0KCkNCg0KZm9yIChpIGluIDE6bGVuZ3RoKHRpY2tlcnMpKXsNCiAgaW5pdGlhbF9tb2RlbCA8LSBnbG0oZm9ybXVsYSA9IENsYXNzX1JldHVybnMgfi4sIGZhbWlseSA9IGJpbm9taWFsLCBkYXRhID0gdHJhaW5fc2V0W1tpXV0pDQogIHN0ZXBfbW9kZWwgPC0gc3RlcChpbml0aWFsX21vZGVsLCBkaXJlY3Rpb24gPSAnYm90aCcsIHRyYWNlID0gMCkNCiAgbG9naXN0aWNfbW9kZWxzW1t0aWNrZXJzW2ldXV0gPC0gc3RlcF9tb2RlbA0KICANCiAgcHJlZGljdGlvbiA8LSBkYXRhLmZyYW1lKFByb2JhYmlsaXR5ID0gcHJlZGljdChzdGVwX21vZGVsLCB0ZXN0X3NldFtbaV1dLCB0eXBlID0gJ3Jlc3BvbnNlJykpICU+JQ0KICAgIG11dGF0ZShDbGFzc19SZXR1cm5zID0gaWZfZWxzZShQcm9iYWJpbGl0eSA+IDAuNSwgJ0luY3JlYXNlJywgJ0RlYWNyZWFzZScpICU+JQ0KICAgICAgICAgICAgIGZhY3RvcihsZXZlbHMgPSBjKCdEZWFjcmVhc2UnLCAnSW5jcmVhc2UnKSkpDQogIGxvZ2lzdGljX3ByZWRpY3Rpb25zW1t0aWNrZXJzW2ldXV0gPC0gcHJlZGljdGlvbg0KICANCiAgbWlzY2xhc3NfdGFibGUgPC0gdGFibGUoUHJlZGljdGVkID0gcHJlZGljdGlvbiRDbGFzc19SZXR1cm5zDQogICAgICAgICAgICAgICAgICAgICAgICAgICxBY3R1YWwgPSB0ZXN0X3NldFtbaV1dJENsYXNzX1JldHVybnMpDQogIG1pc2NsYXNzX3RhYmxlc1tbdGlja2Vyc1tpXV1dIDwtIG1pc2NsYXNzX3RhYmxlDQogIA0KICBhY2N1cmFjeSA8LSByb3VuZCgxMDAqc3VtKGRpYWcobWlzY2xhc3NfdGFibGUpKSAvIHN1bShtaXNjbGFzc190YWJsZSksIDIpDQogIHNlbnNpdGl2aXR5IDwtIHJvdW5kKDEwMCptaXNjbGFzc190YWJsZVsyLDJdIC8gKG1pc2NsYXNzX3RhYmxlWzIsMl0gKyBtaXNjbGFzc190YWJsZVsxLDJdKSwgMikNCiAgc3BlY2lmaWNpdHkgPC0gcm91bmQoMTAwKm1pc2NsYXNzX3RhYmxlWzEsMV0gLyAobWlzY2xhc3NfdGFibGVbMSwxXSArIG1pc2NsYXNzX3RhYmxlWzIsMV0pLCAyKQ0KICBldmFsdWF0aW9uX21ldHJpY3NbW3RpY2tlcnNbaV1dXSA8LSBjKGFjY3VyYWN5ID0gYWNjdXJhY3kNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAsc2Vuc2l0aXZpdHkgPSBzZW5zaXRpdml0eQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICxzcGVjaWZpY2l0eSA9IHNwZWNpZmljaXR5KQ0KICANCiAgcm9jX3RyYWluIDwtIHJvYyh0cmFpbl9zZXRbW2ldXSRDbGFzc19SZXR1cm5zLCBzdGVwX21vZGVsJGZpdHRlZC52YWx1ZXMpDQogIGV2YWx1YXRpb25fcm9jX3RyYWluW1t0aWNrZXJzW2ldXV0gPC0gcm9jX3RyYWluDQogIA0KICByb2NfdGVzdCA8LSByb2ModGVzdF9zZXRbW2ldXSRDbGFzc19SZXR1cm5zLCBwcmVkaWN0aW9uJFByb2JhYmlsaXR5KQ0KICBldmFsdWF0aW9uX3JvY190ZXN0W1t0aWNrZXJzW2ldXV0gPC0gcm9jX3Rlc3QNCn0NCg0KYGBgDQoNClRoZSBtb3N0IGluZm9ybWF0aXZlIHZhcmlhYmxlcyBpbiBvdXIgc3R1ZHkgYXJlICpQYXJraW5zb25fVm9sYXRpbGl0eSogd2hpY2ggYXBwZWFyZWQgaW4gNCBtb2RlbHMgYW5kIHBvc2l0aW9uIG9mICpFTUEyMCogcmVsYXRpdmUgdG8gY2xvc2UgcHJpY2VzIHdoaWNoIGFwcGVhcmVkIGluIDMgbW9kZWxzLCANCmBgYHtyIGluZm9ybWF0aXZlX3ZhcmlhYmxlc30NCmxhcHBseShzZXFfYWxvbmcobG9naXN0aWNfbW9kZWxzKQ0KICAgICAgICxmdW5jdGlvbih4KSBuYW1lcyhsb2dpc3RpY19tb2RlbHNbW3hdXSRjb2VmZmljaWVudHNbLTFdKSkgJT4lDQogIHVubGlzdCgpICU+JQ0KICB0YWJsZSgpICU+JQ0KICBhcy5kYXRhLmZyYW1lKCkgJT4lDQogIGdncGxvdCgpICsNCiAgZ2VvbV9iYXIoYWVzKHggPSBGcmVxLCB5ID0gcmVvcmRlciguLCBGcmVxKSksIHN0YXQgPSAnaWRlbnRpdHknDQogICAgICAgICAgICxmaWxsID0gJ2dyYXknLCBjb2xvciA9ICdibGFjaycpICsNCiAgdGhlbWVfYncoKSArDQogIGxhYnMoeCA9ICcnLCB5ID0gJycsIHRpdGxlID0gJ0hvdyBvZnRlbiBpbmRpY2F0b3JzIGFwcGVhcmVkIGluIGxvZ2lzdGljIG1vZGVscz8nKQ0KYGBgDQoNCk92ZXJhbGwgYWNjdXJhY3kgaW4gb3VyIG1vZGVscyBpcyBhYm91dCAkNDYgXCUkLCBzZW5zaXRpdml0eSBhYm91dCAkMTggXCUkIGFuZCBzcGVjaWZpY2l0eSAkNzggXCUkIHdoaWNoIG1lYW5zIHRoZSBsb2dpc3RpYyBtb2RlbHMgcGVyZm9ybSB3ZWxsIGluIHByZWRpY3RpbmcgZGVjcmVhc2VzIG9mIHN0b2NrIGluZGljZXMgYW5kIHBlcmZvcm0gcG9vcmx5IGluIHByZWRpY3RpbmcgaW5jcmVhc2VzIG9mIHN0b2NrIGluZGljZXMuDQpgYGB7ciBtZXRyaWNzX3Zpc3VhbGl6YXRpb259DQphcy5kYXRhLmZyYW1lKGV2YWx1YXRpb25fbWV0cmljcykgJT4lDQogIHQoKSAlPiUNCiAgYXMuZGF0YS5mcmFtZSgpICU+JQ0KICBwaXZvdF9sb25nZXIoY29scyA9IDE6MywgbmFtZXNfdG8gPSAnTWV0cmljJywgdmFsdWVzX3RvID0gJ1ZhbHVlJykgJT4lDQogIGdncGxvdCgpICsNCiAgZ2VvbV9ib3hwbG90KGFlcyh4ID0gTWV0cmljLCB5ID0gVmFsdWUsIGZpbGwgPSBNZXRyaWMpKSArDQogIGZhY2V0X3dyYXAodmFycyhNZXRyaWMpLCBzY2FsZXMgPSAnZnJlZScpICsNCiAgdGhlbWVfYncoKSArDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICdub25lJywgYXhpcy50ZXh0LnggPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGlja3MueCA9IGVsZW1lbnRfYmxhbmsoKSkgKw0KICBsYWJzKHggPSAnJywgeSA9ICcnKQ0KICANCmBgYA0KDQpJbiBhbGwgY2FzZXMgQVVDIGlzIGFib3V0ICQ1MCBcJSQgd2hpY2ggaW5kaWNhdGVzIHRoYXQgbG9naXN0aWMgbW9kZWxzIHdvdWxkbid0IG91dHBlcmZvcm0gZ3Vlc3NpbmcgYnkgY2hhbmdlIGlmIHN0b2NrIGluZGljZXMgaW5jcmVhc2UgZWl0aGVyIGRlY3JlYXNlLg0KYGBge3Igcm9jLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KZm9yIChpIGluIDE6Nykgew0KICANCiAgaWYgKCEoaSAlJSAyID09IDApKSBwYXIobWZyb3cgPSBjKDEsMikpDQogIA0KICBwbG90KHggPSAxIC0gZXZhbHVhdGlvbl9yb2NfdGVzdFtbaV1dJHNwZWNpZmljaXRpZXMNCiAgICAgLHkgPSBldmFsdWF0aW9uX3JvY190ZXN0W1tpXV0kc2Vuc2l0aXZpdGllcw0KICAgICAsdHlwZSA9ICdsJw0KICAgICAsbHdkID0gMg0KICAgICAsY29sID0gJ29yYW5nZScNCiAgICAgLHhsYWIgPSAnJw0KICAgICAseWxhYiA9ICcnKQ0KcGFyKG5ldyA9IFRSVUUpDQpwbG90KHggPSAxIC0gZXZhbHVhdGlvbl9yb2NfdHJhaW5bW2ldXSRzcGVjaWZpY2l0aWVzDQogICAgICx5ID0gZXZhbHVhdGlvbl9yb2NfdHJhaW5bW2ldXSRzZW5zaXRpdml0aWVzDQogICAgICx0eXBlID0gJ2wnDQogICAgICxsd2QgPSAyDQogICAgICxjb2wgPSAnY3lhbicNCiAgICAgLHhsYWIgPSAnRlBSJw0KICAgICAseWxhYiA9ICdUUFInDQogICAgICxtYWluID0gbmFtZXMoZXZhbHVhdGlvbl9yb2NfdHJhaW4pW1tpXV0pDQphYmxpbmUoYSA9IDANCiAgICAgICAsYiA9IDEpDQpsZWdlbmQoJ2JvdHRvbXJpZ2h0Jw0KICAgICAgICxsZWdlbmQgPSBjKHBhc3RlMCgnVHJhaW4gUk9DLCBBVUMgPSAnLCByb3VuZChhdWMoZXZhbHVhdGlvbl9yb2NfdHJhaW5bW2ldXSlbMV0sIDQpKQ0KICAgICAgICAgICAgICAgICAgICxwYXN0ZTAoJ1Rlc3QgUk9DLCBBVUMgPSAnLCByb3VuZChhdWMoZXZhbHVhdGlvbl9yb2NfdGVzdFtbaV1dKVsxXSwgNCkpKQ0KICAgICAgICxjb2wgPSBjKCdvcmFuZ2UnDQogICAgICAgICAgICAgICAgLCdjeWFuJykNCiAgICAgICAsbHR5ID0gMQ0KICAgICAgICxsd2QgPSAyKQ0KICANCn0NCg0KYGBgDQoNCkxldCdzIGRvIHNvbWUgZGlhZ25vc3RpY3MuIEJlbG93IHdlIHBsb3Qgc3RhbmRhcmRpemVkIHJlc2lkdWFscyBmcm9tIG91ciBtb2RlbHMuIFN0YW5kYXJkaXplZCByZXNpZHVhbHMgd2l0aCBhYm9sdXRlIHZhbHVlIGZyZWF0ZXIgdGdoYW4gMyB3b3VsZCBpbmRpY2F0ZSBpbmZsdWVudGlhbCBvdXRsaWVycyBpbiB0aGUgY29udGludW91cyBwcmVkaWN0b3JzLiBUaGlzIGFpbid0IHRoZSBjYXNlIGluIG91ciBtb2RlbHMuDQpgYGB7ciBkaWFnbm9zdGljczEsIG1lc3NhZ2U9RkFMU0V9DQpkYXRhX3dpdGhfbW9kZWwgPC0gbGFwcGx5KGxvZ2lzdGljX21vZGVscywgZnVuY3Rpb24oeCkgYXVnbWVudCh4KSAlPiUNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtdXRhdGUoaW5kZXggPSAxOm4oKSkpDQoNCm91dGxpZXJfcGxvdHMgPC0gbGlzdCgpDQpmb3IgKGkgaW4gMTo3KSB7DQogIG91dGxpZXJfcGxvdHNbW2ldXSA8LSBnZ3Bsb3QoZGF0YV93aXRoX21vZGVsW1tpXV0sIGFlcyh4ID0gaW5kZXgsIHkgPSAuc3RkLnJlc2lkKSkgKw0KICAgIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gQ2xhc3NfUmV0dXJucykpICsNCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKQ0KICAgICAgICAgICxheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCkNCiAgICAgICAgICAsYXhpcy50aWNrcy54ID0gZWxlbWVudF9ibGFuaygpDQogICAgICAgICAgLGF4aXMudGl0bGUueSA9IGVsZW1lbnRfYmxhbmsoKSkgKw0KICAgIGxhYnModGl0bGUgPSBuYW1lcyhkYXRhX3dpdGhfbW9kZWwpW1tpXV0pDQp9DQoNCnAgPC0gZ2dhcnJhbmdlKHBsb3RsaXN0ID0gb3V0bGllcl9wbG90cywgbmNvbCA9IDQsIG5yb3cgPSAyLCBjb21tb24ubGVnZW5kID0gVFJVRSkNCnANCmBgYA0KDQpWYXJpYW5jZSBpbmZsYXRpb24gZmFjdG9yIChWSUYpIGFib3ZlIDEwIHdvdWxkIGluZGljYXRlIGludGVyY29ycmVsYXRpb24gYW1vbmcgcHJlZGljdG9ycy4gVGhpcyBhbHNvIGFpbid0IHRoZSBjYXNlIGluIG91ciBtb2RlbHMuDQpgYGB7ciB2aWZ9DQpsYXBwbHkobG9naXN0aWNfbW9kZWxzLCBmdW5jdGlvbih4KSB0cnlDYXRjaCgNCiAgZXhwciA9IHt2aWYoeCl9DQogICxlcnJvciA9IGZ1bmN0aW9uKGUpeydObyBwcmVkaWN0b3JzIGluIHRoZSBtb2RlbCd9DQopKQ0KDQpgYGANCg0KSW4gbW9zdCBjYXNlcyBvbmx5IE1BQ0QgSGlzdG9ncmFtIGlzIGluIGxpbmVhciByZWxhdGlvbnNoaXAgdG8gdGhlIGxvZ2l0IG9mIHRoZSBvdXRjb21lIGluIHRoZSB0cmFuaW5nIHNldC4gSW4gZnVydGhlciByZXNlYXJjaCB3ZSB3aWxsIHRyeSBwb2x5bm9taWFsIHRyYW5zZm9ybWF0aW9uIHRvIHRlY2huaWNhbCBpbmRpY2F0b3JzIGxpa2UgU3RvY2gsIEFEWCwgUlNJLg0KYGBge3IgbGluZWFyaXR5LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KZm9yIChpIGluIDE6Nykgew0KICANCiAgaWYgKCEoKGxvZ2lzdGljX21vZGVsc1tbaV1dJGNvZWZmaWNpZW50c1stMV0gJT4lDQogICAgICAgICBuYW1lcygpICU+JQ0KICAgICAgICAgc3RyX2RldGVjdCgnRU1BLisnLCBuZWdhdGUgPSBUUlVFKSAlPiUNCiAgICAgICAgIHN1bSgpKSA9PSAwKSl7DQogICAgDQogIGNvbnRpbnVvdXNfcHJlZGljdG9ycyA8LSB0cmFpbl9zZXRbW2ldXSAlPiUNCiAgICBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lDQogICAgbXV0YXRlKHByb2IgPSBwcmVkaWN0KGxvZ2lzdGljX21vZGVsc1tbaV1dLCB0cmFpbl9zZXRbW2ldXSwgdHlwZSA9ICdyZXNwb25zZScpKSAlPiUNCiAgICBtdXRhdGUobG9naXQgPSBsb2cocHJvYiAvICgxIC0gcHJvYikpKSAlPiUNCiAgICBzZWxlY3QoLXByb2IpICU+JQ0KICAgIHBpdm90X2xvbmdlcihBRFg6UGFya2luc29uX1ZvbGF0aWxpdHksIG5hbWVzX3RvID0gJ0luZGljYXRvcicsIHZhbHVlc190byA9ICdWYWx1ZScpDQogIA0KICBsaW5lYXJpdHlfcGxvdHMgPC0gZ2dwbG90KGNvbnRpbnVvdXNfcHJlZGljdG9ycywgYWVzKGxvZ2l0LCBWYWx1ZSkpICsNCiAgICBnZW9tX3BvaW50KHNpemUgPSAwLjUpICsNCiAgICBnZW9tX3Ntb290aChtZXRob2QgPSAnbG9lc3MnKSArIA0KICAgIGZhY2V0X3dyYXAodmFycyhJbmRpY2F0b3IpLCBzY2FsZXMgPSAnZnJlZScpICsNCiAgICBsYWJzKHRpdGxlID0gbmFtZXModHJhaW5fc2V0KVtpXSkNCiAgDQogIHByaW50KGxpbmVhcml0eV9wbG90cykNCiAgDQogIH0NCiAgDQp9DQpgYGA=