[R-SIG-Finance] calculating beta-based variable dollar amounts to each leg of a spread backtest

Tucker Sferro tsferro2 at gmail.com
Wed Aug 26 15:12:48 CEST 2015


I am currently working on a pairs trading backtest R script and have hit a
wall in my efforts to generate dollar based backtesting that adjusts for
the hedgeRatio ("beta" in my script) to each leg in a stock pair backtest.
My existing script is mostly taken from Joshua Ulrich's pairs quantstrat
backtest example.

The script as-is backtests equal dollar amounts per pair, but this value is
different between sets of pairs because Joshua's script uses a ratio
spread, which is used to adjust the amount to buy for stock B in the pair.
Stock A could be any price and the share amount remains fixed, meaning the
dollar investment amount will vary between pairs. This is nonetheless ok,
as I am only testing for risk-adjusted returns - *most importantly I would
simply like to adjust the dollar invest amount based on the hedge ratio*.
I've tried unsuccessfully to use the IKTrading  osMaxDollar function, but
have hit a wall since Joshua's script uses an order sizing function that
makes the 2nd stock shares to trade the opposite of the first stock.

I've also looked at Ilya Kipnis's example of modeling the spread itself(
https://quantstrattrader.wordpress.com/category/quantstrat/) but this
apparently doesn't work since the dollar investment amount needs to
be adjusted for each leg on a per trade basis. I have tried to resolve this
for literally more than 2 weeks (stack exchange, demo deconstructing, etc)
without any luck and was wondering if anyone on this list would be so kind
as to have a look at my script? Perhaps you could point me in the right
direction with a couple lines of helper code? I really don't know where
else to turn and could very much use the help. Below is the code with the
working script (ie all versions which incorporate osMaxDollar have been
left out because they don't work!).

-T
======================================================

## given 2 stocks, calculate their ratio.  If the ratio falls below it's 2
stdev band, then when it crosses back above it, buy stock 1 and sell stock
2. If the ratio rises above it's 2 stdev band, then when it crosses back
below # it, sell stock 1 and buy stock 2.  If the ratio crosses it's moving
average then flatten any open positions.
# The Qty of Stock A that it buys (sells) = MaxPos / lvls
# The Qty of Stock B that is sells (buys) = MaxPos * Ratio / lvls

try(rm("order_book.pair1",pos=.strategy),silent=TRUE)
try(rm("account.pairs", "portfolio.pair1", pos=.blotter), silent=TRUE)
try(rm("initDate", "endDate", "startDate", "initEq", "SD", "N", "symb1",
"symb2",
"portfolio1.st", "account.st", "pairStrat", "out1"), silent=TRUE)

require(quantstrat)
require(FinancialInstrument)

symb1 <- 'data'
symb2 <- 'panw'
symb1 <- toupper(symb1)
symb2 <- toupper(symb2)

initDate = '2000-01-01'
startDate = '2014-08-21'
endDate = '2015-08-21'
SD = 1.5
N = 20
initEq = 100000

#max position in stock B will be max * ratio, i.e. no hard position limit
in Stock B
lvls = 0 #how many times to fade; Each order's qty will = MaxPos/lvls

portfolio1.st <- 'pair1'
account.st <- 'pairs'

suppressWarnings(getSymbols(c(symb1, symb2), from=startDate, to=endDate,
adjust=TRUE))
MaxPos = 1000
currency("USD")
stock(symb1, currency="USD", multiplier=1)
stock(symb2, currency="USD", multiplier=1)

#Initialize Portfolio, Account, and Orders
initPortf(name=portfolio1.st, c(symb1,symb2), initDate=initDate)
initAcct(account.st, portfolios=portfolio1.st, initDate=initDate,
initEq=initEq)
initOrders(portfolio=portfolio1.st,initDate=initDate)

#create a slot in portfolio for symb1 and symb2 to make them available to
osFUN
pair <- c('long','short')
names(pair) <- c(symb1,symb2)
.blotter[[paste('portfolio',portfolio1.st,sep='.')]]$pair <- pair

# Create initial position limits and levels by symbol
# allow "3" entries for long and short.
addPosLimit(portfolio=portfolio1.st, timestamp=initDate, symbol=symb1,
maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)
addPosLimit(portfolio=portfolio1.st, timestamp=initDate, symbol=symb2,
maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)

# Create a strategy object
pairStrat <- strategy('pairStrat')

calcRatio <- function(x) { #returns the ratio of close prices for 2 symbols
x1 <- get(x[1])
x2 <- get(x[2])
rat <- Ad(x1) / Ad(x2)
colnames(rat) <- 'Ratio'
rat
}
Ratio <- calcRatio(c(symb1[1],symb2[1]))
#let's go ahead and put this in a slot in portfolio
.blotter[[paste('portfolio',portfolio1.st,sep='.')]]$Ratio <- Ratio # TS
change from Ratio
#and make a function to get the most recent Ratio
getRatio <- function(portfolio, timestamp) {
portf <- getPortfolio(portfolio)
toDate <- paste("::", timestamp, sep="")
Ratio <- last(portf$Ratio[toDate])
as.numeric(Ratio)
}




#calculate hedge ratio and egcm / ADF p-value
stckY <- zoo(Ad(stckY))
stckX <- zoo(Ad(stckX))

m <- lm(stckY ~ stckX + 0)
beta <- coef(m)[1]




# Create an indicator - BBands on the Ratio
pairStrat <- add.indicator(strategy = pairStrat, name = "calcRatio",
arguments = list(x=c(symb1,symb2)))
pairStrat <- add.indicator(strategy = pairStrat, name = "BBands", arguments
= list(HLC=quote(Ratio), sd=SD, n=N, maType='EMA'))

#applyIndicators(strategy=pairStrat,mktdata=get(symb1[1])) #for debugging

# 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
#TS - changed for No REV
pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover",
arguments= list(columns=c("Ratio","up"), relationship="gt"),
label="cross.up")
pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover",
arguments= list(columns=c("Ratio","dn"), relationship="lt"),
label="cross.dn")
pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover",
arguments= list(columns=c("Ratio","mavg"), relationship="lt"),
label="cross.mid.fa")
pairStrat <- add.signal(strategy = pairStrat, name = "sigCrossover",
arguments= list(columns=c("Ratio","mavg"), relationship="gt"),
label="cross.mid.fb")

