Signals·TA Strategy Implementations·Intermediate

EMA Ribbon Trend Strategy

Build an EMA ribbon trend-following strategy that stacks multiple exponential moving averages to visually and algorithmically identify trend direction, strength, acceleration, and potential exhaustion points.

trading-signalstrading-strategies

Strategy — EMA Ribbon Trend Following

An EMA ribbon consists of multiple EMAs with increasing periods plotted together. When all EMAs are stacked in ascending order (shortest on top), the market is in a strong uptrend. When in descending order (longest on top), the market is in a strong downtrend. The ribbon gives a richer picture of trend strength than a single crossover.


1–2. Installation and Imports

[ ]
!pip install pandas numpy

import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
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: 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: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
[ ]
import plotly.graph_objects as go

Imports Overview

This section imports necessary libraries:

  • pandas and numpy are used for data manipulation and numerical operations.
  • plotly.graph_objects is used for creating interactive candlestick charts and other visualizations.

3. Dummy Dataset

Dummy Dataset Explanation

The generate_data function creates a synthetic OHLCV (Open, High, Low, Close, Volume) dataset. This dataset simulates realistic price movements over a specified number of periods, which is useful for testing the EMA ribbon strategy without relying on external data sources.

[ ]
def generate_data(periods: int) -> pd.DataFrame:
    """Generates a larger synthetic OHLCV dataset with more realistic price fluctuations."""
    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 # Starting price
    volatility_scale = 0.005 # Controls the general magnitude of price changes
    wick_deviation_scale = 0.002 # Controls how much wicks extend beyond body

    for i in range(periods):
        # Open price drifts slightly from the previous close
        open_price = last_close + np.random.normal(0, last_close * volatility_scale * 0.1)

        # Simulate a price change to determine the closing price
        price_change = np.random.normal(0, last_close * volatility_scale)
        close_price = open_price + price_change

        # Determine the high and low of the candle body
        body_high = max(open_price, close_price)
        body_low = min(open_price, close_price)

        # Simulate wicks extending beyond the body
        # High wick should be above the body_high
        high_wick_extension = np.abs(np.random.normal(0, last_close * wick_deviation_scale))
        high_price = body_high + high_wick_extension

        # Low wick should be below the body_low
        low_wick_extension = np.abs(np.random.normal(0, last_close * wick_deviation_scale))
        low_price = body_low - low_wick_extension

        # Ensure OHLC integrity: High must be the absolute highest, Low the absolute lowest
        high_price = max(high_price, open_price, close_price)
        low_price = min(low_price, open_price, close_price)

        # Ensure High is never less than Low
        if high_price < low_price:
            high_price, low_price = low_price, high_price # Swap if somehow invalid

        # Ensure all values are positive integers
        open_price = max(1, int(open_price))
        high_price = max(1, int(high_price))
        low_price = max(1, int(low_price))
        close_price = max(1, int(close_price))

        price_data.append({
            "open": open_price,
            "high": high_price,
            "low": low_price,
            "close": close_price
        })
        last_close = close_price # Update last_close for the next iteration

    df_large = pd.DataFrame(price_data, index=datetime_index)
    df_large.index.name = "datetime"

    # Simulate volume with some fluctuation
    df_large["volume"] = np.random.uniform(100.0, 500.0, periods)

    return df_large

periods = 500 # Generate 500 data points
df_large = generate_data(periods)
df_large["datetime"] = df_large.index.to_series()
df_large = df_large.reset_index(drop=True)

4. Strategy Function

Strategy Function Application

The ema_ribbon_strategy function calculates multiple Exponential Moving Averages (EMAs) and identifies bullish or bearish alignments within the ribbon. The strategy determines a signal:

  • +1 for a strong bullish alignment (all EMAs stacked shortest on top).
  • -1 for a strong bearish alignment (all EMAs stacked longest on top).
  • 0 for a mixed or unclear trend.

This output is then merged with the main DataFrame df_indicators_large for plotting.

[ ]
def ema_ribbon_strategy(
    df:      pd.DataFrame,
    periods: list = [20, 30, 40, 50, 60],
) -> pd.DataFrame:
    df = df.copy().sort_values("datetime", ignore_index=True)

    ema_cols = []
    for p in periods:
        col = f"ema_{p}"
        df[col] = df["close"].ewm(span=p, adjust=False).mean()
        ema_cols.append(col)

    # Ribbon is bullish when each EMA is above the next longer EMA
    df["ribbon_bullish"] = all(
        (df[ema_cols[i]] > df[ema_cols[i+1]]).all()
        for i in range(len(ema_cols)-1)
    )

    ema_values = df[ema_cols]
    df["ribbon_aligned_bull"] = (ema_values.diff(axis=1).iloc[:, 1:] < 0).all(axis=1)
    df["ribbon_aligned_bear"] = (ema_values.diff(axis=1).iloc[:, 1:] > 0).all(axis=1)

    df["signal"] = np.where(
        df["ribbon_aligned_bull"],  1,
        np.where(df["ribbon_aligned_bear"], -1, 0)
    )

    return df[["datetime", "close"] + ema_cols + ["signal"]]

