Trading Strategy – S&P VWAP Trend Follow (BUGGY)

UPDATE: The exceptional returns seen in this strategy were due to a 2 day look forward bias in the signal (and then subsequent trade direction), ie when returns were calculated for day T the trade signal used was actually from day T+2.

This bias occurred in the lines:

?View Code RSCODE
signal <- na.omit(signal)

Both the signal and trade dataframe had the correct dates for each signal/trades however when indexRet*trade happened then trade was treated as undated vectors (which is 2 elements shorter than index ret) hence the 2 day shift. The moral of this story is to merge dataframes before multiplying!

Thank you for everyone that commented on this, a corrected post is to follow!

Original Post

This strategy is going to use the volume weighted average price (VWAP) as an indicator to determine the direction of the current trend and trade the same direction as the trend. Annualized Sharpe Ratio (Rf=0%) is 8.510472.

Trade logic:

  • All conditions are checked at the close, and the trade held for one day from the close
  • If price/vwap > uLim go long
  • If price/vwap < lLim go short

Initially I thought that the price would be mean reverting to VWAP (this can be see in high freq data) however this didn’t appear to be the case with EOD data. For such a simple strategy I’m amazed that the Sharpe ratio is so high (suspiciously high). The code has been double&tripple checked to see if any forward bias has slipped in, however I haven’t spotted anything.

Onto the code:

?View Code RSPLUS
#Trade logic - Follow the trade demand, ie if price > vwap then go long
#If price/vwap > uLim go LONG
#If price/vwap < lLim go SHORT
#Script parameters
symbol <- "^GSPC"     #Symbol
nlookback <- 3 #Number of days to lookback and calculate vwap
uLim <- 1.001  #If price/vwap > uLim enter a long trade
lLim <- 0.999  #If price/vwap < lLim enter a short trade
#Specify dates for downloading data
startDate = as.Date("2006-01-01") #Specify what date to get the prices from
symbolData <- new.env() #Make a new environment for quantmod to store data in
getSymbols(symbol, env = symbolData, src = "yahoo", from = startDate)
mktdata <- eval(parse(text=paste("symbolData$",sub("^","",symbol,fixed=TRUE))))
mktdata <- head(mktdata,-1) #Hack to fix some stupid duplicate date problem with yahoo
#Calculate volume weighted average price
vwap <- VWAP(Cl(mktdata), Vo(mktdata), n=nlookback)
#Can calculate vwap like this, but it is slower
#vwap <- runSum(Cl(mktdata)*Vo(mktdata),nlookback)/runSum(Vo(mktdata),nlookback)
#Calulate the daily returns
dailyRet <- Delt(Cl(mktdata),k=1,type="arithmetic") #Daily Returns
#signal = price/vwap
signal <- Cl(mktdata) / vwap
signal <- na.omit(signal)
trade <- apply(signal,1, function(x) {if(x<lLim) { return (-1) } else { if(x>uLim) { return(1) } else { return (0) }}})
#Calculate the P&L
#The daily ret is DailyRet(T)=(Close(T)-Close(T-1))/Close(T-1)
#We enter the trade on day T so need the DailyRet(T+1) as our potential profit
#Hence the lag in the line below
strategyReturns <- trade * lag(dailyRet,-1)
strategyReturns <- na.omit(strategyReturns)
#### Performance Analysis ###
#Calculate returns for the index
indexRet <- dailyRet #Daily returns
colnames(indexRet) <- "IndexRet"
zooTradeVec <- cbind(as.zoo(strategyReturns),as.zoo(indexRet)) #Convert to zoo object
colnames(zooTradeVec) <- c(paste(symbol," VWAP Trade"),symbol)
zooTradeVec <- na.omit(zooTradeVec)
#Lets see how all the strategies faired against the index
charts.PerformanceSummary(zooTradeVec,main=paste("Performance of ", symbol, " VWAP Strategy"),geometric=FALSE)
#Lets calculate a table of montly returns by year and strategy
cat("Calander Returns - Note 13.5 means a return of 13.5%\n")
#Calculate the sharpe ratio
cat("Sharpe Ratio")

