Сканер отвечает на один вопрос: куда сейчас смотрит монета и проснулась ли она. Пользователь шлёт тикер — через 5 секунд получает сигнал из двух осей: направление (от -100 до 100) и активность (от 1 до 100), собранные из 15+ метрик пяти бирж. Поверх бота — Telegram Mini App с тем же движком.
Стек: Python 3.12, aiogram 3, aiohttp, SQLite, ванильный JS в одном html-файле.
Cкрипты ключевых механик: github.com. Читать параллельно с кодом — каждый раздел ссылается на конкретный файл.
Нужен был инструмент для быстрой проверки альткоина перед входом: не терминал с сорока индикаторами, а один ответ. Требования:
скан любой монеты с BingX Perpetual по запросу;
бесплатные проверки для всех, вотчлист и массовое сканирование альты доступны в стандартной версии немного (чтобы не перегрузить API), платные функции — вотчлист с автопроверкой и масс-скан топ-100 альты по капитализации
Выглядит архитектура следующим образом:

Модули: metrics.py — сбор данных, scoring.py — две оси и сигнал, exchanges.py — клиенты бирж с фолбэками, payments.py — ончейн-проверка, storage.py — SQLite, bot.py и webapp server.py — два фронта над одним движком. Бот и приложение — отдельные процессы с общей базой пользователей и их настроек.
Новичок сворачивает всё в один рейтинг «бычье/медвежье». Правильно — две независимые оси, потому что перевес без активности не торгуется: у мёртвой монеты перекос стакана в +40% даёт красивый скор и нулевое движение.
Направление — взвешенная сумма блоков, каждый блок возвращает оценку в [-100..100]:
D = 100 <em data-mark="italic"> sum(w_i </em> s_i) / sum(w_i) — сумма только по доступным блокам.
Активность — те же взвешенные блоки, но в [0..100]: приток OI, объём против недели, сила тренда ADX, режим волатильности, модуль дельты, модуль дисбаланса.
Сигнал — пересечение осей:
<code>активность < 40 -> FLAT (монета спит, перевес не важен) |направление| < 22 -> NEUTRAL |направление| >= 22 -> LONG / SHORT |направление| >= 45 и акт >= 60 -> STRONG LONG / STRONG SHORT </code>
Первая версия весов была интуитивной. Потом прошёлся по эмпирике и переставил:
Блок |
Вес |
Обоснование |
|---|---|---|
OI x цена |
18 |
рост OI при росте цены = набор лонгов, классика чтения деривативов |
Тренд ADX/DI/EMA |
18 |
тайм-серийный моментум — самая устойчивая аномалия крипты (Liu, Tsyvinski) |
Фандинг |
17 |
изменения ставки объясняют ~12.5% вариации цены на 7д горизонте (Presto Research), carry-краудинг — BIS WP1087 |
CVD |
16 |
предиктивность order flow растёт с горизонтом (J. Financial Markets) |
L/S позиции |
12 |
толпа контрарно, топ-трейдеры по тренду |
Сектор |
8 |
corr x моментум корзины сектора |
Стакан |
4 |
горизонт предсказания дисбаланса — секунды… минута (Stoikov), для горизонта 10-60 минут почти шум |
Базис |
4 |
та же природа, что фандинг |
TVL |
3 |
медленный фундаментал |
Самая полезная правка — срезать стакан с 8 до 4. Дисбаланс выглядит информативно, но живёт минуту, а сигнал у нас на часы. Математика постоянно правится на основе собранной при тестировании бота статистики. Собираю её ежедневно и открываю позиции, сейчас матожидание такое: винрейт 35-40% с риск-ревардом 3-5.
Отрицательный фандинг — шорты платят лонгам — лонгово. Положительный — шортово. Простое правило, которое легко перевернуть при рефакторинге, поэтому оно живёт в self_test(), и бот при старте его гоняет:
<code class="python">def dir_funding(m):
f = m.get("funding")
if f is None:
return None
if abs(f) < FUNDING_NEUTRAL:
return 0.0, "фандинг ~0, нейтрален"
s = _clamp(-f / FUNDING_SCALE) # f < 0 -> s > 0 -> лонгово
side = "лонгово" if f < 0 else "шортово"
return s, f"ставка {f * 100:+.4f}% -> {side}"
</code>Полный мини-движок двух осей — scoring.py в репозитории, запускается офлайн.
Сейчас отчёт в ботевыглядит так:

