Quantitative Trading Strategies using quantmod
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Strategies
Introduction:
Note: This post is unfinished.
This post goes through a number of the technical trading functions in the TTR package here. I add definitions and show examples of how the functions work.
library(dplyr) library(quantmod) library(tidyquant) library(TTR) library(timetk) library(tidyr) library(ggplot2) library(directlabels) library(data.table) library(quantstrat) library(purrr) library(quantstrat)
We can download some data using the quantmod package which stores the different assets into our Global Environment.
start_date <- "2013-01-01"
end_date <- "2017-08-31"
symbols <- c("AAPL", "AMD", "ADI", "ABBV", "A", "APD", "AA", "CF", "NVDA", "HOG", "WMT", "AMZN"
#,"MSFT", "F", "INTC", "ADBE", "AMG", "AKAM", "ALB", "ALK"
)
getSymbols(symbols,
from = start_date,
to = end_date)
## [1] "AAPL" "AMD" "ADI" "ABBV" "A" "APD" "AA" "CF" "NVDA" "HOG"
## [11] "WMT" "AMZN"
The first few observations for AAPL looks like:
| AAPL.Open | AAPL.High | AAPL.Low | AAPL.Close | AAPL.Volume | AAPL.Adjusted | |
|---|---|---|---|---|---|---|
| 2013-01-02 | 79.11714 | 79.28571 | 77.37572 | 78.43285 | 140129500 | 68.85055 |
| 2013-01-03 | 78.26857 | 78.52428 | 77.28571 | 77.44286 | 88241300 | 67.98149 |
| 2013-01-04 | 76.71000 | 76.94714 | 75.11857 | 75.28571 | 148583400 | 66.08789 |
| 2013-01-07 | 74.57143 | 75.61429 | 73.60000 | 74.84286 | 121039100 | 65.69916 |
| 2013-01-08 | 75.60143 | 75.98428 | 74.46429 | 75.04429 | 114676800 | 65.87595 |
| 2013-01-09 | 74.64286 | 75.00143 | 73.71286 | 73.87143 | 101901100 | 64.84639 |
Bollinger Bands Strategy
The idea of this strategy is
Definition: A Bollinger Band consists of 3 lines. A simple moving average (SMA) and two additional lines plotted 2 standard deviations above and below the SMA. The standard deviation measures a stocks volatility and so when the markers are more volatile then the Bollinger Bands become wider. When the market is flat the Bollinger Banks contract.
chartSeries(NVDA,
theme = chartTheme("white"),
TA = c(addBBands(n = 20, sd = 2, ma = "SMA", draw = 'bands', on = -1)))

chartSeries(AMZN,
theme = chartTheme("white"),
TA = c(addBBands(n = 20, sd = 2, ma = "SMA", draw = 'bands', on = -1)))