10 thoughts on “Trading Strategy – S&P VWAP Trend Follow (BUGGY)

  1. Thank you for sharing this, i’m just trying to recreate and test the results….if you don’t mind what was the vwap formula that you are using.

    where does the look back come in?

    typicalPrice: high + low + close / 3

    Cumulative(Volume x typicalPrice)/Cumulative(Volume)

    many thanks…

    • lookback is basically how many days of data to use in the calculation of VWAP.

      So if lookback=3 then VWAP can be calculated as follows:
      On the close of Day(T), VWAP(T)=(Close(T)*Vol(T)+Close(T-1)*Vol(T-1)+Close(T-2)*Vol(T-2))/(Vol(T)+Vol(T-1)+Vol(T-2)).

  2. Hello and thank for sharing strategies. I couldn’t resist the simplicity of this one, but I cannot re-create the back-test results.
    I am using ^GSPC data from Yahoo.
    I am using Vwap=sum(Close*Volume,3)/sum(Volume,3)
    The system buys at the Close "today" at "today's" signal.
    Sells tomorrow at the close.
    The system buys and sells every single day (entry delay=0)
    Length of each trade =1 Bar.

    I am getting very different results. Good ones until 2000, horrible ones from 2000 till today. This makes sense since the markets changed from short term trend following to mean reverting around 2000.

    • Sanz I would think that around 2006 the effectiveness of this strategy might change mainly due to the introduction of electronic trading, with IB’s offering program trading that basically aims to fill large trades at VWAP or better. I would have expected post 2006 to show more mean reversion as well (as mentioned in my post).

      Sanz are you able to email me an equity curve of your results (gekkoquant [at] Are your results arithmetic or geometric returns?

      What are your sharpe ratios pre & post 2000?

  3. Fix required?
    I’m a newbie here (my first sight of R), so sympathy please.
    The statement:
    zooTradeVec <- cbind(as.zoo(strategyReturns),as.zoo(indexRet)) #Convert to zoo object
    generated an error
    Error in merge.zoo(…, all = all, fill = fill, suffixes = suffixes, retclass = "zoo", :
    series cannot be merged with non-unique index entries in a series
    In addition: [snip]
    I found that length() of strategyReturns and indexRet was different (1655, 1656)
    I did (after some puzzlement):
    qq <- edit(indexRet)
    thinking I was going to find an extra date in one or the other (the lag thing perhaps), not fully understanding the error message.
    Instead I found a duplicated timestamp, 1343343600, at the end of the data (subsequently seen easily with tail()). I deleted the second data and timestamp entries and amended the count, and cbind() now works (with qq).
    And after that fix I got the same charts as you show.

    And for anyone else who is using R for the first time, you will need install.packages() commands for the two library() arguments, but presumably not many are that stupid.


    • I’ve had the same problem this weekend some bad data on yahoo’s side (note the hack I put in the code mktdata <- head(mktdata,-1) to drop the last data row).

  4. OK, now looking at the data….
    I put the data in excel (somewhat labouriouly printing the columns to the console and transferring them text-wise to excel va a text editor)
    I’m wondering if the trade multiplier is working as expected, for instance:
    2006/01/10 generates a long signal with 1289.69 / 1288.382 = 1.00102. The close the next day is higher at 1294.18, ie .3481%. But in the strtategyReturns data I see the 2006/01/10 value is -.348%.

    If I shift the “Trade” column cells up by two in my spereadsheet I see numbers that seems to match the (faulty) strategyReturns data, note that the trade data starts on the 5th (because there is no vwap (n=3) until that day), but the dailyReturns start at the 3rd (so the code uses trade data from the future!). Thus does the trade data need to be properly synced to the lagged dailyReturn? Beyond my powers to code that I’m afraid.

    • Hi Dave, thanks for this analysis you’ve found the problem.

      The calculation being performed is:

      The correct calculation should be:

      I’ll post an update once i’ve figured out how to fix this (the whole point of dataframes is that they should remain in sync automatically!)

      BTW dave a good shortcut to save data in CSV for excel is:
      > tmp < - cbind(vwap,dailyRet,signal,trade,strategyReturns) > colnames(tmp) < - c("vwap","dailyRet","signal","trade","strategyReturns") > tmp < - cbind(symbolData$GSPC,tmp) > write.csv(tmp,”data.csv”)

  5. Pingback: Trading Strategy – VWAP Mean Reversion | Gekko Quant – Quantitative Trading

  6. about the Yahoo data thing, users of xlq (it uses downloaded eod) have been mailed to say there is an update because:
    “Yahoo has begun including the current partial days data in their historic download data, offsetting xlq date references by 1. This version will now detect this, ensuring the active day is not included in the historic data.

    If the current day’s data has already been included in the database, it will not be deleted, but replaced the following day. If you delete the symbols in the stock manager under yahoo (historic) the data will again be retrieved without the current partial day.”

    so I guess a package fix is hoped for?

Leave a Reply

Your email address will not be published. Required fields are marked *