Алготрейдинг давно вышел за пределы простых индикаторов и пересечений скользящих средних. Современные подходы опираются на анализ ликвидности, зон спроса и предложения, поведения цены внутри этих зон и реакции на них.
В этой статье разбирается полностью автоматизированная система, которая:
сканирует все торговые пары на Bybit
находит зоны supply/demand
отслеживает реакцию цены на этих зонах
подтверждает вход через price action (pinbar)
отправляет сигнал в Telegram вместе с графиком
С помощью данной торговой системы мы можем сильно упростить себе жизни. Она будет сканировать сотни монет за несколько минут, на просмотр такого количества активов руками у нас бы ушла целая вечность!
Файл проекта выложил на github. Система полностью протестирована и уже готова к работе!
Теоретическая часть
В основе системы лежит концепция supply & demand зон, используемая в подходах Smart Money.
Цена движется от одной зоны ликвидности к другой. В этих зонах происходят накопление позиций и последующий импульс (распределение).
Зоны делятся на:
Demand (спрос) - область, откуда цена агрессивно росла
Supply (предложение) - область, откуда цена резко падала

пример зон спрос и поддержки
Однако сама зона - это ещё не сигнал. Сигнал появляется только тогда, когда цена:
возвращается в зону
показывает реакцию
формирует паттерн - pinbar. Pinbar - свеча с длинным фитилём.
Таким образом, стратегия строится как цепочка:
Зона → Возврат → Реакция → Сигнал
Давайте разберём логику системы и сразу перейдём к практической части:
Как работает система (единая логика)
Система функционирует как непрерывный поток обработки рыночных данных, в котором сначала происходит загрузка исторических свечей для всех доступных торговых инструментов, после чего на этих данных строится модель зон спроса и предложения с использованием волатильности через ATR и анализа структуры свечей. Далее каждая зона проходит серию фильтров, исключающих уже отработанные или невалидные уровни, после чего остаются только актуальные области ликвидности, находящиеся вблизи текущей цены.
После этого система переключается на младший таймфрейм и анализирует последнюю закрытую свечу, пытаясь определить наличие разворотного паттерна, который выступает триггером входа. В случае совпадения условий - нахождения цены внутри зоны и формирования подтверждающего паттерна - генерируется сигнал, который дополнительно визуализируется на графике и отправляется пользователю через Telegram, при этом система запоминает уже использованные зоны и сигналы, чтобы избежать повторных входов.
Практическая часть (полный разбор кода)
Конфигурация системы
Этот блок задаёт параметры всей системы.
BOT_TOKEN = ... CHAT_ID = ... ZONE_TF = "240" ZONE_LIMIT = 1500 ATR_PERIOD = 100 SCAN_INTERVAL_SEC = 300 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
Здесь определяется:
таймфрейм для поиска зон (4H)
глубина истории
чувствительность ATR
частота сканирования рынка
Также задаются ограничения:
максимальное количество зон
фильтры по размеру зоны через ATR
толерантность входа в зону
Давайте чуть подробнее разберём выбранные параметры. Таймфрейм для основных зон поддержки и сопротивления - старший. Это позволит отсеять некачественным сигналы по младшим периодам. В итоге, мы всё равно получим большое количество сигналов, а их качество кратно вырастет.
ZONE_LIMIT - исторический лимит свечей для ��оны. Нас интересует торговля в рамках нескольких месяцев, не больше. Часто, зоны старших периодов теряют свою актуальность.
Период для ATR (фильтр волатильности) выбран не случайно такой большой. Он позволяет узнать, насколько монета волатильна. Этот фильтр здесь заменяет мультипликатор 'беты' к биткоину. По ATR введены максимальный и минимальный размеры зон, а также движение до и после формирования самой зоны.
Работа с биржей (Bybit API)
def get_symbols() def get_klines(symbol, interval, limit)
Этот блок отвечает за получение данных.
Логика следующая:
сначала запрашивается список всех торговых инструментов
фильтруются только пары с USDT
далее для каждого символа загружаются свечи
Данные приходят в формате raw API и приводятся к удобному виду.
Код функций выглядит следующим образом:
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() # от старых к новым return klines
Вспомогательная логика
def is_zone_broken(df, zone): """Проверяет, пробита ли зона после её формирования.""" start_idx = zone['start_bar'] + 1 if start_idx >= len(df): return False slice_df = df.iloc[start_idx:] if zone['type'] == 'supply': if INVALIDATION_METHOD == "close": break_level = slice_df['close'].max() else: # wick break_level = slice_df['high'].max() return break_level > zone['high'] else: # demand if INVALIDATION_METHOD == "close": break_level = slice_df['close'].min() else: break_level = slice_df['low'].min() return break_level < zone['low']
Функция проверяет, была ли зона уже пробита после формирования.
Если цена:
для supply - ушла выше зоны
для demand - ушла ниже
то зона считается невалидной.
Это критично, потому что пробитие зоны означает, что ликвидность уже забрана, значит, сигнал неактуален
Также я решил внедрить функцию инвалидации зон. На обозначенном ранее примеры вы видели, что зона спроса может превратиться в зону предложение и наоборот. Но это сложно и для системы слабо подходит. Поэтому внедрим функцию, которая будет убирать пробитые зоны:
def remove_overlapping_zones(zones): if not zones: return [] # Сортируем от самой свежей к самой старой zones = sorted(zones, key=lambda z: z['start_bar'], reverse=True) filtered = [] for z in zones: overlap = False for f in filtered: if not (z['high'] < f['low'] or z['low'] > f['high']): overlap = True break if not overlap: filtered.append(z) return filtered
Поиск зон (ядро стратегии)
Order Blocks
def detect_order_blocks(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 }) #аналогично днлает для supply orderblock
Здесь ищутся классические OB:
bearish свеча → bullish импульс → demand
bullish свеча → bearish импульс → supply
Дополнительно проверяется сила импульса через atr, так как нам крайне важно, чтобы ордер блок был не в рендже, а в направленном чётком распределении. Это фильтрует слабые движения.

