Strategy Library

Improved Momentum Strategy on Commodities Futures

Abstract

In this tutorial we implement a correlation-adjusted time-series momentum strategy (TSMOM-CF) that addresses three weaknesses typically found in traditional time-series momentum strategies (TSMOM). Our implementation is based on the paper "Demystifying Time-Series Momentum Strategies: Volatility Estimators, Trading Rules and Pairwise Correlations" by Nick Baltas and Robert Kosowski. We will also compare TSMOM-CF to the basic momentum strategy implemented in our strategy library - Momentum Effect in Commodities Futures.

Introduction

Baltas and Kosowski modify the basic momentum strategy by incorporating trend strength into the trading signal, using an efficient volatility estimator, and adding a dynamic leverage mechanism. The modifications overcome these three weaknesses:

  1. An Oversimplified Trading Signal: The traditional time-series momentum strategy (TSMOM) results in high portfolio turnover which, after accounting for transaction costs, leads to diminished performance. Baltas and Kosowski attribute the traditional strategy's extreme long/short positions to an oversimplified trading signal whose values are a discrete +1 or -1. The traditional trading signal is based on the sign of the past 12-month average simple return. Baltas and Kosowski propose a trading signal with a continuous value between +1 and -1. Their signal is a statistical measure that reflects the strength of the price trend.
  2. An Inefficient Volatility Estimator: The TSMOM generally scales asset positions using the estimated volatility of portfolio constituents. The traditional strategy's volatility estimator is the standard deviation of past daily close-to-close returns, which is subject to large estimation errors. Baltas and Kosowski demonstrate that a more efficient volatility estimator can significantly reduce portfolio turnover which, after taking into account transaction costs, boosts the portfolio performance. They present the Yang and Zhang volatility estimator, a range-based estimator that considers the open, high, low, and close prices of assets. The next section will discuss this estimator in greater detail.
  3. A Fixed Portfolio Allocation Mechanism: The TSMOM does not consider the correlation between assets during portfolio construction. It simply allocates funds to each asset based on the properties of the individual assets. Strategies based on TSMOM significantly underperform in the post-2008 global financial crisis (GFC) period due to the increased level of asset co-movement at the time. As a remedy, Baltas and Kosowski introduce a dynamic leverage adjustment for the overall portfolio by adding a correlation factor to the weighting scheme.

TSMOM-CF Theory

Baltas and Kosowski's modifications to the basic time-series momentum strategy can be summarized in the formula below:

\[r_{t,t+1}^{TSMOM-CF} = \frac{1}{N_t} \sum_{i=1}^{N_t} X_t^i \frac{\sigma_{P,tgt}}{\sigma_t^i} CF(\bar{\rho}_t)r_{t,t+1}^i\]

where:

\[r_{t,t+1}^{TSMOM-CF} = \text{TSMOM-CF portfolio return from time t to time t+1}\] \[N_t = \text{Number of portfolio constituents at time t}\] \[X_t^i = \text{Trading signal value of asset i at time t}\] \[\sigma_{P,tgt} = \text{Target level of volatility for the overall portfolio}\] \[\sigma_t^i = \text{Estimated volatility of asset i at time t}\] \[CF(\bar{\rho}_t) = \text{Correlation factor that adjusts the level of leverage applied to each portfolio constituents at time t}\] \[r_{t,t+1}^i = \text{return of asset i from time t to time t+1}\]

The formula shows that the weights for each portfolio constituent are dependent on three parts:

Part I: Trading Rule Adjustment (\(X_t^i\))

The TREND trading rule determines the trading signal based on the statistical strength of the realized return:

\[ \text{TREND}_i^{12M} \quad \begin{cases} +1, \text{ if } t(r_{t-12,t})>+1 \\ t(r_{t-12,t}), \text{ otherwise} \\ -1, \text{ if } t(r_{t-12,t})<-1 \\ \end{cases} \]

where t() is the t-statistic of the daily futures log-returns over the past 12 months to scale the gross exposure to each portfolio constituents.

