knitr::opts_knit$set(
  root.dir = normalizePath('..')
)
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  echo = TRUE,
  warning = FALSE,
  error = FALSE
)

library(xts)
library(quantmod)
AggregateByInstrument <- function(X, type) {
  #' Aggregate instrument by type
  #' @param X A `list` of `xts` objects storing instruments data
  #' @param type A character specifying `X` inner list name or index

  X <- lapply(X, function(x) {
    y <- x[[type]]
    z <- Reduce(cbind, y)[complete.cases(Reduce(cbind, y)), ]
    colnames(z) <- names(y)
    return(z)
  })
  return(X)
}

AggregateByType <- function(X, y, symbols, type) {
  #' Aggregate type by instrument
  #' @param X A `list` of `xts` objects storing instruments data
  #' @param y A character vector, the names of objects to extract from `symbols` in `X` (e.g., momentum signals, volatility estimators)
  #' @param symbols A character vector, specifying the instruments in `X`
  #' @param type A character specifying `X` inner list name or index

  X <- AggregateByInstrument(X, type=type)
  X <- Reduce(cbind, X)
  idxs <- matrix(1:(length(symbols) * length(y)), length(y))
  idxs <- asplit(idxs, 1)
  X <- lapply(idxs, function(j) {
    jinst <- X[complete.cases(X[, j]), j]
    return(jinst)
  })
  names(X) <- y
  return(X)
}

AvgCor <- function(X, y, symbols, type, ...) {
  #' Average correlation aggregated by instrument
  #' @param X A `list` of `xts` objects storing instruments data
  #' @param y A character vector, the names of objects to extract from `symbols` in `X` (e.g., momentum signals, volatility estimators)
  #' @param symbols A character vector, specifying the instruments in `X`
  #' @param type A character specifying `X` inner list name or index preceding `y`s

  # X aggregated by y
  if (!missing(type)) {
    X <- AggregateByInstrument(X, type=type)
  }
  # y aggregated by X
  X.all <- Reduce(function(...) merge(..., all=TRUE), X)
  idxs <- matrix(1:(length(symbols) * length(y)), length(y))
  idxs <- asplit(idxs, 1)
  X.avg <- lapply(idxs, function(j) {
    x.inst <- X.all[complete.cases(X.all[, j]), j]
    x.inst.avg <- xts::xts(rowMeans(x.inst), zoo::index(x.inst))
    return(x.inst.avg)
  })
  X.avg <- Reduce(function(...) merge(..., all=TRUE), X.avg)
  colnames(X.avg) <- y
  # Calc correlation
  avg.cor <- cor(X.avg, ...)
  return(
    list(
      X.avg=X.avg,
      avg.cor=avg.cor
    )
  )
}

SharpeRatioDownsideRisk <- function(r, rf=0, ...) {
  #' Downside-Risk Sharpe Ratio, Ziemba (2015)
  #' @param r An `xts` object, the return series.
  #' @param rf A numeric, or vector or `xts` object of the same length of `r`.
  #' @param ... Any other pass through paramater.
  r.xs <- r - rf
  avg.bench <- mean(rf, na.rm=TRUE)
  dwn.risk <- sum(min(r.xs, avg.bench)^2 - avg.bench) / (nrow(r.xs) - 1)
  # rxs.annual <- PerformanceAnalytics::Return.annualized(r.xs, ...)
  drsr <- mean(r.xs, na.rm=TRUE)/sqrt(2 * dwn.risk)
  return(drsr)
}

Introduction

These notes aim at reproducing \textcites{baltas-kosowski-2012, baltas-kosowski-2013}. The work has three fundamental aims: first of all, to document empirical time-series momentum patterns, second to examine the information content of traditional and new signals to capture assets' momentum and third, to investigate a family of volatility estimators and assess their efficiency from a momentum investing point of view.

Data and methodology

Data series we analyze are obtained from Bloomberg. For reproducibility purposes, we made efforts to re-create a data set as similar as possible to the one studied by \textcite{baltas-kosowski-2012}. Their data set consists of intra-day future prices of 12 classes of futures contracts, at a 30 minutes frequency. We analyze daily data on the same set of instruments. Next to the asset, for reference we also indicate Bloomberg tickers, class and the Exchange where the symbol considered is traded at. Futures contracts include: 6 commodities futures, among which Cocoa (CC1, Agricultural, ICE), Wheat (W1, Agricultural, CBOT), Crude Oil (CL1, Energy, CBOT), Natural Gas (NG1, Energy, Nymex), Copper (HG1, Industrial Metals, Comex), and Gold (GC1, Precious Metals, Comex); 2 equity indexes, S&P500 (SP1) and Eurostoxx50 (VG1); 2 FX rates: US Dollar Index (DX1) and EUR/USD (EC1); lastly, 2 interest rates: 3-month Eurodollar deposits (ED1) and 10-year US T-Note (TY1).

