Signals·Chart Pattern Detection·Intermediate

Swing High Low Detector

Build a robust swing high and swing low detector using configurable left and right lookback windows to algorithmically identify all meaningful price turning points for market structure analysis, pattern recognition, and trade planning.

market-structuretrading-signals

Strategy — Swing High / Low Detector


1. Dependency Installation

[ ]
!pip install pandas numpy plotly scipy
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: 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.signal import argrelextrema

3. Strategy Overview

Swing High / Low detection identifies pivot points where price reverses direction. These pivots are the foundational building block for market structure analysis, pattern detection, S/R zones, and SMC concepts.

Detection logic:

  • Swing High: A bar whose high is greater than the highs of all order bars to its left and right.
  • Swing Low: A bar whose low is less than the lows of all order bars to its left and right.
  • Signal logic: Swing Low → Buy (+1) (potential reversal from a low); Swing High → Sell (−1) (potential reversal from a high).

Limitation: argrelextrema requires a fixed symmetric window; the detected swings will always lag the current bar by at least order bars.

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 42016 42197 41919 42192 120.627786 2024-01-01 00:00:00+00:00
1 42193 42254 41815 41907 122.217064 2024-01-01 00:01:00+00:00
2 41903 42437 41810 42392 407.992452 2024-01-01 00:02:00+00:00
3 42377 42512 42340 42422 273.207009 2024-01-01 00:03:00+00:00
4 42419 42594 42076 42091 384.229063 2024-01-01 00:04:00+00:00

5. Strategy Function

[ ]
def swing_high_low_detector(
    df: pd.DataFrame,
    order: int = 10,
) -> pd.DataFrame:
    """
    Detect swing highs and swing lows using symmetric rolling extrema.

    Core logic
    ----------
    1. Apply scipy.signal.argrelextrema to the high series to locate local maxima
       (swing highs) and to the low series to locate local minima (swing lows).
    2. Label each detected extremum in the DataFrame.
    3. Assign a signal: +1 at swing lows (bullish reversal candidate),
       -1 at swing highs (bearish reversal candidate).

    Parameters
    ----------
    df : pd.DataFrame
        OHLCV DataFrame with columns: open, high, low, close, volume, datetime.
    order : int
        Minimum number of bars on each side that must be lower (for highs)
        or higher (for lows) to qualify as a swing point.

    Returns
    -------
    pd.DataFrame
        Original DataFrame extended with: swing_type, signal.
    """
    df = df.copy().sort_values("datetime", ignore_index=True)
    df["swing_type"] = "none"
    df["signal"]     = 0

    highs = df["high"].values
    lows  = df["low"].values

    # ── Locate swing extrema ─────────────────────────────────────────────────
    peak_idx   = argrelextrema(highs, np.greater, order=order)[0]  # swing highs
    trough_idx = argrelextrema(lows,  np.less,    order=order)[0]  # swing lows

    # ── Assign labels and signals ────────────────────────────────────────────
    df.loc[peak_idx,   "swing_type"] = "swing_high"
    df.loc[trough_idx, "swing_type"] = "swing_low"

    df.loc[peak_idx,   "signal"] = -1  # sell at swing high
    df.loc[trough_idx, "signal"] = 1   # buy at swing low

    return df

df_signals = swing_high_low_detector(df, order=10)

print("--- Swing Type Distribution ---")
print(df_signals["swing_type"].value_counts())
print("\n--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
--- Swing Type Distribution ---
swing_type
none          468
swing_high     17
swing_low      15
Name: count, dtype: int64

--- Signal Distribution ---
signal
 0    468
-1     17
 1     15
Name: count, dtype: int64

Explanation:

  • argrelextrema(highs, np.greater, order=N): Returns indices i where highs[i] > highs[i±k] for all k ∈ [1, N].
  • The symmetric window introduces a fixed lag of order bars; signals can only be confirmed after the window's right side has been observed.

6. Visualization

[ ]
swing_highs = df_signals[df_signals["swing_type"] == "swing_high"]
swing_lows  = df_signals[df_signals["swing_type"] == "swing_low"]

fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
    subplot_titles=["Price + Swing Points", "Signal"],
    row_heights=[0.7, 0.3])

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=swing_lows["datetime"], y=swing_lows["low"] * 0.999,
    mode="markers", marker=dict(symbol="triangle-up", size=10, color="green"),
    name="Swing Low (+1)"), row=1, col=1)

fig.add_trace(go.Scatter(
    x=swing_highs["datetime"], y=swing_highs["high"] * 1.001,
    mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"),
    name="Swing High (-1)"), row=1, col=1)

fig.add_trace(go.Scatter(
    x=df_signals["datetime"], y=df_signals["signal"],
    mode="lines", name="Signal", line=dict(color="purple", width=1)),
    row=2, col=1)
fig.add_hline(y=0, line_dash="dot", line_color="gray", row=2, col=1)

fig.update_layout(
    title_text="Swing High / Low Detector",
    xaxis_rangeslider_visible=False,
    height=700, xaxis2_title="Datetime",
)
fig.show()

Conclusion

This notebook demonstrates a Swing High/Low Detector strategy, which identifies significant price reversal points in financial data. By leveraging scipy.signal.argrelextrema, we can systematically pinpoint swing highs (local maxima) and swing lows (local minima) based on a defined 'order' parameter, representing the number of preceding and succeeding bars for comparison.

The detected swing points are crucial for market structure analysis, offering insights into potential support and resistance levels, trend changes, and pattern formations. A signal of +1 is assigned to swing lows, indicating a potential bullish reversal, while -1 is assigned to swing highs, suggesting a bearish reversal.

It's important to note the inherent limitation of this method: the argrelextrema function introduces a lag. Signals are confirmed only after observing a symmetrical window of order bars on either side of the pivot. This lag means that while the detected swings are robust, they are not real-time signals and are more suitable for analysis rather than immediate entry/exit decisions without further refinement.

The visualization clearly illustrates how these swing points align with price action, providing a visual foundation for understanding market dynamics and for building more complex trading strategies.