Momentum Strategy with Backtesting

Momentum Strategy with Backtesting

Concept of Momentum Strategy with Backtesting with Simplistic Example

In the dynamic world of financial markets, trading strategies hold a significant role in the decision-making process of traders and investors. One such strategy is the momentum strategy, which revolves around exploiting the continuation of recent trends in asset prices. In the intricate landscape of finance, where complex algorithms and intricate models often take the spotlight, it's fascinating to find that one of the simplest strategies—the momentum strategy—holds a remarkably significant place. This strategy, based on a fundamental concept, proves that sometimes, simplicity can yield some of the most intuitive and powerful results. Let's delve into the provided code and explore the concept of momentum strategy along with the practice of backtesting to assess its effectiveness.

Momentum Strategy: A Brief Overview

The momentum strategy is grounded in the notion that assets exhibiting strong performance in recent history are likely to sustain that performance in the near future. Conversely, assets with recent weak performance are expected to continue declining. This strategy capitalizes on the psychological and market-driven tendency for trends to persist. At its core, the momentum strategy is grounded in human behaviour and market dynamics. This strategy essentially rides the wave of trends, both upward and downward, to make trading decisions.

The Simplicity Advantage:

  1. Easy to Grasp: The beauty of the momentum strategy lies in its simplicity. Unlike complex quantitative models, it doesn't require advanced mathematics or intricate calculations. The idea is straightforward: follow recent trends.
  2. Human Behavior: At its essence, the momentum strategy taps into a psychological phenomenon that has been observed in financial markets for ages—the herding behaviour. Investors tend to flock towards assets that have been performing well, contributing to the continuation of trends.
  3. Visual Intuition: Momentum strategies often provide visually intuitive results. Charts displaying historical prices often reveal trends that are easy to identify. A rising trend line suggests positive momentum, while a falling trend line indicates negative momentum.
  4. Historical Validity: The effectiveness of the momentum strategy has been validated by empirical studies across various asset classes and time periods. It demonstrates that historical performance can influence future outcomes.
  5. Minimal Parameters: Unlike some strategies with a plethora of adjustable parameters, the momentum strategy usually requires only a single parameter—the lookback period. This simplicity reduces the risk of over-optimization.


Code Breakdown:

The code provided implements a complete workflow for a momentum trading strategy, along with a backtesting approach to evaluate its performance.

Fetching Historical Data:

This function fetches historical asset data using the Yahoo Finance API (yfinance). The data comprises daily adjusted closing prices for a chosen asset within a specified timeframe. Adjusted closing prices account for dividends and stock splits, providing a consistent representation of asset value over time.

Implementing the Momentum Strategy:

The momentum_strategy function calculates the momentum signal for the trading strategy. Here's how it works:

- Daily returns are computed by taking the percentage change in asset prices.

- The rolling mean of these daily returns is calculated using a specified lookback period. This moving average smoothes out short-term price fluctuations, making trends more discernible.

- The calculated momentum signal is then shifted by two days to align with the intended execution timeframe. This accounts for the delay between signal calculation and trading action.

Backtesting the Strategy:

The backtest_momentum_strategy function simulates the execution of the momentum trading strategy on historical asset returns:

- The momentum signal is converted into a data frame for consistent handling.

- Strategy returns are computed by multiplying the momentum signal with corresponding asset returns.

- To emulate real-world trading costs, a transaction cost percentage is subtracted from the strategy returns.

- Cumulative strategy returns are calculated by compounding returns over time. The initial position is set to 1 for reference.

Analyzing and Visualizing Results:

This function evaluates the performance of the momentum strategy based on key metrics:

- A plot of the cumulative returns of the momentum strategy is generated using Matplotlib. This visualization helps to understand the growth or decline of the strategy over time.

- Metrics like annualized return, Sharpe ratio, and maximum drawdown are calculated. These metrics provide insights into the strategy's profitability, risk-adjusted returns, and potential downside risk.

In the main part of the code, the chosen asset's historical data is fetched, and the momentum strategy is applied. The performance of the strategy is then analyzed using the analyze_performance function.

import pandas as p
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt


def get_asset_data(ticker, start_date, end_date):
? ? asset_data = yf.download(ticker, start=start_date, end=end_date)['Adj Close']
? ? return asset_data


def momentum_strategy(data, lookback_period):
? ? returns = data.pct_change()
? ? momentum_signal = returns.rolling(lookback_period).mean().shift(2)
? ? return np.where(momentum_signal > 0, 1, -1)