When the absolute value of our t-statistic is greater than 1, the trend is highly statistically significant, so the strategy puts 100% exposure to the asset. When the t-statistic is between -1 and 1, the strength of the trend is not as significant, so the strategy scales its exposure to less than 100%.

Part II: Yang and Zhang Volatility Estimato(\(\sigma_{YZ}\))

Instead of estimating each asset's volatility as the standard deviation of past close-to-close daily logarithmic returns, Baltas and Kosowski adopt a more efficient volatility estimator proposed by Yang and Zhang (2000). The formula for the Yang and Zhang volatility estimator (\(\sigma_{YZ}\)) is shown below:

\[\sigma_{YZ}^2(t) = \sigma_{OJ}^2(t) + k \sigma_{SD}^2(t) + (1-k) \sigma_{RS}^2(t)\]

where:

\[\sigma_{OJ} = \text{Overnight jump estimator (standard deviation of close-to-open daily logarithmic returns)}\] \[\sigma_{SD} = \text{Standard volatility estimator (standard deviation of close-to-close daily logarithmic returns)}\] \[\sigma_{RS} = \text{Rogers and Satchell (1991) range estimator}\] \[k = \text{parameter that minimizes YZ estimator variance, which is a function of the numbers of days in the estimation}\]

The formula for parameter k is below:

\[k = \frac{0.34}{1.34+\frac{N_D+1}{N_D-1}}\]

The Rogers and Satchell range estimator calculation is based on the following formula:

\[\sigma_{RS}^2(\tau) = h(\tau)[h(\tau)-c(\tau)]+l(\tau)[l(\tau)-c(\tau)]\]

where \(h(\tau)\), \(l(\tau)\) and \(c(\tau)\) denote the logarithmic difference between the high, low and closing prices respectively with the opening price. The RS volatility of an asset at the end of month t, assuming a certain estimation period, is equal to the average daily RS volatility over this period.

The estimation period is chosen to be 1 month, or 21 trading days, based on Baltas and Kosowski's suggestions.

Part III: Correlation Factor (CF)

Baltas and Kosowski's correlation factor (CF) is a function of \(\bar{\rho}\), which is the average pairwise signed correlation of all portfolio constituents. The calculations are shown below: \[CF(\bar{\rho}) = \sqrt{\frac{N}{1+(N-1)\bar{\rho}}}\] \[\bar{\rho} = 2 \frac{\sum_{i=1}^N \sum_{j=i+1}^N X_i X_j \rho_{i,j}}{N(N-1)}\]

where:

\[N = \text{number of assets in the portfolio}\] \[\rho_{i,j} = \text{correlation between asset i, j}\] \[X_i = \text{trade signal of asset i}\] \[\bar{\rho} = \text{average pairwise signed correlation for the entire portfolio}\]

Method

The strategy requires the continuous futures contract, so we import the custom data from Quandl. We manually create a universe of tradable commodity futures from all available commodity futures traded on CME and ICE. They are all liquid and active continuous contracts #1. The data from Quandl are non-adjusted price based on spot-month continuous contract calculations. The data resolution is daily.

Step 1: Import the data

from QuantConnect.Python import PythonQuandl
class ImprovedCommodityMomentumTrading(QCAlgorithm):
	def Initialize(self):
		for ticker in tickers:
			data = self.AddData(QuandlFutures, ticker, Resolution.Daily)
			data.SetLeverage(3) # Leverage was set to 3 for each of the futures contract
class QuandlFutures(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "Settle"

Step 2: Set the portfolio target volatility and decide rebalance schedule

def Initialize(self):
	# Last trading date tracker to achieve rebalancing the portfolio every month
    self.nextRebalance = self.Time

	# Set portfolio target level of volatility, set to 12% 
	self.portfolio_target_sigma = 0.12

Step 3: Implement functions to calculate the three components of Baltas and Kosowski weights

1. TREND Trade Signal

def GetTradingSignal(self, history):
	'''
	TREND Trading Signal
	- Uses the t-statistics of historical daily log-returns to reflect the strength of price movement trend
	- TREND Signal Conditions:
		t-stat > 1 => TREND Signal = 1
		t-stat < 1 => TREND Signal = -1
		-1 < t-stat < 1 => TREND Signal = t-stat
	'''
	settle = history.settle.unstack(level = 0)

	# daily futures log-returns based on close-to-close
	log_returns = np.log(settle/settle.shift(1)).dropna()

	# Calculate the t-statistics as
	# (mean-0)/(stdev/sqrt(n)), where n is sample size
	mean = np.mean(log_returns)
	std = np.std(log_returns)
	n = len(log_returns)
	t_stat = mean/(std/np.sqrt(n))

	# cap holding at 1 and -1
	return np.clip(t_stat, a_max=1, a_min=-1)

2. Yang and Zhang Volatility Estimator

def GetYZVolatility(self, history, available_symbols):
	'''
	Yang and Zhang 'Drift-Independent Volatility Estimation'
	
	Formula: sigma_YZ^2 = sigma_OJ^2 + self.k * sigma_SD^2 + (1-self.k)*sigma_RS^2 (Equation 20 in [1])
		where,  sigma_OJ - (Overnight Jump Volitility estimator)
				sigma_SD - (Standard Volitility estimator)
				sigma_RS - (Rogers and Satchell Range Volatility estimator)'''
	YZ_volatility = []

	time_index = history.loc[available_symbols[0]].index
	today = time_index[-1]

	#Calculate YZ volatility for each security and append to list
	for ticker in available_symbols:
	    past_month_ohlc = history.loc[ticker].loc[today-timedelta(self.OneMonth):today]
	    open, high, low, close = past_month_ohlc.open, past_month_ohlc.high, past_month_ohlc.low, past_month_ohlc.settle
	    estimation_period = past_month_ohlc.shape[0]

	    # Calculate constant parameter k for Yang and Zhang volatility estimator
	    # using the formula found in Yang and Zhang (2000)
	    k = 0.34 / (1.34 + (estimation_period + 1) / (estimation_period - 1))

	    # sigma_OJ (overnight jump => stdev of close-to-open log returns)
	    open_to_close_log_returns = np.log(open/close.shift(1))
	    open_to_close_log_returns = open_to_close_log_returns[np.isfinite(open_to_close_log_returns)] 
	    sigma_OJ = np.std(open_to_close_log_returns) 

	    # sigma_SD (standard deviation of close-to-close log returns)
	    close_to_close_log_returns = np.log(close/close.shift(1))
	    close_to_close_log_returns = close_to_close_log_returns[np.isfinite(close_to_close_log_returns)]
	    sigma_SD = np.std(close_to_close_log_returns) 

	    # sigma_RS (Rogers and Satchell (1991))
	    h = np.log(high/open)
	    l = np.log(low/open)
	    c = np.log(close/open)
	    sigma_RS_daily = (h * (h - c) + l * (l - c))**0.5
	    sigma_RS_daily = sigma_RS_daily[np.isfinite(sigma_RS_daily)] 
	    sigma_RS = np.mean(sigma_RS_daily) 
		
	    # daily Yang and Zhang volatility
	    sigma_YZ = np.sqrt(sigma_OJ**2 + k * sigma_SD**2 + (1 - k) * sigma_RS**2) 

	    # append annualized volatility to the list
	    YZ_volatility.append(sigma_YZ*np.sqrt(252)) 

	return YZ_volatility

3. Correlation Factor (CF)

def GetCorrelationFactor(self, history, trade_signals, available_symbols):
	'''
	Calculate the Correlation Factor, which is a function of the average pairwise correlation of all portfolio contituents
	- the calculation is based on past three month pairwise correlation
	- Notations:
	    rho_bar - average pairwise correlation of all portfolio constituents
	    CF_rho_bar - the correlation factor as a function of rho_bar'''

	# Get the past three month simple daily returns for all securities
	settle = history.settle.unstack(level = 0)
	past_three_month_returns = settle.pct_change().loc[settle.index[-1]-timedelta(self.ThreeMonths):]

	# Get number of assets 
	N_assets = len(available_symbols)
	
	# Get the pairwise signed correlation matrix for all assets
	correlation_matrix = past_three_month_returns.corr() 

	# Calculate rho_bar
	summation = 0
	for i in range(N_assets-1):
	    for temp in range(N_assets - 1 - i):
		    j = i + temp + 1
		    x_i = trade_signals[i]
		    x_j = trade_signals[j]
		    rho_i_j = correlation_matrix.iloc[i,j]
		    summation += x_i * x_j * rho_i_j
			
	# Equation 14 in [1]
	rho_bar = (2 * summation) / (N_assets * (N_assets - 1)) 

	# Calculate the correlation factor (CF_rho_bar)
	# Equation 18 in [1]
	return np.sqrt(N_assets / (1 + (N_assets - 1) * rho_bar)) 

Step 4: Construct/Rebalance the Portfolio

For efficiency purposes, a History() request is called once on each rebalance date to get all the data from the past year for all securities. We retrieve our trade signal, Yang and Zhang volatility, and correlation factor by passing the history data frame to each respective function.

def OnData(self, data):
	'''
	Monthly rebalance at the beginning of each month.
	Portfolio weights for each constituents are calculated based on Baltas and Kosowski weights.
	'''

	# skip if less than 30 days passed since the last trading date
	if self.Time < self.nextRebalance:
		return

	'''Monthly Rebalance Execution'''
	# dataframe that contains the historical data for all securities
	history = self.History(self.Securities.Keys, self.OneYear, Resolution.Daily)
	history.replace(0, np.nan, inplace = True)

	# Get the security symbols are are in the history dataframe
	available_symbols = list(set(history.index.get_level_values(level = 0)))

	# Liquidate symbols that are not in the history dataframe anymore
	for security in self.Securities.Keys:
		if security.Value not in available_symbols:
			self.Liquidate(security, 'Not found in history request')

	# Get the trade signals and YZ volatility for all securities
	trade_signals = self.GetTradingSignal(history) 
	volatility = self.GetYZVolatility(history, available_symbols) 
	
	# Get the correlation factor
	CF_rho_bar = self.GetCorrelationFactor(history, trade_signals, available_symbols)

	#Rebalance the portfolio according to Baltas and Kosowski suggested weights
	N_assets = len(available_symbols)
	for symbol, signal, vol in zip(available_symbols, trade_signals, volatility):
		# Baltas and Kosowski weights (Equation 19 in [1])
		weight = (signal*self.portfolio_target_sigma*CF_rho_bar)/(N_assets*vol)
		self.SetHoldings(symbol, weight)

	# Set next rebalance time
	self.nextRebalance = Expiry.EndOfMonth(self.Time)

Summary

The implementation of TSMOM-CF in the post-GFC period, January 2018 to September 2019, shows significant performance improvement over the basic TSMOM. The backtest of TSMOM-CF produces Sharpe ratio of 0.321, compared to TSMOM's Sharpe ratio of -0.746 and SPY Sharpe ratio of 0.46. The exact TSMOM algorithm can be found in the strategy library.

Algorithm

References

  1. Baltas, Nick & Kosowski, Robert. (2017). Demystifying Time-Series Momentum Strategies: Volatility Estimators, Trading Rules and Pairwise Correlations. SSRN Electronic Journal. 10.2139/ssrn.2140091. Online Copy
  2. Yang, Dennis & Zhang, Qiang. (2000). Drift-Independent Volatility Estimation Based on High, Low, Open, and Close Prices. The Journal of Business, 73(3), 477-492. doi:10.1086/209650. Online Copy

    You can also see our Documentation and Videos. You can also get in touch with us via Chat.

    Did you find this page helpful?