Equity Backtesting

Simple LUXOR Backtest



Project Description

A very common trading strategy is to use momentum as a signal to enter or exit a stock. There are a number of ways to determine momentum and create different buy/sell signals. A popular approach is to use moving averages to establish crossover points between fast signals and slow signals.

To simplify the process, a fast signal will have a shorter moving average time period than a slow signal. The goal is to identify new momentum for a stock, either up or down, as a potential signal to to buy or sell.

One such momentum strategy is called the LUXOR trading strategy which was developed by Omega Research in 2000. This strategy was further documented in the book Trading Systems by Emilio Tomasini and Urban Jaekle, published in 2009.

The strategy mostly deals with options trading and setting long and short positions. In this example, we will use the strategy to set a trigger for a stock purchase and assess the 3 month returns.


This project will be a simple test for momentum using a variation of the LUXOR trading strategy. The two statistics we will compare are the 10 day Simple Moving Average, the fast signal, and the 30 day Simple Moving Average, the slow signal. A buy trigger will be considered to be present when the fast signal is equal to or greater than the slow signal. A 3 month return on the stock will be calculated along with an optimal return which identifies the highest return possible within the 3 month period.

The source data will be the backtest environment created in a previous project, Building the Backtest Environment. Recall that the environment contains historical pricing data from 2007-2016. We also have a symbols table that contains all of the stocks that were successfully loaded in the environment.

As before, we will be using the quantmod package to retrieve data from the backtest environment and perform the SMA generation.


Libraries Required

library(quantmod)   # Quantitative financial strategies
library(dplyr)      # Data manipulation
library(lubridate)  # Date and time processing
library(knitr)      # Dynamic report generation

Working directory

setwd("U:/Equity Backtesting")

Prepare the Environment

We will require the two files previously created in order to perform the modified LUXOR backtest. To access the environment we created we simply need to load the environment file. The symbols table is in the form of an .rds.

Load Backtest Environment

load("Environments/bt_env_2007_2016.Rdata")

Read Backtest Symbols Table

bt_env_symbols <- readRDS("Symbols/bt_env_symbols.rds")

We can check to make sure that the data is loaded correctly and contains expected values. This time we can look for symbols that start with “AM”.

Verify Backtest Environment

ls(bt_env_2007_2016, pattern = "^AM")
##  [1] "AM"    "AMAG"  "AMAT"  "AMBA"  "AMBC"  "AMBR"  "AMC"   "AMCN" 
##  [9] "AMCX"  "AMD"   "AMDA"  "AME"   "AMED"  "AMFW"  "AMG"   "AMGN" 
## [17] "AMH"   "AMID"  "AMKR"  "AMMA"  "AMN"   "AMNB"  "AMOT"  "AMOV" 
## [25] "AMP"   "AMPH"  "AMRB"  "AMRC"  "AMRI"  "AMRK"  "AMRN"  "AMRS" 
## [33] "AMSC"  "AMSF"  "AMSWA" "AMT"   "AMTD"  "AMTX"  "AMWD"  "AMX"  
## [41] "AMZN"

Verify Data in Backtest Environment

kable(head(bt_env_2007_2016$AMZN, 10))
AMZN.Open AMZN.High AMZN.Low AMZN.Close AMZN.Volume AMZN.Adjusted
38.68 39.06 38.05 38.70 12405100 38.70
38.59 39.14 38.26 38.90 6318400 38.90
38.72 38.79 37.60 38.37 6619700 38.37
38.22 38.31 37.17 37.50 6783000 37.50
37.60 38.06 37.34 37.78 5703000 37.78
37.49 37.70 37.07 37.15 6527500 37.15
37.17 38.00 37.17 37.40 6465600 37.40
37.36 38.21 37.27 38.20 4466400 38.20
38.40 38.89 37.97 38.66 5643700 38.66
38.70 39.00 37.78 37.88 5026800 37.88

Verify Backtest Symbols

