Quantstrat Case Study - Multiple Symbol Portfolio

One of the main advantages of quantstrat package is that we can backtest strategies with multiple symbols as fast as with one symbol. The package provides fast computations for multiple symbols that allow analysts to get insights of strategies in an efficient approach. 

In this case study we will build a strategy that works with 9 ETFs which are described below, and its signal is based on the three Exponential Moving Average (EMA) Crossover in order to run the trend. There are three EMAs that generate a crossover signal to buy when EMA4 > EMA9 > EMA18 and exit positions when EMA4 < EMA9 < EMA18.

In the performance analysis we will get the same info as we did before, but with the breakdown by symbol. This leads to an analysis by symbols where we can get an overview of the worst and best performing symbols. The steps are the same as before: Objects Initialization, Add Indicators, Add Signal, Add Rules, Apply the Strategy to the Portfolio, and Get Metrics. 

Before proceeding, start a new R session and ensure that the quantstrat and ggplot2 libraries is loaded.

Note: For convenience, the complete R file is attached below.

Downloads

Initialize Currency and Instruments

  • Initialize current (USD)
  • Create symbol variable (we will use the Apple stock - AAPL)
  • Set timezone
  • Define the stock metadata using the stock() function
# Make sure quantstrat, dplyr and ggplot2 are loaded before starting. 
# library(quantstrat)
# library(dplyr)
# library(ggplot2)

# User search() to check what's loaded

# Initialize the currency. The accounting analytics back-end systems need to know what currency the prices are listed in

currency('USD') 

# Set the time to UTC, since that’s the time zone for a “Date” class object. Some orders need the time to be in UTC

Sys.setenv(TZ="UTC")

# Create a Symbols vector

Symbols <- c('SPY',#
             'IWM', # iShares Russell 2000 ETF
             'QQQ', # PowerShares QQQ Trust, Series 1
             'IYR', # iShares U.S. Real Estate ETF
             'TLT', # iShares 20+ Year Treasury Bond ETF
             'EEM', # iShares MSCI Emerging Markets ETF
             'EFA', # iShares MSCI EAFE ETF
             'GLD', # SPDR Gold Trust
             'DBC' # Invesco DB Commodity Index 
)

# Define the stock metadata

stock(Symbols,currency='USD',multiplier=1)

Define Dates and Load Historical Data

# The initDate is used in the initialization of the portfolio and orders objects, and should be less than the startDate, which is the date of the data.

initDate <- '2013-01-10'

startDate <- '2014-07-01'

startEquity <- 100000

endDate <- '2019-08-14'

tradeSize <- startEquity/length(symbols)

# Fetch the data from tiingo api. This command require an api key that can be acquired in the tiingo site

token <- token <- 'your api key'
getSymbols(Symbols,from=startDate,to=endDate,adjust=TRUE,src='tiingo',api.key=token)

Initialize Portfolio, Account and Order Objects

# Name the strategy, portfolio and account with the same name

portName <- stratName <- acctName <- 'multipleSymbols'

# Remove objects associated with a strategy

rm.strat(stratName)

# Initialization of the Portfolio, Account and Orders objects with their required parameters. The startEquity value in the initEq parameter on the account initialization is added in order to compute returns.

initPortf(portName,symbols=Symbols,initDate = startDate)

initAcct(acctName,portfolios = portName,initDate = startDate,initEq = startEquity)

initOrders(portfolio=portName,startDate=startDate)

Initialize and Store the Strategy

# Initialize and store the strategy 
 
strategy(stratName,store=TRUE)

Add Indicators

# Add Indicators

add.indicator(strategy=stratName, name="EMA", arguments=list(x = quote(Cl(mktdata)), n=4), label="EMA4")

add.indicator(strategy=stratName, name="EMA", arguments=list(x = quote(Cl(mktdata)), n=9), label="EMA9")

add.indicator(strategy=stratName, name="EMA", arguments=list(x = quote(Cl(mktdata)), n=18), label="EMA18")

