Notebooks/Support & Resistance Zones
Signals·Patterns·Intermediate

Support & Resistance Zones

Compute dynamic support and resistance zones from historical price clusters, volume nodes, and swing pivot history.

supportresistancezones

Strategy — Support and Resistance Zones


1. Dependency Installation

[1]
!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

[2]
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

Support and Resistance (S/R) zones are price levels where buying or selling pressure has historically reversed price direction. Unlike hard lines, zones account for the fact that price approaches a level multiple times within a narrow band.

Detection logic:

  1. Identify local price maxima (resistance candidates) and minima (support candidates) using argrelextrema.
  2. Cluster nearby levels: if two extrema are within zone_width × price of each other, merge them into a single zone centred on their average.
  3. Score each zone by the number of touches (higher touches = stronger zone).
  4. Emit a Buy (+1) signal when price approaches a support zone from above; Sell (−1) when approaching a resistance zone from below.

Limitation: Historical S/R zones lose relevance after extended trending moves; periodic recalculation on a rolling window is recommended.

4. Data Generation

[3]
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 41997 42436 41945 42426 143.830413 2024-01-01 00:00:00+00:00
1 42399 42476 42203 42440 128.271342 2024-01-01 00:01:00+00:00
2 42469 42496 42348 42423 433.514883 2024-01-01 00:02:00+00:00
3 42384 42613 42280 42526 207.705892 2024-01-01 00:03:00+00:00
4 42536 42620 42444 42496 193.922225 2024-01-01 00:04:00+00:00

5. Strategy Function

[4]
def support_resistance_zones(
    df: pd.DataFrame,
    order: int = 10,
    zone_width: float = 0.005,
    proximity: float = 0.003,
) -> pd.DataFrame:
    """
    Identify support and resistance zones and generate proximity-based signals.

    Core logic
    ----------
    1. Detect local highs (resistance candidates) and lows (support candidates).
    2. Cluster extrema whose price levels lie within zone_width of each other.
    3. Assign each bar a signal when price is within proximity of a zone:
       - Near resistance → Sell (-1)
       - Near support    → Buy  (+1)

    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 local extremum.
    zone_width : float
        Fractional price band used to merge nearby extrema into a single zone.
    proximity : float
        Fractional distance from a zone within which a signal is emitted.

    Returns
    -------
    pd.DataFrame
        Original DataFrame extended with: support_zones (list), resistance_zones (list),
        nearest_support, nearest_resistance, signal.
    """
    df = df.copy().sort_values("datetime", ignore_index=True)
    df["signal"] = 0

    close = df["close"].values
    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]

    def cluster_levels(prices, width):
        """Merge price levels within width fraction into a single zone."""
        sorted_p = sorted(prices)
        zones = []
        current_cluster = [sorted_p[0]]
        for p in sorted_p[1:]:
            if (p - current_cluster[0]) / current_cluster[0] <= width:
                current_cluster.append(p)
            else:
                zones.append(np.mean(current_cluster))
                current_cluster = [p]
        zones.append(np.mean(current_cluster))
        return zones

    # Derive zone price levels
    resistance_levels = cluster_levels(highs[peak_idx].tolist(),  zone_width) if len(peak_idx)   > 0 else []
    support_levels    = cluster_levels(lows[trough_idx].tolist(), zone_width) if len(trough_idx) > 0 else []

    df["nearest_resistance"] = np.nan
    df["nearest_support"]    = np.nan

    for idx, row in df.iterrows():
        c = row["close"]
        # Nearest resistance above current price
        above = [r for r in resistance_levels if r > c]
        if above:
            nr = min(above)
            df.at[idx, "nearest_resistance"] = nr
            if abs(c - nr) / nr < proximity:
                df.at[idx, "signal"] = -1  # approaching resistance → sell

        # Nearest support below current price
        below = [s for s in support_levels if s < c]
        if below:
            ns = max(below)
            df.at[idx, "nearest_support"] = ns
            if abs(c - ns) / ns < proximity:
                df.at[idx, "signal"] = 1   # approaching support → buy

    return df, resistance_levels, support_levels

df_signals, res_levels, sup_levels = support_resistance_zones(df, order=10)

print("--- Resistance Zones ---"); print([f"{r:.0f}" for r in res_levels])
print("--- Support Zones ---");    print([f"{s:.0f}" for s in sup_levels])
print("\n--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
--- Resistance Zones ---
['41836', '42287', '42620', '44810', '45200', '46475', '46779', '47055', '48744', '49413']
--- Support Zones ---
['40700', '40864', '41326', '42785', '44844', '45150', '45564', '45790', '47438']

--- Signal Distribution ---
signal
 0    352
 1     75
-1     73
Name: count, dtype: int64

Explanation:

  • cluster_levels: Merges extrema that are within zone_width fraction of each other, reducing dozens of discrete extrema to a manageable set of meaningful zones.
  • proximity check: Triggers signals when price is within the specified fractional distance of a zone — not only at exact touches — to allow for realistic entry execution.

6. Visualization

[5]
buy_signals  = df_signals[df_signals["signal"] ==  1]
sell_signals = df_signals[df_signals["signal"] == -1]

fig = make_subplots(rows=1, cols=1)

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"))

# Draw resistance zones
for level in res_levels:
    fig.add_hline(y=level, line_dash="dash", line_color="red",   line_width=1, opacity=0.5)

# Draw support zones
for level in sup_levels:
    fig.add_hline(y=level, line_dash="dash", line_color="green", line_width=1, opacity=0.5)

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="Near Support (+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="Near Resistance (-1)"))

fig.update_layout(
    title_text="Support and Resistance Zones",
    xaxis_rangeslider_visible=False,
    height=600, xaxis_title="Datetime",
)
fig.show()
[ ]