Блог им. RomellaAkumov

Альткоин сканнер - ончейн метрики для отбора токенов: от Telegram-бота до приложения

Альткоин сканнер — ончейн метрики для отбора токенов: от Telegram-бота до приложения

Сканер отвечает на один вопрос: куда сейчас смотрит монета и проснулась ли она. Пользователь шлёт тикер — через 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 альты по капитализации

Архитектура

Выглядит архитектура следующим образом:

Альткоин сканнер - ончейн метрики для отбора токенов: от Telegram-бота до приложения
Система модульная и на github загружены именно модули алгоритмы.

Модули: 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 в репозитории, запускается офлайн.

Сейчас отчёт в ботевыглядит так:

Альткоин сканнер - ончейн метрики для отбора токенов: от Telegram-бота до приложения

Сбор метрик: пять бирж и цепочки фолбэков

Все источники опрашиваются параллельно через 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-сглаживание), из тех же свечей, ноль дополнительных запросов.

OI-weighted фандинг

Ставки бирж расходятся, у мелких бирж фандинг шумит. Агрегат взвешен открытым интересом:

F = sum(f_i * OI_i) / sum(OI_i), вес Binance дополнительно умножен на 1.5 — упор на площадку с самой глубокой ликвидностью. Стакан аналогично: дисбаланс в полосе +-0.5% от mid, Binance входит с весом x2.

Рабочий конвейер на одном токене — metrics.py, есть офлайн-режим с фиктивной сессией: тот же код, канned-ответы, полный прогон без сети.


API-бюджет: 10 пользователей не кладут лимиты

Худший сценарий: сигнал по монете прилетает всем, и 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 секунд между монетами внутри цикла — чтобы вотчлист не конкурировал с живыми сканами пользователей за лимиты.

Альткоин сканнер - ончейн метрики для отбора токенов: от Telegram-бота до приложения

Оплата USDT без эквайринга

Флоу: /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 — там и проверяем.

Продление идёт от конца текущего срока: оплатил во время триала — пробный день не сгорел.


Mini App поверх бота

Приложение — один html-файл без сборки и зависимостей плюс aiohttp-бэкенд, переиспользующий Scanner, storage и payments бота.

Авторизация: HMAC без похода в Telegram

Каждый запрос фронта несёт 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 при старте: сломанный знак фандинга не доедет до пользователей;

  • один запрос свечей = шесть метрик, индикаторы считаются локально.

Главный вывод простой: система собирает картину рынка быстрее человека, но решение и риск — по-прежнему на человеке.

Данная публикация является личным мнением автора. Мнение владельца сайта может не совпадать с мнением автора.
463
2 комментария
главная болезнь в кодинге, в том что думаешь что им можно все решить
avatar
Ты учишь систему.Система не думает.Сила тренда в перекрытии фракталов, тайме и объеме. Сила тренда растет в тренде в 2 раза за 4 фрактала.Индикаторы не нужны тк период делает задержку сигналов.Придумай формулу периода.Подсказка -период зависит от перекрытия фракталов.Чем больше перекрытие, тем больше период. Я отказался от индикаторов в 2010г тк начал читать график по свечам. Придумай индюк типа средний размер сделок за тайм.Количество сделок за тайм.Поймешь кто покупает, а кто продает. Давление объема на тайм — тоже полезный индюк  V\(H-L).
ADX слабый индюк .R-квадрат лучше. По коридору тренда лучше -коридор ошибки.По боковику BB (боллинжер б ).По свечам Ишимоку и работать с периодом. Книга Патрик Микула -...5 новых техник Эндрюса про работу регрессии волн графика.
avatar

Читайте на SMART-LAB:
Фото
Почему плечо используют не постоянно, а под конкретную идею
Кредитное плечо не стоит воспринимать как способ для быстрого разгона капитала. Логика опытного инвестора обычно другая: плечо — это не...
Фото
Инарктика: тот случай, когда сильный рост выручки еще ничего не значит
10 августа Инарктика опубликовала свои операционные результаты. Акции AQUA почему-то бурно выросли, и я на следующий день написал в наш...
Фото
Итоги первичных размещений ВДО и некоторых розничных выпусков на 14 августа 2026 г.
Следите за нашими новостями в удобном формате: Telegram , Youtube , RuTube, Smart-lab , ВКонтакте , Сайт
Фото
Облигации с Call-опционами: как избежать рисков убытков
Во 2-ом эшелоне и сегменте ВДО немало облигаций с Call-опционами (право досрочного погашения эмитента в определенную дату). Если эмитент решит...

теги блога Roman crypto_maniac

....все тэги



UPDONW
Новый дизайн