Flag Pennant Detection
Implement flag and pennant continuation pattern detection by identifying sharp impulse pole moves followed by consolidating rectangular or triangular flag formations with measured-move target projections for trade planning.
Strategy — Flag and Pennant Pattern Detection
1. Dependency Installation
!pip install pandas numpy plotly scipyRequirement 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: scipy in /usr/local/lib/python3.12/dist-packages (1.16.3) 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)
2. Library Imports
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
from scipy.stats import linregress3. Strategy Overview
Flag and Pennant are short-term continuation patterns that form after a sharp price move (the flagpole), followed by a brief consolidation.
| Pattern | Consolidation Shape | Bias |
|---|---|---|
| Bull Flag | Parallel channel, slightly downward sloping | Bullish continuation |
| Bear Flag | Parallel channel, slightly upward sloping | Bearish continuation |
| Bull Pennant | Converging lines (symmetrical triangle) after bullish pole | Bullish continuation |
| Bear Pennant | Converging lines after bearish pole | Bearish continuation |
Detection logic:
- Identify the flagpole: a sharp directional move over
pole_barsbars whose magnitude exceedspole_threshold× ATR. - Measure the consolidation channel over the subsequent
flag_barsbars using linear regression on highs and lows. - Classify the consolidation as a Flag (parallel slopes) or Pennant (converging slopes).
- Emit a signal aligned with the flagpole direction on breakout.
Limitation: Flag and pennant identification on synthetic random-walk data is infrequent; live data with strong trending behaviour will produce more signals.
4. Data Generation
def generate_data(periods: int) -> pd.DataFrame:
"""
Generate synthetic OHLCV price data using a geometric random walk.
Parameters
----------
periods : int
Number of 1-minute bars to generate.
Returns
-------
pd.DataFrame
DataFrame with columns: open, high, low, close, volume, datetime.
"""
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 | 41996 | 42152 | 41980 | 42037 | 408.354716 | 2024-01-01 00:00:00+00:00 |
| 1 | 42056 | 42183 | 41913 | 42147 | 192.442383 | 2024-01-01 00:01:00+00:00 |
| 2 | 42144 | 42616 | 42084 | 42453 | 366.862401 | 2024-01-01 00:02:00+00:00 |
| 3 | 42447 | 42451 | 42321 | 42394 | 223.939004 | 2024-01-01 00:03:00+00:00 |
| 4 | 42353 | 42455 | 42164 | 42235 | 375.811312 | 2024-01-01 00:04:00+00:00 |
5. Strategy Function
def flag_pennant_detection(
df: pd.DataFrame,
pole_bars: int = 10,
flag_bars: int = 10,
pole_threshold: float = 1.5,
) -> pd.DataFrame:
"""
Detect Flag and Pennant continuation patterns following a strong directional move.
Core logic
----------
1. Compute ATR over pole_bars to normalise the flagpole magnitude.
2. Scan each bar: if the price move over the prior pole_bars exceeds
pole_threshold × ATR, a flagpole is identified.
3. Over the subsequent flag_bars, compute linear regression slopes of highs
and lows to determine consolidation shape (parallel = flag, converging = pennant).
4. Emit a directional signal matching the flagpole direction.
Parameters
----------
df : pd.DataFrame
OHLCV DataFrame with columns: open, high, low, close, volume, datetime.
pole_bars : int
Number of bars used to measure the flagpole move.
flag_bars : int
Number of bars following the pole used to evaluate the consolidation.
pole_threshold : float
ATR multiplier; flagpole moves must exceed this multiple of ATR.
Returns
-------
pd.DataFrame
Original DataFrame extended with: atr, pattern, signal.
"""
df = df.copy().sort_values("datetime", ignore_index=True)
df["pattern"] = "none"
df["signal"] = 0
# ── ATR (Average True Range) ─────────────────────────────────────────────
tr = pd.concat([
df["high"] - df["low"],
(df["high"] - df["close"].shift(1)).abs(),
(df["low"] - df["close"].shift(1)).abs(),
], axis=1).max(axis=1)
df["atr"] = tr.rolling(pole_bars).mean()
close = df["close"].values
highs = df["high"].values
lows = df["low"].values
for i in range(pole_bars, len(df) - flag_bars):
atr_val = df["atr"].iloc[i]
if np.isnan(atr_val) or atr_val == 0:
continue
# ── Flagpole identification ──────────────────────────────────────────
pole_move = close[i] - close[i - pole_bars]
if abs(pole_move) < pole_threshold * atr_val:
continue # insufficient impulse
pole_direction = 1 if pole_move > 0 else -1 # +1 bullish, -1 bearish
# ── Consolidation regression ─────────────────────────────────────────
flag_slice = np.arange(flag_bars)
slope_h, *_ = linregress(flag_slice, highs[i: i + flag_bars])
slope_l, *_ = linregress(flag_slice, lows[i: i + flag_bars])
norm_h = slope_h / np.mean(highs[i: i + flag_bars])
norm_l = slope_l / np.mean(lows[i: i + flag_bars])
signal_bar = min(i + flag_bars, len(df) - 1)
# Pennant: slopes converge (opposite signs after impulse)
if norm_h < 0 and norm_l > 0:
df.at[signal_bar, "pattern"] = f"{'bull' if pole_direction == 1 else 'bear'}_pennant"
df.at[signal_bar, "signal"] = pole_direction
# Flag: both slopes move against the pole (retracement channel)
elif pole_direction == 1 and norm_h < 0 and norm_l < 0:
df.at[signal_bar, "pattern"] = "bull_flag"
df.at[signal_bar, "signal"] = 1
elif pole_direction == -1 and norm_h > 0 and norm_l > 0:
df.at[signal_bar, "pattern"] = "bear_flag"
df.at[signal_bar, "signal"] = -1
return df
df_signals = flag_pennant_detection(df, pole_bars=10, flag_bars=10, pole_threshold=1.5)
print("--- Pattern Distribution ---")
print(df_signals["pattern"].value_counts())
print("\n--- Signal Distribution ---")
print(df_signals["signal"].value_counts())--- Pattern Distribution --- pattern none 381 bear_flag 58 bull_flag 55 bear_pennant 3 bull_pennant 3 Name: count, dtype: int64 --- Signal Distribution --- signal 0 381 -1 61 1 58 Name: count, dtype: int64
Explanation:
ATR: Normalises the flagpole threshold to current volatility, ensuring the pole magnitude is meaningful relative to market conditions.norm_h / norm_l: Slope signs after the pole move determine whether the consolidation is retracing (flag) or converging (pennant).
6. Visualization
buy_signals = df_signals[df_signals["signal"] == 1]
sell_signals = df_signals[df_signals["signal"] == -1]
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
subplot_titles=["Price + Flag/Pennant Signals", "ATR"],
row_heights=[0.65, 0.35])
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="Bull (+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="Bear (-1)"), row=1, col=1)
fig.add_trace(go.Scatter(
x=df_signals["datetime"], y=df_signals["atr"],
mode="lines", name="ATR", line=dict(color="orange", width=1)),
row=2, col=1)
fig.update_layout(
title_text="Flag and Pennant Detection",
xaxis_rangeslider_visible=False,
height=700, xaxis2_title="Datetime",
)
fig.show()Conclusion
This notebook demonstrates the detection of Flag and Pennant patterns using a synthetic dataset. The flag_pennant_detection function identifies these continuation patterns by analyzing sharp price movements (flagpoles) and subsequent consolidation phases. The visualizations help in understanding how these patterns are identified on price charts and their correlation with ATR. Further work could involve testing on real-world data and optimizing the pattern detection parameters.