Алготрейдинг давно вышел за пределы простых индикаторов и пересечений скользящих средних. Современные подходы опираются на анализ ликвидности, зон спроса и предложения, поведения цены внутри этих зон и реакции на них.

В этой статье разбирается полностью автоматизированная система, которая:

  • сканирует все торговые пары на Bybit

  • находит зоны supply/demand

  • отслеживает реакцию цены на этих зонах

  • подтверждает вход через price action (pinbar)

  • отправляет сигнал в Telegram вместе с графиком

С помощью данной торговой системы мы можем сильно упростить себе жизни. Она будет сканировать сотни монет за несколько минут, на просмотр такого количества активов руками у нас бы ушла целая вечность!

Файл проекта выложил на github. Система полностью протестирована и уже готова к работе!

Теоретическая часть

В основе системы лежит концепция supply & demand зон, используемая в подходах Smart Money.

Цена движется от одной зоны ликвидности к другой. В этих зонах происходят накопление позиций и последующий импульс (распределение).

Зоны делятся на:

  • Demand (спрос) - область, откуда цена агрессивно росла

  • Supply (предложение) - область, откуда цена резко падала

    пример зон спрос и поддержки
    пример зон спрос и поддержки

Однако сама зона - это ещё не сигнал. Сигнал появляется только тогда, когда цена:

  1. возвращается в зону

  2. показывает реакцию

  3. формирует паттерн - 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, так как нам крайне важно, чтобы ордер блок был не в рендже, а в направленном чётком распределении. Это фильтрует слабые движения.

order block
order block

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

Логика основана на свечных тенях - длинный фитиль и маленькое тело.

Это говорит о том, что цена попыталась идти дальше, но была резко отклонена.

rejection block
rejection block

Продвинутый фильтр зон

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)
#далее обрабатываем зоны и формируем сами сигналы при наличии таковых

Это 'сердце' системы. Что тут происходит:

  1. Получение данных

  2. Поиск зон по свечам

  3. Фильтрация всех зон, отсеиваем отработавшие и перекрытые

  4. Проверка 5m свечи (пинбар)

  5. Генерация сигнала - картинка и текст

  6. Отправка

Также реализовано ограничение параллелизма и защита от повторного использования зоны.

Главный цикл

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

Ключевое преимущество - это не просто поиск паттернов, а контекстный анализ рынка, что делает сигналы существенно более качественными.