WebSocket или REST для рыночных данных

WebSocket и REST - два способа доставки рыночных данных в приложение. WebSocket держит одно соединение и присылает каждое обновление цены в момент его появления, REST отвечает на отдельные запросы за историей, каталогами и снимками. Рабочая интеграция использует оба: поток для живых тиков и REST для всего, что уже произошло. Разбираем, как совместить их без потерь.

Обновлено 6 мин чтения Инженерная команда FinaQuotes

Две модели доставки

REST устроен как обычный веб: клиент отправляет HTTPS-запрос, сервер возвращает один ответ, обмен закончен. Каждый запрос самостоятелен, поэтому REST легко кэшировать и отлаживать стандартными инструментами. Он хорош для вопросов с определённым ответом: список инструментов, последние 300 часовых свечей, логотип.

WebSocket начинается как HTTPS-запрос и переходит в постоянное двустороннее соединение. Клиент подписывается один раз, а сервер присылает каждый новый тик сразу, без повторных запросов.

Разница не в скорости одного сообщения, а в том, кто решает, когда данные двигаются. В REST решает клиент и узнаёт об изменении цены только при следующем запросе. В WebSocket решает рынок, и клиент узнаёт сразу.

Когда поток, а когда запрос

Правило простое: если данные меняются сами и важно знать когда - нужен поток; если это свершившийся факт или они меняются редко - запрос.

  • Поток (WebSocket): котировки в терминале, таблица котировок, бегущая строка, формирующаяся свеча на графике, ценовые алерты - всё, что должно реагировать на рынок быстрее секунды.
  • Запрос (REST): история свечей для графика, каталог инструментов с символами и точностью, логотипы, разовый снимок для отчёта.
  • Не опрашивайте REST ради живых цен: запрос последней цены каждую секунду по многим инструментам съедает лимиты, добавляет в среднем полпериода задержки и всё равно пропускает изменения между запросами.

Если сомневаетесь, задайте себе вопрос: изменятся ли эти данные через секунду без моего участия? Если да - это поток. Если нет - это запрос, и его результат можно спокойно кэшировать.

Как они работают вместе

Классический пример - график. При загрузке приложение запрашивает видимый диапазон свечей через REST, затем подписывается по WebSocket на тот же инструмент и обновляет последнюю свечу каждым тиком. Когда минута закрывается, свеча уходит в историю и начинается новая.

Фид FinaQuotes устроен так же: WebSocket передаёт поток тиков, REST отдаёт историю свечей в 9 таймфреймах, каталог инструментов и логотипы. Оба канала описаны в справочнике REST и в документации. Как формируются свечи - в статье о свечах из тиков.

ПараметрWebSocketREST
МодельСервер присылает по одному соединениюЗапрос клиента, один ответ
Для чегоТики, текущая свеча, алертыИстория, каталог, логотипы, снимки
СвежестьДоли секунды после получения ценыНа момент запроса
СостояниеПодписки живут на соединенииКаждый запрос независим
СбойРазрыв: переподключение и дозагрузкаУпал запрос: повторить
ЛимитыБез тарификации за сообщениеЛимиты на IP, при превышении 429

Подключение и аутентификация

Сессия потока проходит одни и те же шаги: открыть соединение, пройти аутентификацию, подписаться на инструменты, обрабатывать сообщения до разрыва.

Аутентификация в WebSocket FinaQuotes построена на HMAC. Вы получаете идентификатор ключа и секрет. Ваш сервер вычисляет подпись короткого сообщения, обычно с меткой времени, с помощью секрета и хеш-функции вроде SHA-256 и отправляет идентификатор и подпись, но не сам секрет. Сервер повторяет вычисление своей копией секрета и принимает сессию, если подписи совпали. Метка времени в подписи не даёт повторно использовать старую подпись.

Отсюда два правила: секрет хранится только на сервере (переменная окружения или хранилище секретов), никогда в коде браузера или мобильного приложения; часы сервера синхронизированы, иначе подпись с сильно отличающимся временем могут отклонить. Формат сообщений и допуски - в документации.

Переподключение и дозагрузка пропуска