kable(head(bt_env_symbols, 10))
Symbol Sector Industry Name Exchange
AAAP Health Care Major Pharmaceuticals Advanced Accelerator Applications S.A. NASDAQ
AAL Transportation Air Freight/Delivery Services American Airlines Group, Inc. NASDAQ
AAME Finance Life Insurance Atlantic American Corporation NASDAQ
AAOI Technology Semiconductors Applied Optoelectronics, Inc. NASDAQ
AAON Capital Goods Industrial Machinery/Components AAON, Inc. NASDAQ
AAPC Consumer Services Services-Misc. Amusement & Recreation Atlantic Alliance Partnership Corp. NASDAQ
AAPL Technology Computer Manufacturing Apple Inc. NASDAQ
AAWW Transportation Transportation Services Atlas Air Worldwide Holdings NASDAQ
ABAC Consumer Non-Durables Farming/Seeds/Milling Aoxin Tianli Group, Inc. NASDAQ
ABAX Capital Goods Industrial Machinery/Components ABAXIS, Inc. NASDAQ
## [1] "Number of Backtest Symbols: 4667"

Process for SMA

Since this is a backtest project, we want to define the criteria for the data we are going to be retrieving. When using quantmod in a normal interactive mode, the symbol and historical data to be evaluated will be retrieved with the getSymbol() function. The symbol will be charted and the addSMA() function is called to add the SMA indicator to the chart. In this scenario, we want the SMA to be calculated and then stored for further use.

To accomplish this we will use the SMA() function call to perform the required calculations. The default parameters will be 10 and 30 to retrieve the 10 day and 30 day moving averages.

The next item to define is the crossover for the SMA values. In this example, we will be looking for the point when the 10 day moving average is equal to or greater than the 30 day moving average. We can use a simple comparison check for this and filter accordingly. We can also calculate the percentage value of the 10 day against the 30 day and filter for values of 100% or greater. This may provide insight as to the degree of the breakout with larger percentages indicating a stronger upward momentum.


The first step in the processing is to retrieve the desired symbol data from the backtest environment. This is accomplished using the following:

get(symbol_name, source_environment)

The second step is to approximate, or substitute, any NA values in the time series. This is needed when a time series has a missing set of values for a given date. If the data is not adjusted, the SMA call will fail and we lose that stock as a potential test object.

Next, we invoke the SMA commands and specify which field to perform the test on. The suggested field to use is adjusted closing price.

We do need an error check for the SMA call for the situations in which there isn’t enough data to perform the calculation. So we will use the function try() to validate the call and test whether the call inherits an error.

If the call is successful, the results will be placed in a data frame and then processed. The processing consists of the following:

The data is grouped and filtered to keep only one occurrence of the SMA threshold per month in order to test the SMA as a trigger for initiating a buy condition for the stock. The data frame created is then joined to a master data frame which will contain all occurrences of the SMA threshold being met for the first time in a given month for all stocks.


Create Symbol Array

bt_symbols <- bt_env_symbols$Symbol

SMA Gather Loop

j <- 1

for(i in 1:length(bt_symbols)) {
        
        sym_sum  <- get(bt_symbols[i], envir = bt_env_2007_2016)
        
        sym_sum  = na.approx(sym_sum)
        
        get_try  <- try(SMA(Ad(sym_sum), 30))
        
        if(inherits(get_try, "try-error")) {
                
        } 
        
        else {
         
              sma_10_sum <- data.frame(SMA(Ad(sym_sum), 10))
              colnames(sma_10_sum) <- c("SMA_10")
              sma_10_sum <- sma_10_sum %>%
                            mutate(Date = rownames(.))
              
              
              sma_30_sum <- data.frame(SMA(Ad(sym_sum), 30))
              colnames(sma_30_sum) <- c("SMA_30")
              sma_30_sum <- sma_30_sum %>%
                            mutate(Date = rownames(.))
              
              sma_sum <- left_join(sma_10_sum, sma_30_sum, by = "Date")
              
              sma_sum <- sma_sum %>%
                         mutate(SMA_Pt = round(SMA_10 / SMA_30, 2)) %>%
                         filter(complete.cases(.),
                                SMA_Pt >= 1) %>%
                         mutate(Symbol = bt_symbols[i],
                                Year = year(Date),
                                Mnth = month(Date)) %>%
                         group_by(Year, Mnth) %>%
                         slice(1L) %>%
                         ungroup() %>%
                         select(Symbol, Date, SMA_Pt)
  
              if(j == 1) { sma_frame <- sma_sum } else
                         { sma_frame <- bind_rows(sma_frame, sma_sum) }
              
              j <- j + 1
              
              }
}
Symbol Date SMA_Pt Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAAP 2016-09-30 1.08 38.08 26.76 -0.297 38.96 0.023 10200 136811
AAAP 2016-10-03 1.07 NA NA NA NA NA NA NA
AAL 2007-02-14 1.02 57.79 32.98 -0.429 57.79 0.000 1102500 2541045
AAL 2007-07-09 1.01 35.86 30.57 -0.148 36.00 0.004 796400 2362508
AAL 2007-08-01 1.03 30.44 27.66 -0.091 32.56 0.070 796400 2339397
AAL 2007-09-05 1.01 31.57 18.04 -0.429 31.57 0.000 898700 2768683
AAL 2007-10-12 1.01 30.15 11.89 -0.606 30.15 0.000 885000 3452402
AAL 2008-02-04 1.00 14.46 8.32 -0.425 15.40 0.065 1235700 3239353
AAL 2008-07-23 1.01 5.06 8.49 0.678 9.39 0.856 4478800 13526492
AAL 2008-08-01 1.44 5.16 10.14 0.965 10.14 0.965 4478800 12791969
## [1] "Symbols meeting threshold: 4636"