def backtest_momentum_strategy(asset_returns, signal, transaction_cost_pct=0.001):
? ? positions = signal.to_frame()? # Convert signal to DataFrame
? ? strategy_returns = asset_returns * positions.iloc[:, 0]
? ? strategy_returns -= abs(strategy_returns) * transaction_cost_pct
? ? cumulative_strategy_returns = (1 + strategy_returns).cumprod()
? ? cumulative_strategy_returns.iloc[0] = 1
? ? return cumulative_strategy_returns


def analyze_performance(cumulative_returns):
? ? daily_returns = cumulative_returns.pct_change().dropna()


? ? plt.figure(figsize=(10, 6))
? ? cumulative_returns.plot(label='Momentum Strategy', color='blue')
? ? plt.title('Momentum Strategy Cumulative Returns')
? ? plt.xlabel('Date')
? ? plt.ylabel('Cumulative Returns')
? ? plt.legend()
? ? plt.grid(True)
? ? plt.show()


? ? total_return = cumulative_returns[-1] / cumulative_returns[0]
? ? num_years = (cumulative_returns.index[-1] - cumulative_returns.index[0]).days / 365
? ? annualized_return = (total_return ** (1 / num_years)) - 1


? ? daily_risk_free_rate = 0
? ? excess_returns = daily_returns - daily_risk_free_rate
? ? sharpe_ratio = excess_returns.mean() / excess_returns.std() * np.sqrt(252)


? ? peak = cumulative_returns.cummax()
? ? drawdown = (cumulative_returns / peak) - 1
? ? max_drawdown = drawdown.min()


? ? print("Annualized Return: {:.2f}%".format(annualized_return * 100))
? ? print("Sharpe Ratio: {:.2f}".format(sharpe_ratio))
? ? print("Maximum Drawdown: {:.2f}%".format(max_drawdown * 100))


if __name__ == "__main__":
? ? start_date = "2000-01-01"
? ? end_date = "2023-07-01"
? ? ticker = "AAPL"? # Replace with the ticker of the asset you want to trade
? ? lookback_period = 60


? ? asset_data = get_asset_data(ticker, start_date, end_date)
? ? momentum_signal = momentum_strategy(asset_data, lookback_period)
? ? cumulative_returns = backtest_momentum_strategy(asset_data.pct_change(), momentum_signal)


? ? analyze_performance(cumulative_returns)        

The above simplistic strategy is slightly updated with the logic of longing the top 10 S&P500 stocks and shorting the bottom 10 S&P500 stocks using a momentum strategy. The code fetches historical data for S&P 500 stocks, implements the momentum strategy, selects top and bottom stocks based on momentum signals, performs backtesting and finally analyzes and visualizes the strategy's performance.

The below code retrieves a list of S&P 500 stock tickers from Wikipedia and then uses Yahoo Finance to fetch historical adjusted closing prices for those tickers within a specified date range.

The top and bottom 10 stocks are selected based on their average momentum signals. The top 10 have the most positive momentum, while the bottom 10 have the most negative momentum. The long and short positions are determined based on the top and bottom tickers. The selected stocks' daily returns are combined to simulate a strategy that goes long on top stocks and short on bottom stocks. Transaction costs are accounted for by subtracting a percentage from the strategy returns.

import pandas as p
import numpy as np
import yfinance as yf
import requests
from bs4 import BeautifulSoup
import matplotlib.pyplot as plt


# Fetch historical data for all stocks in the S&P 500
def get_sp500_tickers():
? ? url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
? ? response = requests.get(url)


? ? if response.status_code == 200:
? ? ? ? soup = BeautifulSoup(response.content, "html.parser")
? ? ? ? table = soup.find("table", {"class": "wikitable"})
? ? ? ? tickers = [row.find_all("td")[0].text.strip() for row in table.find_all("tr")[1:]]
? ? ? ? return tickers
? ? else:
? ? ? ? print("Failed to fetch data from Wikipedia.")
? ? ? ? return []


def get_sp500_data(tickers, start_date, end_date):
? ? sp500_data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']
? ? sp500_data.reset_index(drop=True, inplace=True)
? ? return sp500_data


# Implement the momentum strategy with 2-day shift
def momentum_strategy(data, lookback_period):
? ? returns = data.pct_change()
? ? momentum_signal = returns.rolling(lookback_period).mean()
? ? return momentum_signal.shift(2)