# Prepare df_indicators_large for plotting
df_indicators_large = df_large.copy()

# Calculate SMA_10
df_indicators_large["sma_10"] = df_indicators_large["close"].rolling(window=10).mean()

# Apply the EMA ribbon strategy to get EMA values for the larger dataset
df_ema_results = ema_ribbon_strategy(df_large)

# Merge ema_20 and signal from the results into df_indicators_large
df_indicators_large = pd.merge(
    df_indicators_large,
    df_ema_results[['datetime', 'ema_20', 'signal']],
    on='datetime',
    how='left'
)

print("--- Indicators (Large Dataset) ---")
display(df_indicators_large.tail(10))
--- Indicators (Large Dataset) ---
open high low close volume datetime sma_10 ema_20 signal
490 45606 45623 45335 45414 102.257858 2024-01-01 08:10:00+00:00 45758.9 45848.120023 -1
491 45420 45454 44805 44925 431.992420 2024-01-01 08:11:00+00:00 45648.6 45760.203831 -1
492 44943 45036 44856 44867 151.552587 2024-01-01 08:12:00+00:00 45569.3 45675.136799 -1
493 44818 44841 44578 44591 397.345406 2024-01-01 08:13:00+00:00 45436.8 45571.885675 -1
494 44534 44713 44421 44424 443.146211 2024-01-01 08:14:00+00:00 45264.3 45462.563230 -1
495 44442 44606 44009 44021 197.829890 2024-01-01 08:15:00+00:00 45072.1 45325.271494 -1
496 44008 44128 43853 43863 263.892424 2024-01-01 08:16:00+00:00 44881.2 45186.007542 -1
497 43851 44166 43801 43996 376.574091 2024-01-01 08:17:00+00:00 44739.8 45072.673490 -1
498 43996 44157 43922 44099 236.522485 2024-01-01 08:18:00+00:00 44581.5 44979.942682 -1
499 44113 44247 43897 44247 270.492245 2024-01-01 08:19:00+00:00 44444.7 44910.138617 -1

Code Logic

  • ewm(span=p, adjust=False).mean(): Computes the Exponential Moving Average with span p. adjust=False uses the standard recursive EMA formula rather than a weighted sum — consistent with how EMA is defined on trading platforms.
  • ribbon_aligned_bull: True when each EMA is above the next longer EMA — the ribbon is fanned out in bullish order, indicating a strong uptrend.
  • signal: +1 full bullish alignment, −1 full bearish alignment, 0 mixed (no clear trend).
[ ]
fig = go.FigureWidget(data=[
    go.Candlestick(
        x=df_indicators_large["datetime"],
        open=df_indicators_large['open'],
        high=df_indicators_large['high'],
        low=df_indicators_large['low'],
        close=df_indicators_large['close'],
        name='Price'
    )
])

# Add SMA_10
fig.add_trace(
go.Scatter(
    x=df_indicators_large["datetime"],
    y=df_indicators_large['sma_10'],
    mode='lines',
    name='SMA 10',
    line=dict(color='blue', width=1)
))

# Add EMA_20 (changed from EMA_10)
fig.add_trace(
go.Scatter(
    x=df_indicators_large["datetime"],
    y=df_indicators_large['ema_20'], # Changed from 'ema_10'
    mode='lines',
    name='EMA 20', # Changed from 'EMA 10'
    line=dict(color='orange', width=1)
))

# Add Buy Signals
buy_signals = df_indicators_large[df_indicators_large['signal'] == 1]
fig.add_trace(go.Scatter(
    x=buy_signals['datetime'],
    y=buy_signals['low'] * 0.99, # Plot below the low price
    mode='markers',
    marker=dict(symbol='triangle-up', size=10, color='green'),
    name='Buy Signal'
))

# Add Sell Signals
sell_signals = df_indicators_large[df_indicators_large['signal'] == -1]
fig.add_trace(go.Scatter(
    x=sell_signals['datetime'],
    y=sell_signals['high'] * 1.01, # Plot above the high price
    mode='markers',
    marker=dict(symbol='triangle-down', size=10, color='red'),
    name='Sell Signal'
))

fig.update_layout(
    title_text='Candlestick Chart with SMA, EMA, and Trading Signals',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=600,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles
)

fig.show()

Candlestick Chart with Signals

This chart visualizes the generated OHLCV data using a candlestick plot. It also overlays:

  • SMA 10 (Simple Moving Average): A basic trend indicator.
  • EMA 20 (Exponential Moving Average): A faster, more responsive trend indicator.
  • Buy Signals (Green Up Triangles): Indicate a strong bullish EMA ribbon alignment.
  • Sell Signals (Red Down Triangles): Indicate a strong bearish EMA ribbon alignment.

The chart provides an interactive way to observe price action, trend indicators, and the strategy's generated trading signals.

Conclusion

This notebook demonstrates the implementation and visualization of an EMA Ribbon Trend Following strategy. We generated a synthetic OHLCV dataset, calculated multiple EMAs to form the ribbon, and identified bullish and bearish alignments. The strategy generates trading signals based on the EMA ribbon's configuration, which were then plotted on an interactive candlestick chart alongside SMA and EMA indicators.