Of methodological relevance is that authors adjust futures quotes series for rollovers, so to limit attention to the most liquid contract per instrument. Our data set is simpler, in that it only contemplates each instrument traded in a single Exchange, as indicated above. In addition, authors have to align among the various Exchanges where contracts are traded in order to avoid the introduction of undue lead-lag effects in the analyses. Because our data set consists of daily data this step does not apply and we only refer to a end-of-day characterization of (all) markets. It should be clear that authors' framework is more rigorous, especially in sight of volatility estimations where high-frequency data aim at more or less drastically reduce microstructure noise. Unfortunately we do not have readily available intra-day time-series and therefore use daily data.

Furthermore, as will be better detailed in following sections, we consider daily settlement futures prices as opposed to closing prices (see CME Group Daily Settlement Procedures for information). This is chiefly dictated by the fact closing price data is not readily available in our data set. However, we observe that often the two series are row-wise equal and when they're not prices result to be quite close indeed. However, there are important conceptual differences between the two and thus this "substitution" shouldn't take place when full OCHL data is available.

# Commodities, equity indexes, currencies, and interest rates
# TODO: Eurostoxx50 in USD?
# TODO: 'EC1' for EUR/USD FX rate?
LoadFuturesData <- function(symbols, path) {
  #' @param symbols A character vector, specifying symbols to be loaded. See 'Details'
  #' @param path A character, the relative path to `symbols`. See 'Details'
  #' @details
  #' `symbols` must match corresponding `.RData` files names in `path` base directory.
  #' `path` needs not to be to a `symbols` unique directory (e.g., may keep contracts organized by type in separate folders). However, if several directories after in the relative `path` match `symbols` watch out for potential overwriting. If `path` is missing `sandbox/data/FuturesData/` will be used.
  if (missing(path))
    path <- file.path('sandbox', 'data', 'FuturesData')

    symbols.files <- normalizePath(list.files(path,
                                              pattern ='*.RData',
                                              recursive = TRUE,
                                              full.names = TRUE)
                                   )
  symbols.files.avail <- unlist(strsplit(basename(symbols.files), split='.RData'))

  sm <- match(symbols, symbols.files.avail)

  for (s in symbols.files[sm]) { load(s, envir = globalenv())

    }

  }

symbols <- c('CC1', 'CL1', 'DX1', 'EC1', 'ED1', 'GC1', 'HG1', 'NG1', 'SP1', 'TY1', 'VG1', 'W 1')
path <- paste0(getwd(),"/sandbox/data/FuturesData/data/Balta-Kosowski")

LoadFuturesData(symbols = symbols, path = path)
# Combine future data to list
futures.data <- mget(symbols, inherits = TRUE)

symbols.vars <- c('Open', 'High', 'Low', 'Close', 'Volume', 'Open.Interest')

futures.data <- lapply(futures.data, function(x) {
  # For all but equity indexes, 'Close' (closing price) is in reality 'Settle' (daily settlement price).
  # Renaming is chiefly due to computational and illustration convenience.
  colnames(x) <- symbols.vars[1:ncol(x)]
  # Drop incomplete cases
  #x <- x[complete.cases(x), ]
  # Returns
  x$Return <- PerformanceAnalytics::Return.calculate(x$Close, 'discrete')
  x$Comp.Return <- PerformanceAnalytics::Return.calculate(x$Close, 'log')
  # data.frame(
  #   Date=zoo::index(x),
  #   zoo::coredata(x)
  # )
  return(x)
})
futures.data.monthly <- lapply(futures.data, function(x) x[xts::endpoints(x), ])

# Futures contracts summary statistics
futures.summary <- lapply(futures.data.monthly, function(x) {
  # x.ret <- x$Return
  x.ret <- x$Comp.Return
  avg <- PerformanceAnalytics::Return.annualized(x.ret, scale=12) * 100
  # vol <- PerformanceAnalytics::StdDev.annualized(x.ret, scale=12) * 100
  vol <- sd(x.ret, na.rm=TRUE) * sqrt(12) * 100
  sr <- PerformanceAnalytics::SharpeRatio.annualized(x.ret)
  drsr <- SharpeRatioDownsideRisk(x.ret, scale=1)
  sk <- PerformanceAnalytics::skewness(x.ret, na.rm=TRUE)
  kr <- PerformanceAnalytics::kurtosis(x.ret, na.rm=TRUE)
  jb <- tseries::jarque.bera.test(na.omit(x.ret))$p.value
  lb <- Box.test(as.ts(x.ret), type='Ljung-Box')$p.value
  lf <- nortest::lillie.test(x.ret)$p.value
  tdf <- tryCatch(
    # No closed form solution for t-Student DoF, MLE numerical optimization may fail
    MASS::fitdistr(x.ret, densfun='t')$estimate['df'],
    error = function(cond) {
      message(
        gettextf(
          'In %s:\n %s NA returned. See %s.', 
          'tStudent.DF', cond, '\"?MASS::fitdistr\"'
        )
      )
      return(NA)
    }
  )
  out <- rbind(
    'Annual.Return'=as.numeric(avg)
    , 'Annual.Volatility'=as.numeric(vol)
    , 'Annual.SR'=as.numeric(sr)
    , 'Annual.DRSR'=as.numeric(drsr)
    , 'Skewness'=sk
    , 'Kurtosis'=kr
    , 'JarqueBera.pvalue'=jb
    , 'LjungBox.pvalue'=lb
    , 'Lilliefors.pvalue'=lf
    , 'tStudent.DF'=as.numeric(tdf)
  )
  return(out)
})

