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.
Strategy — Market Structure: Higher Highs / Lower Lows
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.signal import argrelextrema3. Strategy Overview
Market structure analysis classifies price behaviour into trending or ranging regimes based on the sequence of swing highs and lows.
| Condition | Structure | Bias |
|---|---|---|
| Successive Higher Highs (HH) + Higher Lows (HL) | Uptrend | Bullish |
| Successive Lower Lows (LL) + Lower Highs (LH) | Downtrend | Bearish |
| Mixed or alternating | Range / transition | Neutral |
Detection logic:
- Identify swing highs (local maxima) and swing lows (local minima).
- Compare consecutive swing high values: HH if current > previous; LH if current < previous.
- Compare consecutive swing low values: HL if current > previous; LL if current < previous.
- 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.argrelextremato 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.