Nothing
################################################################################
##
## $Id: backtest.R 1622 2010-02-18 17:50:34Z enos $
##
## Result object for a backtest
##
################################################################################
## "in.var", "ret.var", "by.var", and "date.var" are character
## strings, often used as labels by the accessor method. Because the
## original data from which the backtest object was constructed is not
## preserved, they do not refer to actual variables. "buckets" is a
## numeric vector of length 2, specifying the number of in.var and
## by.var buckets. "results" is a five dimensional array storing the
## results of the backtest.
setClass("backtest", representation(in.var = "character",
ret.var = "character",
by.var = "character",
date.var = "character",
buckets = "numeric",
results = "array",
ret.stats = "array",
turnover = "array",
natural = "logical",
do.spread = "logical",
by.period = "logical",
overlaps = "numeric"),
prototype(in.var = "in.var",
ret.var = "ret.var",
by.var = character(),
date.var = character(),
buckets = numeric(),
results = array(dim = c(1,1,1,1,1)),
ret.stats = array(dim = c(1,6)),
turnover = array(),
natural = FALSE,
do.spread = TRUE,
by.period = TRUE,
overlaps = 1),
validity = function(object){
if(length(object@in.var) == 0){
return("in.var not specified")
}
if(length(object@ret.var) == 0){
return("ret.var not specified")
}
if(length(object@by.var) > 0 && length(object@date.var) > 0){
return("Both by.var and date.var specified")
}
if(length(dim(object@results)) != 5){
return("Incorrect number of dimensions in results slot")
}
return(TRUE)
}
)
## Lists the .vars used in the backtest
setMethod("show",
signature(object = "backtest"),
function(object){
cat("An object of class backtest:\nin.vars: ",
paste(object@in.var, collapse = ", "), " (buckets: ",
object@buckets[1], ")\n",
sep = "")
if(length(object@by.var) > 0){
cat("by.var: ", object@by.var, " (buckets: ",
object@buckets[2], ")\n", sep = "")
}
if(length(object@date.var) > 0){
cat("date.var: ", object@date.var, " (buckets: ",
object@buckets[2], ")\n", sep = "")
}
cat("ret.vars: ", paste(object@ret.var, collapse = ", "),
"\n", sep = "")
}
)
## Prints a header listing the .vars, then prints the table returned
## by the "spreads" method
setMethod("summary",
signature(object = "backtest"),
function(object, ...){
## Header
cat("Backtest conducted with:\n\n",
length(object@in.var), ifelse(length(object@in.var) == 1,
" in.var: ", " in.vars: "),
paste(object@in.var, collapse = ", "), ";\n",
length(object@ret.var), ifelse(length(object@ret.var) == 1,
" ret.var: ", " ret.vars: "),
paste(object@ret.var, collapse = ", "), ";\nand ",
ifelse(length(object@by.var) > 0,
paste("by.var: ", object@by.var, sep = ""), "no by.var"), ";\n",
ifelse(isTRUE(object@do.spread),
"do.spread: TRUE", "do.spread: FALSE"), ";\n",
ifelse(isTRUE(object@by.period),
"by.period: TRUE", "by.period: FALSE"),
".\n\n", sep = "")
## Matrix
print(summaryStats(object)[, !names(summaryStats(object))
%in% c("CI(low)",
"CI(high)",
"TURNOVER")])
cat("\n")
if(isTRUE(object@do.spread)){
if(object@natural){
if(length(object@in.var) == 1){
cat("average turnover: ", .bt.mean(turnover(object)),
"\nmean spread: ", mean(summaryStats(object)[, "spread"]),
"\nsd spread: ", sd(summaryStats(object)[, "spread"]),
"\nraw sharpe ratio: ", .bt.sharpe(object), "\n\n",
sep = "")
}
else{
for(i in object@in.var){
x <- summaryStats(object)
cat("summary stats for in.var = ", i,
":\n\naverage turnover: ",
.bt.mean(turnover(object))[1,i],
"\nmean spread: ",
colMeans(summaryStats(object), na.rm = TRUE)[i],
"\nsd spread: ", sd(x[,i], na.rm = TRUE),
"\nraw sharpe ratio: ", .bt.sharpe(object)[,i],
"\n\n", sep = "")
}
}
}
}
}
)
## Returns a data frame summarizing the results of the backtest. The
## entries of the data frame contain means in cases 1, 2, and 4, and
## spreads in cases 3 and 5. When a table of means is returned, the
## ".bt.spread" function is called and summary spread data is attached
## to the right side of the data frame. If a date.var is used,
## ".bt.mean" is called and mean summary data are attached to bottom
## of the data frame.
setMethod("summaryStats",
signature(object = "backtest"),
function(object, mean = FALSE){
num.in <- length(object@in.var)
num.ret <- length(object@ret.var)
if(length(object@by.var > 0)){
by.present <- TRUE
date.present <- FALSE
}
else{
by.present <- FALSE
if(length(object@date.var > 0)){
date.present <- TRUE
}
else{
date.present <- FALSE
}
}
## Case 1: 1 in.var, 1 ret.var
if(num.in == 1 && num.ret == 1){
output <- object@results[1,1, , ,"means"]
n <- object@results[1,1, , ,"counts"]
## When one dimension of a 2D matrix has length 1,
## the subsets above return vectors.
if(is.null(dim(output))){
output <- array(output, dim = c(1, length(output)),
dimnames = list("pooled", names(output)))
n <- array(n, dim = dim(output),
dimnames = dimnames(output))
}
if(isTRUE(object@do.spread)){
spread <- .bt.spread(output, n, object@ret.stats[1,"sd"])
output <- cbind(output, spread)
}
if(object@natural){
turnover <- object@turnover
dimnames(turnover)[[2]] <- "TURNOVER"
output <- cbind(output, turnover)
output <- rbind(output, .bt.mean(output))
}
}
## Case 2: multiple in.vars, no by.var
if(num.in > 1 && num.ret == 1 && !by.present &&
!date.present){
output <- object@results[1, ,1, ,"means"]
n <- object@results[1, ,1, ,"counts"]
spread <- .bt.spread(output, n, object@ret.stats[1,"sd"])
output <- cbind(output, spread)
}
## Case 3: multiple in.vars, with by.var or date.var
if(num.in > 1 && num.ret == 1 && (by.present || date.present)){
output <- t(object@results[1, , ,dim(object@results)[4],1] -
object@results[1, , ,1,1])
if(date.present && mean){
output <- rbind(output, .bt.mean(output))
}
}
## Case 4: single in.var, multiple ret.vars
if(num.in == 1 && num.ret > 1){
output <- object@results[ ,1,1, ,"means"]
n <- object@results[ ,1,1, ,"counts"]
if(isTRUE(object@do.spread)){
spread <- .bt.spread(output, n, object@ret.stats[ ,"sd"])
output <- cbind(output, spread)
}
}
## Case 5: multiple in.vars and ret.vars
if(num.in > 1 && num.ret > 1){
output <- object@results[ , ,1,dim(object@results)[4],1] -
object@results[ , ,1,1,1]
}
x <- data.frame(output)
names(x) <- dimnames(output)[[2]]
x
}
)
## Returns a list of matrices, with one matrix for each in.var,
## containing the means data.
setMethod("means",
signature(object = "backtest"),
function(object){
mean.list <- list()
for(i in object@in.var){
mean.list <- append(mean.list,
list(object@results[ ,i, , ,"means"]))
}
names(mean.list) <- object@in.var
mean.list
}
)
## Returns a list of matrices, one matrix for each in.var, containing
## the counts data
setMethod("counts",
signature(object = "backtest"),
function(object){
count.list <- list()
for(i in object@in.var){
count.list <- append(count.list,
list(object@results[1,i, , ,"counts"]))
}
names(count.list) <- object@in.var
count.list
}
)
## This function computes the counts of non-NA values that went into
## the calculation of spreads displayed by backtests's summary()
## function. It is different from counts because it displays the sum
## of counts from all buckets (or lowest and highest only), thus
## allowing for output that matches the format of spreads output.
setMethod("totalCounts",
signature(object = "backtest"),
function(object, low.high.only = FALSE){
counts <- data.frame(do.call("cbind",
lapply(counts(object), function(x){
if(isTRUE(low.high.only))
x <- x[c("low", "high")]
rowSums(x)
})))
counts
}
)
## Same as the "counts" method, except that the total counts for each
## row and column are added to the margins.
setMethod("marginals",
signature(object = "backtest"),
function(object){
body <- counts(object)
for(i in 1:length(body)){
if(is.null(dim(body[[i]]))){
total <- sum(body[[i]], na.rm = TRUE)
body[[i]] <- append(body[[i]], total)
names(body[[i]])[length(body[[i]])] <- "TOTAL"
}
else{
total <- rowSums(body[[i]], na.rm = TRUE)
body[[i]] <- cbind(body[[i]], TOTAL = total)
total <- colSums(body[[i]], na.rm = TRUE)
body[[i]] <- rbind(body[[i]], TOTAL = total)
}
}
body
}
)
## Returns a list of matrices, one matrix for each in.var, containing
## the NAs data
setMethod("naCounts",
signature(object = "backtest"),
function(object){
na.list <- list()
for(i in object@in.var){
na.list <- append(na.list, list(object@results[ ,i, , ,"NAs"]))
}
names(na.list) <- object@in.var
na.list
}
)
## Accessor Methods
## Returns the turnover for natural portfolios. Passing a "mean"
## argument will append the mean of the turnover(s) as the last row of
## the matrix
setMethod("turnover",
signature(object = "backtest"),
function(object, mean = FALSE){
if(!isTRUE(object@natural)){
stop("Cannot calculate turnover if not a natural backtest.")
}
if(isTRUE(mean)){
return(rbind(object@turnover, .bt.mean(turnover(object))))
}
object@turnover
}
)
## Returns the confidence intervals for each spread if for all cases
## except multiple in.var and a by.var or multiple in.var and multiple
## ret.var. Must vectorize or loop to get these.
setMethod("ci",
signature(object = "backtest"),
function(object){
## array(dim = c(1, length(object@ret.var), length(object@in.var)))
if((length(object@in.var) == 1 && length(object@ret.var) == 1)
|| (length(object@in.var) == 1 && length(object@ret.var > 1))){
summaryStats(object)[, c("CI(low)", "spread", "CI(high)")]
}
}
)
## Plotting methods
setMethod("plot",
signature(x = "backtest", y = "missing"),
function(x, type = "return", ...){
if(!x@natural){
stop("Can only plot natural backtests")
}
if(type == "return"){
btplot <- .plot.return(x, ...)
}
else if(type == "turnover"){
btplot <- .plot.turnover(x, ...)
}
else if(type == "cumreturn"){
btplot <- .plot.cumreturn(x, ...)
}
else if(type == "cumreturn.split"){
btplot <- .plot.cumreturn.split(x, ...)
}
else{
stop("Unknown type specified.")
}
invisible(btplot)
}
)
## Plots turnover for each period specified by "date.var"
.plot.turnover <- function(object, main = "Turnover",
xlab = "Date", ylab = "Turnover (%)", ...){
turnovers <- object@turnover[-1,]
turnovers <- 100 * as.data.frame(turnovers)
## Stop informatively if we can't call as.Date on what was the
## contents of the date.var column.
if(inherits(try(as.Date(rownames(turnovers)), silent = TRUE), "try-error")){
stop(paste("Error converting dates: Please ensure contents of date.var",
"column cate be converted to Date's via as.Date."))
}
datevar <- as.Date(rownames(turnovers))
invars <- names(turnovers)[1]
if(length(names(turnovers)) > 1){
for(i in 2:length(names(turnovers))){
invars <- paste(invars, "+", names(turnovers)[i])
}
}
form <- as.formula(paste(invars, "~", "datevar"))
btplot <- xyplot(form, turnovers, type = "b", horizontal = FALSE,
auto.key = TRUE, scales = list(tick.number = 10),
ylim = c(0, 100), main = main, xlab = xlab, ylab = ylab, ...)
print(btplot)
return(btplot)
}
## Plots returns for each period specified by "date.var"
.plot.return <- function(object, main = "Spread Return",
xlab = "Date", ylab = "Return (%)", ...){
spreads <- object@results[1, , ,dim(object@results)[4],1] -
object@results[1, , ,1,1]
## Fix for 1D array
if(is.null(dim(spreads))){
spreads <- array(spreads, dim = c(1, length(spreads)),
dimnames = list(object@in.var,
names(spreads)))
}
spreads <- 100 * data.frame(t(spreads))
invars <- names(spreads)[1]
if(length(names(spreads)) > 1){
for(i in 2:length(names(spreads))){
invars <- paste(invars, "+", names(spreads)[i])
}
}
form <- as.formula(paste(invars, "~", "date"))
spreads$date <- rownames(spreads)
btplot <- barchart(form, spreads, horizontal = FALSE, stack = FALSE,
auto.key = TRUE,
scale = list(
y = list(tick.number = 10),
x = list(tick.number = 10, rot = 90)),
origin = 0,
main = main, xlab = xlab, ylab = ylab, ...)
print(btplot)
return(btplot)
}
.plot.cumreturn <- function(object, main = "Backtest Fanplot", xlab = "Date",
ylab = "Cumulative Returns", ...){
## Function to compute cumsums of a matrix, by columns
cum.matrix <- function(x){
for(i in 1:dim(x)[2]){
x[ ,i] <- cumsum(x[ ,i])
}
x
}
returns <- means(object)
## Calculate cumulative returns
returns <- lapply(returns, cum.matrix)
new.returns <- data.frame()
for(i in 1:length(returns)){
this.returns <- data.frame(returns[[i]])
## We need quantile names that don't begin with a number so that
## the formula used with lattice graphics works. But, the "Xn"
## naming scheme that is data.frame's default needs to get changed
## to "Qn".
names(this.returns) <- gsub("X([1-9]+)", "Q\\1",
names(this.returns),
perl = TRUE)
this.returns$group <- names(returns)[i]
if(inherits(try(as.Date(rownames(this.returns)), silent = TRUE), "try-error")){
stop(paste("Error converting dates: Please ensure contents of date.var",
"column cate be converted to Date's via as.Date."))
}
this.returns$date <- as.Date(rownames(this.returns))
new.returns <- rbind(new.returns, this.returns)
}
invars <- names(new.returns)[1:(length(names(new.returns)) - 2)]
lhs <- paste(rev(invars), collapse = " + ")
form <- as.formula(paste(lhs, "~ date | group"))
btplot <- xyplot(form, new.returns,
type = "l", horizontal = FALSE,
auto.key = list(
space = "right",
points = FALSE,
lines = TRUE,
cex = 0.75,
title = "quantile",
cex.title = 1),
as.table = TRUE,
main = "Cumulative Quantile Return",
xlab = xlab,
ylab = ylab, ...)
print(btplot)
return(btplot)
}
.plot.cumreturn.split <- function(object,
xlab = "Date",
ylab = "Return (%)", ...){
## No notion of spread return without 2 or more buckets.
stopifnot(isTRUE(object@buckets[1] >= 2))
## Data setup. Calculate cumulative returns and spread returns
## before plotting.
## Function to compute the cumulative return of each quantile, and
## the spread cumulative return. Since we're dealing with portfolio
## return and fixed theoretical assets, we take a cumulative sum
## without compounding.
## Also, convert return into percent.
cumulate <- function(x){
x <- data.frame(x)
for(i in 1:ncol(x)){
x[[i]] <- 100 * cumsum(x[[i]])
}
x$spread <- x[[ncol(x)]] - x[[1]]
x
}
returns <- means(object)
## Calculate cumulative returns
returns <- lapply(returns, cumulate)
new.returns <- data.frame()
for(i in 1:length(returns)){
this.returns <- returns[[i]]
## We need quantile names that don't begin with a number so that
## the formula used with lattice graphics works. But, the "Xn"
## naming scheme that is data.frame's default needs to get changed
## to "Qn".
names(this.returns) <- gsub("X([1-9]+)", "Q\\1",
names(this.returns),
perl = TRUE)
this.returns$group <- names(returns)[i]
if(inherits(try(as.Date(rownames(this.returns)), silent = TRUE), "try-error")){
stop(paste("Error converting dates: Please ensure contents of date.var",
"column cate be converted to Date's via as.Date."))
}
this.returns$date <- as.Date(rownames(this.returns))
new.returns <- rbind(new.returns, this.returns)
}
## Plotting
##
## There are two regions to this plot, the top plot of spread return
## and the bottom, larger plot of cumulative quantile return.
## Compute ylim as 50% more space than we would need normally.
y.max <- max(new.returns$spread)
y.min <- min(new.returns$spread)
y.dist <- y.max - y.min
ylim <- c(y.min - 0.5 * y.dist, y.max + 0.05 * y.dist)
## Panel function
top.panel <- function(x, y, subscripts, ...){
op <- options(digits = 2)
## Annotate panel with total return.
pushViewport(viewport(x = 0.05, y = 0.95))
grid.text(paste("Total return:", format(tail(y, n = 1))),
hjust = 0, vjust = 1, gp = gpar(cex = 0.75))
popViewport()
## Annotate panel with worst drawdown. Remember, y is now in pct.
dd <- 1 - (y/100 + 1)/cummax(y/100 + 1)
dd.max <- max(dd)
## If there are two troughs of equal value the below makes the
## largest drawdown extend to the furthest trough.
dd.end.idx <- max(which.max(dd), length(dd) - which.max(rev(dd)) + 1)
dd.end <- x[dd.end.idx]
dd.start.idx <- max(which(dd == 0 & seq(dd) < dd.end.idx))
dd.start <- x[dd.start.idx]
dd.ret <- y[dd.end.idx] - y[dd.start.idx]
if(all(dd == 0)) dd.ret <- "--"
pushViewport(viewport(x = 0.05, y = 0.05))
grid.text(paste("Worst drawdown:", format(dd.ret)),
hjust = 0, vjust = 0, gp = gpar(cex = 0.75))
popViewport()
panel.xyplot(x, y, col.line = "black", ...)
if(!all(dd == 0)){
panel.lines(x[dd.start.idx:dd.end.idx], y = y[dd.start.idx:dd.end.idx],
col ="red", default.units = "native",...)
}
options(op)
}
## I'm fixing the layout of the top and bottom plots to have one row
## of panels. The layout of date labels doesn't seem to fare very
## well using a layout other than the default, however. That said,
## this plot is most useful with a couple in.var's. I should allow
## the user to be able to modify such layout characteristics.
top.plot <- xyplot(formula("spread ~ date | group"), new.returns,
panel = top.panel,
type = "l",
horizontal = FALSE,
as.table = TRUE,
layout = c(length(object@in.var),1),
scale = list(x = list(alternating = 1)),
main = "Cumulative Spread Return",
ylim = ylim,
xlab = NULL,
ylab = ylab,
...)
## The bottom plot doesn't use spread, so subtract the last _3_
## columns when computing in.var's.
invars <- names(new.returns)[1:(length(names(new.returns)) - 3)]
lhs <- paste(rev(invars), collapse = " + ")
form <- as.formula(paste(lhs, "~ date | group"))
bottom.panel <- function(x, y, subscripts, groups, ...){
panel.superpose(x, y, subscripts, groups, ...)
}
bottom.plot <- xyplot(form, new.returns,
panel = bottom.panel,
type = "l", horizontal = FALSE,
auto.key = list(
x = 0.1,
y = 0.8,
corner = c(0,1),
points = FALSE,
lines = TRUE,
cex = 0.75,
title = "quantile",
cex.title = 1),
as.table = TRUE,
layout = c(length(object@in.var),1),
scale = list(x = list(alternating = 1)),
main = "Cumulative Quantile Return",
xlab = xlab,
ylab = ylab, ...)
## Calculate panel width. Assuming 0.15 npc used for margins (not a
## very safe assumption). So far I don't have any other good way to
## ensure that the aspects of the panels in the two lattice plots
## are the same.
panel.width <- 0.85 / length(object@in.var)
print(top.plot, position = c(0, 0.6, 1, .98),
panel.width = list(panel.width, "npc"), more = TRUE)
print(bottom.plot, position = c(0, 0, 1, 0.6),
panel.width = list(panel.width, "npc"))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.