futures.summary.tab <- t(Reduce(cbind, futures.summary))

rownames(futures.summary.tab) <- names(futures.data.monthly)

futures.summary.tab

Time-series Momentum Strategies

Time-series momentum is defined analogously to \textcite{moskowitz-ooi-pedersen-2012}. Generally, univariate time-series momentum is the trading rule that imposes to take a long/short position on a given asset depending on a metric of its past performance. The rule is completely identified by two parameters, namely the lookback period $J$ over which such performance is taken into account and the holding period $K$ during which the asset is effectively held in the portfolio. Since the same holds in principle for every tradable assets, time-series momentum can and is also be aggregated at portfolio level. Adopting \textcite{baltas-kosowski-2012} symbols and notation for readability purposes, such quantity is expressed as $$ R^{\textrm{TS}}(t,t+K) = \sum_{i=1}^{M} X_{i}(t-J, t) \frac{10\% / \sqrt{M}}{\sigma_{i}(t,D)} R_{i}(t,t+K) $$ where $M$ is the number of available assets, $X_{i}(t-J, t)$ the momentum signal, $\sigma_{i}(t,D)$ the volatility estimates, and $\sqrt{M}$ is the scaling factor used in order to achieve a 10% ex-ante volatility.

The outline of subsections is as follows. First of all, in order to compute the above quantity we need a momentum signal and a volatility estimator. Each class of such elements shall receive individual treatment: many alternatives have consolidated in decades of research, each presents pros and cons by its own and in relation with the objectives of the particular study we aim to replicate.

Volatility estimation

Traditional daily volatility estimators, like the standard deviation of daily past returns, provide relatively noisy volatility estimates. Authors study volatility estimators, the realized variance of \textcite{andersen-bollerslev-1998} and volatility range estimators including the estimators of \textcites{parkinson-1980, garman-klass-1980, rogers-satchell-1991, yang-zhang-2000}.

Denote $S_{o}(t)$, $S_{h}(t)$, $S_{l}(t)$, $S_{c}(t)$ for the opening, high, low, and closing prices in the time period $t$, for a given asset. In other words, the asset OHLC data. Variance estimators investigated are defined as follows, volatility being the square root.

  1. Realized variance (RV)
    $$ \sigma_{\textrm{RV}}^{2}(t) = \sum_{t}^{T}R_{t}^{2} $$ with $R_{t}$ the continuously compounded returns.

  2. Parkinson estimator (PK)
    $$ \sigma_{\textrm{PK}}^{2}(t) = \frac{1}{4 \ln{2}}[S_{h}(t) - S_{l}(t)]^2 $$

  3. Garman-Klass estimator (GK)
    $$ \sigma_{\textrm{GK}}^{2}(t) = \frac{1}{2}[S_{h}(t) - S_{l}(t)]^2 - (2\ln{2} - 1)S_{c}^{2}(t) $$
  4. Garman-Klass-Yang-Zhang estimator (GKYZ)
    Yang-Zhang modification of the Garman-Klass estimator. $$ \sigma_{\textrm{GKYZ}}^{2}(t) = \sigma_{\textrm{GK}}^{2}(t) + [S_{o}(t) - S_{c}(t - 1)]^2 $$
  5. Rogers-Satchell estimator (RS)
    $$ \sigma_{\textrm{RS}}^{2}(t) = S_{h}(t)[S_{h}(t) - S_{c}(t)] + S_{l}(t)[S_{l}(t) - S_{c}(t)] $$
  6. Yang-Zhang estimator (YZ)
    $$ \sigma_{\textrm{YZ}}^{2}(t) = \sigma_{o}^{2}(t, D) + k\sigma_{\textrm{STDEV}}^{2}(t, D) + (1 - k)\sigma_{\textrm{RS}}^{2}(t, D) $$ where $\sigma_{o}^{2}(t, D)$ is the variance of opening prices over a $D$ time period, annualized with respect to a given year of 261 trading days. The parameter $k$ is chosen so to minimize the variance of the estimator.

To compute volatility estimations we use functionality provided in the TTR package, TTR::volatility() specifically.

To assess volatility estimates efficiency, authors assume $\sigma_{\textrm{RV}}$ as the "true" volatility and compute for the generic estimator $\sigma_{0}$ a bias $$ \textrm{BIAS} = \frac{1}{T - D}\sum_{t=D}^{T}[\sigma_{\textrm{RV}} - \sigma_{0}] $$ with $T$ the total number of observations in the sample period and $D$ the time period over which volatility estimates are based.

Also, in order to assess volatility estimates persistence and to investigate the role of volatility estimators choice on portfolio turnover, authors compute the volatility turnover (VTO). This quantity is expressed as $$ \textrm{VTO} = \frac{1}{T - D - 1} \sum_{t = D+1}^{T} \Bigl| \frac{1}{\sigma{(t, D)}} - \frac{1}{\sigma{(t-1, D)}} \Bigr| $$