We set the initial quantstrat parameters and initialise the strategy.
rm.strat("BollingerBandsStrat")
currency('USD')
## [1] "USD"
stock(symbols, currency = 'USD', multiplier = 1)
## [1] "AAPL" "AMD" "ADI" "ABBV" "A" "APD" "AA" "CF" "NVDA" "HOG"
## [11] "WMT" "AMZN"
init_date = as.Date(start_date) - 1
init_equity = 1000
portfolio.st <- account.st <- 'BollingerBandsStrat'
initPortf(portfolio.st,
symbols = symbols,
initDate = init_date)
## [1] "BollingerBandsStrat"
initAcct(account.st,
portfolios = 'BollingerBandsStrat',
initDate = init_date)
## [1] "BollingerBandsStrat"
initOrders(portfolio = portfolio.st,
initDate = init_date)
BBands_Strategy <- strategy("BollingerBandsStrat")
Adding indicators:
We add the Bollinger Bands indicator from the TTR package to the strategy. It takes a Simple Moving Average (SMA) and the High Low Close data from each of our stocks, add the indicator to the defined BBands_Strategy and creates a new column in our data alled BollingerBands_Label.
# Add indicators
BBands_Strategy <- add.indicator(
strategy = BBands_Strategy,
name = "BBands",
arguments = list(
HLC = quote(HLC(mktdata)),
maType = 'SMA'),
label = 'BollingerBands_Label')
Adding signals:
The first signal states that when the close is greater than the upper Bollinger Band, add a value of 1 into a column called Close.gt.LowerBBand. The
sigCrossoverfunction only returns the first occurence of the relationship switching from false to true. Replacing withsigComparisonhere would return all occurences of the relationship being greater than the upper band.The second signal states that when the close is less than the lower Bollinger Band add a value of 1 into a column called Close.lt.LowerBBand. Again using the
sigCrossoverfunction to only return the first occurence.The third signal
The below code creates 3 new columns in our mktdata. dn.BollingerBands_Label, mavg.BollingerBands_Label, up.BollingerBands_Label. Which are the lower, middle and upper Bollinger Band values. It also creates the following: Close.gt.UpperBBand, Close.lt.LowerBBand, Cross.MiddleBBand. Which takes on the values according to whether the `sigCrossover’ has occured.
# Add Signals
BBands_Strategy <- add.signal(
BBands_Strategy,
name = "sigCrossover",
arguments = list(
columns = c("Close","up"),
relationship = "gt"),
label = "Close.gt.UpperBBand")
BBands_Strategy <- add.signal(
BBands_Strategy,
name = "sigCrossover",
arguments = list(
columns = c("Close","dn"),
relationship = "lt"),
label = "Close.lt.LowerBBand")
BBands_Strategy <- add.signal(
BBands_Strategy,
name = "sigCrossover",
arguments = list(
columns = c("High","Low","mavg"),
relationship = "op"),
label = "Cross.MiddleBBand")
The mktdata (which gets presented after running applyStrategy and updatePortf) would look like:
| index | WMT.Open | WMT.High | WMT.Low | WMT.Close | WMT.Volume | WMT.Adjusted | dn.BollingerBands_Label | mavg.BollingerBands_Label | up.BollingerBands_Label | pctB.BollingerBands_Label | Close.gt.UpperBBand | Close.lt.LowerBBand | Cross.MiddleBBand |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2013-02-13 | 71.29 | 71.70 | 71.21 | 71.39 | 3968600 | 59.98333 | 68.71632 | 70.09250 | 71.46868 | 0.9871567 | NA | NA | NA |
| 2013-02-14 | 71.10 | 71.23 | 70.75 | 70.82 | 6821000 | 59.50441 | 68.82043 | 70.18133 | 71.54224 | 0.7762866 | NA | NA | NA |
| 2013-02-15 | 69.54 | 70.00 | 68.13 | 69.30 | 25683700 | 58.22726 | 68.84929 | 70.19050 | 71.53171 | 0.1096177 | NA | NA | 1 |
| 2013-02-19 | 69.19 | 69.45 | 68.54 | 68.76 | 14682300 | 57.77356 | 68.82222 | 70.18217 | 71.54211 | 0.0347239 | NA | 1 | NA |
| 2013-02-20 | 68.72 | 69.85 | 68.30 | 69.21 | 11973600 | 58.15165 | 68.78472 | 70.16833 | 71.55195 | 0.1211618 | NA | NA | NA |
| 2013-02-21 | 70.00 | 71.47 | 69.72 | 70.26 | 20413100 | 59.03389 | 68.86119 | 70.22117 | 71.58114 | 0.5963869 | NA | NA | 1 |
| 2013-02-22 | 70.22 | 70.54 | 69.89 | 70.40 | 9165200 | 59.15152 | 68.90249 | 70.24950 | 71.59651 | 0.5100843 | NA | NA | NA |
| 2013-02-25 | 70.50 | 71.34 | 70.44 | 70.44 | 11817600 | 59.18514 | 69.00983 | 70.32100 | 71.63217 | 0.6597812 | NA | NA | NA |
| 2013-02-26 | 70.69 | 71.39 | 70.61 | 71.11 | 10557600 | 59.74808 | 69.14416 | 70.41200 | 71.67984 | 0.7463509 | NA | NA | NA |
| 2013-02-27 | 70.92 | 71.96 | 70.58 | 71.66 | 8819000 | 60.21020 | 69.20490 | 70.49383 | 71.78277 | 0.8515172 | NA | NA | NA |
| 2013-02-28 | 71.59 | 71.88 | 70.78 | 70.78 | 18883600 | 59.47081 | 69.28076 | 70.56167 | 71.84258 | 0.7283528 | NA | NA | NA |
| 2013-03-01 | 70.78 | 71.90 | 70.78 | 71.74 | 8902300 | 60.27741 | 69.33128 | 70.63400 | 71.93672 | 0.8221462 | NA | NA | NA |
| 2013-03-04 | 71.52 | 73.26 | 71.51 | 73.26 | 10567200 | 61.55456 | 69.27376 | 70.75150 | 72.22924 | 1.1513879 | 1 | NA | NA |
| 2013-03-05 | 73.47 | 74.04 | 72.99 | 73.72 | 9142000 | 61.94105 | 69.24347 | 70.95300 | 72.66253 | 1.2693145 | NA | NA | NA |
| 2013-03-06 | 73.75 | 74.13 | 73.25 | 73.38 | 7149600 | 61.65540 | 69.17483 | 71.10567 | 73.03650 | 1.1424682 | NA | NA | NA |
| 2013-03-07 | 73.49 | 73.61 | 73.20 | 73.32 | 6680000 | 61.60497 | 69.14018 | 71.22567 | 73.31116 | 1.0157056 | NA | NA | NA |
In row 4 of the below table, the close is 68.76 which is less than the lower Bollinger Band dn.BollingerBands_Label column 68.822 and a 1 is indicated in the Close.lt.LowerBBand column. In row 13 of the same data, there is a 1 in the column Close.gt.UpperBBand. The close is 73.26 and the up.BollingerBands_Label is 72.23 so the close is gt then Upper Bollinger Band.
Add the rules
- When the close is greater than the upper Bollinger Band we go short by a quantity of 100 at the market price
- When the close is lower than the lower Bollinger Band we go long by a quantity of 100 at the market price.
- When the Close crosses the middle Bollinger Band we exit all our positions.
# Add rules
BBands_Strategy <- add.rule(
BBands_Strategy,
name = 'ruleSignal',
arguments = list(
sigcol = "Close.gt.UpperBBand",
sigval = TRUE,
orderqty = -100,
ordertype = 'market',
orderside = NULL,
threshold = NULL),
type = 'enter')
BBands_Strategy <- add.rule(
BBands_Strategy,
name = 'ruleSignal',
arguments = list(
sigcol = "Close.lt.LowerBBand",
sigval = TRUE,
orderqty = 100,
ordertype = 'market',
orderside = NULL,
threshold = NULL),
type = 'enter')
BBands_Strategy <- add.rule(
BBands_Strategy,
name = 'ruleSignal',
arguments = list(
sigcol = "Cross.MiddleBBand",
sigval = TRUE,
orderqty = 'all',
ordertype = 'market',
orderside = NULL,
threshold = NULL),
type = 'exit')
Apply the strategy and update the portfolio
out <- applyStrategy(
strategy = BBands_Strategy,
portfolios = 'BollingerBandsStrat',
parameters = list(
sd = 1.6, # number of standard deviations
n = 20) # MA periods
)
updatePortf(Portfolio = 'BollingerBandsStrat', Dates = paste('::',as.Date(Sys.time()),sep=''))
Find the best performance stocks:
tradeStats(portfolio.st) %>%
tibble::rownames_to_column("ticker") %>%
t() %>%
kable() %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
| ticker | A | AA | AAPL | ABBV | ADI | AMD | AMZN | APD | CF | HOG | NVDA | WMT |
| Portfolio | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat | BollingerBandsStrat |
| Symbol | A | AA | AAPL | ABBV | ADI | AMD | AMZN | APD | CF | HOG | NVDA | WMT |
| Num.Txns | 181 | 188 | 163 | 182 | 172 | 165 | 176 | 171 | 175 | 182 | 201 | 184 |
| Num.Trades | 70 | 68 | 57 | 63 | 67 | 64 | 62 | 71 | 71 | 68 | 73 | 66 |
| Net.Trading.PL | 3302.7855 | -663.6981 | -5547.4283 | -288.0025 | 1756.0009 | 966.0000 | 34942.0472 | 8510.1655 | 7601.2010 | 4524.0020 | -2515.9992 | 3987.0002 |
| Avg.Trade.PL | 48.525511 | -6.774974 | -97.323304 | -2.285762 | 26.208969 | 16.046875 | 529.436174 | 119.861486 | 107.059169 | 66.529441 | -34.465742 | 60.545452 |
| Med.Trade.PL | 66.88130 | 76.89585 | 159.99990 | 77.00000 | 84.99980 | 18.50000 | 1180.00030 | 148.01180 | 129.00010 | 122.99995 | 35.00000 | 42.00020 |
| Largest.Winner | 711.7301 | 995.0003 | 965.1434 | 920.9999 | 1265.0001 | 152.0000 | 9117.9932 | 1882.5164 | 939.4002 | 1174.0003 | 1655.0003 | 1356.0006 |
| Largest.Loser | -528.0000 | -3104.6764 | -11317.0036 | -1817.0016 | -2617.0005 | -414.0000 | -13718.9819 | -1626.9989 | -903.9999 | -1227.0002 | -4844.0012 | -1395.0009 |
| Gross.Profits | 6415.110 | 8216.008 | 15231.716 | 8640.999 | 9168.000 | 2080.000 | 96296.006 | 16046.256 | 11782.200 | 11733.502 | 11577.998 | 9388.999 |
| Gross.Losses | -3018.324 | -8676.707 | -20779.144 | -8785.002 | -7411.999 | -1053.000 | -63470.963 | -7536.091 | -4180.999 | -7209.500 | -14093.998 | -5392.999 |
| Std.Dev.Trade.PL | 175.69143 | 525.90402 | 1607.09458 | 413.50580 | 444.63931 | 80.12096 | 3565.68276 | 463.03163 | 276.80923 | 404.27517 | 794.13687 | 370.13593 |
| Std.Err.Trade.PL | 20.99914 | 63.77523 | 212.86488 | 52.09683 | 54.32134 | 10.01512 | 452.84216 | 54.95174 | 32.85121 | 49.02557 | 92.94669 | 45.56058 |
| Percent.Positive | 65.71429 | 72.05882 | 68.42105 | 66.66667 | 71.64179 | 78.12500 | 67.74194 | 67.60563 | 76.05634 | 64.70588 | 61.64384 | 72.72727 |
| Percent.Negative | 34.28571 | 27.94118 | 31.57895 | 33.33333 | 28.35821 | 20.31250 | 32.25806 | 32.39437 | 23.94366 | 35.29412 | 38.35616 | 25.75758 |
| Profit.Factor | 2.1253880 | 0.9469040 | 0.7330290 | 0.9836081 | 1.2369133 | 1.9753086 | 1.5171663 | 2.1292547 | 2.8180346 | 1.6275056 | 0.8214843 | 1.7409606 |
| Avg.Win.Trade | 139.4589 | 167.6736 | 390.5568 | 205.7381 | 191.0000 | 41.6000 | 2292.7620 | 334.2970 | 218.1889 | 266.6705 | 257.2889 | 195.6041 |
| Med.Win.Trade | 111.23030 | 93.71700 | 410.00060 | 189.00015 | 132.49990 | 31.00000 | 1834.00115 | 223.84460 | 179.20000 | 180.99995 | 91.99980 | 98.99985 |
| Avg.Losing.Trade | -125.7635 | -456.6688 | -1154.3969 | -418.3334 | -390.1052 | -81.0000 | -3173.5481 | -327.6561 | -245.9411 | -300.3958 | -503.3571 | -317.2352 |
| Med.Losing.Trade | -93.50035 | -129.76180 | -400.99975 | -288.99940 | -142.00030 | -33.00000 | -1961.00005 | -205.00030 | -166.99940 | -155.50005 | -138.99980 | -136.00040 |
| Avg.Daily.PL | 48.525511 | -6.774974 | -97.323304 | -2.285762 | 26.208969 | 16.046875 | 529.436174 | 119.861486 | 107.059169 | 66.529441 | -34.465742 | 60.545452 |
| Med.Daily.PL | 66.88130 | 76.89585 | 159.99990 | 77.00000 | 84.99980 | 18.50000 | 1180.00030 | 148.01180 | 129.00010 | 122.99995 | 35.00000 | 42.00020 |
| Std.Dev.Daily.PL | 175.69143 | 525.90402 | 1607.09458 | 413.50580 | 444.63931 | 80.12096 | 3565.68276 | 463.03163 | 276.80923 | 404.27517 | 794.13687 | 370.13593 |
| Std.Err.Daily.PL | 20.99914 | 63.77523 | 212.86488 | 52.09683 | 54.32134 | 10.01512 | 452.84216 | 54.95174 | 32.85121 | 49.02557 | 92.94669 | 45.56058 |
| Ann.Sharpe | 4.38449749 | -0.20450380 | -0.96133704 | -0.08775051 | 0.93571231 | 3.17939560 | 2.35706295 | 4.10931348 | 6.13964943 | 2.61238437 | -0.68895769 | 2.59669262 |
| Max.Drawdown | -1221.745 | -4652.209 | -12870.005 | -4976.003 | -3965.999 | -669.000 | -48292.017 | -2705.000 | -1757.999 | -3237.000 | -14917.000 | -3010.999 |
| Profit.To.Max.Draw | 2.70333546 | -0.14266301 | -0.43103544 | -0.05787828 | 0.44276383 | 1.44394619 | 0.72355742 | 3.14608694 | 4.32378029 | 1.39759098 | -0.16866657 | 1.32414540 |
| Avg.WinLoss.Ratio | 1.1088981 | 0.3671669 | 0.3383211 | 0.4918040 | 0.4896115 | 0.5135802 | 0.7224601 | 1.0202679 | 0.8871590 | 0.8877303 | 0.5111458 | 0.6165902 |
| Med.WinLoss.Ratio | 1.1896244 | 0.7222233 | 1.0224460 | 0.6539811 | 0.9330959 | 0.9393939 | 0.9352377 | 1.0919233 | 1.0730577 | 1.1639864 | 0.6618700 | 0.7279379 |
| Max.Equity | 3659.7860 | 1090.9615 | 6834.5749 | 607.9984 | 2919.9995 | 1058.0000 | 37225.0306 | 9494.1666 | 7601.2010 | 6193.0030 | 2511.9993 | 4036.9994 |
| Min.Equity | -555.0795 | -3561.2473 | -6035.4303 | -4687.0026 | -1045.9995 | -101.0000 | -11066.9863 | -848.2877 | -1744.5987 | -847.9955 | -12405.0003 | -2489.9988 |
| End.Equity | 3302.7855 | -663.6981 | -5547.4283 | -288.0025 | 1756.0009 | 966.0000 | 34942.0472 | 8510.1655 | 7601.2010 | 4524.0020 | -2515.9992 | 3987.0002 |
Performance plots and trade statistics
for(sym in symbols){
chart.Posn(Portfolio = 'BollingerBandsStrat', Symbol = sym)
plot(add_BBands(on = 1, sd = 1.6, n = 20))
perf <- tradeStats(portfolio.st) %>%
tibble::rownames_to_column("ticker") %>%
filter(ticker == sym) %>%
t() %>%
kable() %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
}
























Zooming in one some of the charts, it’s possible to see when we entered and exited each trade.
Here - everything works. chart.posn plots each plot in sequence find a way to plot it in one instance.
chart.Posn(Portfolio = 'BollingerBandsStrat', Symbol = "AMD")

plot(add_BBands(on = 1, sd = 1.6, n = 20))

zoom_Chart('2017-01::2017-03')

chart.Posn(Portfolio = 'BollingerBandsStrat', Symbol = "WMT")

plot(add_BBands(on = 1, sd = 1.6, n = 20))

zoom_Chart('2017-01::2017-03')

chart.Posn(Portfolio = 'BollingerBandsStrat', Symbol = "AMZN")

plot(add_BBands(on = 1, sd = 1.6, n = 20))

zoom_Chart('2017-01::2017-03')

ADX: Directional Movement Index
The Directional Movement Index (DMI) Wilder (1978) tries to identify the direction an asset is moving towards through comparing previous highs and lows. It trys to determine if an asset is trending and to measure the strength of that trend. It has four components:
Positive Direction Indicator (+DI): Computes the difference between todays high and the previous days high. - Helps identify the presence of an uptrend.
Negative Direction Indicator (-DI): Computes the difference between todays low and the previous days low. - Helps identify the presence of a downtrend.
Average Directional Index (ADX): A smoothed average of the difference of the +DI and -DI.
Average Directional Rating (ADXR): Simple average of todays ADX and the ADX from 14 or n periods previously.
The ADX is relative to its own asset price. An ADX of 60 for \(Asset_{i}\) is different to an ADX for \(Asset_{j}\).
Using the TTR package along with the tidyquant package in R we can calculate the ADX using:
# adx_calc <- asset_prices %>%
# group_by(symbol) %>%
# tq_mutate(
# select = c("high", "low", "close"),
# mutate_fun = ADX,
# n = 14,
# maType = "SMA"
# )
# adx_calc %>%
# group_by(symbol) %>%
# drop_na() %>%
# slice(1:5)
The first few observations of each of the assets looks like:
Where the newely created columns are defined as:
DIp: Directional Index positiveDIn: Directional Index negativeDX: Directional IndexADX: Average Directional Index which takes on values between 0 and 100
As defined previously.
ADX Trend Strength
| ADX Value | Strength |
|---|---|
| 0-25 | Weak Trend |
| 26-50 | Strong Trend |
| 51-75 | Very Strong |
| 76-100 | Strongestng |
We can plot the ADX using the following, where I first put the Directional Index columns to longer format using the pivot_longer function and then take a random sample of the grouped data using a combination of group_by, nest and sample_n. Finally I filter the data between a period of 3 months and use ggplot to plot the data.
# adx_calc %>%
# pivot_longer(
# cols = c("DIp", "DIn", "ADX"),
# names_to = "Indicator",
# values_to = "Indicator_value"
# ) %>%
# group_by(symbol) %>%
# nest() %>%
# ungroup() %>%
# sample_n(2) %>%
# unnest() %>%
# filter(date > "2014-09-01" & date < "2015-01-01") %>%
# ggplot(aes(x = date, y = Indicator_value, color = Indicator)) +
# geom_line() +
# geom_dl(aes(label = Indicator), method = list(dl.combine("first.points", "last.points"))) +
# facet_wrap(~symbol, scales = "free") +
# theme_tq()
We can create a simple trading strategy based on the ADX by:
- Adding a buy signal when the
DIpis greater than theDInand also when theADXis greater than 30. - We will hold the asset until the
DIpis less than theDIn. - This creates multiple consecutive rows of
1’s followed by consecutive rows of-1’s. Since we cannot “buy”, “buy” and “buy” and then followed by “sell”, “sell” and “sell” we will only take the first occurrence of the first “buy” and the first “sell”. I call thisbuy_hereandsell_here.
# adx_signals <- adx_calc %>% # mutate( # adx_signal_buy = case_when( # DIp > DIn & ADX > 30 ~ 1, # TRUE ~ 0 # ), # adx_signal_buy_hold_until = case_when( # DIp < DIn ~ -1, # TRUE ~ 0 # ) # ) %>% # group_by(grp = rleid(adx_signal_buy)) %>% # mutate(buy_here = +(all(adx_signal_buy == 1) * row_number() == 1)) %>% # ungroup %>% # group_by(grp = rleid(adx_signal_buy_hold_until)) %>% # mutate(sell_here = -1 *(all(adx_signal_buy_hold_until == -1) * row_number() == 1)) %>% # ungroup %>% # select(-grp)
Wilder, J Welles. 1978. New Concepts in Technical Trading Systems. Trend Research.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.