Все источники опрашиваются параллельно через asyncio.gather, на каждую метрику — своя цепочка:
история OI: Binance -> Bybit -> OKX -> локальные снапшоты (фоновая задача пишет OI в SQLite каждые 15 минут — для монет, которых нет на больших биржах);
CVD: Binance -> OKX;
базис: Binance -> BingX;
фандинг: агрегат по пяти биржам сразу.
Недоступный источник возвращает None и выпадает ренормализацией. Ошибка сети — не ошибка скана.
Дорогая привычка — дёргать эндпоинт на каждый индикатор. 192 часовые свечи Binance дают сразу: цену, изменение за 24ч, CVD, объём к неделе, режим волатильности, ADX/DI/EMA. Индикаторы считаются локально.
CVD из фьючерсных klines: в поле [10] лежит квотовый объём покупок тейкеров, в [7] — весь объём. Дельта:
<code class="python"># покупки - продажи = tb - (vol - tb) = 2*tb - vol cvd24 = sum(2 * tb - v for tb, v in zip(tbuy[-24:], qvol[-24:])) / sum(qvol[-24:]) </code>
ADX — по Уайлдеру (RMA-сглаживание), из тех же свечей, ноль дополнительных запросов.
Ставки бирж расходятся, у мелких бирж фандинг шумит. Агрегат взвешен открытым интересом:
F = sum(f_i * OI_i) / sum(OI_i), вес Binance дополнительно умножен на 1.5 — упор на площадку с самой глубокой ликвидностью. Стакан аналогично: дисбаланс в полосе +-0.5% от mid, Binance входит с весом x2.
Рабочий конвейер на одном токене — metrics.py, есть офлайн-режим с фиктивной сессией: тот же код, канned-ответы, полный прогон без сети.
Худший сценарий: сигнал по монете прилетает всем, и 10 пользователей одновременно жмут скан одного тикера. Наивная реализация — 10 x 12 = 120 запросов залпом. Решение из трёх слоёв, файл api_guard.py:
TTL-кэш по символу. Свечи 120с, фандинг 300с: часовые данные чаще не обновляются.
Дедупликация. Одновременные корутины по одному ключу встают на per-key asyncio.Lock. Запрос делает первая, остальные забирают из кэша:
<code class="python">async def fetch(self, key, ttl, weight, upstream):
cached = self.cache_get(key, ttl)
if cached is not None:
return cached
lock = self._locks.setdefault(key, asyncio.Lock())
async with lock:
cached = self.cache_get(key, ttl) # сосед мог уже наполнить
if cached is not None:
return cached
if not self.allow(weight): # weight-бюджет
return None # молча в фолбэк
return self.cache_put(key, await upstream())
</code>Weight-бюджет. Binance считает лимит в weight (2400/мин на IP). Берём 240-300 — десятую часть: скользящее окно 60 секунд, при исчерпании вызов не падает с ошибкой, а пропускается — и срабатывает фолбэк-биржа. Graceful degradation вместо 429.
Вывод офлайн-демо:
<code>10 одновременных вызовов по ARB -> HTTP: 1, дедуп-ожиданий: 9 повтор в TTL -> HTTP не вырос: True, попаданий в кэш: 10 бюджет 10/мин: прошло 8 новых + 1 было, отклонено 3 (уйдут в фолбэк) </code>
Дополнительно: негативный кэш («символа нет на Binance» помним час, чтобы не долбить 400-ми), stale-фолбэк (данные до 15 минут давности лучше пропуска), и общий принцип — fail-open на метриках, fail-closed там, где деньги.
Подписчик добавляет до 10 монет. Фоновый цикл раз в 10 минут сканирует каждую и сравнивает с прошлым состоянием из SQLite. Оповещение — при выполнении любого из двух условий:
<code class="python">def watch_should_alert(prev, ev):
if prev is None:
return False, "первая проверка - состояние зафиксировано"
reasons = []
if ev["signal"] != prev["signal"] and ev["signal"] in DIRECTIONAL:
reasons.append(f"сигнал: {prev['signal']} -> {ev['signal']}")
if abs(ev["activity"] - prev["activity"]) > 10:
reasons.append(f"активность {prev['activity']} -> {ev['activity']}")
return bool(reasons), "; ".join(reasons)
</code>Важная деталь: монета сканируется один раз за цикл независимо от числа подписчиков на неё. Список — SELECT DISTINCT token FROM watchlist, рассылка — по watchers после скана.
<code>2026-08-10 12:51:04 INFO bot: watch loop: 7 монет</code>
Пауза 8 секунд между монетами внутри цикла — чтобы вотчлист не конкурировал с живыми сканами пользователей за лимиты.