vol.estimators <- c('close', 'parkinson', 'garman.klass', 'gk.yz', 'rogers.satchell', 'yang.zhang')

EstimateVolatility <- function(X, estimator, ...) {
  #' @param X A list of `xts` objects storing assets data.
  #' @param estimator A character vector specifying volatility estimators. Any of those supported by `TTR::volatility()`.
  #' @param ... Any other pass through parameter.
  vol.estm <- vol.bias <- vol.abrd <- vto <- vector('list', length(estimator))
  vol.calcs <- lapply(X, function(x) {
    # Get dates, normalized OHLC prices, and convert
    x <- na.omit(x[, 1:4])
    # x <- x[, !vapply(x, is.character, FUN.VALUE=logical(1L))]
    # x <- xts::xts(x[, -1], order.by=x[, 1])
    for (v in 1:length(estimator)) {
      vol.estm[[v]] <- TTR::volatility(x, calc=estimator[v], ...) 
        vol.bias[[v]] <- 1 / nrow(x) * sum(vol.estm[[1]] - vol.estm[[v]], na.rm=TRUE)
      vol.abrd[[v]] <- abs((1 / vol.estm[[v]]) - (1 / lag(vol.estm[[v]])))
      vto[[v]] <- 1 / (nrow(x) + 1) * sum(vol.abrd[[v]], na.rm=TRUE)
    }
    out <- list(vol.est=vol.estm, vol.bias=vol.bias, vol.abrd=vol.abrd, vto=vto)
    out <- lapply(out, function(x) setNames(x, estimator))
    return(out)
  })
  return(vol.calcs)
}

# 30-days volatility estimates
vol30 <- EstimateVolatility(
  futures.data, estimator = vol.estimators,
  n=30, N=261, type='continuous'
)
# 60-days volatility estimates
vol60 <- EstimateVolatility(
  futures.data, estimator=vol.estimators, 
  n=60, N=261, type='continuous'
)
# Futures closing prices plots (1st column)
layout(matrix(1:length(symbols), 4, 3, byrow=TRUE))

mapply(
  function(x, y) {
    plot(x$Close, main=y, yaxis.right=FALSE)
  }, 
  futures.data, symbols
)
# Volatility YZ estimators plots (2nd column)
vol60.by.est <- AggregateByType(vol60, vol.estimators, symbols=symbols, type='vol.est')

#layout(matrix(1:length(symbols), 4, 3, byrow=TRUE))
mapply(
  function(x, y) {
    plot(
      vol60.by.est$yang.zhang[, y] * sqrt(261), 
      main=paste(symbols[y], '60-days annualized YZ volatility'),
      yaxis.right=FALSE
    )
  }, 
  futures.data, 1:length(symbols)
)
tab2.panels <- list()
# Panel A
# Volatility estimators correlation matrix across futures contracts
tab2.panels$A <- AvgCor(vol60, vol.estimators, symbols=symbols, type='vol.est')$avg.cor
# Panel B
# Annualized volatility estimators bias across futures contracts
# NOTE: assuming close-to-close is the "true" volatility
# Panel C
# Average absolute reciprocal volatility change across futures contracts
avg.bias <- avg.abrd <- avg.vto <- matrix(
  NA, length(vol60), length(vol.estimators), 
  dimnames=list(names(vol60), vol.estimators)
)
for (a in 1:length(vol60)) {
  avg.bias[a, ] <- vapply(vol60[[a]]$vol.bias, mean, FUN.VALUE=double(1L), na.rm=TRUE)
  avg.abrd[a, ] <- vapply(vol60[[a]]$vol.abrd, mean, FUN.VALUE=double(1L), na.rm=TRUE)
  avg.vto[a, ] <- vapply(vol60[[a]]$vto, mean, FUN.VALUE=double(1L), na.rm=TRUE)
}
panels.BC <- lapply(list(avg.bias, avg.abrd), function(x) {
  avg.rank <- rowMeans(apply(x, 1, function(r) rank(abs(r))))
  rbind(x, Avg.Rank=avg.rank)
})
tab2.panels[c('B', 'C')] <- panels.BC
vol60.inst <- AggregateByInstrument(vol60, type='vol.est')
#layout(matrix(1:length(vol60.inst), 4, 3, byrow=FALSE))
mapply(function(x, y) plot(x * sqrt(261), main=y, yaxis.right=FALSE), vol60.inst, symbols)
vol.avg.ranks <- sapply(tab2.panels[c('B', 'C')], function(x) x[nrow(x), ])
barplot(
  t(vol.avg.ranks), 
  main='Average ranks across futures contracts',
  beside=TRUE, 
  ylim=c(0, ceiling(max(vol.avg.ranks))),
  col=c('darkblue', 'darkred')
)
legend(
  'topright',
  c('BIAS', 'VTO'),
  fill=c('darkblue', 'darkred'),
  bty='n'
)

Momentum signals