Process for Returns

The goal for the backtest is to determine if an SMA crossover trigger, indicating a buy signal, is a potential predictor for positive returns based on the expectation of an upward momentum shift in stock price. For this example, we will look at a 3 month return from the date of the trigger.

Using the backtest environment again, we will retrieve the adjusted closing prices beginning with the date of the trigger and ending 3 months after. If the trigger date is such that the calculated end date is greater than 12/30/2016, the last trading day of 2016 and the last date in our backtest environment, the recorded returns will be NA.

During the 3 month testing window, it can be expected that the stock price will have volatility and that the starting and ending prices for the window may not reflect the low and high stock prices observed. To determine the potential for returns, we will also record the highest stock price during the window. It is possible that a 3 month return may show a negative value but there is an opportunity for a positive return within the window.


The sma_frame will be updated with the following:

Add Fields to sma_frame

sma_frame <- sma_frame %>%
             mutate(Trigger_Cl = 0,
                    Period_Cl  = 0,
                    Return_3mt = 0,
                    Period_Hi  = 0,
                    Return_Hi  = 0,
                    Volume_Min = 0,
                    Volume_Avg = 0)
Symbol Date SMA_Pt Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAAP 2016-09-30 1.08 0 0 0 0 0 0 0
AAAP 2016-10-03 1.07 0 0 0 0 0 0 0
AAL 2007-02-14 1.02 0 0 0 0 0 0 0
AAL 2007-07-09 1.01 0 0 0 0 0 0 0
AAL 2007-08-01 1.03 0 0 0 0 0 0 0

Generating the returns requires a processing loop to retrieve the symbol and date of the SMA trigger. A target end date is generated for the 3 month window. Each month, on average, contains 4.33 weeks. So a 3 month testing window will consist of 13 weeks.

If the calculated end date falls outside the range of the backtest environment, we simply set a value of NA to the desired fields. If the date range is within the backtest environment range, we can process the record and retrieve/calculate the statistics. We will be using the adjusted close field for all of the data points.

Generate Returns

