Generating Buy/Sell Signals with Moving Averages Using pandas-ta

Generating Buy/Sell Signals with Moving Averages Using pandas-ta

This post is part of the T4p Series.

In the previous post, I had discussed the engulfing pattern. In this post, we will discuss Moving Averages.

What Is Moving Average

Moving Average

Moving Averages are one of the most widely used indicators. They smooth out price fluctuations by calculating the average price over a specific number of periods, creating a trend-following line that updates as new data becomes available, hence helping traders identify the overall direction of the price movement.

Types Of Moving Averages

  • Simple Moving Average (SMA) – Calculates the arithmetic mean of prices over a set period, giving equal weight to all data points
  • Exponential Moving Average (EMA) – Gives more weight to recent prices, making it more responsive to current market movements than SMA
  • Weighted Moving Average (WMA) – Assigns different weights to each period, typically giving higher importance to more recent data points
  • Hull Moving Average (HMA) – Reduces lag while maintaining smoothness by using weighted moving averages of different periods
  • Double Exponential Moving Average (DEMA) – Applies exponential smoothing twice to reduce lag and improve responsiveness
  • Triple Exponential Moving Average (TEMA) – Further reduces lag by applying exponential smoothing three times while minimizing noise

Usually, traders rely on SMAs and EMAs most of the time.

Usage

Moving averages are primarily used to identify trend direction and generate buy/sell signals when prices cross above or below the average line. They’re important because they filter out market noise and provide clear, objective reference points for making trading decisions and managing risk.

Development

Visualization

I am using the same yfinance based function to fetch OHLC data, in this case for TSLA

def get_tsla_recent_data():
    session = requests.Session()
    session.headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'})
    tsla = yf.Ticker('TSLA')
    df = tsla.history(period='4mo', interval='1d')
    return df

Let’s calculate SMA for 20 and 50.

df = get_tsla_recent_data()
df['SMA_short'] = ta.sma(df['Close'], length=20)
df['SMA_long'] = ta.sma(df['Close'], length=50)

When you run, it displays like below:

Modifying the visualize_interactive function:

def visualize_interactive(df, title="TSLA Candlestick Chart"):
    fig = go.Figure()
    
    # Add candlestick chart
    fig.add_trace(go.Candlestick(
        x=df.index,
        open=df['Open'],
        high=df['High'],
        low=df['Low'],
        close=df['Close'],
        name='TSLA'
    ))
    
    # Add SMA 50 line
    fig.add_trace(go.Scatter(
        x=df.index,
        y=df['SMA_short'],
        mode='lines',
        name='SMA Short',
        line=dict(color='blue', width=2)
    ))
    
    # Add SMA 200 line
    fig.add_trace(go.Scatter(
        x=df.index,
        y=df['SMA_long'],
        mode='lines',
        name='SMA Long',
        line=dict(color='red', width=2)
    ))
    
    fig.update_layout(title=title, xaxis_rangeslider_visible=False)
    fig.show()

When you run it, it generates a chart like the one below:

SMA Crossover

This looks good, but unfortunately, it is not crossing over or under in this setup. So, I changed the time duration from 1 day to 4 hours, and now it generates a chart like the one below:

SMA Crossover

SMA Crossover/Crossunder

SMA crossover is a popular trading strategy that generates buy and sell signals when two simple moving averages of different periods intersect.

  • Golden Cross – Short SMA crosses above long SMA = Buy signal
  • Death Cross – Short SMA crosses below long SMA = Sell signal

Let’s write a function that will generate these crossover signals

def generate_ma_crossover_signals(df, short_ma_col='SMA_short', long_ma_col='SMA_long'):    
    df = df.copy()
    
    # Initialize signal column
    df['Signal'] = 0
    
    # Generate crossover signals
    # Buy signal: Short MA crosses above Long MA (Golden Cross)
    df['Signal'] = np.where(
        (df[short_ma_col] > df[long_ma_col]) & 
        (df[short_ma_col].shift(1) <= df[long_ma_col].shift(1)), 
        1, df['Signal']
    )
    
    # Sell signal: Short MA crosses below Long MA (Death Cross)
    df['Signal'] = np.where(
        (df[short_ma_col] < df[long_ma_col]) & 
        (df[short_ma_col].shift(1) >= df[long_ma_col].shift(1)), 
        -1, df['Signal']
    )
        
    # Add signal markers for plotting
    df['Buy_Signal'] = np.where(df['Signal'] == 1, df['Close'], np.nan)
    df['Sell_Signal'] = np.where(df['Signal'] == -1, df['Close'], np.nan)
    
    return df

And now, when you call it:

df_with_signals = generate_ma_crossover_signals(df)
print("Buy Signals:")
print(df_with_signals[df_with_signals['Signal'] == 1][['Close', 'SMA_short', 'SMA_long']])
print("\nSell Signals:")
print(df_with_signals[df_with_signals['Signal'] == -1][['Close', 'SMA_short', 'SMA_long']])

And it produces the output

SMA Buy/Sell Signals

And this is exactly what is happening.

SMA Crossover

 

 

But it is not so good. How cool would it be to add a visual hint about it? The following function does this:

def visualize_interactive_with_signals(df, title="TSLA with MA Crossover Signals"):
    fig = go.Figure()
    
    # Candlestick chart
    fig.add_trace(go.Candlestick(
        x=df.index, open=df['Open'], high=df['High'], 
        low=df['Low'], close=df['Close'], name='TSLA'
    ))
    
    # Moving averages
    fig.add_trace(go.Scatter(x=df.index, y=df['SMA_short'], mode='lines', 
                            name='SMA Short', line=dict(color='blue', width=2)))
    fig.add_trace(go.Scatter(x=df.index, y=df['SMA_long'], mode='lines', 
                            name='SMA Long', line=dict(color='red', width=2)))
    
    # Buy signals with text
    buy_signals = df[df['Signal'] == 1]
    if not buy_signals.empty:
        fig.add_trace(go.Scatter(
            x=buy_signals.index, 
            y=buy_signals['Close'],
            mode='markers+text',
            name='Buy Signal',
            marker=dict(symbol='triangle-up', size=15, color='green'),
            text='BUY',
            textposition='top center',
            textfont=dict(color='green', size=12, family='Arial Black')
        ))
    
    # Sell signals with text
    sell_signals = df[df['Signal'] == -1]
    if not sell_signals.empty:
        fig.add_trace(go.Scatter(
            x=sell_signals.index, 
            y=sell_signals['Close'],
            mode='markers+text',
            name='Sell Signal',
            marker=dict(symbol='triangle-down', size=15, color='red'),
            text='SELL',
            textposition='bottom center',
            textfont=dict(color='red', size=12, family='Arial Black')
        ))
    
    fig.update_layout(title=title, xaxis_rangeslider_visible=False)
    fig.show()

And it produces the following chart, with signals. Cool, no?

SMA Crossover chart

Conclusion

As you see, generating signals based on Moving Averages in Python is easy. I used SMA for the sake of the post, but you can do something similar with EMAs as well. As always, the code is available on GitHub

 


Looking to create something similar or even more exciting? Schedule a meeting or email me at kadnan @ gmail.com.

Love What You’re Learning Here?
If my posts have sparked ideas or saved you time, consider supporting my journey of learning and sharing. Even a small contribution helps me keep this blog alive and thriving.

👉 Support My Work Here

If you like this post, then you should subscribe to my blog for future updates.

* indicates required



Leave a Reply

Your email address will not be published. Required fields are marked *

4 + 6 =