Authors empirically investigate five momentum signals, summarized below. All the signals may possibly result in a value in the set ${-1, 0, 1}$, for short, inactive, and long positions, respectively.

  1. Return Sign (SIGN)
    This momentum measurement has been proposed since the first studies documenting momentum anomaly. In the time-series momentum literature, it was introduced by \textcite{moskowitz-ooi-pedersen-2012} and consists in the past returns sign. $$ \textrm{SIGN(t-J, t)} = \begin{cases} +1 & R(t-J, t) > 0\ -1 & \text{otherwise} \end{cases} $$

  2. Moving Average methodology (MA)
    The signal is generated depending on the relationship between a short-term moving average of the asset prices and a longer-term one, over the lookback period. This market-timing feature is a peculiarity of this methodology, in contrast to the other signals studied. $$ \textrm{MA(t-J, t)} = \begin{cases} +1 & A_{J}(t) < A_{1}(t)\ -1 & \text{otherwise} \end{cases} $$

  3. Time-Trend t-statistic (TREND)
    t-statistic of the slope coefficient from a least-squares fit of a linear trend on the asset price. $$ \textrm{TREND(t-J, t)} = \begin{cases} +1 & t(\beta) > +2\ -1 & t(\beta) < -2\ 0 & \text{otherwise} \end{cases} $$

  4. Statistically Meaningful Trend (SMT) A more robust version of the previous signal, it is represented by the statistically meaningful trend methodology of \textcite{bryhn-dimberg-2011}. $$ \textrm{SMT(t-J, t)} = \begin{cases} +1 & t(\beta) > +2, R^2 > 0.65 \ -1 & t(\beta) < -2, R^2 > 0.65\ 0 & \text{otherwise} \end{cases} $$

  5. Ensemble Empirical Mode Decomposition (EEMD) This signal processing technique was proposed by \textcite{wu-huang-2009}. \textcite{baltas-kosowski-2012} apply the methodology to extract assets' price trends and deduce a momentum trading signal. In simple words and without making justice to the topic, since the Empirical Mode Decomposition (EMD) a time series is decomposed as a number $n$ of intrinsic mode functions (IMFs) and a (local) mean signal (trend) which is found as a residual. The latter is extracted from the former ones via a so called sifting procedure. The IMFs are oscillating components and in EEMD more noise is added to them. Following \textcite{baltas-kosowski-2012} notation, we decompose the price process as $$ S(t) = \sum_{i=1}^{n}c_{i}(t) + p_{i}(t) $$ where $c_{i}(t)$ are the IMFs and $p(t)$ the trend. The EEMD momentum signal they deduce from the long-term trend $p(t)$ is $$ \textrm{EEMD(t-J, t)} = \begin{cases} +1 & p(t) > p(t-J) \ -1 & \textrm{otherwise} \end{cases} $$ For the ensemble empirical mode decomposition algorithms we used Rlibeemd, an R interface to the libeemd C package, both authored by \textcite{luukko-al-2016}. In particular, we point to the "development" version freely available on GitHub under helske/Rlibeemd as opposed to its CRAN version, because the former allows for OpenMP parallel computing capabilities and thus computational efficiency gains. Core functionality shall be the same.

To notice is that TREND and SMT are the only signals that can result in inactive positions. Further, given that SMT is more restrictive than TREND, it tends to hold more frequent or longer inactivity periods.

# 12 months lookback as 261 trading days
futures.data <- lapply(futures.data, na.omit)
signals <- c('SIGN', 'MA', 'TREND', 'SMT', 'EEMD')
mom.sign <- ExpectedReturns::MomSignal(futures.data, lookback=261, signal='SIGN')
mom.ma <- ExpectedReturns::MomSignal(futures.data, lookback=261, signal='MA')
mom.trend <- ExpectedReturns::MomSignal(futures.data, lookback=261, signal='TREND')
mom.smt <- ExpectedReturns::MomSignal(futures.data, lookback=261, signal='SMT')
mom.eemd <- ExpectedReturns::MomSignal(futures.data, lookback=261, signal='EEMD')

Next, we further analyze generated momentum trading signals, across instruments and through several statistics. First of all, we consider the correlation matrix the average correlation among signals. We then study the activity matrix, representing the number of long or short positions over the periods considered. Finally, the position agreement matrix, denoting the number of periods during which signals pairs agree on the position, on average across instruments.

mom.signals <- list(mom.sign, mom.ma, mom.trend, mom.smt, mom.eemd)

# Panel A1
# Momentum signals correlation matrix across instruments
ExpectedReturns::SignalStats(mom.signals, symbols=symbols, signals=signals, method='correlation')

# Panel A2
# Position agreement matrix
ExpectedReturns::SignalStats(mom.signals, symbols=symbols, signals=signals, method='agreement')

