Signals·Chart Pattern Detection·Intermediate

Market Structure Hh Ll

Analyze market structure systematically by identifying sequences of higher highs, higher lows, lower highs, and lower lows to algorithmically determine the prevailing directional bias and structural trend state of any market.

market-structuretrading-signals

Strategy — Market Structure: Higher Highs / Lower Lows


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

Market structure analysis classifies price behaviour into trending or ranging regimes based on the sequence of swing highs and lows.

ConditionStructureBias
Successive Higher Highs (HH) + Higher Lows (HL)UptrendBullish
Successive Lower Lows (LL) + Lower Highs (LH)DowntrendBearish
Mixed or alternatingRange / transitionNeutral

Detection logic:

  1. Identify swing highs (local maxima) and swing lows (local minima).
  2. Compare consecutive swing high values: HH if current > previous; LH if current < previous.
  3. Compare consecutive swing low values: HL if current > previous; LL if current < previous.
  4. Confirm uptrend (HH + HL) → Buy (+1); confirm downtrend (LH + LL) → Sell (−1).

Limitation: Structure labels lag by at least one swing; the signal fires after the second confirming swing is identified.

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 42023 42171 41592 41798 402.797519 2024-01-01 00:00:00+00:00
1 41780 41939 41716 41929 452.684057 2024-01-01 00:01:00+00:00
2 41928 42266 41887 42211 167.676252 2024-01-01 00:02:00+00:00
3 42192 42214 41637 41674 287.668459 2024-01-01 00:03:00+00:00
4 41695 41771 41552 41553 136.051594 2024-01-01 00:04:00+00:00

5. Strategy Function

[ ]
def market_structure_hh_ll(
    df: pd.DataFrame,
    order: int = 10,
) -> pd.DataFrame:
    """
    Classify market structure as uptrend, downtrend, or ranging using HH/HL/LH/LL.

    Core logic
    ----------
    1. Detect swing highs and lows via scipy argrelextrema.
    2. Label each swing high as HH (Higher High) or LH (Lower High) by
       comparing it with the immediately preceding swing high.
    3. Label each swing low as HL (Higher Low) or LL (Lower Low) similarly.
    4. Emit a bullish signal (+1) at bars where both the latest swing high is HH
       and the latest swing low is HL.  Emit bearish (-1) where both LH and LL.

    Parameters
    ----------
    df : pd.DataFrame
        OHLCV DataFrame with columns: open, high, low, close, volume, datetime.
    order : int
        Bars on each side required to qualify as a swing high or low.

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

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

    peak_idx   = argrelextrema(highs, np.greater, order=order)[0]
    trough_idx = argrelextrema(lows,  np.less,    order=order)[0]

    # ── Label swing highs ────────────────────────────────────────────────────
    for i in range(1, len(peak_idx)):
        prev_i, curr_i = peak_idx[i-1], peak_idx[i]
        label = "HH" if highs[curr_i] > highs[prev_i] else "LH"
        df.at[curr_i, "swing_high_label"] = label

    # ── Label swing lows ─────────────────────────────────────────────────────
    for i in range(1, len(trough_idx)):
        prev_i, curr_i = trough_idx[i-1], trough_idx[i]
        label = "HL" if lows[curr_i] > lows[prev_i] else "LL"
        df.at[curr_i, "swing_low_label"] = label

    # ── Derive structure and signal ──────────────────────────────────────────
    # Forward-fill the labels so every bar carries the latest swing classification
    df["_sh"] = df["swing_high_label"].replace("none", np.nan).ffill()
    df["_sl"] = df["swing_low_label"].replace("none", np.nan).ffill()

    uptrend   = (df["_sh"] == "HH") & (df["_sl"] == "HL")
    downtrend = (df["_sh"] == "LH") & (df["_sl"] == "LL")

    df.loc[uptrend,   "structure"] = "uptrend"
    df.loc[downtrend, "structure"] = "downtrend"
    df.loc[uptrend,   "signal"]   = 1
    df.loc[downtrend, "signal"]   = -1

    df.drop(columns=["_sh", "_sl"], inplace=True)
    return df

df_signals = market_structure_hh_ll(df, order=10)

print("--- Structure Distribution ---")
print(df_signals["structure"].value_counts())
print("\n--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
--- Structure Distribution ---
structure
downtrend    207
ranging      199
uptrend       94
Name: count, dtype: int64

--- Signal Distribution ---
signal
-1    207
 0    199
 1     94
Name: count, dtype: int64

Explanation:

  • ffill(): Propagates the most recent swing label forward so every bar is classified, not just bars that coincide with a swing extremum.
  • HH + HL: Both higher swing highs and higher swing lows must be present to confirm an uptrend; a single HH without a corresponding HL is insufficient.

6. Visualization

[ ]
fig = make_subplots(rows=1, cols=1, shared_xaxes=True,
    subplot_titles=["Price + Market Structure and Signals"]
)

colors = {"uptrend": "rgba(0,200,0,0.15)", "downtrend": "rgba(200,0,0,0.15)", "ranging": "rgba(128,128,128,0.05)"}

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)

# Add background color for market structure
for struct, color in colors.items():
    mask = df_signals["structure"] == struct
    fig.add_trace(go.Scatter(
        x=df_signals.loc[mask, "datetime"],
        y=df_signals.loc[mask, "close"],
        mode="markers", marker=dict(color=color, size=4),
        name=struct,
        showlegend=False # Moved showlegend inside go.Scatter
    ),
        row=1, col=1
    )

# Add buy signals (green triangles above high)
buy_signals = df_signals[df_signals["signal"] == 1]
if not buy_signals.empty:
    fig.add_trace(go.Scatter(
        x=buy_signals["datetime"],
        y=buy_signals["high"] * 1.002, # Position slightly above the high
        mode="markers",
        marker=dict(symbol="triangle-up", size=10, color="green"),
        name="Buy Signal"),
        row=1, col=1
    )

# Add sell signals (red triangles below low)
sell_signals = df_signals[df_signals["signal"] == -1]
if not sell_signals.empty:
    fig.add_trace(go.Scatter(
        x=sell_signals["datetime"],
        y=sell_signals["low"] * 0.998, # Position slightly below the low
        mode="markers",
        marker=dict(symbol="triangle-down", size=10, color="red"),
        name="Sell Signal"),
        row=1, col=1
    )

fig.update_layout(
    title_text="Market Structure: Higher Highs / Lower Lows",
    xaxis_rangeslider_visible=False,
    height=700, xaxis_title="Datetime",
)
fig.show()

Conclusion

This notebook successfully implements a market structure strategy based on Higher Highs (HH), Higher Lows (HL), Lower Highs (LH), and Lower Lows (LL) to identify uptrends and downtrends.

Key takeaways:

  • We used scipy.signal.argrelextrema to detect swing highs and lows in synthetic OHLCV data.
  • Market structure labels (HH, HL, LH, LL) were derived by comparing consecutive swing points.
  • Trend signals (+1 for uptrend, -1 for downtrend) were generated when both swing high and swing low conditions aligned (e.g., HH + HL for an uptrend).
  • The strategy's output was visually represented with a candlestick chart, showing price action alongside market structure classifications (uptrend, downtrend, ranging) and explicit buy/sell signals.

This analysis provides a foundational understanding of how to programmatically identify market structure, which can be a valuable component in more complex trading strategies or for market analysis.