Бот ищет зоны спроса и предложения и присылает касание в Telegram

На графике легко найти красивый блок после того, как цена уже ушла на несколько процентов. Сложнее объяснить программе, какую именно свечу считать его началом. Моя первая версия часто ошибалась здесь: видела пинбар, замечала рост в следующих десяти свечах и проводила зону по пинбару. Между ними могло пройти полчаса боковика. На картинке получался аккуратный прямоугольник, но к началу импульса он уже не имел отношения.
Бот получает свечи бессрочных контрактов, отмечает зоны на 15-минутном графике и ждёт возврата цены. Если на закрытой пятиминутной свече появляется пинбар с касанием зоны, бот отправляет график и ориентиры по цене.
Ниже разберу правила отбора и несколько ошибок, которые всплыли при попытке связать поиск зон, график и уведомление.
Код прикладываю на github - можете пользоваться и модернизировать!
Что в этой версии называется блоком
Термин order block часто используют для любой свечи перед сильным движением. По одним OHLC-свечам нельзя узнать, где стояли реальные лимитные заявки и чей объём двигал цену. Поэтому в коде «блок» означает конкретный ценовой рисунок. Название помогает читать график, но не доказывает присутствие крупного участника.
Для зоны спроса нужны три события подряд:
Цена подходит к локальному минимуму после снижения.
У минимума появляется последняя медвежья свеча либо свеча с длинным нижним фитилём. Первая даёт кандидат на order block, вторая на rejection block.
Следующая свеча закрывается вверх за границей кандидата. В пределах четырёх свечей цена закрывается хотя бы на два ATR выше этой границы.
Зона предложения строится зеркально: подход к локальному максимуму после роста, последняя бычья свеча или верхний фитиль, затем быстрый выход вниз.
От ответа биржи до закрытой свечи
BingxClient приводит ответ к таблице с колонками time, open, high, low, close, volume. В описании свечного метода BingX свечи перечислены массивами по позициям. Клиент также принимает ответ с именованными полями. В выдаче может быть текущий интервал, который ещё меняется. Если считать его закрытым, зона способна появиться и исчезнуть между двумя проходами сканера.
Перед расчётом я оставляю только интервалы, у которых уже наступило время закрытия:
def closed_candles(df, interval_minutes):
if df.empty:
return df.copy()
times = pd.to_datetime(df["time"], utc=True)
now = pd.Timestamp.now(tz="UTC")
return df.loc[
times + pd.Timedelta(minutes=interval_minutes) <= now
].reset_index(drop=True)
Здесь time рассматривается как время открытия свечи. Для каждого другого источника данных это нужно проверить отдельно. Пятиминутный сигнал тоже считается только после закрытия своего интервала.
Волатильность измеряется через ATR за 20 свечей. При оценке кандидата в базу попадают предыдущие интервалы: диапазон самой исходной свечи не должен увеличивать порог, с которым эту свечу сравнивают. В коде это сдвиг скользящего среднего на одну позицию.
prev_close = df["close"].shift(1)
tr = np.maximum(
df["high"] - df["low"],
np.maximum(
(df["high"] - prev_close).abs(),
(df["low"] - prev_close).abs(),
),
)
df["atr"] = tr.rolling(20).mean().shift(1)
ATR здесь служит единицей измерения, чтобы один и тот же порог можно было применить к инструментам с разной ценой. Движение на 100 пунктов само по себе ничего не говорит о силе импульса.
Где провести границы зоны
Для спроса нижняя граница проходит по минимуму исходной свечи, верхняя по верхней границе её тела. Для предложения верхняя граница проходит по максимуму, нижняя по нижней границе тела. Так в зону входят фитиль и тело исходной свечи.
if zone_type == "demand":
low = candle["low"]
high = max(candle["open"], candle["close"])
else:
low = min(candle["open"], candle["close"])
high = candle["high"]
Ширину я ограничил 1.2 ATR. Раньше широкий прямоугольник просто обрезался до заданного размера. Такой приём меняет цену границы без связи со свечой, поэтому теперь слишком широкий кандидат пропускается целиком.
Сам факт медвежьей свечи в спросе ещё ничего не решает. За шесть свечей до неё цена должна заметно снизиться: разница между первым и последним закрытием составляет минимум 0.75 ATR. Минимум исходной свечи должен находиться рядом с минимумом этого участка. Для предложения условия обращаются в другую сторону.
Пинбар проходит отдельное правило: тело занимает не больше 35% диапазона, нужный фитиль занимает хотя бы 55% и как минимум вдвое длиннее противоположного. Это геометрическое определение. Универсального «правильного» пинбара на рынке нет.
Главное условие: выход сразу после базы
В старой версии достаточно было увидеть движение в ближайших десяти свечах. Поэтому медленный рост после нескольких пересечений зоны мог подтвердить свечу задним числом.
Теперь первая свеча после кандидата должна закрыться за ближней границей зоны хотя бы на 0.15 ATR. Её тело должно быть не меньше 0.55 ATR. Затем за четыре свечи должно появиться закрытие на расстоянии минимум 2 ATR от той же границы. Сравниваются именно закрытия, чтобы один длинный фитиль не выглядел готовым импульсом.
Сокращённый фрагмент для спроса:
first = df.iloc[i + 1]
if first["close"] <= first["open"]:
return None
if abs(first["close"] - first["open"]) < 0.55 * atr:
return None
if first["close"] <= zone["high"] + 0.15 * atr:
return None
future = df.iloc[i + 1:i + 5]
distances = future["close"] - zone["high"]
confirmed = distances[distances >= 2 * atr]
if confirmed.empty:
return None
Предложение проверяется зеркально. Запомнить индекс свечи подтверждения полезно для следующего шага: нельзя искать пробой зоны внутри того импульса, который её создал.
Кандидаты дополнительно получают простую оценку: расстояние закрытия от базы в ATR, размер первой импульсной свечи и небольшой бонус для свечи противоположного цвета. Близкие пересекающиеся зоны одного направления объединяются выбором кандидата с более высокой оценкой. Оценка служит для отбора прямоугольников на графике. Она не является вероятностью отскока.
Параметр | Значение в коде | Что ограничивает |
|---|---|---|
История для ATR | 20 свечей | Масштаб движения |
Подход к экстремуму | 6 свечей | Положение базы |
Максимальная ширина | 1.2 ATR | Слишком размытые зоны |
Первая свеча выхода | тело от 0.55 ATR | Слабый старт |
Подтверждение | закрытие от 2 ATR за 4 свечи | Позднее движение |
Значения подобраны вручную. Их удобно вынести в конфигурацию и фиксировать версию при каждой проверке результатов. Подбирать пороги на той же истории, на которой потом заявляется качество сигналов, нельзя.
Когда зона перестаёт быть интересной
После подтверждения импульса бот просматривает последующие свечи. При INVALIDATION_METHOD = "close" зона спроса снимается, если цена закрылась ниже её нижней границы. Зона предложения снимается после закрытия выше верхней границы. Можно выбрать проверку по фитилю, но она жёстче и будет выбрасывать часть резких возвратов.
Для уведомлений я оставляю только первый возврат. Если после формирования цена уже коснулась зоны, повторное касание не считается свежим. Последняя закрытая 15-минутная свеча из этой проверки исключена: в ней может находиться пятиминутный пинбар, который бот оценивает прямо сейчас.
Это консервативное правило. Зона способна дать несколько реакций, но их нельзя смешивать в одну статистику с первым возвратом. Если захочется изучить вторые касания, им лучше дать отдельную метку и отдельные результаты проверки.
Что именно запускает уведомление
Одного входа текущей цены в прямоугольник недостаточно. Бот ждёт закрытую свечу 5m с длинным фитилём в сторону зоны. Фитиль должен пересечь прямоугольник, а закрытие остаться рядом с его ближней границей. Поэтому свеча, которая задела уровень только тенью и закрылась далеко, не отправляет сигнал.
def candle_retests_zone(candle, zone):
width = zone["high"] - zone["low"]
if zone["type"] == "demand":
return (
candle["low"] <= zone["high"]
and candle["high"] >= zone["low"]
and zone["low"] <= candle["close"]
<= zone["high"] + 0.25 * width
)
# Для предложения условие отражается относительно границ зоны.
Сканер обходит список контрактов, затем ждёт пять минут перед следующим обходом. На один инструмент берёт до 1000 свечей 15m, выбирает ближайшие действующие зоны, затем проверяет последние свечи 5m. Схема работы получается такой:
BingX: 15m свечи → закрытые интервалы → ATR → зоны
↓
BingX: 5m свечи → закрытый пинбар → первое касание
↓
график → Telegram sendPhoto
На графике зоны спроса синие, предложения красные. Сигнальная зона получает более заметную границу. В подписи есть границы зоны, цена закрытия сигнальной свечи и ориентир для защитного уровня. Для покупки ориентиром служит минимум пинбара или зоны, если он ниже. Для продажи берётся максимум из тех же двух точек. Бот показывает эти значения для оценки сценария. Ордер он не размещает.
Неприятную ошибку нашёл в построении картинки: клиент BingX возвращал DataFrame с колонкой time, а функция графика пыталась собрать из него новый набор колонок с timestamp. После исправления график работает с тем же рядом свечей, на котором были рассчитаны зоны.
Отправка и повторы
В прежнем порядке зона помечалась использованной до отправки фотографии. Если Telegram отвечал ошибкой, уведомление терялось. Сейчас бот сначала проверяет HTTP-ответ и поле ok, затем записывает пару «инструмент, зона» в память.
async with session.post(url, data=data) as resp:
payload = await resp.json()
if resp.status != 200 or not payload.get("ok"):
raise RuntimeError(f"Telegram rejected signal: {resp.status} {payload}")
used_zones.setdefault(symbol, set()).add((zone["type"], zone["time"]))
Клиент биржи синхронный. Его запросы выполняются через asyncio.to_thread, чтобы ожидание сети не останавливало остальные задачи. В описании метода свечей указан предел в один запрос в секунду на IP. В боте интервал между стартами запросов равен 1.1 секунды, одновременно обрабатывается до десяти инструментов. При нескольких копиях на одном IP этого локального ограничения уже недостаточно.
Здесь есть следствие, которое легко пропустить: на инструмент обычно нужны два запроса, для 15m и 5m. Сто инструментов займут только на интервал между запросами около 220 секунд. Если список контрактов длиннее, обход может идти дольше заданной пятиминутной паузы. SCAN_INTERVAL_SEC отсчитывается после завершения обхода. При большом списке лучше заранее выбрать наблюдаемые инструменты или перестроить цикл так, чтобы обновлять их по расписанию.
Состояние отправленных сигналов пока живёт в памяти процесса. После перезапуска оно исчезает. Исторический фильтр касаний снимает много старых зон, но повтор того же уведомления вблизи перезапуска ещё возможен. Для постоянной работы идентификатор сигнала стоит хранить в SQLite вместе со статусом отправки. Здесь понадобится аккуратно разобрать пограничный случай: Telegram принял сообщение, а процесс остановился до записи результата.
Запуск
В каталоге бота лежат v4.py, bingx_client.py и список зависимостей. Достаточно установить их, задать BOT_TOKEN и CHAT_ID в переменных окружения и запустить основной файл:
python -m pip install -r requirements.txt
python v4.pyТокен и номер чата не записаны в исходник. Бот при старте проверяет, что обе переменные заданы. Перед длительным запуском я бы ограничил список наблюдаемых контрактов и посмотрел журнал полного обхода: при лимите свечного метода время до следующей проверки конкретной монеты зависит от длины этого списка.
Как я проверял изменение
Сначала сделал искусственные ряды, где положение исходной свечи известно заранее. Проверки покрывают спрос у основания падения, зеркальное предложение у вершины роста и блок с длинным фитилём. Отдельно проверяется случай, когда сильное движение начинается только через пять свечей: исходная свеча не должна стать зоной. Ещё один ряд возвращается к базе после подтверждения, и зона снимается как уже посещённая. Есть проверка отбора закрытых интервалов.
Такие тесты полезны для логики индексов и границ. Они не отвечают на вопрос, принесут ли сигналы результат на живом рынке. Для этого нужен отдельный журнал всех найденных зон, включая те, по которым не было отправки, и проверка по времени на новых данных. Я бы фиксировал минимум: время появления кандидата, время подтверждения, границы, тип блока, первый возврат, спред, цену возможного входа и дальнейшие максимальное благоприятное и неблагоприятное отклонения. Издержки и задержку отправки придётся учитывать отдельно.
Главное практическое изменение получилось простым: зона теперь привязана к свече у экстремума и к быстрому выходу сразу после неё. Прямоугольников стало труднее нарисовать слишком много, а каждый оставшийся можно проверить по понятным условиям. Следующий этап для меня состоит в сборе истории первых возвратов и сравнении ручных порогов на отрезке, который не участвовал в их выборе.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.