7
votes

Here is an example of a multi-timeframe strategy I'm working on with quantstrat. Is this the correct way of doing a multi-timeframe strategy or am I doing it all wrong? I haven't come across any other examples doing multi-timeframe in the quantstrat demos or from googling.

To keep the strategy part simple (this is not a strategy that someone would trade) and keep the focus on the multi-timeframe aspect, I will demonstrate a simple strategy that uses tick data and 5 minute OHLC data. The strategy logic is to buy when the tick data crosses above the 30-period SMA of the 5-minute data and close the position when the tick data crosses below the same SMA.

For example: if the strategy is flat, the time is 13:02 and the previous observed 30-period SMA of the 5-minute data is 90.55 (for the period of 12:55- ending 13:00), and the tick data crosses from below 90.55 to above it (90.56) it's a buy, and when the tick data closes below it again, it exits the position.

To achieve this, I need to get both the tick data and 5-minute, 30-period SMA into the same object for quantstrat to process. I get the 5-minute OHLC xts and calculate a 30-period SMA of it. Then I merge this into the tick data xts object, which will give me an object with all the tick data and then every 5 mins I will get a row with for the last observed 5-minute, 30-period SMA.

If there is a 30-period SMA value at 13:00, this is for the 5 mins 12:55-13:00. Since the next update to the SMA is 5 minutes later, I need to fill-down the rows until the next value is observed (at 13:05) and so on.

Here's the head of the tick data (the tick data I have doesn't include milliseconds, but I've made the rows unique using make.index.unique(clemtick):

head(clemtick)
                    Price Volume
2013-01-15 09:00:00 93.90      1
2013-01-15 09:00:00 93.89      1
2013-01-15 09:00:00 93.89      1
2013-01-15 09:00:00 93.88      2
2013-01-15 09:00:00 93.89      1
2013-01-15 09:00:00 93.89      2

Here's the head of the 1 min data (each minute represents the previous minute of data, e.g. time stamp 09:01:00 == data from 09:00:00 - 09:01:00):

head(clemin)
                     Open  High   Low Close Volume
2013-01-15 09:01:00 93.90 94.04 93.87 93.97   1631
2013-01-15 09:02:00 93.97 93.98 93.90 93.91    522
2013-01-15 09:03:00 93.91 93.97 93.90 93.96    248
2013-01-15 09:04:00 93.95 93.98 93.93 93.95    138
2013-01-15 09:05:00 93.95 93.96 93.91 93.92    143
2013-01-15 09:06:00 93.93 93.97 93.91 93.91    729

Convert the 1 minute data to 5 minute data:

cle5min <- to.minutes5(clemin)
                    clemin.Open clemin.High clemin.Low clemin.Close clemin.Volume
2013-01-15 09:04:00       93.90       94.04      93.87        93.95          2539
2013-01-15 09:09:00       93.95       93.97      93.81        93.89          2356
2013-01-15 09:14:00       93.90       94.05      93.86        93.89          4050
2013-01-15 09:19:00       93.90       94.03      93.84        94.00          2351
2013-01-15 09:24:00       93.99       94.21      93.97        94.18          3261
2013-01-15 09:29:00       94.18       94.26      94.18        94.19          1361

You will notice the first OHLC is 09:04:00, this is due to the way that to.minutes5 function works, which is discussed in this thread. Essentially the first time stamp 09:04:00 == the OHLC 4 minutes of data from 09:00:00 - 09:04:00. the 09:09:00 time stamp is the next full 5 mins from 09:04:00 - 09:09:00. Ideally I would like each time stamp to be 5, 10, 15 etc but I haven't worked out how to do this yet.

To get the 30 SMA of the 5min data into the tick data

clemtick$sma30 <- SMA(cle5min$clemin.Close, 30)

This creates a new column with with the SMA. The SMA needs 30 periods to calculate the first value and the SMA will only appear for every 5 minutes time stamp (11:29:00, 11:34:00, 11:39, ...). It looks like:

clemtick["2013-01-15 11:28:59::2013-01-15 11:29:00"]
                    Price Volume    SMA30
2013-01-15 11:28:59 93.87      1       NA
2013-01-15 11:28:59 93.87      1       NA
2013-01-15 11:28:59 93.88      1       NA
2013-01-15 11:29:00 93.87      1 93.92633
2013-01-15 11:29:00 93.87      1       NA
2013-01-15 11:29:00 93.88      1       NA
2013-01-15 11:29:00 93.88      1       NA

