Листая телеграм-каналы с торговыми сигналами, я часто задавался вопросом: а кто из этих экспертов действительно попадает в цель? Одни обещают золотые горы, другие скромно молчат о своих неудачах. Решил разобраться раз и навсегда — создать систему, которая автоматически проверит, кто из гуру трейдинга говорит дело, а кто просто красиво упаковывает воздух.

Архитектура системы

Система состоит из четырех компонентов:

  • TG-Reader — собирает сообщения из телеграм-каналов через MTProto API

  • Trade-Radar — извлекает торговые прогнозы из текста с помощью AI

  • Analyzer — сравнивает прогнозы с реальными движениями цен

  • Visualizer — показывает статистику точности каналов

В этой статье разберем первую часть — автоматизацию сбора данных из Telegram.

Bot API vs MTProto API

Первая мысль — использовать Bot API. Создал бота, добавил в каналы и готово! Но есть проблема: боты видят только сообщения, адресованные им напрямую. В каналах они практически слепы.

Для полноценного анализа нужна вся история сообщений. Поэтому используем MTProto API — тот же протокол, что и в официальном клиенте Telegram. С ним мы эмулируем обычного пользователя и получаем доступ к полной истории каналов.

Настройка доступа

Для работы с MTProto нужны API ID и API Hash:

  1. Идем на my.telegram.org

  2. Выбираем "API development tools" → "Create Application"

  3. Заполняем форму (платформа — Desktop)

  4. Получаем ключи

Реализация на Go

Выбрал Go за производительность и отличную поддержку конкурентности. Для MTProto использую библиотеку gotd/td.

Структура проекта

tg-reader/
├── cmd/main.go              # Точка входа
├── internal/
│   ├── config/              # Конфигурация
│   ├── storage/             # PostgreSQL
│   │   ├── models.go        # Модели Channel, Message
│   │   └── postgres.go      # Реализация хранилища
│   └── telegram/            # MTProto API
├── configs/config.yaml      # Настройки
└── go.mod

Модели данных

type Channel struct {
    ID        int64
    Username  string
    Title     sql.NullString
    Link      sql.NullString
    CreatedAt time.Time
}

type Message struct {
    ID             int64
    TelegramID     int64
    ChannelID      int64
    Text           sql.NullString
    SentAt         time.Time
    SenderUsername sql.NullString
    IsForward      sql.NullBool
    MessageType    sql.NullString
    RawData        []byte         // JSONB для полных данных
    CreatedAt      time.Time
}

Интерфейс Telegram Reader

type Reader interface {
    GetLastMessages(ctx context.Context, channel string, limit int) ([]storage.Message, error)
    GetMessagesFromDate(ctx context.Context, channel string, startDate time.Time) error
    GetMessagesInDateRange(ctx context.Context, channel string, startDate, endDate time.Time) error
    SubscribeToChannel(ctx context.Context, channel string) (<-chan storage.Message, error)
    Close() error
}
  • GetLastMessages — для отладки, получает N последних сообщений

  • GetMessagesFromDate — собирает историю с указанной даты

  • GetMessagesInDateRange — работает в диапазоне дат

  • SubscribeToChannel — подписка на новые сообщения в реальном времени

Конфигурация

telegram:
  api_id: "ВАШ_API_ID"
  api_hash: "ВАШ_API_HASH"
  start_date: "2024-01-01"
  channels:
    - "trading_signals"
    - "crypto_experts"
    - "btc_analytics"

С какими проблемами столкнулся

Доступ только к публичным каналам

Первая неприятность — приватные каналы остались недоступными. MTProto API позволяет читать только те каналы, к которым у аккаунта есть доступ как у обычного пользователя. Это значит, что для закрытых VIP-каналов нужно либо получать приглашения, либо ограничиться публичными источниками.

Кроме того, для программного доступа к каналу нужно знать его точное имя (username). Ссылки вида t.me/channel_name работают, а вот по ID каналов получить данные сложнее.

FLOOD_WAIT — главный враг автоматизации

Самая серьезная проблема — лимиты Telegram на частоту запросов. При активном чтении сообщений API начинает возвращать ошибку FLOOD_WAIT_X, где X — количество секунд, которые нужно подождать перед следующим запросом.

