In quantitative trading, a variety of strategies are employed to gain an edge in financial markets. While momentum and trend-based strategies are widely popular, they often fail to generate significant alpha unless packaged as YouTube financial courses.
The momentum and trend approach capitalizes on the notion that assets exhibiting recent price strength will likely continue on the same trajectory in the near future, or conversely, that weakness will persist. It’s important to distinguish between trend and momentum:
- Trend-following focuses on directional absolute returns, examining beta across the market.
- Momentum, on the other hand, looks at relative returns and is market-neutral, assessing a class or sector cross-section.
Reversion strategies operate on the assumption that prices will revert back to a mean or means over a period of time.
In this article, we will analyze these strategies and provide Python code implementations for studying them. We’ll progress to more advanced strategies and their analysis, including Pairs Trading.
We’ll tailor our analysis to the latest post-pandemic market regime, with most stock prices starting in the 2021s. All strategies discussed will be long-only and unleveraged.
Preparing A Python Environment
Preparing a Python environment involves setting up the necessary tools and libraries to write, execute, and manage Python code effectively. Here’s a step-by-step guide to preparing a Python environment:
1. Install Python:
If Python is not already installed on your system, download and install it from the official Python website (https://www.python.org/). Make sure to choose the appropriate version for your operating system (Windows, macOS, or Linux). It’s recommended to install the latest stable version of Python.
2. Choose a Text Editor or IDE:
Decide on a text editor or Integrated Development Environment (IDE) for writing Python code. Some popular options include:
Visual Studio Code: A lightweight and customizable editor with built-in support for Python and numerous extensions.
PyCharm: A powerful IDE specifically designed for Python development, with features like code completion, debugging, and version control integration.
Jupyter Notebook: An interactive notebook environment that allows you to write and execute Python code in a web browser, ideal for data analysis and visualization.
3. Set Up a Virtual Environment (Optional but Recommended):
Virtual environments help manage dependencies and isolate project environments. To create a virtual environment, open a terminal or command prompt, navigate to your project directory, and run:
python -m venv myenvReplace myenv with the name you want to give your virtual environment. Activate the virtual environment by running:
4. Install Required Packages:
Once inside your virtual environment, you can install Python packages using pip, the Python package manager. For example:
python -m venv myenv
pip install numpy pandas matplotlib
pip install -r requirements.txt5. Start Coding:
You’re now ready to write Python code! Open your chosen text editor or IDE, create a new Python file, and start coding.
By following these steps, you’ll have a fully functional Python environment set up on your system, ready for development and experimentation.
Momentum & Trend Strategies
Let’s illustrate this with an example: if an asset’s price has experienced an uptrend over the past week, it’s probable that it will continue in the same direction. This strategy relies on the assumption that future movements mimic past patterns, either upward or downward. As the community at WallStreetBets used to chant, stocks tend to rise.
Despite its straightforwardness, this strategy comes with certain limitations:
It disregards market noise and significant events, potentially smoothing out important fluctuations.
Frequent trading may lead to the accumulation of transaction fees.
Given its popularity, there’s limited or no competitive advantage associated with this approach.
The Moving Average Crossover strategy
The Moving Average Crossover strategy operates by computing two or more moving averages for an asset’s price: a short-term moving average (referred to as fast SMA) and a long-term moving average (slow SMA).
The following code implements this strategy:
def double_simple_moving_average_signals(ticker_ts_df, short_window=5, long_window=30):
"""
Generate trading signals based on a double simple moving average (SMA) strategy.
Parameters:
- ticker_ts_df (pandas.DataFrame): A DataFrame containing historical stock data.
- short_window (int): The window size for the short-term SMA.
- long_window (int): The window size for the long-term SMA.
Returns:
- signals (pandas.DataFrame): A DataFrame containing the trading signals.
"""
signals = pd.DataFrame(index=ticker_ts_df.index)
signals['signal'] = 0.0
signals['short_mavg'] = ticker_ts_df['Close'].rolling(window=short_window,
min_periods=1,
center=False).mean()
signals['long_mavg'] = ticker_ts_df['Close'].rolling(window=long_window,
min_periods=1,
center=False).mean()
# Generate signal when SMAs cross
signals['signal'] = np.where(
signals['short_mavg'] > signals['long_mavg'], 1, 0)
signals['orders'] = signals['signal'].diff()
signals.loc[signals['orders'] == 0, 'orders'] = None
return signalsThis function takes a time series of any stock and sets a short (fast SMA) and long (slow SMA) rolling window, then compares the two across the timeline. A buy signal (1) is generated when the fast SMA is higher, and a sell signal (-1) when it’s lower.
We’ll create some utility functions to calculate our capital over the strategy’s timeline and a graphing function to visualize the entry and exit signals against the stock’s time series.
def load_ticker_time_series(ticker, start_date, end_date):
"""
Retrieve and cache time series financial data from Yahoo Finance API.
Parameters:
- ticker (str): The stock ticker symbol (e.g., 'AAPL' for Apple Inc.).
- start_date (str): The start date in 'YYYY-MM-DD' format for data retrieval.
- end_date (str): The end date in 'YYYY-MM-DD' format for data retrieval.
Returns:
- df (pandas.DataFrame): A DataFrame containing the financial time series data."""
dir_path = './data'
cached_file_path = f'{dir_path}/{ticker}_{start_date}_{end_date}.pkl'
try:
if os.path.exists(cached_file_path):
df = pd.read_pickle(cached_file_path)
else:
df = yf.download(ticker, start=start_date, end=end_date)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
df.to_pickle(cached_file_path)
except FileNotFoundError:
print(
f'Error downloading and caching or loading file with ticker: {ticker}')
return df
def calculate_profit(signals, prices):
"""
Compute cumulative profit based on trading signals and stock prices.
Parameters:
- signals (pandas.DataFrame): A DataFrame containing trading signals (1 for buy, -1 for sell).
- prices (pandas.Series): A Series containing stock prices corresponding to the signal dates.
Returns:
- cum_profit (pandas.Series): A Series containing cumulative profit over time.
"""
profit = pd.DataFrame(index=prices.index)
profit['profit'] = 0.0
buys = signals[signals['orders'] == 1].index
sells = signals[signals['orders'] == -1].index
while sells[0] < buys[0]:
# These are long-only strategies; we cannot start with a sell
sells = sells[1:]
if len(buys) == 0 or len(sells) == 0:
# no actions taken
return profit
if len(sells) < len(buys):
# Assume we sell at the end
sells = sells.append(pd.Index(prices.tail(1).index))
buy_prices = prices.loc[buys]
sell_prices = prices.loc[sells]
profit.loc[sells, 'profit'] = sell_prices.values - buy_prices.values
profit['profit'] = profit['profit'].fillna(0)
# Make profit cumulative
profit['cum_profit'] = profit['profit'].cumsum()
return profit['cum_profit']
def plot_strategy(prices_df, signal_df, profit):
"""
Visualize a trading strategy with buy and sell signals and cumulative profit.
Parameters:
- prices (pandas.Series): A Series containing stock prices.
- signals (pandas.DataFrame): A DataFrame with buy (1) and sell (-1) signals.
- profit (pandas.Series): A Series containing cumulative profit over time.
Returns:
- ax1 (matplotlib.axes.Axes): The top subplot displaying stock prices and signals.
- ax2 (matplotlib.axes.Axes): The bottom subplot displaying cumulative profit.
"""
fig, (ax1, ax2) = plt.subplots(2, 1, gridspec_kw={'height_ratios': (3, 1)},
figsize=(18, 12))
ax1.set_xlabel('Date')
ax1.set_ylabel('Price in $')
ax1.plot(prices_df.index, prices_df, color='g', lw=0.25)
# Plot the Buy and Sell signals
ax1.plot(signal_df.loc[signal_df.orders == 1.0].index,
prices_df[signal_df.orders == 1.0],
'^', markersize=12, color='blue', label='Buy')
ax1.plot(signal_df.loc[signal_df.orders == -1.0].index,
prices_df[signal_df.orders == -1.0],
'v', markersize=12, color='red', label='Sell')
ax2.plot(profit.index, profit, color='b')
ax2.set_ylabel('Cumulative Profit (%)')
ax2.set_xlabel('Date')
return ax1, ax2Let’s combine everything
aapl_ts_df = load_ticker_ts_df('AAPL',
start_date='2021-01-01',
end_date='2023-01-01')
signal_df = double_simple_moving_average_signals(aapl_ts_df, 5, 30)
profit_series = calculate_profit(signal_df, aapl_ts_df["Adj Close"])
ax1, ax2 = plot_strategy(aapl_ts_df["Adj Close"], signal_df, profit_series)
# Add short and long moving averages
ax1.plot(signal_df.index, signal_df['short_mavg'],
linestyle='--', label='Fast SMA')
ax1.plot(signal_df.index, signal_df['long_mavg'],
linestyle='--', label='Slow SMA')
ax1.legend(loc='upper left', fontsize=10)
plt.show()
In the span of 2 years, this strategy has yielded a return of 30%. Compared to the S&P 500’s latest return of 10%, it appears to be a robust strategy.
Naive Momentum Strategy
This strategy operates based on the frequency of price increases or decreases. It operates under the assumption that a continuous increase in price over a certain number of consecutive days indicates a buying signal, while a decrease signals a selling opportunity.
We will utilize some of the utility functions from the Simple Moving Average (SMA) strategy mentioned earlier.
Below is the code to implement a basic version of this strategy:
def naive_momentum_signals(ticker_ts_df, nb_conseq_days=2):
"""
Generate naive momentum trading signals based on consecutive positive or negative price changes.
Parameters:
- ticker_ts_df (pandas.DataFrame): A DataFrame containing historical stock data.
- nb_conseq_days (int): The number of consecutive positive or negative days to trigger a signal.
Returns:
- signals (pandas.DataFrame): A DataFrame with 'orders' column containing buy (1) and sell (-1) signals.
"""
signals = pd.DataFrame(index=ticker_ts_df.index)
signals['orders'] = 0
price = ticker_ts_df['Adj Close']
price_diff = price.diff()
signal = 0
cons_day = 0
for i in range(1, len(ticker_ts_df)):
if price_diff[i] > 0:
cons_day = cons_day + 1 if price_diff[i] > 0 else 0
if cons_day == nb_conseq_days and signal != 1:
signals['orders'].iloc[i] = 1
signal = 1
elif price_diff[i] < 0:
cons_day = cons_day - 1 if price_diff[i] < 0 else 0
if cons_day == -nb_conseq_days and signal != -1:
signals['orders'].iloc[i] = -1
signal = -1
return signals
signal_df = naive_momentum_signals(aapl_ts_df)
profit_series = calculate_profit(signal_df, aapl_ts_df["Adj Close"])
ax1, _ = plot_strategy(aapl_ts_df["Adj Close"], signal_df, profit_series)
ax1.legend(loc='upper left', fontsize=10)
plt.show()
This strategy, unfortunately, has not been profitable; it hasn’t yielded any returns.
However, we might have fared better by simply reversing the model (a practice often suggested for many of WSB’s stock analyses). To achieve this, we can multiply the trading signals by -1. This reversal approach is typically effective when the strategy’s original signals are ineffective.
Admittedly, this strategy is typically applied to the broader market rather than individual instruments and operates within shorter timeframes.
Mean Reversion
In mean reversion strategies, we operate under the assumption that a stock’s price will tend to stay close to its mean or average value over time.
Below is the signal code for implementing this strategy:
def mean_reversion_signals(ticker_ts_df, entry_threshold=1.0, exit_threshold=0.5):
"""
Generate mean reversion trading signals based on moving averages and thresholds.
Parameters:
- ticker_ts_df (pandas.DataFrame): A DataFrame containing historical stock data.
- entry_threshold (float): The entry threshold as a multiple of the standard deviation.
- exit_threshold (float): The exit threshold as a multiple of the standard deviation.
Returns:
- signals (pandas.DataFrame): A DataFrame with 'orders' column containing buy (1) and sell (-1) signals.
"""
signals = pd.DataFrame(index=ticker_ts_df.index)
signals['mean'] = ticker_ts_df['Adj Close'].rolling(
window=20).mean() # Adjust the window size as needed
signals['std'] = ticker_ts_df['Adj Close'].rolling(
window=20).std() # Adjust the window size as needed
signals['signal'] = np.where(ticker_ts_df['Adj Close'] > (
signals['mean'] + entry_threshold * signals['std']), 1, 0)
signals['signal'] = np.where(ticker_ts_df['Adj Close'] < (
signals['mean'] - exit_threshold * signals['std']), -1, 0)
signals['orders'] = signals['signal'].diff()
signals.loc[signals['orders'] == 0, 'orders'] = None
return signalsIn this function, we calculate the standard deviation and the mean of the stock’s price data.
If the price diverges from its mean by a certain number of standard deviations, it will generate a signal.
Let’s test it together with the functions we created:
signal_df = mean_reversion_signals(aapl_ts_df)
profit_series = calculate_profit(signal_df, aapl_ts_df["Adj Close"])
ax1, _ = plot_strategy(aapl_ts_df["Adj Close"], signal_df, profit_series)
ax1.plot(signal_df.index, signal_df['mean'], linestyle='--', label="Mean")
ax1.plot(signal_df.index, signal_df['mean'] +
signal_df['std'], linestyle='--', label="Ceiling STD")
ax1.plot(signal_df.index, signal_df['mean'] -
signal_df['std'], linestyle='--', label="Floor STD")
ax1.legend(loc='upper left', fontsize=10)
plt.show()Mean Reversion
In mean reversion strategies, we operate under the assumption that a stock’s price will tend to stay close to its mean or average value over time.
Below is the signal code for implementing this strategy:
def mean_reversion_signals(ticker_ts_df, entry_threshold=1.0, exit_threshold=0.5):
"""
Generate mean reversion trading signals based on moving averages and thresholds.
Parameters:
- ticker_ts_df (pandas.DataFrame): A DataFrame containing historical stock data.
- entry_threshold (float): The entry threshold as a multiple of the standard deviation.
- exit_threshold (float): The exit threshold as a multiple of the standard deviation.
Returns:
- signals (pandas.DataFrame): A DataFrame with 'orders' column containing buy (1) and sell (-1) signals.
"""
signals = pd.DataFrame(index=ticker_ts_df.index)
signals['mean'] = ticker_ts_df['Adj Close'].rolling(
window=20).mean() # Adjust the window size as needed
signals['std'] = ticker_ts_df['Adj Close'].rolling(
window=20).std() # Adjust the window size as needed
signals['signal'] = np.where(ticker_ts_df['Adj Close'] > (
signals['mean'] + entry_threshold * signals['std']), 1, 0)
signals['signal'] = np.where(ticker_ts_df['Adj Close'] < (
signals['mean'] - exit_threshold * signals['std']), -1, 0)
signals['orders'] = signals['signal'].diff()
signals.loc[signals['orders'] == 0, 'orders'] = None
return signals
In this function, we calculate the standard deviation and the mean of the stock’s price data.
If the price diverges from its mean by a certain number of standard deviations, it will generate a signal.
Let’s test it together with the functions we created:
signal_df = mean_reversion_signals(aapl_ts_df)
profit_series = calculate_profit(signal_df, aapl_ts_df["Adj Close"])
ax1, _ = plot_strategy(aapl_ts_df["Adj Close"], signal_df, profit_series)
ax1.plot(signal_df.index, signal_df['mean'], linestyle='--', label="Mean")
ax1.plot(signal_df.index, signal_df['mean'] +
signal_df['std'], linestyle='--', label="Ceiling STD")
ax1.plot(signal_df.index, signal_df['mean'] -
signal_df['std'], linestyle='--', label="Floor STD")
ax1.legend(loc='upper left', fontsize=10)
plt.show()
A 10% return on paper may seem decent, but in reality, you could have achieved similar results by investing in a broad S&P 500 index fund.
In future articles, we will delve into more advanced versions of reversion strategies, particularly Pair-Trading and Statistical-Arbitrage. Additionally, we will explore strategy metrics such as the Sharpe ratio, which helps evaluate the effectiveness of strategies. This analysis will help us understand why this strategy, despite returning the same as the S&P 500, is considered weak.