# Test the indicators while building the strategy.
# This allows a user to see how the indicators will be appended to the mktdata object in the backtest. If the call to applyIndicators fails, it means that there most likely is an issue with labeling (column naming).

test <- applyIndicators(stratName, mktdata=OHLC(QQQ))

tail(test,10)

           QQQ.Open QQQ.High QQQ.Low QQQ.Close EMA.EMA4 EMA.EMA9 EMA.EMA18
2019-08-01   191.51   194.98  189.23    190.15 191.8198 192.5779  192.0640
2019-08-02   188.82   188.99  186.21    187.35 190.0319 191.5323  191.5678
2019-08-05   183.51   183.51  179.20    180.73 186.3111 189.3718  190.4270
2019-08-06   182.38   183.80  181.07    183.26 185.0907 188.1495  189.6726
2019-08-07   181.32   184.51  179.89    184.25 184.7544 187.3696  189.1018
2019-08-08   185.15   188.32  184.57    188.26 186.1566 187.5477  189.0132
2019-08-09   187.32   188.00  185.03    186.49 186.2900 187.3361  188.7476
2019-08-12   185.34   185.90  183.50    184.35 185.5140 186.7389  188.2847
2019-08-13   184.27   189.68  184.02    188.39 186.6644 187.0691  188.2958
2019-08-14   185.31   185.95  182.42    182.76 185.1026 186.2073  187.7130

Add Signals

# Add Signals
 
add.signal(strategy=stratName, name="sigCrossover", arguments = list(columns=c("EMA.EMA4","EMA.EMA9"),relationship="gt"),label="Up4X9")
 
add.signal(strategy=stratName, name="sigCrossover", arguments = list(columns=c("EMA.EMA9","EMA.EMA18"),relationship="gt"),label="Up9X18")
 
add.signal(strategy=stratName, name="sigCrossover", arguments = list(columns=c("EMA.EMA4","EMA.EMA9"),relationship="lt"),label="Down4X9")
 
add.signal(strategy=stratName, name="sigCrossover",  arguments = list(columns=c("EMA.EMA9","EMA.EMA18"),relationship="lt"),label="Down9X18")
 
# We take the columns Up4X9 and Up9X18 to create the longEntry column in the mktdata object, which is True when EMA4 > EMA9 and EMA9 > EMA 18
 
add.signal(strategy=stratName, name="sigFormula",  arguments = list(columns=c("Up4X9","Up9X18"),formula="Up4X9 & Up9X18"),label="longEntry")
 
# Define the long exit
 
add.signal(strategy=stratName, name="sigFormula",  arguments = list(columns=c("Down4X9","Down9X18"),formula=("Down4X9 & Down9X18")),label="longExit")
 

Add Rules

# Rule to enter in a long position(type="enter")

add.rule(strategy = stratName,name='ruleSignal', 
         arguments = list(sigcol="longEntry",sigval=TRUE,
                          orderqty= 100,  ordertype="market", orderside="long",
                          replace=FALSE, tradeSize = tradeSize, maxSize=tradeSize,threshold=NULL),
         label="longEnter", enabled=TRUE,  type="enter")

# Rule to exit (type="exit") of a long position. When the column shortEntry is True, we should exit our long position

add.rule(strategy = stratName,name="ruleSignal",
         arguments = list(sigcol="longExit", sigval=TRUE, 
                          orderqty= "all", ordertype="market", replace=FALSE,
                          orderside="long"),label="longExit", enabled=TRUE, type="exit")
                          

Apply Strategy

We will now apply the strategy to portfolio and then update the portfolio, account and equity.

# Apply strategy to the portfolio
 
t1 <- Sys.time()
 
results <- applyStrategy(stratName,portfolios = portName,symbols = Symbols)
 
t2 <- Sys.time()
 
print(t2-t1)
 
# Set up analytics. Update portfolio, account and equity
 
updatePortf(portName)
 