for(i in 1:nrow(sma_frame)) {
        
        Symbol   <- sma_frame$Symbol[i]
        Beg_Date <- as.Date(sma_frame$Date[i])
        End_Date <- Beg_Date + dweeks(13)
        
        if(End_Date > "2016-12-31") {
                
                sma_frame$Trigger_Cl[i]     <- NA
                sma_frame$Period_Cl[i]      <- NA
                sma_frame$Return_3mt[i]     <- NA
                sma_frame$Period_Hi[i]      <- NA
                sma_frame$Return_Hi[i]      <- NA
                sma_frame$Volume_Min[i]     <- NA
                sma_frame$Volume_Avg[i]     <- NA
                
        } 
        else {
                
                sym_sum    <- get(Symbol, envir = bt_env_2007_2016)
                
                date_range <- paste(as.character(Beg_Date),"/",as.character(End_Date), sep = "")
                
                sym_sum    <- data.frame(sym_sum[date_range])
                
                colnames(sym_sum) <- c("Open", "High", "Low", "Close", "Volume", "AdjClose")
                
                sma_frame$Trigger_Cl[i] <- as.numeric(sym_sum$AdjClose[1])
                sma_frame$Period_Cl[i]  <- as.numeric(sym_sum$AdjClose[nrow(sym_sum)])
                
                sma_frame$Return_3mt[i] <- round((sma_frame$Period_Cl[i]  - sma_frame$Trigger_Cl[i])
                                                 / sma_frame$Trigger_Cl[i], 3)
                
                sma_frame$Period_Hi[i]  <- max(sym_sum$AdjClose)
                
                sma_frame$Return_Hi[i]  <- round((sma_frame$Period_Hi[i]  - sma_frame$Trigger_Cl[i])
                                                 / sma_frame$Trigger_Cl[i], 3)
                
                sma_frame$Volume_Min[i] <- min(sym_sum$Volume)
                sma_frame$Volume_Avg[i] <- round(mean(sym_sum$Volume),0)

        }
}
Symbol Date SMA_Pt Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAAP 2016-09-30 1.08 38.08 26.76 -0.297 38.96 0.023 10200 136811
AAAP 2016-10-03 1.07 NA NA NA NA NA NA NA
AAL 2007-02-14 1.02 57.79 32.98 -0.429 57.79 0.000 1102500 2541045
AAL 2007-07-09 1.01 35.86 30.57 -0.148 36.00 0.004 796400 2362508
AAL 2007-08-01 1.03 30.44 27.66 -0.091 32.56 0.070 796400 2339397
AAL 2007-09-05 1.01 31.57 18.04 -0.429 31.57 0.000 898700 2768683
AAL 2007-10-12 1.01 30.15 11.89 -0.606 30.15 0.000 885000 3452402
AAL 2008-02-04 1.00 14.46 8.32 -0.425 15.40 0.065 1235700 3239353
AAL 2008-07-23 1.01 5.06 8.49 0.678 9.39 0.856 4478800 13526492
AAL 2008-08-01 1.44 5.16 10.14 0.965 10.14 0.965 4478800 12791969

Once the data has been retrieved and the desired information stored in the data frame we can filter to remove records that do not meet our acceptance criteria. We will define the rejection criteria as the following:

The purpose of the acceptance criteria is to eliminate irregularly traded stocks which might otherwise skew the results.

Clean SMA frame

sma_final <- sma_frame %>%
             filter(complete.cases(.),
                    Trigger_Cl >= 5,
                    Trigger_Cl < 1000,
                    Volume_Min > 50000,
                    Volume_Avg >= 100000)
Symbol Date SMA_Pt Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAL 2007-02-14 1.02 57.79 32.98 -0.429 57.79 0.000 1102500 2541045
AAL 2007-07-09 1.01 35.86 30.57 -0.148 36.00 0.004 796400 2362508
AAL 2007-08-01 1.03 30.44 27.66 -0.091 32.56 0.070 796400 2339397
AAL 2007-09-05 1.01 31.57 18.04 -0.429 31.57 0.000 898700 2768683
AAL 2007-10-12 1.01 30.15 11.89 -0.606 30.15 0.000 885000 3452402
AAL 2008-02-04 1.00 14.46 8.32 -0.425 15.40 0.065 1235700 3239353
AAL 2008-07-23 1.01 5.06 8.49 0.678 9.39 0.856 4478800 13526492
AAL 2008-08-01 1.44 5.16 10.14 0.965 10.14 0.965 4478800 12791969
AAL 2008-09-02 1.12 8.82 6.00 -0.320 10.86 0.231 1936600 9793477
AAL 2008-10-23 1.00 7.14 7.65 0.071 10.86 0.521 1839000 6936735
## [1] "Total Remaining Records: 177239"

Review Results

The first thing we will review is the overall 3 month totals based on the closing prices from the beginning and end of the test window.

3 Month Summary Statistics

pos_ret_3mt <- nrow(sma_final %>%
                    filter(Return_3mt > 0))

neg_ret_3mt <- nrow(sma_final %>%
                    filter(Return_3mt <= 0))

pos_pct_3mt <- round(pos_ret_3mt / nrow(sma_final), 3)

avg_ret_3mt <- round(mean(sma_final$Return_3mt), 3)