Now I need to fill-down the SMA30 column with a repeating value. The value for SMA30 at 11:29:00 is for the OHLC from 11:24:00 - 11:29:00. The next update of this value won't be until 11:34:00, so I need to fill down the rows until the next value, since this is what the strategy will reference when processing row by row.

clemtick  <- na.locf(clemtick)

Now if I query that object again,

clemtick["2013-01-15 11:33:58::2013-01-15 11:34:01"]
                    Price Volume    SMA30
2013-01-15 11:33:58 93.84      1 93.92633
2013-01-15 11:34:00 93.84      1 93.92267
2013-01-15 11:34:00 93.85      1 93.92267
2013-01-15 11:34:01 93.84      1 93.92267

Now that we have the final object here is running the strategy:

require(quantstrat)

options("getSymbols.warning4.0"=FALSE)
rm(list=ls(.blotter), envir=.blotter)
Sys.setenv(TZ="UTC")

symbols  <- "clemtick"
currency('USD')
stock(symbols, currency="USD", multiplier=1)

account.st  <- 0
strategy.st  <- portfolio.st <- account.st  <- "multi"
rm.strat(portfolio.st)
rm.strat(strategy.st)

initDate <- "1980-01-01"
tradeSize  <- 1000
initEq  <- tradeSize*length(symbols)
initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD')
initAcct(account.st, portfolios=portfolio.st,
         initDate=initDate, currency='USD', initEq=initEq)
initOrders(portfolio.st, initDate=initDate)

strategy(strategy.st, store=TRUE)

add.signal(strategy.st, name="sigCrossover",
  arguments=list(columns=c("Price", "sma30"), relationship="gt"),
  label="golong") 

add.signal(strategy.st, name="sigCrossover",
  arguments=list(columns=c("Price", "sma30"), relationship="lt"),
  label="exitlong")

#enter rule
add.rule(strategy.st, name="ruleSignal",
  arguments=list(sigcol="golong",
                 sigval=TRUE,
                 ordertype="market",
                 orderside="long",
                 replace=TRUE,
                 prefer="Price",
                 orderqty=1),
  type="enter", path.dep=TRUE, label="long")

#exit rule
add.rule(strategy.st, name = "ruleSignal",
  arguments=list(sigcol="exitlong",
                 sigval=TRUE,
                 ordertype="market",
                 orderside="long",
                 replace=TRUE,
                 prefer="Price",
                 orderqty=-1),
  type="exit", path.dep=TRUE, label="exitlong")

#apply strategy
t1 <- Sys.time()
out2 <- applyStrategy(strategy=strategy.st, portfolios=portfolio.st, debug=TRUE)
t2 <- Sys.time()
print(t2-t1)
head(mktdata)
nrow(mktdata)

So to sum up is the this best way to do multi time frame strategies?

1
Surely there must be some people doing multi time frame strategies?GeV 126
Yes, but we're busing trading, not testing. :) I'll try to respond in the next few days.Joshua Ulrich

1 Answers

0
votes

Here are two approaches for incorporating multi time frame indicators/signals into your strategy. Both run out of the box using just quantstrat sample data.

Both follow the same strategy (and give identical results): The strategy uses an SMA(20) on 1 minute bars and an SMA(10) on 30 minute bars to generate trade signals. A long position is entered when SMA(20, 1 min bars) crosses above SMA(10, 30 minute bars). Exit from a long position when SMA(20, 1 min bars) crosses below SMA(10, 30 minute bars)