rejection Blocks
def detect_rejection_blocks(df): zones = [] 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 if full == 0: continue upper_wick = h - max(o,c) lower_wick = min(o,c) - l body_ratio = body / full upper_ratio = upper_wick / 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 }) #аналогично для supply
Логика основана на свечных тенях - длинный фитиль и маленькое тело.
Это говорит о том, что цена попыталась идти дальше, но была резко отклонена.

Продвинутый фильтр зон
def detect_rejection_zones(df)
Это более сложная версия, где проверяется:
размер зоны относительно ATR
наличие импульса ДО зоны
наличие движения ПОСЛЕ зоны
Таким образом зона становится не просто паттерном, а подтверждённой структурой рынка.
Выбор ближайших зон
def get_nearest_zones(price, zones, n=2): if not zones: return [] def zone_distance(z): if price < z["low"]: return z["low"] - price elif price > z["high"]: return price - z["high"] else: return 0 zones = sorted(zones, key=zone_distance) return zones[:n]
Система не работает со всеми зонами сразу.
Она берёт текущую свечу, сортирует зоны по расстоянию и выбирает ближайшие. Это позволяет не только обозначить зону, в которую мы уже пришли, а также найти зону, до которых мы можем потянуть наши позиции.
Это уменьшает шум и повышает релевантность сигналов.
Логика сигналов (entry engine)
Pinbar
def is_bullish_pinbar(candle): o = float(candle[1]) h = float(candle[2]) l = float(candle[3]) c = float(candle[4]) body = abs(c - o) total_range = h - l if total_range == 0: return False if body > 0.3 * total_range: return False upper_wick = h - max(o, c) lower_wick = min(o, c) - l if c <= o: return False if body == 0: return False if lower_wick >= body * 2 and upper_wick <= body * 0.5: return True return False #is_bearish_pinbar аналогично
Проверяется, чтобы у свечи по м5 таймфрейму было маленькое тело, длинный фитиль и направление свечи. Это классический price action триггер.
Проверка входа
def check_long_signal(symbol, klines_5m, zones): last = klines_5m[-2] timestamp = last[0] 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}_{timestamp}_long" if key in sent_signals: return None sent_signals[key] = True return zone return None #check_short_signal аналоргично
Сигнал появляется, если цена находится внутри зоны и есть пинбар. Обратите внимание на индекс [-2] - это очень важно, так как биржа возвращает ещё и незакрытую свечу!
Также используется защита:
sent_signals = {}
Чтобы не отправлять дубликаты.
Генерация графика
def generate_chart(symbol, klines, supply_zones, demand_zones, signal_zone=None): for zone in supply_zones: abs_start = zone['start_bar'] rel_start = max(0, abs_start - offset) if rel_start >= len(df): continue x_end = len(df) color = 'red' ax.add_patch(patches.Rectangle( (rel_start, zone['low']), x_end - rel_start, zone['high'] - zone['low'], facecolor=color, alpha=0.2, edgecolor=color, linewidth=1 )) # Рисуем все DEMAND зоны (синие) for zone in demand_zones: abs_start = zone['start_bar'] rel_start = max(0, abs_start - offset) if rel_start >= len(df): continue x_end = len(df) color = 'blue' ax.add_patch(patches.Rectangle( (rel_start, zone['low']), x_end - rel_start, zone['high'] - zone['low'], facecolor=color, alpha=0.2, edgecolor=color, linewidth=1 ))
Здесь происходит построение свечного графика, отрисовка зон разного цвета, выделение сигнальной зоны.
График сохраняется в память и отправляется в Telegram. Это важный UX-элемент - визуальное подтверждение сигнала. В этом и есть суть данной системы!
Telegram-интеграция
async def send_telegram(image, caption): url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto" data = aiohttp.FormData() data.add_field("chat_id", CHAT_ID) data.add_field("caption", caption) data.add_field("photo", image, filename="chart.png") async with aiohttp.ClientSession() as session: async with session.post(url, data=data) as resp: return await resp.text()
Отправляется изображение, а также текст сигнала.
Формат сигнала:
инструмент
направление
зона
цена входа
стоп
Обработка одного символа
async def async def process_symbol(symbol, semaphore): async with semaphore: try: klines_zone = get_klines(symbol, ZONE_TF, ZONE_LIMIT) if not klines_zone: return supply_zones, demand_zones = find_supply_demand_zones(klines_zone) current_price = float(klines_zone[-2][4]) supply_zones = get_nearest_zones(current_price, supply_zones, 2) demand_zones = get_nearest_zones(current_price, demand_zones, 2) supply_zones = remove_overlapping_zones(supply_zones) demand_zones = remove_overlapping_zones(demand_zones) # Фильтруем использованные зоны (одноразовые) used = used_zones.get(symbol, set()) supply_zones = [z for z in supply_zones if (z['high'], z['low']) not in used] demand_zones = [z for z in demand_zones if (z['high'], z['low']) not in used] if not supply_zones and not demand_zones: return # Получаем последнюю 5m свечу klines_5m = get_klines(symbol, "5", 2) if not klines_5m: return(symbol, semaphore) #далее обрабатываем зоны и формируем сами сигналы при наличии таковых
Это 'сердце' системы. Что тут происходит:
Получение данных
Поиск зон по свечам
Фильтрация всех зон, отсеиваем отработавшие и перекрытые
Проверка 5m свечи (пинбар)
Генерация сигнала - картинка и текст
Отправка
Также реализовано ограничение параллелизма и защита от повторного использования зоны.
Главный цикл
async def main_loop(): symbols = get_symbols() semaphore = asyncio.Semaphore(MAX_CONCURRENT) while True: tasks = [process_symbol(symbol, semaphore) for symbol in symbols] await asyncio.gather(*tasks) await asyncio.sleep(SCAN_INTERVAL_SEC)
Система работает бесконечно: получает список монет, параллельно обрабатывает их, ждёт заданный интервал, повторяет.
Итоги
Примеры сигналов из бота:



В результате получается система, которая:
автоматически сканирует рынок
использует multi-timeframe анализ
работает с контекстом (зоны), а не индикаторами
фильтрует шум через ATR и структуру движения
даёт подтверждённые сигналы через price action
Ключевое преимущество - это не просто поиск паттернов, а контекстный анализ рынка, что делает сигналы существенно более качественными.