# Panel B
# Momentum signals long/short activity
ls.positions <- ExpectedReturns::SignalStats(
  mom.signals, symbols=symbols, signals=signals, method='activity'
)
lapply(ls.positions, function(x) {
  y <- rbind(x, colMeans(x))
  rownames(y)[nrow(y)] <- 'Avg'
  return(y)
})
AvgSignalSpeed <- function(X, signal, lookback) {
  #' @param X A list of `xts` objects storing assets data.
  #' @param signal A string specifying the momentum signal. See `ExpectedReturns::MomSignal()`.
  #' @param lookback A numeric, the momentum `signal` lookback period. Used only when `signal` is used.
  ninst <- length(X)
  nsg <- length(signal)
  nlb <- length(lookback)
  mom.speed <- array(
    NA, dim=c(ninst, nsg, nlb),
    dimnames=list(names(X), signal, as.character(lookback))
  )
  for (b in 1:nlb) {
    for (s in 1:nsg) {
      mom.speed[, s, b] <- unlist(
        ExpectedReturns::MomSignal(X, lookback=lookback[b], signal=signal[s], speed=TRUE)$mom.speed
      )
    }
  }
  avg.mom.speed <- apply(mom.speed, 3, colMeans)
  return(avg.mom.speed)
}
PlotAvgSignalSpeed <- function(avg.speed, ...) {
  #' @param avg.speed A `matrix` storing average momentum speed across assets and over lookback periods.
  #' @param ... Any other pass through parameter.
  nsg <- nrow(avg.speed)
  x <- matrix(rep(as.numeric(colnames(avg.speed)), nsg), ncol=nsg)
  matplot(
    x=x, y=t(avg.speed),
    xlab='Lookback (months)', 
    ylab='Avg. speed',
    type='b', pch=0:nsg, 
    ...
  )
  legend(
    'topleft', rownames(avg.speed), 
    col=1:nsg, pch=0:nsg, 
    lty=1:nsg, bty='n'
  )
  grid()
}

# Lookback periods (in months)
lb.months <- c(1, 3, 6, 12, 24)
futures.data.monthly <- lapply(futures.data, na.omit)
avg.mom.speed <- AvgSignalSpeed(futures.data.monthly, signal=signals, lookback=lb.months[2:5])
PlotAvgSignalSpeed(avg.mom.speed, main='Average Momentum Signals Speed')

Also, to each momentum signal can be associated a so called momentum speed, which is an activity to turnover-ratio and is used to assess signals trading intensity. Letting $X(t)$ the momentum signal in $t$, its speed is defined as $$ \textrm{SPEED}{X} = \sqrt{\frac{\textrm{E}[X^2]}{E[(\Delta X)^2]}} = \sqrt{\frac{\frac{1}{T-J}\sum{t=1}^{T}X^{2}(t-J,t)}{\frac{1}{T-J-1}\sum_{t=1}^{T}[X(t-J,t) - X(t-J-1,t-1)]^2}} $$ where $\textrm{E}[X]$ is the expected value of the momentum signal. The higher the speed, the larger the signal activity and thus the portfolio turnover.

Returns predictability

Trading a given future contract or a portfolio based on momentum signals detailed above generally leads to different returns streams. Following \textcite{baltas-kosowski-2012}, we examine the time-series return predictability using a pooled panel regression to assess the relationship between the signal and the returns generated by the strategy. The analysis is carried on lookback-investment horizon grid. In other words, we now quantitatively study what in these literature strains is informally referred to as signal's "predictive capability" or "predictive power".

The cross-sectional regressions studied to assess in-sample return predictability are of two kinds, depending on the independent variable of choice. These models are explained in \textcite{baltas-kosowski-2013}. Either there is a model where pooled futures contracts volatility-scaled returns are regressed on the same quantity lagged over time by a period $\lambda$: $$ \frac{R(t-1, t)}{\sigma(t-2,t)} = \alpha + \beta_{\lambda}\frac{R(t-1-\lambda, t-\lambda)}{\sigma(t-2-\lambda,t-\lambda)} + \epsilon(t) $$

In the other kind of models the independent variable is given by a momentum signal computed on contracts returns and lagged over time: $$ \frac{R(t-1, t)}{\sigma(t-2,t)} = \alpha + \beta_{\lambda}X(t-1-\lambda, t-\lambda) + \epsilon(t) $$

The objective of these cross-sectional regressions in this context is to compute the Newey-West t-statistics associated with the estimated $\hat{\beta_{\lambda}}$, indicated as $t(\hat{\beta_{\lambda}})$.