Approach 1: Build price data and indicators on lower time frequencies within a custom function called by add.indicator. (you can't go to higher time frequencies than the raw market data for the symbol).

from <- "2002-10-20"
to <- "2002-10-24"

symbols <- "GBPUSD"
# Load 1 minute data stored in the quantstrat package
getSymbols.FI(Symbols = symbols,
              dir=system.file('extdata',package='quantstrat'),
              from=from, 
              to=to
)

currency(c('GBP', 'USD'))
exchange_rate('GBPUSD', tick_size=0.0001)

strategy.st <- "multiFrame"
portfolio.st <- "multiFrame"
account.st <- "multiFrame"

initEq <- 50000

rm.strat(strategy.st)
initPortf(portfolio.st, symbols = symbols)
initAcct(account.st, portfolios = portfolio.st, initEq = initEq)
initOrders(portfolio.st)
strategy(strategy.st, store = TRUE)

# Create an SMA on 20 1 minute bars:
add.indicator(strategy.st, name = "SMA", 
              arguments = list(x = quote(Cl(mktdata)),
                                n = 20), 
              label = "MA20")

# Define the function that add.indicator will use to create an SMA(10) on 30 minute bars:
ind30minMA <- function(x, n30min = 10) {

  if (!is.OHLC(x)) 
    stop("Must pass in OHLC data")
  x.h <- to.period(x[, 1:4], period = "minutes", k = 30, indexAt = "endof") 
  #^ Ensure that the timestamp on the lower frequency data is at the END of the bar/candle, to avoid look forward bias.

  # If you need to know what symbol you are currently processing:
  # symbol <- parent.frame(n = 2)$symbol
  sma.h <- SMA(Cl(x.h), n = n30min)
  r <- merge(sma.h, xts(, index(x)), fill= na.locf) 
  #^ Carry forward the last value, no lookforward bias introduced

  r <- r[index(x)]
  # if you don't return the same # of rows in the argument x, then quantstrat won't work correctly. So let's check the data is OK after the merge above:
  stopifnot(NROW(r) == NROW(x))
  r
}

add.indicator(strategy.st, name = "ind30minMA", 
              arguments = list(x = quote(mktdata),
                               n30min = 10), 
              label = "MA30minbar")

add.signal(strategy.st, name = "sigCrossover", 
              arguments = list(columns = c("SMA.MA20", "SMA.MA30minbar"),
                               relationship = "gt"),
              label = "FastCrossUp")

add.signal(strategy.st, name = "sigCrossover", 
           arguments = list(columns = c("SMA.MA20", "SMA.MA30minbar"),
                            relationship = "lt"),
           label = "FastCrossDn")

add.rule(strategy.st,name='ruleSignal', 
         arguments = list(sigcol="FastCrossUp",
                          sigval=TRUE, 
                          orderqty= 100, 
                          ordertype='market', 
                          orderside='long', 
                          threshold=NULL),
         type='enter',
         label='enterL',
         storefun=FALSE
)

add.rule(strategy.st,name='ruleSignal',
         arguments = list(sigcol="FastCrossDn",
                          sigval=TRUE,
                          orderqty='all',
                          ordertype='market',
                          orderside='long',
                          threshold=NULL,
                          orderset='default',
                          replace = TRUE),
         type='exit',
         label='exitL'
)


applyStrategy(strategy.st, portfolio.st)


tail(mktdata)
# Open   High    Low  Close Volume SMA.MA20 SMA.MA30minbar FastCrossUp FastCrossDn
# 2002-10-24 17:54:00 1.5552 1.5552 1.5552 1.5552      0 1.555115        1.55467          NA          NA
# 2002-10-24 17:55:00 1.5552 1.5552 1.5551 1.5551      0 1.555120        1.55467          NA          NA
# 2002-10-24 17:56:00 1.5551 1.5551 1.5551 1.5551      0 1.555125        1.55467          NA          NA
# 2002-10-24 17:57:00 1.5551 1.5551 1.5551 1.5551      0 1.555130        1.55467          NA          NA
# 2002-10-24 17:58:00 1.5551 1.5551 1.5551 1.5551      0 1.555130        1.55467          NA          NA
# 2002-10-24 17:59:00 1.5551 1.5551 1.5551 1.5551      0 1.555135        1.55478          NA          NA

tx <- getTxns(portfolio.st, "GBPUSD")
# Record total PL earned.  This number should be identical to the result from the second approach listed below:
sum(tx$Net.Txn.Realized.PL)
# -0.03

Approach 2: The idea is that we have already computed daily market data with names in the global namespace of [symbol].d (see below for what I mean). This daily data could be loaded from disk into memory as well, obviously. We use these precomputed data sets on different time frequencies, instead of computing the bar data inside the indicator function (such as is done in indDailyMA above):

This approach is arguably more advanced and efficient memory wise, as we are not computing the aggregations (which can be computationally expensive when working with tick data for example).

library(quantstrat)

from <- "2002-10-20"
to <- "2002-10-24"

symbols <- "GBPUSD"
# Load 1 minute data stored in the quantstrat package
getSymbols.FI(Symbols = symbols,
              dir=system.file('extdata',package='quantstrat'),
              from=from, 
              to=to
)

currency(c('GBP', 'USD'))
exchange_rate('GBPUSD', tick_size=0.0001)

strategy.st <- "multiFrame"
portfolio.st <- "multiFrame"
account.st <- "multiFrame"

# Parameters:

initEq <- 50000



rm.strat(strategy.st)
initPortf(portfolio.st, symbols = symbols)
initAcct(account.st, portfolios = portfolio.st, initEq = initEq)
initOrders(portfolio.st)
strategy(strategy.st, store = TRUE)


GBPUSD <- GBPUSD[, colnames(GBPUSD) != "Volume"]

# Before running the backtest, create the lower frequency market data
GBPUSD.30m <- to.period(OHLC(GBPUSD), period = "minutes", k = 30, indexAt = "endof", name = "GBPUSD") 

GBPUSD.1m.idx <- index(GBPUSD)

NROW(GBPUSD)
# 5276

# Add the lower frequency data indicators to the higher frequency data that will be processed in quantstrat.  Fill forward the lower frequency moving average

GBPUSD <- merge(GBPUSD, setNames(SMA(Cl(GBPUSD.30m), n = 10), "SMA.MA30minbar"))
GBPUSD$SMA.MA30minbar <- na.locf(GBPUSD$SMA.MA30minbar)

# Note: Short hand for the above will the fill argument, which can be helpful in special cases where NAs only exist in the new data to be added:
# GBPUSD <- merge(GBPUSD, setNames(SMA(Cl(GBPUSD.30m), n = 10), "SMA.MA30minbar"),  fill = na.locf)

NROW(GBPUSD)
# 5276

# After doing this merge, sometimes extra rows will appear beyond what GBPUSD (based on the original 1 min bar data) 
GBPUSD <- GBPUSD[GBPUSD.1m.idx, ]

# Now GBPUSD, which will be the raw data used in applyStrategy, already contains the 30 min bar indicators.

add.indicator(strategy.st, name = "SMA", 
              arguments = list(x = quote(Cl(mktdata)),
                               n = 20), 
              label = "MA20")



add.signal(strategy.st, name = "sigCrossover", 
           arguments = list(columns = c("SMA.MA20", "SMA.MA30minbar"),
                            relationship = "gt"),
           label = "FastCrossUp")

add.signal(strategy.st, name = "sigCrossover", 
           arguments = list(columns = c("SMA.MA20", "SMA.MA30minbar"),
                            relationship = "lt"),
           label = "FastCrossDn")

add.rule(strategy.st,name='ruleSignal', 
         arguments = list(sigcol="FastCrossUp",
                          sigval=TRUE, 
                          orderqty= 100, 
                          ordertype='market', 
                          orderside='long', 
                          threshold=NULL),
         type='enter',
         label='enterL',
         storefun=FALSE
)

add.rule(strategy.st,name='ruleSignal',
         arguments = list(sigcol="FastCrossDn",
                          sigval=TRUE,
                          orderqty='all',
                          ordertype='market',
                          orderside='long',
                          threshold=NULL,
                          orderset='sysMACD',
                          replace = TRUE),
         type='exit',
         label='exitL'
)


applyStrategy(strategy.st, portfolio.st)


tail(mktdata)
# Open   High    Low  Close SMA.MA30minbar SMA.MA20 FastCrossUp FastCrossDn
# 2002-10-24 17:54:00 1.5552 1.5552 1.5552 1.5552        1.55467 1.555115          NA          NA
# 2002-10-24 17:55:00 1.5552 1.5552 1.5551 1.5551        1.55467 1.555120          NA          NA
# 2002-10-24 17:56:00 1.5551 1.5551 1.5551 1.5551        1.55467 1.555125          NA          NA
# 2002-10-24 17:57:00 1.5551 1.5551 1.5551 1.5551        1.55467 1.555130          NA          NA
# 2002-10-24 17:58:00 1.5551 1.5551 1.5551 1.5551        1.55467 1.555130          NA          NA
# 2002-10-24 17:59:00 1.5551 1.5551 1.5551 1.5551        1.55478 1.555135          NA          NA

tx <- getTxns(portfolio.st, "GBPUSD")
sum(tx$Net.Txn.Realized.PL)
# -0.03

# Same result as the first approach, as we would expect

You may also find these other references on this topic useful:

Generating indicators of different periodicity in quantstrat

http://r.789695.n4.nabble.com/R-Quantstrat-package-question-td3772989.html