Three-Way Moving Average Trading Strategy (No short selling required) w/ Python code

Tony Vuolo
4 min readMar 20, 2023

--

The trading strategies I have shown so far in this blog rely on you always being in the market, i.e., always going short on a stock whenever you sell. That strategy can be volatile and requires a margin account with a sufficient balance. What if you want a more traditional buy low / sell high method that isolates you from the volatility of short selling? The three-way moving average approach is for patient traders who wait for good times to buy and sell a stock. Like all my articles, I will provide python code to generate a quality chart with backtesting code and key performance indicator calculations.

The 3-way moving average method first calculates the stock's 5, 10, and 20-day moving averages. We then chart those over the OHLC candlestick data.

We will trigger a buy signal when the 5-day and 10-day moving averages are above the 20-day moving average and one has crossed the 20-day moving average. A sell signal occurs when the 5-day moving average exceeds the 10-day or 20-day moving average.
We want to chart these buy and sell signals to get an excellent visual indication of how this strategy works. Please experiment with different time frames, stock symbols, and moving average lengths.

The three-way moving average chart with Buy and Sell signals

import yfinance as yf
import matplotlib.pyplot as plt
from mpl_finance import candlestick_ohlc
import matplotlib.dates as mdates
import matplotlib.colors as mcolors
import numpy as np
import matplotlib.ticker as ticker


def major_date_formatter(x, pos=None):
dt = mdates.num2date(x)
if dt.day == 1:
return f'{dt.strftime("%b")} {dt.year}'
return ''

def minor_date_formatter(x, pos=None):
dt = mdates.num2date(x)
if dt.day == 1:
return f'{dt.day}\n\n{dt.strftime("%b")} {dt.year}' if dt.month == 1 else f'{dt.day}\n\n{dt.strftime("%b")}'
return f'{dt.day}'

# Set the start and end dates
start_date = '2022-01-01'
end_date = '2022-12-31'

symbol = 'IBM'

# Get the data from Yahoo Finance using yfinance library
stock_ohlc = yf.Ticker(symbol)
df = stock_ohlc.history(start=start_date, end=end_date)

# Calculate the moving averages
ma_5 = df['Close'].rolling(window=5).mean()
ma_10 = df['Close'].rolling(window=10).mean()
ma_20 = df['Close'].rolling(window=20).mean()

# Find where the moving averages cross
cross_buy = ((ma_5 > ma_20) & (ma_10 > ma_20)) & ((ma_5.shift() < ma_20.shift()) | (ma_10.shift() < ma_20.shift()))
cross_sell = (ma_5 < ma_10) | (ma_5 < ma_20)

# Convert the date to the matplotlib date format
df['Date'] = mdates.date2num(df.index)

# Create the plot
fig, ax = plt.subplots(figsize=(16, 8))

# Plot the moving averages
ax.plot(df.index, ma_5, color='tab:blue', label='5-day MA', linestyle='--')
ax.plot(df.index, ma_10, color='tab:orange', label='10-day MA', linestyle='--')
ax.plot(df.index, ma_20, color='tab:green', label='20-day MA', linestyle='--')

# Plot the candlesticks
candlestick_ohlc(ax, df[['Date', 'Open', 'High', 'Low', 'Close']].values, width=0.6, colorup='gray', colordown='black')

# Initialize buy and sell signal flags
first_buy_signal = False

# Add arrows for buy and sell signals
for date, row in df.iterrows():
if cross_buy.loc[date] and not first_buy_signal:
first_buy_signal = True
ax.annotate('Buy Here', xy=(date, row['Low']), xytext=(date, row['Low'] - 20),
arrowprops=dict(facecolor='green', arrowstyle='->', linewidth=2, mutation_scale=20))
elif cross_sell.loc[date] and first_buy_signal:
first_buy_signal = False
if date == df.index[-1]: # Check if it's the last index
ax.annotate('Sell Here', xy=(date, row['High']), xytext=(date, row['High'] + 20),
arrowprops=dict(facecolor='red', arrowstyle='->', linewidth=2, mutation_scale=20),
textcoords="data", ha='right', va='center')
else:
ax.annotate('Sell Here', xy=(date, row['High']), xytext=(date, row['High'] + 20),
arrowprops=dict(facecolor='red', arrowstyle='->', linewidth=2, mutation_scale=20))

