Signals·TA Strategy Implementations·Intermediate

MACD Momentum Strategy

Implement a MACD momentum strategy that captures trend acceleration and deceleration phases using MACD line and signal line crossovers combined with histogram direction and magnitude changes for precise trade timing.

trading-signalstrading-strategies

Strategy — MACD Momentum


1–2. Installation and Imports

[ ]
!pip install pandas numpy plotly

import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4)
Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

3. Strategy Overview

MACD (Moving Average Convergence Divergence) is a trend-following momentum indicator constructed from three components:

ComponentDefinition
MACD LineFast EMA − Slow EMA (typically 12 − 26 periods)
Signal LineEMA of the MACD Line over N periods (typically 9)
HistogramMACD Line − Signal Line

Signal logic:

  • MACD Line crosses above the Signal Line → Buy (+1): fast momentum is accelerating above slow momentum — bullish momentum is building.
  • MACD Line crosses below the Signal Line → Sell (−1): fast momentum is decelerating below slow momentum — bearish momentum is building.

Histogram interpretation:

  • Histogram above zero and growing → momentum is accelerating bullishly.
  • Histogram above zero but shrinking → bullish momentum is weakening (potential reversal warning).
  • Histogram below zero and growing in magnitude → momentum is accelerating bearishly.

Why it works: MACD captures the difference between short-term and long-term momentum. When the short-term average (fast EMA) is rising faster than the long-term average (slow EMA), the asset is gaining momentum. The crossover of the signal line provides a smoothed trigger that reduces noise compared to a raw MACD zero-line crossover.


4. Data Generation

