# SCM Repository

 [blotter] / pkg / quantstrat / demo / pair_trade.R

revision 1376, Mon Jan 21 23:41:11 2013 UTC revision 1428, Sat Apr 6 22:34:04 2013 UTC
# Line 1  Line 1
1  #Kindly contributed to quantstrat by Garrett See  #Kindly contributed to quantstrat by Garrett See
2  #code borrowed heavily from existing quantstrat demos  #code borrowed heavily from existing quantstrat demos
3
4  #This is a simple pairs trading example intended to illustrate how you can extend  # This is a simple pairs trading example intended to illustrate how you can
5  #existing quantstrat functionality.  It uses addPosLimits to specify levels and  # extend existing quantstrat functionality.  It uses addPosLimits to specify
6  #position limits, and shows how to pass a custom order sizing function to osFUN  # levels and position limits, and shows how to pass a custom order sizing
7    # function to osFUN
8  #Note that it would be easier to build a spread first and treat it as a single instrument
9  #instead of dealing with a portfolio of stocks.  # Note that it would be easier to build a spread first and treat it as a single
10    # instrument instead of dealing with a portfolio of stocks.
11  ## given 2 stocks, calculate the ratio of their notional values.  If the ratio falls below it's
12  # 2 stdev band, then when it crosses back above it, buy stock 1 and sell stock 2.  ## given 2 stocks, calculate the ratio of their notional values.  If the ratio
13  # If the ratio rises above it's 2 stdev band, then when it crosses back below  # falls below it's 2 stdev band, then when it crosses back above it, buy stock 1
14  # it, sell stock 1 and buy stock 2.  If the ratio crosses it's moving average,  # and sell stock 2.  If the ratio rises above it's 2 stdev band, then when it
15  # then flatten any open positions.  # crosses back below it, sell stock 1 and buy stock 2.  If the ratio crosses
16    # its moving average, then flatten any open positions.
17
18  # The Qty of Stock A that it buys (sells) = MaxPos / lvls  # The Qty of Stock A that it buys (sells) = MaxPos / lvls
19  # The Qty of Stock B that is sells (buys) = MaxPos * Ratio / lvls  # The Qty of Stock B that is sells (buys) = MaxPos * Ratio / lvls
20
21  suppressWarnings(rm("order_book.pair1",pos=.strategy))  suppressWarnings(rm("order_book.pair1",pos=.strategy))
22  suppressWarnings(rm("account.pairs", "portfolio.pair1", pos=.blotter))  suppressWarnings(rm("account.pairs", "portfolio.pair1", pos=.blotter))
23  suppressWarnings(rm("initDate", "endDate", "startDate", "initEq", "SD", "N", "symb1", "symb2",  suppressWarnings(rm("initDate", "endDate", "startDate", "initEq", "SD", "N",
24          "portfolio1.st", "account.st", "pairStrat", "out1"))                      "symb1", "symb2", "portfolio1.st", "account.st",
25                        "pairStrat", "out1"))
26
27  require(quantstrat)  require(quantstrat)
28
# Line 37  Line 39
39  #  {initDate="1999-12-31"  #  {initDate="1999-12-31"
40  #  endDate=Sys.Date()}  #  endDate=Sys.Date()}
41
42  initDate = '2009-01-01'  initDate <- '2009-01-01'
43  endDate = '2011-05-01'  endDate <- '2011-05-01'
44  startDate = '2009-01-02'  startDate <- '2009-01-02'
45  initEq = 100000  initEq <- 100000
46  SD = 2  SD <- 2
47  N = 20  N <- 20
48
49  MaxPos = 1500  #max position in stockA;  MaxPos <- 1500  #max position in stockA;
50  #max position in stock B will be max * ratio, i.e. no hard position limit in Stock B  # max position in stock B will be max * ratio, i.e. no hard position limit in
51  lvls = 3        #how many times to fade; Each order's qty will = MaxPos/lvls  # Stock B
52    lvls <- 3  #how many times to fade; Each order's qty will = MaxPos/lvls
53
54  symb1 <- 'SPY' #change these to try other pairs  symb1 <- 'SPY' #change these to try other pairs
55  symb2 <- 'DIA' #if you change them, make sure position limits still make sense  symb2 <- 'DIA' #if you change them, make sure position limits still make sense
# Line 56  Line 59
59
61
62  #generic used to make sure the timestamps of all symbols are the same  # The following function is used to make sure the timestamps of all symbols are
63  #deletes rows where one of the stocks is missing data  # the same deletes rows where one of the stocks is missing data
64  alignSymbols <- function(symbols, env=.GlobalEnv) {  alignSymbols <- function(symbols, env=.GlobalEnv) {
65      # This is a simplified version of qmao::alignSymbols()
66          if (length(symbols) < 2)          if (length(symbols) < 2)
67                  stop("Must provide at least 2 symbols")                  stop("Must provide at least 2 symbols")
68          if (any(!is.character(symbols)))          if (any(!is.character(symbols)))
# Line 85  Line 89
89  initAcct(account.st, portfolios=portfolio1.st, initDate=initDate, initEq=initEq)  initAcct(account.st, portfolios=portfolio1.st, initDate=initDate, initEq=initEq)
90  initOrders(portfolio=portfolio1.st,initDate=initDate)  initOrders(portfolio=portfolio1.st,initDate=initDate)
91
92  #osFUN will need to know which symbol is leg 1 and which is leg 2 as well as what the  # osFUN will need to know which symbol is leg 1 and which is leg 2 as well as
93  #values are for MaxPos and lvls.  So, create a slot in portfolio to hold this info.  # what the values are for MaxPos and lvls.  So, create a slot in portfolio to
94    # hold this info.
95  pair <- c(1,2,MaxPos,lvls)  pair <- c(1,2,MaxPos,lvls)
96  names(pair) <- c(symb1,symb2,"MaxPos","lvls")  names(pair) <- c(symb1,symb2,"MaxPos","lvls")
97  .blotter[[paste('portfolio',portfolio1.st,sep='.')]]\$pair <- pair  .blotter[[paste('portfolio',portfolio1.st,sep='.')]]\$pair <- pair
98
99  # Create initial position limits and levels by symbol  # Create initial position limits and levels by symbol
100  # allow 3 entries for long and short if lvls=3.  # allow 3 entries for long and short if lvls=3.
102  addPosLimit(portfolio=portfolio1.st, timestamp=initDate, symbol=symb2, maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)              maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)
104                maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)
105
106  # Create a strategy object  # Create a strategy object
107  pairStrat <- strategy('pairStrat')  pairStrat <- strategy('pairStrat')
108
109  # Indicator function  # Indicator function
110  calcRatio <- function(x) { #returns the ratio of notional close prices for 2 symbols  calcRatio <- function(x) {
111      #returns the ratio of notional close prices for 2 symbols
112          x1 <- get(x[1])          x1 <- get(x[1])
113          x2 <- get(x[2])          x2 <- get(x[2])
114          mult1 <- getInstrument(x[1])\$multiplier          mult1 <- getInstrument(x[1])\$multiplier
# Line 109  Line 117
117          colnames(rat) <- 'Ratio'          colnames(rat) <- 'Ratio'
118          rat          rat
119  }  }
120  Ratio <- calcRatio(c(symb1[1],symb2[1])) #Indicator used for determining entry/exits  # Indicator used for determining entry/exits
121    Ratio <- calcRatio(c(symb1[1], symb2[1]))
122
123  #Put a slot in portfolio to hold hedge ratio so that it's available for order sizing function.  # Store hedge ratio in portfolio so that it's available for order sizing
124  #In this example, the hedge ratio happens to be the same as the Ratio indicator.  # function. In this example, the hedge ratio happens to be the same as the
125    # Ratio indicator.
126  .blotter[[paste('portfolio',portfolio1.st,sep='.')]]\$HedgeRatio <- Ratio  .blotter[[paste('portfolio',portfolio1.st,sep='.')]]\$HedgeRatio <- Ratio
127  #and make a function to get the most recent HedgeRatio  #and make a function to get the most recent HedgeRatio
128  getHedgeRatio <- function(portfolio, timestamp) {  getHedgeRatio <- function(portfolio, timestamp) {
129          portf <- getPortfolio(portfolio)          portf <- getPortfolio(portfolio)
130          timestamp <- format(timestamp,"%Y-%m-%d %H:%M:%S") #ensures you don't get last value of next day if using intraday data and timestamp=midnight    timestamp <- format(timestamp,"%Y-%m-%d %H:%M:%S")
131      # above line ensures you don't get last value of next day if using intraday
132      # data and timestamp=midnight
133          toDate <- paste("::", timestamp, sep="")          toDate <- paste("::", timestamp, sep="")
134          Ratio <- last(portf\$HedgeRatio[toDate])          Ratio <- last(portf\$HedgeRatio[toDate])
135          as.numeric(Ratio)          as.numeric(Ratio)
136  }  }
137
138  # Create an indicator - BBands on the Ratio  # Create an indicator - BBands on the Ratio
139  pairStrat <- add.indicator(strategy = pairStrat, name = "calcRatio", arguments = list(x=c(symb1,symb2)))  pairStrat <- add.indicator(strategy=pairStrat, name = "calcRatio",
140  pairStrat <- add.indicator(strategy = pairStrat, name = "BBands", arguments = list(HLC=quote(Ratio), sd=SD, n=N, maType='SMA'))                             arguments=list(x=c(symb1,symb2)))
141    pairStrat <- add.indicator(strategy=pairStrat, name = "BBands",
142                               arguments=list(HLC=quote(Ratio), sd=SD, n=N,
143                                              maType='SMA'))
144
145  #applyIndicators(strategy=pairStrat,mktdata=get(symb1[1])) #for debugging  #applyIndicators(strategy=pairStrat,mktdata=get(symb1[1])) #for debugging
146
147  # Create signals - buy when crossing lower band from below, sell when crossing upper band from above, flatten when crossing mavg from above or from below  # Create signals - buy when crossing lower band from below, sell when crossing
148  pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover", arguments= list(columns=c("Ratio","up"), relationship="lt"), label="cross.up")  # upper band from above, flatten when crossing mavg from above or from below
149  pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover", arguments= list(columns=c("Ratio","dn"), relationship="gt"), label="cross.dn")  pairStrat <- add.signal(strategy=pairStrat, name="sigCrossover",
150  pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover", arguments= list(columns=c("Ratio","mavg"), relationship="lt"), label="cross.mid.fa")                          arguments=list(columns=c("Ratio","up"),
151  pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover", arguments= list(columns=c("Ratio","mavg"), relationship="gt"), label="cross.mid.fb")                                          relationship="lt"),
152                            label="cross.up")
154                            arguments=list(columns=c("Ratio","dn"),
155                                            relationship="gt"),
156                            label="cross.dn")
158                            arguments=list(columns=c("Ratio","mavg"),
159                                      relationship="lt"),
160                            label="cross.mid.fa")
162                            arguments=list(columns=c("Ratio","mavg"),
163                                           relationship="gt"),
164                            label="cross.mid.fb")
165
166  #make an order sizing function  #make an order sizing function
167  #######################_ORDER SIZING FUNCTION_##########################################################  #######################_ORDER SIZING FUNCTION_##################################
168  #check to see which stock it is. If it's the second stock, reverse orderqty and orderside  # check to see which stock it is. If it's the second stock, reverse orderqty and
169  osSpreadMaxPos <- function (data, timestamp, orderqty, ordertype, orderside, portfolio, symbol, ruletype, ..., orderprice)  # orderside
170  {  osSpreadMaxPos <- function (data, timestamp, orderqty, ordertype, orderside,
171                                portfolio, symbol, ruletype, ..., orderprice) {
172          portf <- getPortfolio(portfolio)          portf <- getPortfolio(portfolio)
173          #check to make sure pair slot has the things needed for this function          #check to make sure pair slot has the things needed for this function
174          if (!any(portf\$pair == 1) && !(any(portf\$pair == 2))) stop('pair must contain both values 1 and 2')    if (!any(portf\$pair == 1) && !(any(portf\$pair == 2)))
175          if (!any(names(portf\$pair) == "MaxPos") || !any(names(portf\$pair) == "lvls")) stop('pair must contain MaxPos and lvls')      stop('pair must contain both values 1 and 2')
176      if (!any(names(portf\$pair) == "MaxPos") || !any(names(portf\$pair) == "lvls"))
177        stop('pair must contain MaxPos and lvls')
178
179          if (portf\$pair[symbol] == 1) legside <- "long"          if (portf\$pair[symbol] == 1) legside <- "long"
180          if (portf\$pair[symbol] == 2) legside <- "short"          if (portf\$pair[symbol] == 2) legside <- "short"
# Line 155  Line 186
186          qty <- orderqty          qty <- orderqty
187          if (legside == "short") {#symbol is 2nd leg          if (legside == "short") {#symbol is 2nd leg
188                  ## Comment out next line to use equal ordersizes for each stock.                  ## Comment out next line to use equal ordersizes for each stock.
190                    maxpos=round(MaxPos*ratio,0), longlevels=lvls,
191                    minpos=round(-MaxPos*ratio,0), shortlevels=lvls)
192                  ##                  ##
193                  qty <- -orderqty #switch orderqty for Stock B                  qty <- -orderqty #switch orderqty for Stock B
194          }          }
# Line 163  Line 196
196          if (qty > 0) orderside = 'long'          if (qty > 0) orderside = 'long'
197          if (qty < 0) orderside = 'short'          if (qty < 0) orderside = 'short'
198
199          orderqty <- osMaxPos(data=data,timestamp=timestamp,orderqty=qty,ordertype=ordertype,    orderqty <- osMaxPos(data=data,timestamp=timestamp, orderqty=qty,
200                                          orderside=orderside,portfolio=portfolio,symbol=symbol,ruletype=ruletype, ...)                         ordertype=ordertype, orderside=orderside,
201                           portfolio=portfolio, symbol=symbol, ruletype=ruletype,
202                           ...)
203
205          if (!is.null(orderqty) & !orderqty == 0 & !is.null(orderprice)) {          if (!is.null(orderqty) & !orderqty == 0 & !is.null(orderprice)) {
# Line 175  Line 210
210      }      }
211          return(0) #so that ruleSignal function doesn't also try to place an order          return(0) #so that ruleSignal function doesn't also try to place an order
212  }  }
213  ########################################################################################################  ################################################################################
214
215  # Create entry and exit rules for longs  and for shorts. Both symbols will get the same buy/sell signals, but osMaxPos will reverse those for the second symbol.  # Create entry and exit rules for longs  and for shorts. Both symbols will get
216  # orderqty's are bigger than PosLimits allow. osMaxPos will adjust the orderqty down to 1/3 the max allowed. (1/3 is because we are using 3 levels in PosLimit)  # the same buy/sell signals, but osMaxPos will reverse those for the second
217  pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments = list(sigcol="cross.dn", sigval=TRUE, orderqty=1e6, ordertype='market', orderside=NULL, osFUN='osSpreadMaxPos'), type='enter' )  # symbol.
218  pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments = list(sigcol="cross.up", sigval=TRUE, orderqty=-1e6, ordertype='market', orderside=NULL, osFUN='osSpreadMaxPos'), type='enter')  # orderqty's are bigger than PosLimits allow. osMaxPos will adjust the orderqty
219  pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments = list(sigcol="cross.mid.fb", sigval=TRUE, orderqty='all', ordertype='market', orderside=NULL), type='exit')  # down to 1/3 the max allowed. (1/3 is because we are using 3 levels in
220  pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments = list(sigcol="cross.mid.fa", sigval=TRUE, orderqty='all', ordertype='market', orderside=NULL), type='exit')  # PosLimit)
222  #applySignals(strategy=pairStrat, mktdata=applyIndicators(strategy=pairStrat,mktdata=get(symb1))) #for debugging                        arguments=list(sigcol="cross.dn", sigval=TRUE,
223                                         orderqty=1e6, ordertype='market',
225                          type='enter')
227                          arguments=list(sigcol="cross.up", sigval=TRUE,
228                                         orderqty=-1e6, ordertype='market',
230                          type='enter')
232                          arguments=list(sigcol="cross.mid.fb", sigval=TRUE,
233                                         orderqty='all', ordertype='market',
234                                         orderside=NULL),
235                          type='exit')
237                          arguments=list(sigcol="cross.mid.fa", sigval=TRUE,
238                                         orderqty='all', ordertype='market',
239                                         orderside=NULL),
240                          type='exit')
241
242
243    ## for debugging
244    # applySignals(strategy=pairStrat,
245    #              mktdata=applyIndicators(strategy=pairStrat, mktdata=get(symb1)))
246    ##
247
248  out1<-applyStrategy(strategy=pairStrat, portfolios=portfolio1.st)  out1<-applyStrategy(strategy=pairStrat, portfolios=portfolio1.st)
249
250  updatePortf(Portfolio=portfolio1.st,Dates=paste("::",as.Date(Sys.time()),sep=''))  updatePortf(Portfolio=portfolio1.st,
251                Dates=paste("::", as.Date(Sys.time()), sep=''))
252  updateAcct(account.st,Dates=paste(startDate,endDate,sep="::"))  updateAcct(account.st,Dates=paste(startDate,endDate,sep="::"))
253  updateEndEq(account.st,Dates=paste(startDate,endDate,sep="::"))  updateEndEq(account.st,Dates=paste(startDate,endDate,sep="::"))
254  getEndEq(account.st,Sys.time())  getEndEq(account.st,Sys.time())
# Line 204  Line 264
264  ret1\$total <- rowSums(ret1)  ret1\$total <- rowSums(ret1)
265  #ret1  #ret1
266
267  if("package:PerformanceAnalytics" %in% search() || require("PerformanceAnalytics",quietly=TRUE)) {  if("package:PerformanceAnalytics" %in% search() ||
268       require("PerformanceAnalytics",quietly=TRUE)) {
269  #       getSymbols("SPY", from='1999-01-01')  #       getSymbols("SPY", from='1999-01-01')
270  #       SPY.ret <- Return.calculate(SPY\$SPY.Close)  #       SPY.ret <- Return.calculate(SPY\$SPY.Close)
271  #       tmp <- merge(SPY.ret,ret1\$total,all=FALSE)  #       tmp <- merge(SPY.ret,ret1\$total,all=FALSE)

Legend:
 Removed from v.1376 changed lines Added in v.1428