Любое долгое соединение рано или поздно рвётся. Хороший клиент считает разрыв штатным событием и восстанавливается сам.

  • Заметить разрыв: событие закрытия плюс ping/pong на уровне протокола, чтобы поймать «мёртвое» соединение. Отсутствие тиков - не признак: тихий инструмент может молчать долго.
  • Переподключиться с паузой, удваивая её после каждой неудачи до предела и добавляя случайную добавку, чтобы клиенты не ломились одновременно.
  • Заново пройти аутентификацию и подписаться: подписки принадлежат соединению, новое начинается пустым.
  • Закрыть пропуск: запомнить время последнего тика, после переподключения запросить через REST свечи с этого момента и слить их с графиком, заменив последнюю сохранённую свечу, а не добавив дубль.
// Иллюстрация. Типы сообщений и имена полей условные;
// реальный протокол описан в документации (/docs/).
import crypto from 'node:crypto';

const WS_URL = process.env.FEED_WS_URL;
const KEY_ID = process.env.FEED_KEY_ID;
const SECRET = process.env.FEED_SECRET; // не покидает ваш сервер

const sign = (ts) =>
  crypto.createHmac('sha256', SECRET).update(String(ts)).digest('hex');

let lastTickTime = null; // время (UTC) последнего обработанного тика
let attempt = 0;

function connect() {
  const ws = new WebSocket(WS_URL);

  ws.onopen = async () => {
    attempt = 0;
    const ts = Date.now();
    ws.send(JSON.stringify({ type: 'auth', key: KEY_ID, ts, sig: sign(ts) }));
    ws.send(JSON.stringify({ type: 'subscribe', symbols: ['EURUSD', 'XAUUSD'] }));
    // Закрываем дыру после разрыва свечами из REST.
    if (lastTickTime) await backfillCandles(lastTickTime);
  };

  ws.onmessage = (event) => {
    const tick = JSON.parse(event.data);
    lastTickTime = tick.time;
    applyTick(tick);
  };

  ws.onclose = () => {
    // Экспоненциальная пауза со случайной добавкой, не больше 30 секунд.
    const delay = Math.min(30000, 1000 * 2 ** attempt++) + Math.random() * 1000;
    setTimeout(connect, delay);
  };
}

connect();

Ниже - схема такого клиента на JavaScript. Реальных адресов и ключей в ней нет, формат сообщений условный.

Лимиты и ответ 429

REST-эндпоинты - общий ресурс, поэтому у них есть лимиты: в FinaQuotes это разумные лимиты на IP-адрес, по умолчанию 120 запросов в минуту. При превышении сервер отвечает статусом 429 Too Many Requests.

429 - сигнал притормозить, а не повод немедленно повторить. Подождите, учтите заголовок Retry-After, если он есть, и распределите запросы во времени. Обычно хватает двух привычек: кэшировать историю у себя (закрытые свечи не меняются) и загружать только тот диапазон, который видит пользователь.

Поток WebSocket не тарифицируется за сообщение, так что активный рынок не увеличивает счёт. Как устроена стоимость доступа - на странице тарифов.

Чек-лист интеграции

Перед запуском проверьте, что интеграция:

  • берёт живые цены по WebSocket, а историю и каталог - через REST, без опроса REST ради котировок;
  • хранит секрет HMAC только на сервере;
  • переподключается автоматически с ограниченной экспоненциальной паузой;
  • после каждого переподключения заново подписывается и дозагружает пропуск из REST;
  • обрабатывает 429 паузой, кэширует историю и укладывается в лимиты;
  • работает в UTC и переводит время в локальное только при отображении.

Частые вопросы

Можно ли получать живые цены только через REST?

Технически можно опрашивать, но это неверный инструмент: опрос добавляет задержку, пропускает изменения между запросами и упирается в лимиты. Для живых цен - поток WebSocket, для истории и справочных данных - REST.

Можно ли подключаться к WebSocket прямо из браузера?

Не с вашим секретом: всё, что отдаётся в браузер, становится публичным. Подключайтесь с сервера и передавайте цены во фронтенд, либо используйте готовые виджеты, если они подходят под задачу.

Что делать, если пришёл ответ 429?

Притормозить и повторить позже: выдержать паузу, учесть Retry-After, кэшировать историю и запрашивать только нужный диапазон. Лимит по умолчанию - 120 запросов в минуту на IP, подробнее - в справочнике REST.

Нужен фид, о котором эта статья?

Расскажите, что вы строите и какие классы активов нужны. Ответим в течение одного рабочего дня: условия доступа и стоимость.

Написать нам