avg_pos_ret <- round(mean(sma_final$Return_3mt[sma_final$Return_3mt > 0]), 3)
avg_neg_ret <- round(mean(sma_final$Return_3mt[sma_final$Return_3mt <= 0]), 3)
## [1] "           Total Records: 177239"
## [1] "  Total Positive Returns: 96750"
## [1] "  Total Negative Returns: 80489"
## [1] "Percent Positive Returns: 0.546"
## [1] "          Average Return: 0.015"
## [1] " Average Positive Return: 0.141"
## [1] " Average Negative Return: -0.136"

We see 54.6% of the stocks ended the 3 month testing window with a positive return. The average return, when factoring all stocks, is 1.5%. When looking at just the positive return stocks the average is 14.1%.


Now we can look at whether there was a potential for higher returns by looking at the period high stock price observed within the testing period instead of using the testing window closing price.

3 Month Period Statistics

pos_ret_per <- nrow(sma_final %>%
                    filter(Return_Hi > 0))

neg_ret_per <- nrow(sma_final %>%
                    filter(Return_Hi <= 0))

pos_pct_per <- round(pos_ret_per / nrow(sma_final), 3)

avg_ret_per <- round(mean(sma_final$Return_Hi[sma_final$Return_Hi > 0]), 3)
## [1] "  Total Positive Returns: 164329"
## [1] "  Total Negative Returns: 12910"
## [1] "Percent Positive Returns: 0.927"
## [1] " Average Positive Return: 0.144"

The results indicate there is a larger opportunity for positive returns within the testing window than what is illustrated by looking at the closing price of the testing window. 92.7% of stocks had an opportunity for a positive return within the testing period. The average return is 14.4%, which is slightly higher than the return for stocks with a positive return using the testing window closing price.

These statistics reveal that there is a positive momentum uplift in share price following a crossover of the 10 day and 30 day moving averages. The uplift may be short lived, however. This means that having an appropriate exit strategy, sell condition, is critical to maximizing investment returns.


Of course, using the average return potentially creates a false expectation of returns because large outliers can skew the statistical result. So we can look at the positive returns broken out by percentages of occurence. This helps identify a potential threshold for returns within the 3 month window that could be used as an exit point for the stock. That is, if we employ a strategy that sought a 5% return, or bounceback, from the entry point of the stock, triggered by the SMA, what would be the percentage of investments that would yield the desired return?

Generate Quantiles for Positive Returns

quantile(sma_final$Return_Hi[sma_final$Return_Hi > 0], prob = seq(0, 1, length = 11), type = 5)
##    0%   10%   20%   30%   40%   50%   60%   70%   80%   90%  100% 
## 0.001 0.020 0.039 0.059 0.080 0.104 0.131 0.165 0.215 0.306 6.874

Approximately 75% of the observations in our testing window generated returns of 5% or more. And over 50% generated returns of 10% or more.


Next, we can look at the crossover percentage to see if there is a difference in returns based on the degree of the momentum trigger. First, let’s get a feel of the percentage breakdown of occurrence for the SMA trigger.

Generate Quantiles for SMA Trigger

quantile(sma_final$SMA_Pt[sma_final$Return_Hi > 0], prob = seq(0, 1, length = 11), type = 5)
##   0%  10%  20%  30%  40%  50%  60%  70%  80%  90% 100% 
## 1.00 1.00 1.00 1.00 1.00 1.01 1.02 1.03 1.04 1.06 3.00

The largest group size for the SMA trigger will be 1.00%, meaning that the crossover point of the 10 day SMA and the 30 day SMA was the point where those two values were equal. We can then group the remaining observations by .02% increments above that point to capture the observations in the remaining 3 tiers.

Potential Returns by SMA Trigger

sma_ct_10 <- nrow(sma_final %>%
                  filter(SMA_Pt == 1.00))

sma_ct_12 <- nrow(sma_final %>%
                  filter(SMA_Pt  > 1.00,
                         SMA_Pt <= 1.02))

sma_ct_14 <- nrow(sma_final %>%
                  filter(SMA_Pt  > 1.02,
                         SMA_Pt <= 1.04))

sma_ct_16 <- nrow(sma_final %>%
                  filter(SMA_Pt  > 1.04))

sma_po_10 <- nrow(sma_final %>%
                  filter(SMA_Pt == 1.00,
                         Return_Hi > 0))