TsmomPredictability <- function(X, signal=NULL, estimator, lookback, holding, lags, ...) {
  #' Compute Newey-West cross-sectional regressions t-statistics for TSMOM strategies returns
  #' @note
  #' When `signal=NULL` cross-sectional regressions on scaled lagged `X` returns series are run by default.
  #' @param X A list of `xts` objects storing assets data.
  #' @param signal A string specifying the momentum signal. See `ExpectedReturns::MomSignal()`.
  #' @param estimator A string specifying the volatility estimator. One accepted by `TTR::volatility()`.
  #' @param lookback A numeric, the momentum `signal` lookback period. Used only when `signal` is used.
  #' @param holding A numeric, the momentum `signal` holding period. Used only when `signal` is used.
  #' @param lags A numeric vector, cross-sectional regressions are computed for each lag specified.
  #' @param ... Any other pass through parameter.

  # Instruments returns series
  ret.inst <- Reduce(cbind, lapply(X, function(x) x$Comp.Return))
  # Volatility estimates
  vol <- AggregateByType(
    EstimateVolatility(X, estimator, ...), 
    y=estimator, symbols=names(X), type='vol.est'
  )
  vol.inst <- Reduce(cbind, vol)
  # Dependent variable
  y <- ret.inst[zoo::index(vol.inst), ] / lag(vol.inst, 1)
  y <- y[-1, ]
  data.inst <- mapply(
    function(x, s) {
      df.inst <- data.frame(
        Date=as.Date(names(x), '%Y-%m-%d'),
        Symbol=rep(s, length(x)),
        Ret.Scaled=as.numeric(x)
      )
      return(df.inst)
    }, 
    asplit(y, 2), names(X),
    SIMPLIFY=FALSE
  )
  data <- Reduce(function(...) merge(..., all=TRUE), data.inst)
  # Regressions
  nlags <- length(lags)
  nw.tstat <- vector('numeric', nlags)
  is.signal <- !is.null(signal)
  if (is.signal) {
    mom.sig <- ExpectedReturns::MomSignal(X, lookback, signal)
    mom.sig.inst <- Reduce(cbind, mom.sig)
    mom.sig.inst <- zoo::na.locf(mom.sig.inst)
    mom.sig.inst <- mom.sig.inst[data$Date, ]
  }
  for (l in lags) {
    # Independent variables
    if (is.signal) {
      # On lagged signal
      x.inst <- lag(mom.sig.inst, l)
    } else {
      # On lagged returns
      x.inst <- lag(ret.inst, l) / lag(vol.inst, 1 + l)
      x.inst <- x.inst[data$Date, ]
    }
    x <- Reduce(append, asplit(x.inst, 1))
    data.reg <- cbind(data, 'x'=x)
    dates <- unique(data.reg$Date)
    # Time-series regressions
    betas <- matrix(NA, length(X), 2)
    for (i in 1:length(X)) {
      aind <- which(data.reg[, 'Symbol'] == names(X)[i])
      tsreg <- lm(
        Ret.Scaled ~ x, 
        data=data.reg[aind, ]
      )
      betas[i, ] <- tsreg$coefficients
    }
    # Cross-sectional regressions 
    mnwts <- matrix(NA, length(dates), 1)
    for (t in 1:length(dates)) {
      tind <- which(data.reg[, 'Date'] == dates[t])
      yt <- data.reg[tind, 'Ret.Scaled']
      datacs <- data.frame('Ret.Scaled'=yt, 'x'=betas[, 2])
      csreg <- lm(Ret.Scaled ~ x, data=datacs)
      mnwts[t, ] <- lmtest::coeftest(
        csreg, vcov.=sandwich::NeweyWest(csreg)
      )['x', 't value']
      # TODO: confidence intervals
    }
    nw.tstat[l] <- mean(mnwts, na.rm=TRUE)
  }
  return(nw.tstat=nw.tstat)
}

The following bar plots display Newey-West t-statistics computed on volatility-scaled TSMOM returns and two momentum signals (SIGN and TREND), for all the lags specified.

# Time lags (in months)
lags <- 1:24
returns.pred <- TsmomPredictability(
  futures.data.monthly, 
  estimator='yang.zhang',
  lags=lags, 
  n=2, N=12
)
sign.pred <- TsmomPredictability(
  futures.data.monthly, 
  signal='SIGN',
  estimator='yang.zhang',
  lookback=1, 
  lags=lags, 
  n=2, N=12
)
# trend.pred <- TsmomPredictability(
#   futures.data.monthly, 
#   signal='TREND',
#   estimator='yang.zhang',
#   lookback=1, 
#   lags=lags, 
#   n=2, N=12
# )

# Bar plots
mapply(
  function(x, title) {
    barplot(
      x, main=title, xlab='lag', names.arg=as.character(lags),
      ylim=c(floor(min(x)), ceiling(max(x)))
    )
  },
  list(
    returns.pred
    , sign.pred
    # , trend.pred
  ), 
  list(
    'Newey-West t-stats TSMOM Lagged Returns'
    , 'Newey-West t-stats SIGN momentum signal'
  )
)

Time-series momentum strategies profitability

With what examined in past sections, we now return to study time series momentum strategies profitability. The time series momentum strategy return was defined as $$ R^{\textrm{TS}}(t,t+K) = \sum_{i=1}^{M} X_{i}(t-J, t) \frac{10\% / \sqrt{M}}{\sigma_{i}(t,D)} R_{i}(t,t+K) $$ where all the symbols have the usual meaning. We are closely following \textcite{baltas-kosowski-2012}, therefore: we compute strategies over a lookback-holding period grid of $J = K = 1, 3, 6, 12, 24$ months, and for all the momentum signals implemented.

Furthermore, in order to evaluate time-series momentum strategies performance, we examine a set of measures, namely the simple arithmetic average return, dollar growth, the Sharpe ratio and the (symmetric) downside-risk Sharpe ratio of \textcite{ziemba-2005}. In reported results, these measurements are all annualized and the former two are expressed in percentage terms.

