Master Momentum Trading Strategies with Python | by Aydar Murt | The Capital | Jan, 2025
While momentum and trend-following algorithms share certain similarities, they have notable differences. For example, the Moving Average Convergence Divergence (MACD) indicator functions as both a momentum and a trend-following tool.
Momentum algorithms, a subset of trend-following strategies, generally focus on short-term price movements. They aim to identify if a stock has recently demonstrated strong momentum, assuming the trend will persist for a short while.
In contrast, trend-following algorithms are oriented toward identifying long-term trends and capitalize on the directional movement of these trends, regardless of the time it takes for profits to materialize.
Momentum trading encompasses several strategies, including but not limited to the following:
- Price Rate of Change (ROC)
- Absolute Momentum
- Relative Momentum
- Dual Momentum
Each of these algorithms will be explored below, along with Python code to implement them. For each, I will first define the methodology, describe the formula used, and provide an example implementation in Python from scratch.
Definition
The Price Rate of Change (ROC) is a momentum-based trading strategy that measures the percentage change in a stock’s price over a specified period. A positive ROC suggests the stock is gaining momentum, triggering a buy signal as the indicator anticipates continued price growth.
Formula
Python Code Implementation
To illustrate this concept, I’ll use a dataframe containing Apple’s historical stock prices. Details on how the dataset was obtained using Python will be shared at the conclusion of this guide.
- Period (n): Use a 10-day period.
- ROC Calculation: Compute the percentage change in price over 10 days.
- Signals:
- If the ROC is positive for a day, trigger a buy signal.
- If the ROC is negative for a day, trigger a sell signal.
4. Performance Assessment:
- Calculate daily returns based on the strategy.
- Compute cumulative returns to evaluate its effectiveness over time.
# Define the time period for calculating the ROC
n = 10# Calculate the ROC indicator
df['ROC'] = df['Adj Close'].pct_change(periods=n)
# Generate buy signals when the ROC becomes above its signal line (0)
df['Buy'] = (df['ROC'] > 0) & (df['ROC'].shift(1) < 0)
# Generate sell signals when the ROC becomes below its signal line (0)
df['Sell'] = (df['ROC'] < 0) & (df['ROC'].shift(1) > 0)
# Buy securities when a buy signal is generated and sell them when a sell signal is generated
# 1 for Buy, -1 for Sell, 0 for Hold
df['Signal'] = np.where(df['Buy']==True, 1, np.where(df['Sell']==True,-1,0))
# Calculate the daily returns of the strategy
df['Returns'] = df['Signal'].shift(1) * df['Adj Close'].pct_change()
df['Returns+1'] = 1 + df['Returns']
# Calculate the cumulative returns of the strategy
df['Cumulative_Returns'] = (1+df['Returns']).cumprod()
# Print the final cumulative return
print('Final Cumulative Return Over The Whole Period: ', df['Cumulative_Returns'][-1]-1) Final Cumulative Return Over The Whole Period: 0.04052975893266497
Plotting the cumulative return and the signal:
df[['Returns+1','Cumulative_Returns']].plot(figsize=(8,4))
plt.title("Price Rate of Change (ROC)")
plt.show()df[['Signal']].plot(figsize=(8,4))
plt.show()
The cumulative return during the timeframe from mid-August 2022 to mid-December 2022 did not show strong performance. To address this, you can experiment with altering the look-back period in days to determine the most optimal interval. Test periods of 15 days and 5 days — analysis reveals that the 5-day period typically generates better returns compared to the 15-day period.
Absolute Momentum, also referred to as time-series momentum, involves purchasing assets that exhibit positive returns over a given timeframe and selling those with negative returns. This strategy is based on analyzing historical performance within a set period to make trading decisions.
For implementing Absolute Momentum, the process involves the following steps:
- Calculate Stock Returns: Determine the percentage change in the stock price for the desired period.
- Generate a Trading Signal: Establish signals based on the calculated returns — positive returns imply a buy signal, while negative returns trigger a sell signal.
- Smooth Signals Using Moving Averages: To filter out noise, compute a moving average of the raw signals.
- Determine Final Signals: Convert the smoothed signal into actionable trading instructions:
- Use +1+1+1 to indicate a buy order, −1–1−1 for a sell order, and 000 to signify holding the position.
def absolute_momentum(df, window):
"""
Calculate the daily return
Calculate a signal: if return is positive, then 1, if negative -1, else 0
Calculate a moving average over "window" period to smooth the signal
Calulate the final signal:
if the smoothed signal is positive then a buy (1) order signal is triggered,
when negative a sell order is trigerred (-1),
else stay in a hold position (0)
"""
df['returns'] = df['Adj Close'].pct_change()
df['signals']=np.where(df['returns']>0,1,np.where(df['returns']<0,-1,0))
df['signals_ma']=df['signals'].rolling(window).mean()
df['signals_final']=np.where(df['signals_ma']>0,1,np.where(df['signals_ma']<0,-1,0))
return df#Calculate the signals
df = absolute_momentum(df, 30)
df[['returns']].plot(figsize=(8,4))
plt.title("Returns")
plt.show()
df[['signals_ma','signals_final']].plot(figsize=(8,4))
plt.legend(loc = 'upper left')
plt.title("Absolute Momentum")
plt.show()
In the second graph, the blue line represents the smoothed signal. When this line is above zero, it triggers a buy signal represented by +1+1; when it falls below zero, a sell signal is generated and represented by −1–1.
Relative momentum is a strategy within algorithmic trading that evaluates how a stock’s performance compares to that of the broader market or a specific benchmark. This technique identifies stocks that are outperforming or underperforming either the market as a whole or a chosen index.
Additionally, relative momentum can assess the strength of a stock’s performance over time relative to its own historical behavior. This is useful for determining whether a stock has been overbought or oversold within a given time window, often using the Relative Strength Index (RSI) as a metric.
In this example, the stock under analysis is Apple, and it is benchmarked against the S&P 500 index. The steps are outlined as follows:
- Calculate 14-Day Moving Average Returns: Compute the rolling mean returns for both the stock and the index over a 14-day window.
- Compute the Relative Strength Ratio: Divide the rolling average return of the stock by that of the index to obtain the relative strength ratio.
- Generate the Relative Strength Line: Apply a rolling mean to the relative strength ratio to smooth it further and observe trends.
- Establish Trading Signals: Create signals to buy or sell based on whether the relative strength ratio is above or below the relative strength line.
window = 14benchmark_ticker = 'SPY'
benchmark_data = download_stock_data(benchmark_ticker,timestamp_start,timestamp_end).set_index('Date')
stock_returns = df['Adj Close'].pct_change()
benchmark_returns = benchmark_data['Adj Close'].pct_change()
# Calculate rolling mean of the return over 14 days for the stock and the index
stock_rolling_mean = stock_returns.rolling(window=window).mean()
benchmark_rolling_mean = benchmark_returns.rolling(window=window).mean()
# Calculate relative strength
relative_strength = stock_rolling_mean / benchmark_rolling_mean
# Calculate rolling mean of relative strength
relative_strength_rolling_mean = relative_strength.rolling(window=window).mean()
# Create signal based on relative strength rolling mean
signal = np.where(relative_strength > relative_strength_rolling_mean, 1, -1)
df_rel=pd.concat((stock_rolling_mean,benchmark_rolling_mean,relative_strength,relative_strength_rolling_mean),axis=1)
df_rel.columns=['Stock_ma','Benchmark_ma','RS','RS_ma']
df_rel['signal']=signal
df_rel=df_temp.dropna()
df_rel.head()
download_stock_data* will be explained in “Loading Dataset” part.
Here is when plotting the whole period:
df_rel[['RS','RS_ma']].plot(figsize=(6,4))
plt.title('Relative Momentum')df_rel[['signal']].plot(figsize=(6,4))
plt.title('Signal')
plt.show()
Let’s plot the ratio and the relative strength line for a certain period (to see more clearly):
df_rel.query("Date>'2022-08-30' and Date<'2022-11-30'") [['RS','RS_ma','signal']].plot(figsize=(8,8))
Definition
Dual Momentum is an algorithm that combines two indicators: The relative strength and the absolute momentum. It is based on the idea that when a stock is performing well relative to its peers and at the same time, its absolute momentum is positive, it is likely to continue to perform well in the near future.
Python Code
In this example, I build 2 datasets:
- df_global: in which we have 4 stocks (‘NFLX’, ‘AMZN’, ‘GOOG’, ‘AAPL’) that make up our portfolio. The goal is to know which one has a strong dual momentum.
- df_global_market: which represents the market or a specific benchmark. For illustration, I put only 6 stocks : ‘MSFT’, ‘META’, ‘GM’, ‘GS’, ‘TTE’, ‘F’.
Then:
- The momentum for each stock in the portfolio is computed
- The momentum for the whole market is computed
- Then, the dual momentum is computed for each stock in the portfolio
- Also a rank among the 4 stocks is given for the whole period
def dual_momentum(df_global,df_global_market,window):
"""
dual_momentum:
Calculate the momentum for each stock
Calculate the momentum of the market
Calculate the dual momentum as the product of the stock's momentum and the market one
"""
# Calculate 10-days momentum of the prices
mom = df_global.pct_change(window).dropna()
# Calculate 10-days momentum of the global stock market
global_mom = df_global_market.pct_change(window).dropna()
global_mom_mean = global_mom.mean(axis=1)# Create a data frame to hold the final results
results = pd.DataFrame()
for ticker in df_global.columns:
mom_ticker = mom[ticker]
# Calculate the dual momentum score for this stock
dual_mom = pd.DataFrame(mom_ticker * global_mom_mean)
dual_mom.columns=[ticker+'_dual_mom']
results=pd.concat((dual_mom,results),axis=1)
return results
window=10
results=dual_momentum(df_global,df_global_market,window)
results.iloc[-30:,:].plot(figsize=(12,6))
plt.title("Dual Momentum")
plt.show()
results.apply(lambda x: x.rank(ascending=False),axis=1).iloc[-30:,:].plot(figsize=(12,6))
plt.title("Rank of Dual Momentum (1:Best, 4:Worst)")
plt.show()
I only plotted the last 30 days for clarity in the chart:
In the last period, Google is ranked N°1 against the other stocks, because it’s showing a strong dual momentum. The following one in the ranking is Apple. The last one is Netflix.
This ranking, as you can see in the graph, was not constant over the past 30 days. Apple was often ranked 4. On the other hand, we can see Netflix taking the first place on the podium more often than the other stocks.
There are different solutions to download datasets. Here are two methods you can use:
First method
import pandas_datareader.data as web
import yfinance as yfinstart = dt.datetime(2022, 1, 1)
end = dt.datetime.today()
yfin.pdr_override()
# Define the ticker symbol for the stock
ticker = 'AAPL'
# Load the stock prices from Yahoo Finance
df = web.DataReader(ticker, start, end)
df.tail()
I find it slower than the second method, which I used for the whole datasets.
Second method
AAPL
def download_stock_data(ticker,timestamp_start,timestamp_end):
url=f"https://query1.finance.yahoo.com/v7/finance/download/{ticker}?period1={timestamp_start}&period2={timestamp_end}&interval
=1d&events=history&includeAdjustedClose=true"
df = pd.read_csv(url)
return dfdatetime_start=dt.datetime(2022, 1, 1, 7, 35, 51)
datetime_end=dt.datetime.today()
# Convert to timestamp:
timestamp_start=int(datetime_start.timestamp())
timestamp_end=int(datetime_end.timestamp())
df = download_stock_data("AAPL",timestamp_start,timestamp_end)
df = df.set_index('Date')
SPY
benchmark_ticker = 'SPY'
benchmark_data = download_stock_data(benchmark_ticker,timestamp_start,timestamp_end).set_index('Date')
df_global with 4 stocks:
tickers=['AAPL','GOOG','AMZN','NFLX']df_global=pd.DataFrame()
for ticker in tickers:
df = download_stock_data(ticker,timestamp_start,timestamp_end)[['Date','Adj Close']]
df = df.set_index('Date')
df.columns=[ticker]
df_global=pd.concat((df_global, df),axis=1)
df_global.head()
df_global_market with 6 stocks
tickers=['MSFT','META','GM','GS','TTE','F']df_global_market=pd.DataFrame()
for ticker in tickers:
df = download_stock_data(ticker,timestamp_start,timestamp_end)[['Date','Adj Close']]
df = df.set_index('Date')
df.columns=[ticker]
df_global_market=pd.concat((df_global_market, df),axis=1)
df_global_market.head()