How to Implement the Time Series Momentum (TSMOM) Strategy in Python
The Time Series Momentum (TSMOM) strategy invests in assets with positive 12-month returns while shorting those with negative returns, applying inverse-volatility weighting to target 10% annualized portfolio volatility.
The Time Series Momentum (TSMOM) strategy, first formalized by Moskowitz, Ooi & Pedersen (2012), captures absolute momentum across diversified futures contracts by taking long positions in past winners and short positions in past losers. The awesome-systematic-trading repository provides a complete, production-ready implementation of this academic approach as a QuantConnect algorithm in static/strategies/time-series-momentum-effect.py. This guide examines the architecture, signal generation mechanics, and risk management systems that transform the paper's theoretical framework into executable Python code.
TSMOM Strategy Architecture Overview
The implementation follows a modular architecture designed for QuantConnect's event-driven backtesting engine. The TimeSeriesMomentum class inherits from QCAlgorithm and manages a universe of 70+ futures contracts spanning commodities, currencies, equity indices, and bond futures.
Universe Definition and Data Ingestion
The strategy defines its tradeable universe in self.symbols (lines 24-85), encompassing major futures markets from the CME, ICE, and other exchanges. Data ingestion leverages a custom QuantpediaFutures class (lines 204-232) that reads back-adjusted daily price series from Quantpedia's public data repository.
# From Initialize method (line ~101)
self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
Each symbol maintains a RollingWindow[float] storing exactly 12 months of historical prices (approximately 252 trading days), initialized at line 106:
self.data[symbol] = RollingWindow[float](self.period) # self.period = 252
Signal Generation Mechanics
The core TSMOM signal calculates the 12-month total return by comparing the most recent back-adjusted price to the price 12 months prior. Implemented in line 136:
performance = back_adjusted_prices[0] / back_adjusted_prices[-1] - 1
Assets with positive performance enter the long portfolio, while those with negative performance become shorts. This binary classification occurs during the monthly rebalancing cycle triggered when self.recent_month != self.Time.month.
Volatility Estimation and Weighting
Position sizing employs inverse-volatility weighting derived from 60-day historical volatility (line 147):
volatility_3M = np.std(daily_rets) * sqrt(252)
The strategy calculates separate volatility-adjusted weights for the long and short legs, then applies portfolio-level volatility targeting to achieve 10% annualized volatility (self.targeted_volatility = 0.10).
Core Implementation Components
The Initialize Method Setup
The Initialize method configures the algorithm's parameters and warm-up period. Critical settings include:
- Leverage Cap: Maximum 4x portfolio leverage (line 186)
- Warm-up Period: 252 trading days to populate rolling windows
- Fee Model: Custom 0.5 basis point transaction cost structure
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(10000000)
self.targeted_volatility = 0.10
self.leverage_cap = 4.0
self.period = 12 * 21 # ~252 trading days
# Set warm-up to populate RollingWindows
self.SetWarmUp(self.period, Resolution.Daily)
Monthly Rebalancing and Execution Logic
The OnData method triggers rebalancing on the first trading day of each new month. The execution workflow (lines 190-202) involves:
- Liquidating positions no longer in the long/short sets
- Calculating inverse-volatility weights for qualifying assets
- Determining portfolio leverage based on volatility targeting
- Executing trades via
SetHoldings
The leverage calculation (lines 182-187) ensures the volatility-targeted exposure respects the 4x cap:
leverage = self.targeted_volatility / portfolio_volatility
leverage = min(self.leverage_cap, leverage) # Line 186
Custom Fee Model Implementation
Transaction costs are modeled using the CustomFeeModel class (lines 332-336), applying a conservative 0.5 basis point commission:
class CustomFeeModel:
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * 0.00005 * parameters.Order.AbsoluteQuantity
return OrderFee(CashAmount(fee, "USD"))
Practical Implementation Examples
Running the Algorithm on QuantConnect
Deploy the strategy by inheriting from the TimeSeriesMomentum class or running it directly:
from AlgorithmImports import *
class MyTSMOM(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(10_000_000)
# Initialize the TSMOM strategy
self.tsmom = TimeSeriesMomentum()
self.tsmom.Initialize()
Extracting Individual Asset Signals
Access the 12-month momentum signal for specific contracts after warm-up completion:
symbol = "CME_CL1" # Crude Oil Futures
window = self.data[symbol]
prices = np.array([x for x in window])
# Calculate TSMOM signal
tsmom_signal = prices[0] / prices[-1] - 1
print(f"{symbol} 12-month return: {tsmom_signal:.2%}")
Customizing Strategy Parameters
Modify the look-back period or volatility target to test variations:
# Extend look-back to 18 months (~378 trading days)
self.period = 18 * 21
self.SetWarmUp(self.period, Resolution.Daily)
# Increase volatility target to 15%
self.targeted_volatility = 0.15
Expanding the Asset Universe
Add new futures contracts by extending self.symbols in the Initialize method:
# Add cryptocurrency futures
self.symbols.append("CME_BTC1")
Summary
- The awesome-systematic-trading repository implements TSMOM in
static/strategies/time-series-momentum-effect.pyas a complete QuantConnect algorithm trading 70+ futures contracts. - Signal generation uses 12-month (
self.period = 252) total return calculations stored inRollingWindowobjects, with positive performers going long and negative performers going short. - Risk management combines inverse-volatility weighting (based on 60-day historical volatility) with portfolio-level volatility targeting (10% annualized) and a 4x leverage cap.
- Execution occurs monthly, utilizing the
CustomFeeModel(0.5 bps) andQuantpediaFuturesdata handler for back-adjusted continuous contract prices.
Frequently Asked Questions
How does the TSMOM implementation differ from the original academic paper?
The repository implementation follows Moskowitz, Ooi & Pedersen (2012) closely but substitutes the paper's GARCH volatility estimation with a simpler 60-day historical standard deviation (np.std(daily_rets) * sqrt(252)). This pragmatic simplification reduces computational complexity while maintaining the strategy's risk-adjusted return characteristics.
What data source does the algorithm use for futures prices?
The strategy utilizes Quantpedia's public futures data via the custom QuantpediaFutures class (lines 204-232), which reads back-adjusted daily CSV files. The GetSource method constructs URLs pointing to Quantpedia's data repository, while the Reader method parses price data into QuantConnect's data format.
Why is the leverage capped at 4x?
The 4x leverage cap (self.leverage_cap = 4.0) implemented in line 186 serves as a circuit breaker to prevent excessive exposure during periods of abnormally low volatility estimates. This risk control ensures that even when historical volatility collapses (potentially inflating position sizes under the volatility-targeting regime), gross exposure remains bounded.
How can I modify the rebalancing frequency?
The current implementation rebalances monthly by checking if self.recent_month != self.Time.month in the OnData method. To change frequency, modify the conditional logic to trigger on different time intervals (e.g., weekly using self.Time.isoweekday()) and adjust the volatility calculation window accordingly to match the rebalancing period.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →