Strategy Functions
Total Return Index
EPFR’s function ret.to.idx() uses other functions within library('EPFR.r') to compute a total-return index series, from percentage returns. To calculate the total return index we sum 1 plus today’s percentage return multiplied by the previous days total-return index.
ret.to.idx <- function(x) {
# -----------------------------------------------------------------
# Name : ret.to.idx
# Args : x = a file of total returns indexed so that time runs forward
# Output : computes a total-return index
# -----------------------------------------------------------------
if (is.null(dim(x))) {
z <- x
w <- !is.na(z)
n <- find.data(w, T)
m <- find.data(w, F)
if (n > 1) n <- n - 1
z[n] <- 100
while (n < m) {
n <- n + 1
z[n] <- (1 + zav(z[n])/100) * z[n - 1]
}
} else {
z <- fcn.mat.vec(ret.to.idx, x,, T)
}
#
# RETURN RESULT
z
}Compunding Flows
EPFR’s function compound.flows() uses other functions within library('EPFR.r') to compute a rolling cumulative sum of flows over a trailing lookback period (typically we use 20 days).
compound.flows <- function(x, y, n = F) {
# -----------------------------------------------------------------
# Args : x = a matrix/data-frame of percentage flows
# : y = number of trailing rows to compound/sum
# : n = if T, flows get summed. Otherwise they get compounded.
# Output : compounded flows over <n> trailing periods indexed by last day in the flow window
# -----------------------------------------------------------------
#
# FIND NA's :
# creates new df with value of 1 for all non-NA flows, else NA
h <- nonneg(mat.to.obs(x))
#
# PRELIMINARIES :
# convert all NA values to 0 in order to do computations
# convert to log form
z <- zav(x)
if (!n) z <- log(1 + z/100)
#
# ROLLING WINDOWS
# calculate rolling sum
# reverse log form
z <- mat.rollsum(z, y)
if (!n) z <- 100 * exp(z) - 100
#
# ENSURE FINAL DATE IN EACH ROLLING WINDOW IS NON-NA
# multiply compounded flow matrix by h (1/NA df)
z <- z * h
#
# RETURN RESULT
z
}Please refer to the library documentation for more details.
Model
EPFR’s function bbk() creates a standardized backtest model based on our inputs (e.g. ranking factors into 5 equal baskets based on compounded flows).
bbk <- function(x, y, floW = 1, retW = 5, nBin = 5, doW = NULL, sum.flows = F, lag = 0, delay = 2, idx = NULL, prd.size = 1, sprds = F) {
# -----------------------------------------------------------------
# Args : x = predictor indexed by yyyymmdd or yyyymm (compounded percentage flow)
# : y = total return index indexed by the same date format as <x>
# : floW = number of <prd.size>'s over which the predictor should be compounded/summed
# : retW = return window in days or months depending on whether <x> is YYYYMMDD or YYYYMM
# : nBin = number of bins to divide the variable into
# : doW = day of the week you will trade on (5 = Fri)
# : sum.flows = T/F depending on whether <x> should be summed or compounded
# : lag = predictor lag in days or months depending on whether <x> is YYYYMMDD or YYYYMM
# : delay = delay in knowing data in days or months depending on whether <x> is YYYYMMDD or YYYYMM
# : idx = the index within which you are trading
# : prd.size = size of each period in days or months depending on whether <x> is YYYYMMDD or YYYYMM
# : sprds = T/F depending on whether spread changes, rather than returns, are needed
# Output : standard model output
# -----------------------------------------------------------------
#
# GET DATA
x <- bbk.data(x, y, floW, sum.flows, lag, delay, doW, retW, idx, prd.size, sprds)
#
# BIN RETURNS
z <- lapply(bbk.bin.xRet(x$x, x$fwdRet, nBin, T, T), mat.reverse)
#
# SUMMARY
z <- c(z, bbk.summ(z$rets, z$bins, retW, ifelse(is.null(doW), 1, 5)))
#
# RETURN RESULT
z
}Please refer to the library documentation for more details.
Example
Step 0: Download and read the files
Users can download the corresponding percentage flow and percentage returns files available in the users FTP, under the folders Strategies/daily and Returns/daily. For this example, we download the full file paths /Strategies/daily/FloPctSector-US-daily.csv and /Returns/PsuedoReturns-Sector-US-daily.csv, and save to our local folder EPFR.
For convenience, we can save the local paths to these files as flow.file and ret.file, then we can read the files.
flow.file <- "C:\\EPFR\\FloPctSector-US-daily.csv"
ret.file <- "C:\\EPFR\\PsuedoReturns-Sector-US-daily.csv"Step 1: Ensure that x and y have the same column names and order
Step 2: Compound (or Sum Flows) over lookback period (\(y\))
EPFR’s function compound.flows() uses other functions within library('EPFR.r') to either compound, or compute a rolling cumulative sum of flows, over a trailing lookback period (typically we use y = 20 days).
Step 3: Convert the percentage returns to total return index
EPFR’s function ret.to.idx() will convert our percentage returns data \(y\) to total index returns indexed, so that time moves forward. We also use the functions map.rname() to ensure the row names of the matrices line up with our flow file and ret.idx.gaps.fix() to replace any NA values.
y[is.na(y)] <- 0
y <- ret.to.idx(map.rname(y, dimnames(x)[[1]])) # CONVERT TO A TOTAL-RETURN INDEX
y <- ret.idx.gaps.fix(y)Step 4: Define variables
hz <- c(5, 10, 20, 45, 65, 130) # RETURN HORIZON (IN WEEKDAYS OR MONTHS) - holding periods
nBin <- 5 # Number of bins (to sort)
doW <- 5 # Day of the week to trade on
delay <- 2 # Delay in getting the data (able to trade on data @ T+2)
idx <- NULL # Optional: Universe that is being used for signal (e.g. ACWI/EM/EAFE/G10)Step 5: Create standardized backtest model
Step 6: Evaluate output
\(Z\) has 4 different outputs that you can view. Within the final “rets” output, the overall portfolio will go long the top quintile and short the bottom. However, the returns of each individual quintile are also presented.
z[["raw"]] # Flow percentage compounded over trailing 20 days
z[["raw.fwd.rets"]] # One-week ahead return (Friday to Friday)
z[["bins"]] # Twenty-day flow percentage ranked into quintiles (computed only where forward returns are available)
z[["rets"]] # Quintile Returns over the equal-weight universe, *column TxB tells us top - bottom = Q1 - Q5 = overall portfolio returnsStep 7: Re-balance as needed
Strategies need to be rebalanced and the below code outputs summary statistics across 6 different rebalancing options found in \(hz\). Depending on how the signal we are backtesting is indexed, holding periods can be specified in a daily or monthly format. For this example, we have a daily signal and \(hz\) contains options for a weekly (5 days), fortnightly (10 days), monthly (20 days), bi-monthly (45 days), quarterly (65 days), and semi-annual (130 days) rebalanced portfolio.
fcn <- function(retW) {as.matrix(bbk(x, y, 1, retW, nBin, doW, T, 0, delay)$summ)} # DEFINE SUMMARY FUNCTION
sapply(split(hz, hz), fcn, simplify = "array") # WRITE SUMMARIESStep 8: Find the mean one-week returns per calendar year