Back to Home

Automation of searching for supply demand zones on Bybit

The system automates the search for supply/demand zones on Bybit through order and rejection blocks with ATR filters. Entry confirmation by pinbar on 5m-TF with sending charts to Telegram. Full code breakdown for middle/senior developers.

Python-bot: demand/supply zones Bybit + Telegram
Advertisement 728x90

Automated Supply/Demand Zone Scanner with Telegram Alerts for Bybit

A Python-based system scans all USDT pairs on Bybit, identifies supply and demand zones using Smart Money concepts, tracks price retests with pinbar formation, and instantly sends trading signals to Telegram with chart markup. Processing hundreds of assets takes minutes instead of hours of manual analysis.

The logic follows a chain: zone detection → price retest → price action reaction → signal generation. Zones are filtered by ATR, breakouts, and overlaps to exclude irrelevant levels.

Theory of Supply and Demand Zones

Liquidity zones are key areas where positions accumulate and subsequent momentum occurs. Demand zones form where price aggressively rose from, supply zones where it sharply fell from. A signal triggers when price returns to a zone with pinbar confirmation (a candle with a long wick).

Google AdInline article slot

Zone validity criteria:

  • Impulse into zone > 2 ATR
  • Impulse out of zone > 2 ATR
  • Zone size 1–6 ATR
  • No breakout after formation
MAX_ZONE_ATR = 7
MIN_ZONE_ATR = 1
LOOKBACK_PRE = 8
LOOKAHEAD_POST = 8
MIN_PRE_MOVE_ATR = 2
MIN_POST_MOVE_ATR = 2
MIN_ZONE_SIZE_ATR = 1.0
MAX_ZONE_SIZE_ATR = 6.0

Parameters adjust sensitivity: 4H timeframe for zones, ATR period 100 for volatility assessment, scanning every 300 seconds.

Integration with Bybit API

The system loads symbol lists and historical candles:

Google AdInline article slot
def get_symbols():
    data = client.get_instruments_info(category="linear")
    symbols = []
    for item in data["result"]["list"]:
        if item["quoteCoin"] == "USDT":
            symbols.append(item["symbol"])
    return symbols

def get_klines(symbol, interval, limit):
    data = client.get_kline(
        category="linear",
        symbol=symbol,
        interval=interval,
        limit=limit
    )
    klines = data["result"]["list"]
    klines.reverse()  # from oldest to newest
    return klines

Only USDT pairs are filtered. Data is processed in a pandas DataFrame with computed ATR.

Order Block Detection

Order blocks are foundational structures for zones. Demand OB: a bearish candle followed by a bullish impulse above the previous high.

def detect_order_blocks(df):
    for i in range(1, len(df)):
        o1 = df.open.iloc[i-1]
        c1 = df.close.iloc[i-1]
        h1 = df.high.iloc[i-1]
        l1 = df.low.iloc[i-1]
        o2 = df.open.iloc[i]
        c2 = df.close.iloc[i]
        atr = df.atr.iloc[i]

        # DEMAND OB
        if c1 < o1 and c2 > o2 and c2 > h1:
            impulse = c2 - l1
            if impulse > atr * 1.5:
                zones.append({
                    "type": "demand",
                    "kind": "orderblock",
                    "low": l1,
                    "high": o1,
                    "start_bar": i-1
                })

Similarly for supply. Impulse is filtered >1.5 ATR to exclude range-bound movements.

Google AdInline article slot

Rejection Blocks and Advanced Zones

Rejection blocks are detected by candle proportions: lower wick >55% of range, body <35%.

def detect_rejection_blocks(df):
    for i in range(2, len(df)-1):
        o = df.open.iloc[i]
        c = df.close.iloc[i]
        h = df.high.iloc[i]
        l = df.low.iloc[i]
        atr = df.atr.iloc[i]
        body = abs(c - o)
        full = h - l
        upper_wick = h - max(o,c)
        lower_wick = min(o,c) - l
        body_ratio = body / full
        lower_ratio = lower_wick / full

        # DEMAND rejection
        if lower_ratio > 0.55 and body_ratio < 0.35:
            zones.append({
                "type": "demand",
                "kind": "rejection",
                "low": l,
                "high": min(o,c),
                "start_bar": i
            })

An advanced filter checks impulses before/after the zone relative to ATR.

Invalidation function:

def is_zone_broken(df, zone):
    start_idx = zone['start_bar'] + 1
    slice_df = df.iloc[start_idx:]
    if zone['type'] == 'supply':
        break_level = slice_df['high'].max() if INVALIDATION_METHOD == "wick" else slice_df['close'].max()
        return break_level > zone['high']
    else:
        break_level = slice_df['low'].min() if INVALIDATION_METHOD == "wick" else slice_df['close'].min()
        return break_level < zone['low']

Overlapping zones are removed by sorting by freshness and excluding overlaps.

Selecting Nearest Zones and Pinbar Trigger

Two zones closest to current price are selected:

def get_nearest_zones(price, zones, n=2):
    def zone_distance(z):
        if price < z["low"]:
            return z["low"] - price
        elif price > z["high"]:
            return price - z["high"]
        return 0
    return sorted(zones, key=zone_distance)[:n]

Pinbar on 5m timeframe:

def is_bullish_pinbar(candle):
    o, h, l, c = float(candle[1]), float(candle[2]), float(candle[3]), float(candle[4])
    body = abs(c - o)
    total_range = h - l
    if body > 0.3 * total_range or c <= o or lower_wick < body * 2 or upper_wick > body * 0.5:
        return False
    return True

Signal Generation and Sending

Long signal check:

def check_long_signal(symbol, klines_5m, zones):
    last = klines_5m[-2]  # closed candle
    price = float(last[4])
    if not is_bullish_pinbar(last):
        return None
    for zone in zones:
        if price_in_zone(price, zone):
            key = f"{symbol}_{last[0]}_long"
            if key in sent_signals:
                return None
            sent_signals[key] = True
            return zone
    return None

The signal is visualized on a chart with zone markup and sent to Telegram via bot API.

Key Points

  • The system uses 4H for zones and 5m for triggers, filtering noise from lower timeframes.
  • ATR normalization adapts zones to asset volatility without tying to BTC.
  • Duplicate protection via sent_signals and invalidation of broken zones.
  • Processing all Bybit USDT pairs in minutes with focus on nearest zones.
  • Price action (pinbar) as the final trigger after structural confirmation.

— Editorial Team

Advertisement 728x90

Read Next