[ ]
def generate_data(periods: int) -> pd.DataFrame:
    start_date     = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
    price_data = []; last_close = 42000
    for i in range(periods):
        open_price  = last_close + np.random.normal(0, last_close * 0.0005)
        close_price = open_price + np.random.normal(0, last_close * 0.005)
        body_high   = max(open_price, close_price)
        body_low    = min(open_price, close_price)
        high_price  = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        low_price   = min(body_low  - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        if high_price < low_price: high_price, low_price = low_price, high_price
        price_data.append({"open": max(1,int(open_price)), "high": max(1,int(high_price)),
                            "low":  max(1,int(low_price)),  "close": max(1,int(close_price))})
        last_close = close_price
    df = pd.DataFrame(price_data, index=datetime_index)
    df.index.name = "datetime"
    df["volume"] = np.random.uniform(100.0, 500.0, periods)
    df["datetime"] = df.index.to_series()
    return df.reset_index(drop=True)

df = generate_data(500)
display(df.head())
open high low close volume datetime
0 41990 42143 41742 41774 208.965374 2024-01-01 00:00:00+00:00
1 41795 42059 41787 41982 160.862954 2024-01-01 00:01:00+00:00
2 41986 42061 41738 41757 397.028968 2024-01-01 00:02:00+00:00
3 41767 42223 41696 42184 310.361470 2024-01-01 00:03:00+00:00
4 42179 42410 42111 42388 279.201697 2024-01-01 00:04:00+00:00

5. Strategy Function

[ ]
def macd_momentum_strategy(
    df:     pd.DataFrame,
    fast:   int = 12,
    slow:   int = 26,
    signal: int = 9,
) -> pd.DataFrame:
    """Calculates MACD, Signal Line, Histogram, and generates buy/sell signals.

    Args:
        df (pd.DataFrame): Input DataFrame with a 'close' price column.
        fast (int): The period for the fast Exponential Moving Average (EMA).
        slow (int): The period for the slow Exponential Moving Average (EMA).
        signal (int): The period for the Signal Line EMA.

    Returns:
        pd.DataFrame: The original DataFrame with added 'macd', 'signal_line',
                      'histogram', 'signal', and 'crossover' columns.
    """
    # Create a copy of the DataFrame and sort by datetime to ensure correct calculations
    df = df.copy().sort_values("datetime", ignore_index=True)

    # Calculate the Fast and Slow Exponential Moving Averages (EMAs)
    ema_fast          = df["close"].ewm(span=fast,   adjust=False).mean()
    ema_slow          = df["close"].ewm(span=slow,   adjust=False).mean()

    # Calculate the MACD line (Fast EMA - Slow EMA)
    df["macd"]        = ema_fast - ema_slow

    # Calculate the Signal line (EMA of the MACD line)
    df["signal_line"] = df["macd"].ewm(span=signal, adjust=False).mean()

    # Calculate the Histogram (MACD line - Signal line)
    df["histogram"]   = df["macd"] - df["signal_line"]

    # Generate buy (+1), sell (-1), or neutral (0) signals based on MACD and Signal line crossover
    df["signal"] = np.where(df["macd"] > df["signal_line"],  1,
                   np.where(df["macd"] < df["signal_line"], -1, 0))

    # Identify crossover events: where the signal changes direction and is not NaN
    df["crossover"] = df["signal"].diff().ne(0) & df["signal"].notna()

    return df

df_signals = macd_momentum_strategy(df, fast=12, slow=26, signal=9)

print("--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
print(f"\nTotal crossovers: {df_signals['crossover'].sum()}")
--- Signal Distribution ---
signal
-1    255
 1    244
 0      1
Name: count, dtype: int64

Total crossovers: 43

Explanation:

  • ewm(span=fast, adjust=False): Exponential weighting prioritizes recent prices. The fast EMA (span=12) reacts quickly to new price information; the slow EMA (span=26) provides a stable long-term reference.
  • macd = ema_fast − ema_slow: When positive, short-term momentum exceeds long-term momentum (bullish). When negative, long-term momentum dominates (bearish).
  • signal_line: A 9-period EMA of the MACD line, acting as a smoothed trigger. Crossovers of MACD above/below the signal line are the primary trade triggers.
  • histogram: The momentum of momentum — a growing histogram indicates an accelerating trend; a shrinking histogram indicates weakening momentum.
  • crossover: Flags only the candles where the signal changes direction, enabling precise identification of entry/exit timing.

6. Visualization

[ ]
buy_signals  = df_signals[(df_signals["crossover"]) & (df_signals["signal"] ==  1)]
sell_signals = df_signals[(df_signals["crossover"]) & (df_signals["signal"] == -1)]

fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
    subplot_titles=["Price + Crossover Signals", "MACD Histogram + Lines"],
    row_heights=[0.55, 0.45])

fig.add_trace(go.Candlestick(
    x=df_signals["datetime"],
    open=df_signals["open"], high=df_signals["high"],
    low=df_signals["low"],   close=df_signals["close"],
    name="Price"), row=1, col=1)

fig.add_trace(go.Scatter(
    x=buy_signals["datetime"],  y=buy_signals["low"]  * 0.999,
    mode="markers", marker=dict(symbol="triangle-up",   size=10, color="green"), name="Buy (+1)"),
    row=1, col=1)
fig.add_trace(go.Scatter(
    x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
    mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"),   name="Sell (−1)"),
    row=1, col=1)

fig.add_trace(go.Bar(
    x=df_signals["datetime"], y=df_signals["histogram"],
    name="Histogram",
    marker_color=["green" if v >= 0 else "red" for v in df_signals["histogram"]]),
    row=2, col=1)
fig.add_trace(go.Scatter(
    x=df_signals["datetime"], y=df_signals["macd"],
    mode="lines", name="MACD", line=dict(color="blue", width=1)), row=2, col=1)
fig.add_trace(go.Scatter(
    x=df_signals["datetime"], y=df_signals["signal_line"],
    mode="lines", name="Signal Line", line=dict(color="orange", width=1)), row=2, col=1)

fig.update_layout(
    title_text="MACD Momentum Strategy — Crossover Signals",
    xaxis_rangeslider_visible=False,
    height=700, yaxis=dict(autorange=True),
    xaxis2_title="Datetime",
)
fig.show()

Conclusion

This notebook demonstrated the implementation and visualization of a MACD (Moving Average Convergence Divergence) momentum strategy. We explored how the MACD line, Signal line, and Histogram are calculated and used to generate buy and sell signals.

Key Takeaways:

  • MACD as a Momentum Indicator: MACD effectively identifies shifts in momentum by comparing short-term and long-term exponential moving averages.
  • Signal Generation: Crossovers between the MACD line and the Signal line serve as primary triggers for potential entry (buy) or exit (sell) points.
  • Histogram for Confirmation: The MACD histogram provides a visual representation of the strength and direction of momentum, aiding in confirming signals or warning of potential reversals.
  • Visualization for Clarity: Plotly was used to create an interactive chart, clearly showing price action, MACD components, and the generated buy/sell signals, which is crucial for understanding strategy performance.

Next Steps:

  1. Backtesting: Integrate this strategy into a robust backtesting framework to evaluate its historical performance with various assets and market conditions.
  2. Parameter Optimization: Experiment with different fast, slow, and signal periods to find optimal settings that yield better results.
  3. Risk Management: Incorporate stop-loss and take-profit mechanisms to manage risk effectively.
  4. Combine with other indicators: Explore combining MACD with other technical indicators (e.g., RSI, Bollinger Bands) to filter signals and improve accuracy.
  5. Live Trading Integration: Consider connecting the strategy to a live trading platform for real-time execution (with extreme caution and thorough testing).

This notebook provides a solid foundation for further exploration and development of momentum-based trading strategies.