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.
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_subplotsRequirement 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:
| Component | Definition |
|---|---|
| MACD Line | Fast EMA − Slow EMA (typically 12 − 26 periods) |
| Signal Line | EMA of the MACD Line over N periods (typically 9) |
| Histogram | MACD 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:
- Backtesting: Integrate this strategy into a robust backtesting framework to evaluate its historical performance with various assets and market conditions.
- Parameter Optimization: Experiment with different
fast,slow, andsignalperiods to find optimal settings that yield better results. - Risk Management: Incorporate stop-loss and take-profit mechanisms to manage risk effectively.
- Combine with other indicators: Explore combining MACD with other technical indicators (e.g., RSI, Bollinger Bands) to filter signals and improve accuracy.
- 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.