# Select the top and bottom stocks based on momentum signals
def select_top_bottom_stocks(momentum_signals):
? ? mean_momentum = momentum_signals.mean(axis=0)
? ? top_10_tickers = mean_momentum.nlargest(10).index
? ? bottom_10_tickers = mean_momentum.nsmallest(10).index
? ? return top_10_tickers, bottom_10_tickers


# Implement long and short positions for the selected stocks
def backtest_momentum_strategy(asset_returns, top_tickers, bottom_tickers, transaction_cost_pct=0.001):
? ? long_positions = asset_returns[top_tickers]
? ? short_positions = -asset_returns[bottom_tickers]
? ? positions = long_positions.combine_first(short_positions).fillna(0)
? ? strategy_returns = positions.mean(axis=1)
? ? strategy_returns -= abs(strategy_returns) * transaction_cost_pct
? ? cumulative_strategy_returns = (1 + strategy_returns).cumprod()
? ? cumulative_strategy_returns.iloc[0] = 1
? ? return cumulative_strategy_returns


# Analyze and visualize the results
def analyze_performance(cumulative_returns):
? ? if cumulative_returns is None or cumulative_returns.empty or len(cumulative_returns) < 2:
? ? ? ? print("Insufficient data to analyze performance.")
? ? ? ? return


? ? daily_returns = cumulative_returns.pct_change().dropna()


? ? plt.figure(figsize=(10, 6))
? ? cumulative_returns.plot(label='Momentum Strategy', color='blue')
? ? plt.title('Momentum Strategy Cumulative Returns')
? ? plt.xlabel('Date')
? ? plt.ylabel('Cumulative Returns')
? ? plt.legend()
? ? plt.grid(True)
? ? plt.show()


? ? total_return = cumulative_returns.iloc[-1] / cumulative_returns.iloc[0]
? ? print("Total Return:", total_return)
? ??
? ? daily_risk_free_rate = 0
? ? excess_returns = daily_returns - daily_risk_free_rate
? ? sharpe_ratio = excess_returns.mean() / excess_returns.std() * np.sqrt(252)


? ? peak = cumulative_returns.cummax()
? ? drawdown = (cumulative_returns / peak) - 1
? ? max_drawdown = drawdown.min()


? ? print("Sharpe Ratio: {:.2f}".format(sharpe_ratio))
? ? print("Maximum Drawdown: {:.2f}%".format(max_drawdown * 100))


if __name__ == "__main__":
? ? start_date = "2019-01-01"
? ? end_date = "2023-07-01"
? ? lookback_period = 60
? ? sp500_tickers = get_sp500_tickers()
? ? sp500_tickers.remove('BRK.B')
? ? sp500_tickers.remove('BF.B')
? ??
? ? momentum_signals = calculate_momentum_signals(sp500_tickers, start_date, end_date, lookback_period)


? ? top_10_tickers, bottom_10_tickers = select_top_bottom_stocks(momentum_signals)
? ? asset_data = get_sp500_data(sp500_tickers, start_date, end_date)


? ? cumulative_returns = backtest_momentum_strategy(asset_data.pct_change(), top_10_tickers, bottom_10_tickers)
? ??
? ? analyze_performance(cumulative_returns)        

Conclusion: Backtesting for Informed Decision-Making

The provided codes exemplify how a momentum trading strategy can be implemented and evaluated using Python. Backtesting, a pivotal step in strategy development, helps traders and investors gauge the historical effectiveness of their strategies. It provides a foundation for making informed decisions by analyzing the strategy's performance and identifying its strengths and weaknesses. However, it's essential to remember that while past performance can offer valuable insights, future market dynamics may differ. Hence, rigorous risk management and continuous refinement of strategies are integral to successful trading endeavours.

Dima M.

Technologist

2 周

Buy and hold seems to outperform last 2-3 years or possibly I just need to change the look back period ?

回复
Dima M.

Technologist

2 周

Here is the implementation for the missing function, should work fine in Python 3.10: def calculate_momentum_signals(tickers, start_date, end_date, lookback_period): # Fetch historical data for all stocks sp500_data = get_sp500_data(tickers, start_date, end_date) # Calculate returns returns = sp500_data.pct_change() # Calculate momentum signals momentum_signals = returns.rolling(window=lookback_period).mean() # Shift the signals by 2 days momentum_signals = momentum_signals.shift(2) return momentum_signals

A P

Manager at Self Employed

4 个月

NameError: name 'calculate_momentum_signals' is not defined

回复

要查看或添加评论,请登录

社区洞察

其他会员也浏览了