Эти ограничения зависят от многих факторов:

  • Возраст аккаунта (новые аккаунты ограничены сильнее)

  • Предыдущая активность аккаунта

  • Текущая нагрузка на серверы Telegram

  • Количество уже отправленных запросов

Без правильной обработки система просто встает колом после первых нескольких сотен сообщений.

Решение проблемы FLOOD_WAIT

Пришлось добавить собственный парсер ошибок FLOOD_WAIT, так как стандартные методы библиотеки не всегда корректно их обрабатывали:

// isFloodWaitError проверяет, является ли ошибка FloodWaitError, и извлекает время ожидания
func isFloodWaitError(err error) (time.Duration, bool) {
    if err == nil {
        return 0, false
    }

    s := err.Error()
    if strings.Contains(s, "rpc error code 420: FLOOD_WAIT") {
        // Пример: "rpc error code 420: FLOOD_WAIT (19)"
        parts := strings.Split(s, "(")
        if len(parts) > 1 {
            waitStr := strings.TrimSuffix(strings.TrimSpace(parts[1]), ")")
            if seconds, parseErr := strconv.Atoi(waitStr); parseErr == nil {
                return time.Duration(seconds) * time.Second, true
            }
        }
        // Если не удалось распарсить время, по умолчанию ждем 5 секунд
        return 5 * time.Second, true
    }
    return 0, false
}

И применяем это в основном цикле чтения сообщений:

func (c *Client) GetMessagesFromDate(ctx context.Context, channel string, startDate time.Time) error {
    // ... инициализация и подготовка запроса
    
    for {
        historyResult, err := c.client.API().MessagesGetHistory(ctx, request)
        if err != nil {
            // Проверяем на FloodWaitError с нашей собственной реализацией
            if wait, ok := isFloodWaitError(err); ok {
                log.Printf("Получена FLOOD_WAIT ошибка. Ожидание %v перед повторной попыткой...", wait)
                select {
                case <-time.After(wait):
                case <-ctx.Done():
                    return ctx.Err()
                }
                continue // Повторяем запрос
            }
            log.Printf("Ошибка при получении истории сообщений для %s: %v", channel, err)
            return fmt.Errorf("failed to get history for %s: %w", channel, err)
        }
        
        // ... обработка полученных сообщений
        
        // Добавляем паузу между успешными запросами
        select {
        case <-time.After(1 * time.Second):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
}

Ключевые моменты решения:

  • Парсим строку ошибки для извлечения времени ожидания

  • Перехватываем ошибки и ждем указанное Telegram время

  • Добавляем задержку в 1 секунду между всеми запросами для профилактики

  • Используем context для возможности прерывания долгих операций

Запуск и использование

Два режима работы:

Отладочный — быстрая проверка последних сообщений:

./tg-reader -c config.yaml -d -l 10

Полная обработка — сбор истории в базу данных:

./tg-reader -c config.yaml

При первом запуске система попросит номер телефона и код подтверждения из Telegram.

Пример работы с базой данных

// Сохранение канала
channel := &storage.Channel{
    Username: "trading_signals",
    Title:    sql.NullString{String: "Trading Signals Pro", Valid: true},
}
err := storage.SaveChannel(ctx, channel)

// Сохранение сообщения
message := &storage.Message{
    TelegramID:  12345,
    ChannelID:   channel.ID,
    Text:        sql.NullString{String: "BUY BTCUSDT at $50000", Valid: true},
    SentAt:      time.Now(),
    MessageType: sql.NullString{String: "text", Valid: true},
}
err = storage.SaveMessage(ctx, message)

Результат

На выходе получаем структурированную базу данных со всеми сообщениями из торговых каналов. Каждое сообщение содержит текст, время публикации, метаданные и сырые данные в JSONB для гибкости анализа.

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

Что дальше

В следующей статье покажу самое интересное — как с помощью AI вытащить из телеграм-сообщений конкретные торговые прогнозы. Сохраню их в базе данных для последующего анализа.

Полный код проекта будет доступен после публикации серии статей.

Комментарии (0)