Реализация книги ордеров (Order Book) в мобильном приложении биржи
Order book — один из самых требовательных к производительности компонентов биржевого приложения. Данные обновляются несколько раз в секунду, список может содержать сотни уровней цен, и при этом интерфейс должен оставаться отзывчивым.
Источник данных: WebSocket
Биржевой стакан обновляется через WebSocket — HTTP polling здесь неприемлем по задержкам. Стандартный подход: получить snapshot через REST, затем подписаться на incremental updates через WS.
Пример для Binance API (паттерн используется большинством бирж):
GET https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=100
→ snapshot с полными bids и asks
WSS wss://stream.binance.com:9443/ws/btcusdt@depth@100ms
→ incremental updates каждые 100ms
Применение инкрементальных обновлений: если цена уровня уже есть в стакане — обновляем количество. Если количество 0 — удаляем уровень. Если цена новая — добавляем. Это стандартный diff-алгоритм для order book.
Синхронизация snapshot и стрима
Критичный момент который часто реализуют неправильно: WebSocket начинает слать обновления до того как REST-запрос за snapshot вернулся. Нужно буферизировать WS-события, получить snapshot с его lastUpdateId, затем применить все буферизированные события у которых firstUpdateId <= lastUpdateId + 1.
Потеряли событие? Рассинхронизировались? Единственный выход — переподключиться и получить новый snapshot. Не пытайтесь восстановить состояние по частичным данным.
Рендеринг: главная проблема
100 уровней цен × обновления каждые 100ms = потенциальный лаг UI. На мобильном это особенно заметно.
Не обновляйте список при каждом WebSocket-событии. Батчинг: накапливаем обновления и применяем к UI раз в 250-500ms. Пользователь не видит разницы между 100ms и 250ms, но это 2.5x меньше работы для рендерера.
Виртуализация. Отображаем только видимые уровни, не рендерим все 100. На iOS — UITableView с reloadRows(at:with:) для измененных строк, не reloadData(). На Android — RecyclerView с DiffUtil для вычисления минимального количества изменений. В Flutter — ListView.builder с itemCount.
Цвета и подсветка изменений: когда цена ушла вниз — красный, вверх — зелений, затем плавно возвращается к нейтральному. Реализуем через анимацию цвета фона с CABasicAnimation (iOS) или ValueAnimator (Android). Не держите таймеры на каждую строку — используйте один таймер для всего списка.
Глубина стакана: depth chart
Визуализация объёмов через аккумулированную гистограмму (depth chart) — накапливаем объём от лучшей цены к худшей. Рисуем через CAShapeLayer / Canvas / CustomPainter в Flutter. Обновляем не чаще 1 раза в секунду — это визуализация, не торговый инструмент.
Точность чисел
Цены и объёмы на бирже — десятичные числа с высокой точностью. BTC торгуется с 8 знаками после запятой. Никакого Double — только Decimal (iOS) или BigDecimal (Android). Суммирование объёмов с Double даст заметные ошибки округления при отображении.
Форматирование: разные торговые пары имеют разный tick size (минимальный шаг цены). Для BTC/USDT tick size 0.01, для некоторых альткоинов — 8 знаков. Количество отображаемых знаков берём из метаданных пары, не hardcode.
Офлайн и переподключение
При потере соединения — очищаем стакан и показываем состояние «Нет данных / Переподключение». Не показываем устаревший стакан как актуальный — это вводит в заблуждение при трейдинге.
Логика переподключения: exponential backoff от 1s до 30s. После восстановления — заново получаем snapshot и переподписываемся.
Сроки: реализация order book с WebSocket, корректной синхронизацией и оптимизированным рендерингом — 2-3 недели. С depth chart, сортировкой, фильтрацией глубины и поддержкой нескольких торговых пар — 4-5 недель.