sma_po_12 <- nrow(sma_final %>%
                  filter(SMA_Pt  > 1.00,
                         SMA_Pt <= 1.02,
                         Return_Hi > 0))

sma_po_14 <- nrow(sma_final %>%
                  filter(SMA_Pt  > 1.02,
                         SMA_Pt <= 1.04,
                         Return_Hi > 0))

sma_po_16 <- nrow(sma_final %>%
                  filter(SMA_Pt  > 1.04,
                         Return_Hi > 0))

sma_pt_10 <- round(sma_po_10 / sma_ct_10, 3)
sma_pt_12 <- round(sma_po_12 / sma_ct_12, 3)
sma_pt_14 <- round(sma_po_14 / sma_ct_14, 3)
sma_pt_16 <- round(sma_po_16 / sma_ct_16, 3)

sma_re_10 <- round(mean(sma_final$Return_Hi[sma_final$SMA_Pt == 1.00 &
                                            sma_final$Return_Hi > 0]), 3)

sma_re_12 <- round(mean(sma_final$Return_Hi[sma_final$SMA_Pt  > 1.00 &
                                            sma_final$SMA_Pt <= 1.02 &
                                            sma_final$Return_Hi > 0]), 3)

sma_re_14 <- round(mean(sma_final$Return_Hi[sma_final$SMA_Pt  > 1.02 &
                                            sma_final$SMA_Pt <= 1.04 &
                                            sma_final$Return_Hi > 0]), 3)

sma_re_16 <- round(mean(sma_final$Return_Hi[sma_final$SMA_Pt  > 1.04 &
                                            sma_final$Return_Hi > 0]), 3)
## [1] "      Total Count - SMA 1.00: 70779"
## [1] "      Total Count - SMA 1.02: 47258"
## [1] "      Total Count - SMA 1.04: 28272"
## [1] "      Total Count - SMA 1.06: 30930"
## [1] "% Positive Return - SMA 1.00: 0.933"
## [1] "% Positive Return - SMA 1.02: 0.923"
## [1] "% Positive Return - SMA 1.04: 0.919"
## [1] "% Positive Return - SMA 1.06: 0.926"
## [1] "   Average Return - SMA 1.00: 0.139"
## [1] "   Average Return - SMA 1.02: 0.131"
## [1] "   Average Return - SMA 1.04: 0.133"
## [1] "   Average Return - SMA 1.06: 0.186"

Based on the findings, there doesn’t appear to be a significant difference between the degree of momentum exhibited at the crossover point and the related percentage of positive returns or potential yield. The average return is significantly higher for a momentum percentage greater than 1.04%, so it does make sense to explore that range of observations further to determine whether there is a pattern or whether there are some outliers that are skewing the results.


Conclusion

Overall, the LUXOR trading strategy does provide the opportunity to take advantage of momentum shifts in stock prices to realize positive returns regardless of the degree of the momentum at the crossover point. Using this type of trigger is not dependent on whether a stock is undervalued or overvalued, it simply is identifying trading sentiment changes indicating that a stock is moving in a positive direction in relation to its prior position and breaking through a slower trend line.

There are a number of permutations available for this type of test in which the fast and slow signals are adjusted to accomodate a specific investment strategy. For long term growth investors, a common crossover point is the 50 day and 200 day moving averages.

As before, further analysis can be done to look at specific companies, sectors, or industries to determine whether any of those categories have tendencies towards better or worse results.




sessionInfo()
## R version 3.4.0 (2017-04-21)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 14393)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] knitr_1.16      lubridate_1.6.0 dplyr_0.5.0     quantmod_0.4-9 
## [5] TTR_0.23-1      xts_0.9-7       zoo_1.8-0      
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_0.12.11     magrittr_1.5     lattice_0.20-35  R6_2.2.1        
##  [5] rlang_0.1.1      stringr_1.2.0    highr_0.6        tools_3.4.0     
##  [9] grid_3.4.0       DBI_0.6-1        htmltools_0.3.6  lazyeval_0.2.0  
## [13] yaml_2.1.14      rprojroot_1.2    digest_0.6.12    assertthat_0.2.0
## [17] tibble_1.3.3     evaluate_0.10    rmarkdown_1.5    stringi_1.1.5   
## [21] compiler_3.4.0   backports_1.1.0