#make an order sizing function
#######################_ORDER SIZING
FUNCTION_##########################################################
#check to see which stock it is. If it's the second stock, reverse orderqty
and orderside
osSpreadMaxPos <- function (data, timestamp, orderqty, ordertype,
orderside, portfolio, symbol, ruletype, ..., orderprice)
{
portf <- getPortfolio(portfolio)
    legside <- portf$pair[symbol] #"long" if symbol=symb1, "short" if
symbol=symb2
if (legside != "long" && legside != "short") stop('pair must contain "long"
and "short"')
ratio <- getRatio(portfolio, timestamp)
pos <- getPosQty(portfolio, symbol, timestamp)
    PosLimit <- getPosLimit(portfolio, symbol, timestamp)
qty <- orderqty
if (legside == "short") {#symbol is 2nd leg
## Comment out next line to use equal ordersizes for each stock.
addPosLimit(portfolio=portfolio, timestamp=timestamp, symbol=symbol,
maxpos=MaxPos*ratio, longlevels=lvls, minpos=-MaxPos*ratio,
shortlevels=lvls)
#TODO: is it okay that MaxPos and lvls come from .GlobalEnv ?
qty <- -orderqty #switch orderqty for Stock B
}
if (qty > 0) orderside = 'long'
if (qty < 0) orderside = 'short'

orderqty <-
osMaxPos(data=data,timestamp=timestamp,orderqty=qty,ordertype=ordertype,
orderside=orderside,portfolio=portfolio,symbol=symbol,ruletype=ruletype,
...)
orderqty <- round(orderqty,0)

#Add the order here instead of in the ruleSignal function
if (!is.null(orderqty) & !orderqty == 0 & !is.null(orderprice)) {
            addOrder(portfolio = portfolio, symbol = symbol,
                timestamp = timestamp, qty = orderqty, price =
as.numeric(orderprice),
                ordertype = ordertype, side = orderside,
                status = "open", ... = ...)
    }
return(0) #so that ruleSignal function doesn't also try to place an order
}
########################################################################################################

# 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.
# 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)
pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments =
list(sigcol="cross.dn", sigval=TRUE, orderqty=1e6, ordertype='market',
orderside=NULL, prefer = "Open", TxnFees="pennyPerShare",
osFUN='osSpreadMaxPos'), type='enter' )
pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments =
list(sigcol="cross.up", sigval=TRUE, orderqty=-1e6, ordertype='market',
orderside=NULL, prefer = "Open",
TxnFees="pennyPerShare",osFUN='osSpreadMaxPos'), type='enter')
pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments =
list(sigcol="cross.mid.fb", sigval=TRUE, orderqty='all',
ordertype='market', orderside=NULL, prefer = "Open",
TxnFees="pennyPerShare"), type='exit')
pairStrat <- add.rule(strategy = pairStrat, name='ruleSignal', arguments =
list(sigcol="cross.mid.fa", sigval=TRUE, orderqty='all',
ordertype='market', orderside=NULL, prefer = "Open",
TxnFees="pennyPerShare"), type='exit')
#applySignals(strategy=pairStrat,
mktdata=applyIndicators(strategy=pairStrat,mktdata=get(symb1))) #for
debugging

out1<-try(applyStrategy(strategy=pairStrat, portfolios=portfolio1.st))

updatePortf(Portfolio=portfolio1.st
,Dates=paste("::",as.Date(Sys.time()),sep=''))
updateAcct(account.st,Dates=paste(startDate,endDate,sep="::"))
updateEndEq(account.st,Dates=paste(startDate,endDate,sep="::"))
getEndEq(account.st,Sys.time())

#dev.new()
#chart.Posn(Portfolio=portfolio1.st,Symbol=symb1)
#dev.new()
#chart.Posn(Portfolio=portfolio1.st,Symbol=symb2)
#dev.new()
#chartSeries(Cl(get(symb1))/Cl(get(symb2)),TA="addBBands()")

ret1 <- PortfReturns(account.st)
ret1$total <- rowSums(ret1)
#ret1

if("package:PerformanceAnalytics" %in% search() ||
require("PerformanceAnalytics",quietly=TRUE)) {
tmp <- ret1$total
#dev.new()
charts.PerformanceSummary(tmp,geometric=FALSE,wealth.index=TRUE, main =
paste(symb1,symb2,sep="/"))
}
table.Drawdowns(ret1)
getEndEq(account.st, Sys.time())
table.AnnualizedReturns(ret1)

	[[alternative HTML version deleted]]



More information about the R-SIG-Finance mailing list