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")
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"
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:
Date field from the row names for both tablesNA)Year and Month columns from DateYear and MonthDate, SMA_Pt, and Symbol columns of the data frameThe 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"
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:
NA values$5.00$1,000100,00050,000 on trigger dateThe 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"
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.
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