dateRange <- time(getPortfolio(portName)$summary)[-1]
 
updateAcct(portName,dateRange)
 
updateEndEq(acctName)

Plot Accumulated Equity Returns

# Plot the equity curve of the strategy
 
final_acct <- getAccount(acctName)
 
options(width=70)
 
plot(final_acct$summary$End.Eq, main = "Multiple Symbols Portfolio Equity")

Multiple Symbols Portfolio Equity Triple EMA Crossover Strategy

Portfolio Statistics

tStats <- tradeStats(Portfolios = portName, use="trades", inclZeroDays=FALSE)
 
# Get performance information for each symbol
 
tab.profit <- tStats %>% 
  select(Net.Trading.PL, Gross.Profits, Gross.Losses, Profit.Factor)
 
# t: transpose
 
t(tab.profit)
 
                    DBC          EEM         EFA          GLD          IWM        IYR
Net.Trading.PL 266.4998  3713.685370 -629.807645   211.000000  9403.746347 4037.07263
Gross.Profits  266.4998  4862.953129  125.911927  2752.000000 11603.902733 4354.18955
Gross.Losses     0.0000 -1149.267759 -755.719572 -2541.000000 -2200.156385 -317.11692
Profit.Factor        NA     4.231349    0.166612     1.083038     5.274126   13.73055
                      QQQ       SPY         TLT
Net.Trading.PL 6569.62866 -708.0645 3222.506379
Gross.Profits  6890.56991    0.0000 3799.799792
Gross.Losses   -320.94126 -708.0645 -577.293413
Profit.Factor    21.46988    0.0000    6.582094
 
# Average trade profit per symbol
 
tab.wins <- tStats %>% 
  select(Avg.Trade.PL, Avg.Win.Trade, Avg.Losing.Trade, Avg.WinLoss.Ratio)
 
(t(tab.wins))
 
                       DBC         EEM         EFA         GLD         IWM       IYR
Avg.Trade.PL      266.4998  742.737074 -157.451911   52.750000  3134.58212  807.4145
Avg.Win.Trade     266.4998 2431.476565   62.955964 2752.000000 11603.90273 1451.3965
Avg.Losing.Trade       Nan -383.089253 -377.859786 -847.000000 -1100.07819 -158.5585
Avg.WinLoss.Ratio       NA    6.347024    0.166612    3.249115    10.54825    9.1537
                    
                     QQQ       SPY         TLT
Avg.Trade.PL      1313.92573 -236.0215  537.084397
Avg.Win.Trade     1722.64248       Nan  949.949948
Avg.Losing.Trade  -320.94126 -236.0215 -288.646706
Avg.WinLoss.Ratio    5.36747       NaN    3.291047
 

Cumulative Returns

# Returns on Equity(ROE) by Symbol
 
rets <- PortfReturns(Account = acctName)
 
rownames(rets) <- NULL
 
# Chart the cumulative returns for all the symbols
 
charts.PerformanceSummary(rets, colorset = bluefocus)
 

Cumulative Returns by ETF’s Triple EMA Cross Over Strategy

Performance Statistics for Each Symbol

# Performance Statistics and risk metrics for each symbol 
# Performance : annualized and cumulative returns s 
# Risk metrics : Sharpe and Calmar ratio 
 
tab.perf <- table.Arbitrary(rets,
                            metrics=c(
                              "Return.cumulative",
                              "Return.annualized",
                              "SharpeRatio.annualized",
                              "CalmarRatio"),
                            metricsNames=c(
                              "Cumulative Return",
                              "Annualized Return",
                              "Annualized Sharpe Ratio",
                              "Calmar Ratio"))
 
round(tab.perf,5)
 
                        DBC.DailyEqPL EEM.DailyEqPL EFA.DailyEqPL GLD.DailyEqPL