The Sharpe ratio as the usual meaning. We further detail only the (symmetric) downside-risk Sharpe ratio (DRSR) of \textcite{ziemba-2005}. It is directly comparable to the Sharpe ratio and it aims at removing potential bias in the latter. It is defines as follows: $$ \textrm{DRSR} = \frac{\bar{R} - R_f}{\sqrt{2 \sigma_{x^{-}}^2}} $$ where $\sigma_{x^{-}}$ is the so called downside risk, $$ \sigma_{x^{-}} = \sqrt{\frac{\sum_{i=1}^{n}(x_{i} - \bar{x}){-}^2}{n - 1}}, $$ The $x{i}$ returns considered are restricted to those below $\bar{x}$. In turn, the $\bar{x}$ benchmark can be $0$, as Ziemba originally did for illustration and interpretation purposes, or one of the risk-free rate, average or median returns. The $\sqrt{2}$ factor compensates for the fact only returns in excess of the specified benchmark are taken into account in the downside risk. However, generally, the compensation is only partial because there is no guarantee half the returns distribution will indeed lay above (below) the benchmark.

Following authors, for each momentum signal we report its associated turnover across contracts, defined as $$ X_{\textrm{turnover}}(t-1, t) = \frac{|\Delta{X_{t}}|}{\sigma} $$ Authors claim that momentum signals able to reduce long-short swings can reduce the TSMOM strategy turnover thus improving its performance as transaction costs would decrease accordingly.

TSMOM <- function(X, signal, estimator, lookback, holding, ...) {
  # Momentum signals
  mom.sig.inst <- ExpectedReturns::MomSignal(X, lookback, signal)
  mom.sig <- Reduce(cbind, mom.sig.inst)
  # Volatility 
  # TODO: are authors estimating volatility on daily data nevertheless?
  vol.est <- AggregateByType(
    EstimateVolatility(X, estimator, ...), 
    y=estimator, symbols=names(X), type='vol.est'
  )
  vol <- Reduce(cbind, vol.est)
  # Aggregated TSMOM 
  ret.inst <- Reduce(cbind, lapply(X, function(x) x$Comp.Return))
  ret.ptf <- mom.sig * (0.1 / sqrt(length(X)) / vol) * ret.inst
  ret.ptf <- xts::xts(rowSums(ret.ptf), zoo::index(ret.ptf))
  # Portfolio signal turnover
  sig.turnover.avg <- rowMeans(abs(diff(mom.sig))/vol, na.rm=TRUE)
  sig.turnover.ptf <- mean(sig.turnover.avg)
  return(list(ret.ptf=ret.ptf, sig.turnover.ptf=sig.turnover.ptf))
}

# Time series momentum strategies performance measurements
TsmomStats <- function(X, signal, estimator, lookback, holding, measure, ...) {
  nsg <- length(signal)
  nlb <- length(lookback)
  nhp <- length(holding)
  nms <- length(measure)
  measures.avail <- c(
    'Annual.Mean', 'Annual.Vol', 'Dollar.Growth',
    'SR', 'DRSR', 'Signal.Turnover'
  )
  measure <- match.arg(measure, measures.avail, several.ok=TRUE)
  out <- array(
    NA, dim=c(nlb, nhp, nsg, nms),
    dimnames=list(as.character(lookback), as.character(holding), signal, measure)
  )
  for (b in 1:nlb) {
    for (h in 1:nhp) {
      for (s in 1:nsg) {
        for (m in measure) {
          tsmom <- TSMOM(
            X, signal=signal[s], estimator=estimator, 
            lookback=lookback[b], holding=holding[h], 
            ...
          )
          ret.ptf <- tsmom$ret.ptf
          switch (m,
            Annual.Mean = {
              out[b, h, s, m] <- PerformanceAnalytics::Return.annualized(ret.ptf, scale=12, geometric=FALSE) * 100
            },
            Annual.Vol = {
              out[b, h, s, m] <- PerformanceAnalytics::StdDev.annualized(ret.ptf, scale=12) * 100
            },
            Dollar.Growth = {
              out[b, h, s, m] <- sum(ret.ptf, na.rm=TRUE)
            },
            SR = {
              out[b, h, s, m] <- PerformanceAnalytics::SharpeRatio.annualized(ret.ptf)
            }, 
            DRSR = {
              out[b, h, s, m] <- SharpeRatioDownsideRisk(ret.ptf, scale=1)
            },
            Signal.Turnover = {
              out[b, h, s, m] <- tsmom$sig.turnover.ptf * 100
            }
          )
        }
      }
    }
  }
  return(out)
}

# Lookback and holding periods (months)
lb.months <- hp.months <- c(1, 3, 6, 12, 24)
tsmom.strats <- TsmomStats(
  futures.data.monthly, signal=signals, estimator='yang.zhang', 
  lookback=lb.months[2:5], holding=hp.months[2:5], n=2, N=12, type='continuous',
  measure=c(
    'Annual.Mean', 'Annual.Vol', 'Dollar.Growth',
    'SR', 'DRSR', 'Signal.Turnover'
  ) # 'NW.tstat', 
)


JustinMShea/ExpectedReturns documentation built on Sept. 9, 2023, 9:41 p.m.