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.
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 npRequirement 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 goImports Overview
This section imports necessary libraries:
pandasandnumpyare used for data manipulation and numerical operations.plotly.graph_objectsis 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:
+1for a strong bullish alignment (all EMAs stacked shortest on top).-1for a strong bearish alignment (all EMAs stacked longest on top).0for 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 spanp.adjust=Falseuses 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.