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
sigCrossover
function only returns the first occurence of the relationship switching from false to true. Replacing withsigComparison
here 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
sigCrossover
function 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
DIp
is greater than theDIn
and also when theADX
is greater than 30. - We will hold the asset until the
DIp
is 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_here
andsell_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.