Quantitative Trading Strategies using quantmod

[This article was first published on Posts on Matthew Smith R Shenanigans, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
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 with sigComparison 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 positive
  • DIn: Directional Index negative
  • DX: Directional Index
  • ADX: Average Directional Index which takes on values between 0 and 100

As defined previously.

ADX Trend Strength

ADX ValueStrength
0-25Weak Trend
26-50Strong Trend
51-75Very Strong
76-100Strongestng

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 the DIn and also when the ADX is greater than 30.
  • We will hold the asset until the DIp is less than the DIn.
  • 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 this buy_here and sell_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.

To leave a comment for the author, please follow the link and comment on their blog: Posts on Matthew Smith R Shenanigans.

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.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)