Флоу: /subscribe показывает адрес кошелька -> пользователь переводит 10 USDT в любой из четырёх сетей -> присылает /paid 0x<хэш> -> бот сам находит транзакцию и активирует 30 дней.
Проверка — через публичные JSON-RPC, без API-ключей и без эксплорер-сервисов. Новичок парсит tx.value и получает ноль: USDT — это ERC20, натив в транзакции не двигается. Правильно — читать логи события Transfer из receipt:
<code class="python">def parse_receipt(receipt, chain, wallet):
if receipt.get("status") != "0x1": # reverted не считается
return None
wallet_topic = "0x" + "0" * 24 + wallet.replace("0x", "")
total = 0.0
for lg in receipt.get("logs", []):
if lg["address"].lower() != CHAINS[chain]["usdt"]:
continue # чужой токен
t = lg.get("topics", [])
if len(t) < 3 or t[0].lower() != TRANSFER_TOPIC:
continue
if t[2].lower() != wallet_topic:
continue # не наш получатель
total += int(lg["data"], 16) / 10 ** CHAINS[chain]["decimals"]
return total or None
</code>TRANSFER_TOPIC — это keccak256("Transfer(address,address,uint256)"). Несколько Transfer-логов на наш адрес в одной транзакции суммируются.
Ловушки, каждая закрыта тестом:
децималы: у USDT в Ethereum/Polygon/Arbitrum их 6, у BEP20-версии в BSC — 18. Перепутал — 10 USDT распарсится как 10^-11;
подтверждения: eth_blockNumber - receipt.blockNumber + 1 >= 3, иначе просим подождать. Хэш при отказе не сгорает — повторный /paid через минуту сработает;
дедуп: использованные хэши — в SQLite, сравнение регистронезависимое, проверка до похода в RPC;
порядок сетей: перебор BSC -> ETH -> Polygon -> Arbitrum, где нашёлся receipt — там и проверяем.
Продление идёт от конца текущего срока: оплатил во время триала — пробный день не сгорел.
Приложение — один html-файл без сборки и зависимостей плюс aiohttp-бэкенд, переиспользующий Scanner, storage и payments бота.
Каждый запрос фронта несёт initData из Telegram.WebApp. Подпись проверяется локально:
<code class="python">secret = hmac.new(b"WebAppData", bot_token.encode(), sha256).digest()
check_string = "\n".join(f"{k}={v}" for k, v in sorted(pairs.items())) # без hash
calc = hmac.new(secret, check_string.encode(), sha256).hexdigest()
ok = hmac.compare_digest(calc, their_hash)
</code>Плюс TTL: initData старше суток отклоняется — строку можно украсть из логов прокси. Сравнение только compare_digest, обычный == уязвим к timing-атаке. Валидные результаты кэшируются на 5 минут по sha256 от строки — HMAC на каждый запрос не нужен. Полный код с пятью тест-кейсами (валидная, чужой токен, подмена поля, просрочка, мусор) — auth.py
Бесплатно: скан с кулдауном 6 секунд (в боте — 10, лимиты раздельные), 3 монеты в вотчлисте, 2 массовых скана на аккаунт. Подписка: 10 USDT и масс-скан раз в 12 часов.
Масс-скан — это топ-100 по капитализации с CoinGecko (один запрос, кэш 6 часов, стейблы и обёртки исключены), каждая монета в облегчённом режиме: без корреляций и TVL, стакан и фандинг только Binance+BingX. Занимает ~6 минут, поэтому в приложении это фоновый джоб:
<code>POST /api/mass_scan -> {"status": "started"}
GET /api/mass_status -> {"status": "running", "progress": 34, "total": 95}
GET /api/mass_status -> {"status": "done", "result": {...}}
</code>Глобальный asyncio.Lock — один скан на всех; свежий результат моложе 30 минут отдаётся следующим подписчикам мгновенно из общего кэша. Бесплатная попытка списывается при выдаче результата и возвращается, если скан упал — счётчик mass_used в SQLite декрементится по списку consumers джоба.
Ранжирование результата — сила сигнала abs(direction) с приоритетом реальных сигналов над FLAT: монета с dir -60 при спящей активности не вытесняет живой SHORT.
скрин: экран скана в приложении с разбором по блокам скрин: результат масс-скана, топ-10 по силе сигнала
UI коротко: цвета берутся из themeParams Telegram с тёмным дефолтом, скелетоны на загрузке, хаптика, safe-area, нижние табы. Дизайн — отдельная тема, здесь важно одно: весь фронт — 900 строк в одном файле, грузится мгновенно.
Три процесса: server.py (порт 8080, web app), туннель, bot.py. Telegram открывает Mini App только по HTTPS, быстрый путь без домена — cloudflared quick tunnel:
<code class="bash">nohup python3 webapp_server.py > webapp.log 2>&1 & nohup ./cloudflared tunnel --url http://localhost:8080 --protocol http2 > tunnel.log 2>&1 & grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' tunnel.log | tail -1 export WEBAPP_URL="https://<ссылка>" # ДО запуска бота nohup python3 bot.py > bot.log 2>&1 & </code>
Два подводных камня, собранных лбом:
quick-туннель по умолчанию ходит по QUIC и поднимает одно UDP-соединение; на VPS оно молча отваливается — процесс жив, а Cloudflare отдаёт 1033. Флаг --protocol http2 уводит на TCP и лечит;
ссылка живёт, пока жив процесс, и меняется при каждом рестарте — для прода нужен именованный туннель или свой домен с certbot.
Проверка цепочки: curl localhost:8080/health отвечает — жив сервер, curl https://<ссылка>/health — жив туннель.
Токен бота и адрес кошелька — только через переменные окружения, в репозиторий не попадают.
Честно:
пороги сигналов (+-22/45, активность 40/60) выведены из логики шкал, а не из бэктеста — перед доверием денег стоит месяц собирать сигналы и сверять с фактом. Именно этим сейчас я и занимаюсь, часто меняя математику. Но могу сказать точно — глобально математика и модели выстроены верно и прогнозная модель действительно показывает себя отлично.
метрики только деривативные: OI, фандинг, дельта. Ончейн-потоков и китов нет. Не всегда они так нужно в интрадей торговле, однако они позволили бы забирать движения большие. Сейчас также пытаюсь найти решения для этого — однако кошельки китов постоянно меняются и не так просто отслеживать все монеты, их притоки и оттоки с бирж (на биржи)
публичные RPC для оплаты иногда тормозят, повтор через минуту решает;
сигнал — оценка перевеса, не гарантия движения.
Инженерные идеи, которые переношу в следующие проекты:
две оси вместо одного скора: направление отдельно, активность отдельно, сигнал — их пересечение;
ренормализация весов: недоступная метрика исключается вместе с весом, система деградирует плавно;
кэш + per-key лок: N одновременных пользователей = 1 HTTP-запрос;
weight-бюджет с graceful degradation: исчерпание лимита — это фолбэк, а не 429;
fail-open на метриках, fail-closed на деньгах;
ончейн-проверка платежа по хэшу: логи Transfer вместо tx.value, децималы per-chain;
инварианты в self_test при старте: сломанный знак фандинга не доедет до пользователей;
один запрос свечей = шесть метрик, индикаторы считаются локально.
Главный вывод простой: система собирает картину рынка быстрее человека, но решение и риск — по-прежнему на человеке.
ADX слабый индюк .R-квадрат лучше. По коридору тренда лучше -коридор ошибки.По боковику BB (боллинжер б ).По свечам Ишимоку и работать с периодом. Книга Патрик Микула -...5 новых техник Эндрюса про работу регрессии волн графика.