# Format the x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))

# Add the legend and title
ax.legend()
ax.set_title(symbol + ' Stock Price Chart with Buy and Sell Signals')

# Remove gridlines
ax.grid(False)

# Add padding to the top and bottom of the chart
ax.set_ylim(df['Low'].min() - 20, df['High'].max() + 20)

# Adjust the legend's location so it doesn't conflict with the last 'Sell Here' signal
ax.legend(loc='upper left')

# Format the x-axis
ax.xaxis.set_major_locator(mdates.DayLocator(interval=7)) # Show a tick for every 7th day
ax.xaxis.set_major_formatter(ticker.FuncFormatter(major_date_formatter))
ax.xaxis.set_minor_locator(mdates.MonthLocator()) # Show a tick for each month
ax.xaxis.set_minor_formatter(ticker.FuncFormatter(minor_date_formatter))
ax.tick_params(axis='x', which='major', length=10, labelsize=10)
ax.tick_params(axis='x', which='minor', length=5, labelsize=10)

plt.show()

Here again, is the chart output. I have tick marks every seven days, and when one falls on the first of the month, I put the month and year on the first level. This could be better, but it does make a readable chart. Feel free to modify the code if you like a different x-axis label.

IBM Stock Price 3-way moving average strategy

Backtest

import pandas as pd
import numpy as np

signals = pd.DataFrame(columns=['Type', 'Date', 'Price'])

for date, row in df.iterrows():
if cross_buy.loc[date] and not first_buy_signal:
first_buy_signal = True
new_signal = pd.DataFrame({'Type': ['Buy'], 'Date': [date], 'Price': [row['Low']]})
signals = pd.concat([signals, new_signal], ignore_index=True)
elif cross_sell.loc[date] and first_buy_signal:
first_buy_signal = False
new_signal = pd.DataFrame({'Type': ['Sell'], 'Date': [date], 'Price': [row['High']]})
signals = pd.concat([signals, new_signal], ignore_index=True)


signals.reset_index(drop=True, inplace=True)

# 2. Calculate the returns for each trade
signals['Returns'] = np.nan

for i in range(0, len(signals) - 1, 2):
buy_price = signals.iloc[i]['Price']
sell_price = signals.iloc[i + 1]['Price']
signals.iloc[i + 1, signals.columns.get_loc('Returns')] = sell_price - buy_price

# 3. Calculate the metrics
total_net_profit = signals['Returns'].sum()
profit_factor = signals[signals['Returns'] > 0]['Returns'].sum() / abs(signals[signals['Returns'] < 0]['Returns'].sum())
percent_profitable = len(signals[signals['Returns'] > 0]) / (len(signals) / 2) * 100
average_trade_net_profit = signals['Returns'].mean()
drawdown = (signals['Price'].cummax() - signals['Price']).max()

print(f"Total Net Profit: {total_net_profit:.2f}")
print(f"Profit Factor: {profit_factor:.2f}")
print(f"Percent Profitable: {percent_profitable:.2f}%")
print(f"Average Trade Net Profit: {average_trade_net_profit:.2f}")
print(f"Maximum Drawdown: {drawdown:.2f}")

Results for using the 3-way moving average

Total Net Profit: 21.65
Profit Factor: 5.30
Percent Profitable: 57.14%
Average Trade Net Profit: 3.09
Maximum Drawdown: 9.05

Of course, past results or results of a strategy in backtest on historical data do not guarantee that it will work in the future. By giving the code for creating quality charts for the moving average trade strategy and backtesting, I hope you will learn how to investigate an approach. This is not meant to be investment advice. It is for learning purposes only.

--

--

Tony Vuolo
Tony Vuolo

Written by Tony Vuolo

I have a passion for quantitative finance, economics, and computer science. https://www.linkedin.com/in/tonyvuolo/

Responses (1)