In the world of quantitative trading, various strategies are employed to gain a competitive edge in financial markets. Most popular trading strategies involve momentum and trend - being popular means they will not generate much alpha, unless you're planning to sell these as youtube Financial courses.
Momentum and trend approach capitalizes on the idea that assets that have exhibited recent price strength, will continue to do so in the near future.
There is a slight detail between trend and momentum worth noting:
Reversion is the assumption that prices reverts back to a mean or means, over a period of time.
In this article, we will explore these strategies, and provide Python code implementations for you to try out.
We will keep to the latest post-pandemic market regime, therefore most stock prices will start in the 2021s. All strategies are long-only, and unleveraged.
Prepare Your Python
Have a jupyter environment ready, and pip install these libraries:
Momentum & Trend Strategies
Let's describe it with an example, say if an asset's price has risen for the past week, it's likely to continue upward. This approach capitalizes on the belief that the future mirrors the past, either in an upward or downward trend. As the WSB crowd used to say, stonks only go up.
While the strategy is easy to grasp and implement, it does have drawbacks:
Let's implement a few of these strategies.
Simple Moving Average (SMA) Crossover
The Moving Average Crossover strategy involves calculating two or more moving averages for an asset's price: a short-term moving average (called a fast SMA) and a long-term moving average (slow SMA).
The code below 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: - aapl_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 signals
This function takes a timeseries of any stock, and sets a short (fast SMA) and long (slow SMA) rolling windonw, and compares the 2 across the timeline. When the fast SMA is higher, a buy signal is generated (1), and when it's lower, a sell signal is produced (-1).
We will create some utility functions to calculate our capital over the strategy's timeline, and a graphing function to help us visualize the entries and exit signals against the stock's timeseries.
def load_ticker_ts_df(ticker, start_date, end_date): """ Load 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 dfdef calculate_profit(signals, prices): """ Calculate 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 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'].c*msum() return profit['cum_profit']def plot_strategy(prices_df, signal_df, profit): """ Plot 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, ax2
Let's run everything together:
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 averagesax1.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()
Throughout 2 years, this strategy gave us 30% return. Comparing it agianst the S&P 500's latest return of 10% - that is quite a good strategy.
Naive Momentum
This strategy is based on the number of times a price increases or decreases. It assumes that when a price increases for a certain number of consecutive days, it's a signal to buy, and when it decreases, it's a signal to sell.
We willl reuse some of the utility functions from the SMA strategy above.
Here's the code to implement a naive version of it:
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 signalssignal_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()
Recommended by LinkedIn
This strategy is a losing one, it didn't give us any returns.
We would have done better inversing this model (just like you ought to do with most of WSB's stock analysis). Hint to achieve this: signals['orders'] = signals['orders'] * -1
Granted, this strategy is usually based on the market in general rather than a single instrument, and is in a shorter timeframe.
Reversion Strategies
An example: Elon tweets that he will install blockchain in teslas, the market gets overzelous in its buying of Tesla stock.
The next day, everyone realizes that fundamentally nothing has changed, so the market loses interest and the price reverts back to an acceptable level.
Therefore, any instrument that divereges too fast from a benchmark in either direction, will eventually revert back to the benchmark in the longer timeframe.
Just like trends and momentum - this is a simple strategy, it smooths everything and is gernerally used by all market participants.
Mean Reversion
Here we assume that the price of a stock will stay in the vicinity of its mean.
Below is the signal code:
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 find the standard deviation and the mean.
if the price diverges from its mean by a standard deviaion and a factor, 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()
10% on paper is not bad return, though in truth you would have done just as well using an S&P500 broad index.
In future articles, we will look at more advanced versions of reversion strategies, mainly: Pair-Trading and Statistical-Arbitrage. We will also look at strategies' metrics, like the sharpe-ratio, which is why this strategy is weak, even though it returned the same as the S&P500.
Conclusion
We've studied simple stratgies of momentum and mean reversion trading strategies, and used python to make an analysis. Momentum strategies harness the power of trends to predict future price movements. On the other hand, mean reversion strategies are grounded in the idea that price or returns tend to revert to the mean after experiencing extreme movements.
In real portoflio trading, no single strategy guarantees success, unless you are shilling it on youtube as a finfluencer!
References
Github
Article here is also available on Github
Kaggle notebook available here
Media
All media used (in the form of code or images) are either solely owned by me, acquired through licensing, or part of the Public Domain and granted use through Creative Commons License.
CC Licensing and Use
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
Made with :heartpulse: by Adam