Cumulative Return             0.00266       0.03719      -0.00635       0.00190
Annualized Return             0.00052       0.00716      -0.00124       0.00037
Annualized Sharpe Ratio       0.24670       0.46053      -0.23570       0.04058
Calmar Ratio                  0.10078       0.23305      -0.10153       0.01016
                        IWM.DailyEqPL IYR.DailyEqPL QQQ.DailyEqPL SPY.DailyEqPL
Cumulative Return             0.09275       0.04061       0.06655      -0.00714
Annualized Return             0.01748       0.00781       0.01267      -0.00140
Annualized Sharpe Ratio       0.38301       0.52618       0.57011      -0.24452
Calmar Ratio                  0.25103       0.38344       0.26922      -0.14163
                        TLT.DailyEqPL
Cumulative Return             0.03236
Annualized Return             0.00624
Annualized Sharpe Ratio       0.56136
Calmar Ratio                  0.32163

Risk Statistics by Symbol

# Risk Statistics by Symbol
 
tab.risk <- table.Arbitrary(rets,
                            metrics=c(
                              "StdDev.annualized",
                              "maxDrawdown",
                              "VaR",
                              "ES"),
                            metricsNames=c(
                              "Annualized StdDev",
                              "Max DrawDown",
                              "Value-at-Risk",
                              "Conditional VaR"))
 
round(tab.risk,5)
 
 
                 DBC.DailyEqPL EEM.DailyEqPL EFA.DailyEqPL GLD.DailyEqPL IWM.DailyEqPL
Annualized StdDev       0.00210       0.01555       0.00528       0.00914       0.04565
Max DrawDown            0.00515       0.03072       0.01225       0.03649       0.06962
Value-at-Risk          -0.00016      -0.00149      -0.00053      -0.00084      -0.00464
Conditional VaR        -0.00016      -0.00342      -0.00110      -0.00115      -0.01233
                  IYR.DailyEqPL QQQ.DailyEqPL SPY.DailyEqPL TLT.DailyEqPL
Annualized StdDev       0.01484       0.02223       0.00572       0.01114
Max DrawDown            0.02036       0.04705       0.00987       0.01944
Value-at-Risk          -0.00144      -0.00230      -0.00044      -0.00065
Conditional VaR        -0.00358      -0.00500      -0.00044      -0.00065

The results above show that the symbol IWM has the best performance among all ETFs in the portfolio. The annual returns for IWM was 1,75% and the cumulative return was 9,3%. In the second place comes the QQQ ETF which has an annual return in the whole period of 1,23% and cumulative returns of 6,65%. 

The worst performance among the ETF’s in the portfolio was for SPY ETF with an annualized return of -0.14% and a cumulative return of -0,71% in the whole period. Regards, the ETF with the Maximum Drawdown level, we can observe that the IWM ETF has the Maximum Drawdown lever among the rest of the ETF’s with a 7% of Maximum Drawdown.

In terms of Sharpe and Calmar Ratio, the best level of Sharpe Ratio is observed for IWR ETF with a value of 0.38 and Calmar ratio of 0.25. Remember, when we define Calmar Ratio in the risk management section, we point out that this ratio takes into account the Maximum Drawdown level in its calculation. So for a conservative strategy this ratio is fundamental.

Finally the QQQ ETF has the higher Sharpe Ratio with a value of 0.57 which is followed by the TLT ETF with a Sharpe Ratio value of 0.56. Overall, this strategy has positive returns at the end of the period, but is affected by a significant drawdown at the end of 2018. The exit rule of this strategy given by  triple EMA cross down is to exit the position when the market reverse.

Lesson Resources

Member-only

Related Downloads

Membership
Learn the skills required to excel in data science and data analytics covering R, Python, machine learning, and AI.
I WANT TO JOIN
JOIN 30,000 DATA PROFESSIONALS

Free Guides - Getting Started with R and Python

Enter your name and email address below and we will email you the guides for R programming and Python.

Saylient AI Logo

Take the Next Step in Your Data Career

Join our membership for lifetime unlimited access to all our data analytics and data science learning content and resources.