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).
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:
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.
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
No comments yet.