FinaQuotes Feed

A real-time market quotes feed delivered over a single WebSocket connection. Authenticate once, subscribe to the instruments you need, and receive a live stream of price ticks.

What it is

The feed is a streaming API: prices arrive as notifications the moment they change, so there is no polling. Quotes are sourced from an aggregator and delivered to you normalized under a single, simple JSON protocol.

What you need to connect

  • An access token (WebApiId, WebApiKey and a Secret) - provided by the operator.
  • The endpoint URL: wss://finaquotes.com/feed (secure WebSocket only).
  • Any standard WebSocket client in your language of choice.
Token Credentials are issued by the operator. Keep the Secret private - it never leaves your side and is only used to compute request signatures.

FinaQuotes Feed

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

Что это

Фид - это потоковый API: цены приходят уведомлениями в момент изменения, опрос (polling) не нужен. Котировки поступают от агрегатора и доставляются вам в нормализованном виде по единому простому JSON-протоколу.

Что нужно для подключения

  • Токен доступа (WebApiId, WebApiKey и Secret) - предоставляется оператором.
  • URL точки входа: wss://finaquotes.com/feed (только защищённый WebSocket).
  • Любой стандартный WebSocket-клиент на вашем языке.
Токен Учётные данные выдаёт оператор. Держите Secret в секрете - он никогда не покидает вашу сторону и используется только для вычисления подписи запросов.

Quick start

The minimal path from zero to a live price stream is five steps:

  1. Open a WebSocket connection to wss://finaquotes.com/feed.
  2. Send a Login request with an HMAC signature.
  3. Receive Login → Authenticated: true, then a SessionInfo message arrives automatically.
  4. Send FeedSubscribe with the symbols you want. You get an immediate snapshot of the last prices.
  5. Receive FeedTick notifications as prices update.

Every message is JSON. Requests carry an Id you choose; the matching response echoes the same Id. Notifications (like FeedTick) have no Id.

Skip ahead to the Full client example for a copy-paste runnable client in Node.js and Python that already does all of this, including heartbeat and reconnect.

Быстрый старт

Минимальный путь от нуля до живого потока цен - пять шагов:

  1. Откройте WebSocket-соединение к wss://finaquotes.com/feed.
  2. Отправьте запрос Login с HMAC-подписью.
  3. Получите Login → Authenticated: true, затем автоматически придёт сообщение SessionInfo.
  4. Отправьте FeedSubscribe с нужными символами. В ответ сразу придёт снапшот последних цен.
  5. Принимайте уведомления FeedTick по мере обновления цен.

Все сообщения - JSON. Запросы несут выбранный вами Id; соответствующий ответ возвращает тот же Id. Уведомления (например FeedTick) поля Id не содержат.

Перейдите сразу к разделу Полный клиент - там готовый к запуску клиент на Node.js и Python, который уже делает всё перечисленное, включая heartbeat и реконнект.

Authentication

Authentication uses an HMAC-SHA256 signature. You never send the Secret over the wire - instead you sign a short message with it, and the server verifies the signature using its copy of your secret.

The Login request

json
{
  "Id": "1",
  "Request": "Login",
  "Params": {
    "AuthType": "HMAC",
    "WebApiId": "YOUR_WEB_API_ID",
    "WebApiKey": "YOUR_WEB_API_KEY",
    "Timestamp": 1700000000000,
    "Signature": "<base64 HMAC-SHA256>"
  }
}

How to compute the signature

The signature is computed step by step from four values you already have:

  1. Timestamp - current time in milliseconds since the Unix epoch (ms). It must be within ±60 seconds of the server clock, so keep your machine time synchronized.
  2. Build the message string by concatenating, in this exact order, with no separators:
    message = Timestamp + Id + WebApiKey
    where Timestamp is the millisecond value as a string, Id is your request id, and WebApiKey is your key.
  3. Sign the message with HMAC-SHA256 using your Secret as the key.
  4. Base64-encode the raw HMAC digest. That string is the Signature.
Formula Signature = Base64( HMAC_SHA256( Timestamp + Id + WebApiKey, key = Secret ) )

Example calculation

For these inputs:

Timestamp1700000000000
Id"1"
WebApiKey"YOUR_WEB_API_ID_KEY"
Secret"YOUR_SECRET"

The message string is 17000000000001YOUR_WEB_API_ID_KEY and the resulting signature is:

signature
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=

Use this vector to verify your own implementation before going live.

Reference implementations

javascript
const crypto = require("crypto");

function sign(timestamp, id, webApiKey, secret) {
  const message = String(timestamp) + id + webApiKey;
  return crypto.createHmac("sha256", secret)
    .update(message)
    .digest("base64");
}

// Example
const sig = sign(1700000000000, "1", "YOUR_WEB_API_ID_KEY", "YOUR_SECRET");
console.log(sig); // dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
python
import hmac, hashlib, base64

def sign(timestamp, id, web_api_key, secret):
    message = str(timestamp) + id + web_api_key
    digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
    return base64.b64encode(digest).decode()

# Example
sig = sign(1700000000000, "1", "YOUR_WEB_API_ID_KEY", "YOUR_SECRET")
print(sig)  # dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=

Success and failure

On success the server replies with Authenticated: true and then sends SessionInfo automatically:

json
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }

On failure the server returns an error and closes the connection:

json
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }
Rate limiting Failed login attempts are rate-limited to prevent brute force. Do not loop Login on a signature error - fix the parameters first (see Troubleshooting).

Аутентификация

Аутентификация использует подпись HMAC-SHA256. Сам Secret никогда не передаётся по сети - вы подписываете им короткое сообщение, а сервер проверяет подпись своей копией вашего секрета.

Запрос Login

json
{
  "Id": "1",
  "Request": "Login",
  "Params": {
    "AuthType": "HMAC",
    "WebApiId": "YOUR_WEB_API_ID",
    "WebApiKey": "YOUR_WEB_API_KEY",
    "Timestamp": 1700000000000,
    "Signature": "<base64 HMAC-SHA256>"
  }
}

Как вычислить подпись

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

  1. Timestamp - текущее время в миллисекундах от Unix-эпохи (ms). Оно должно быть в пределах ±60 секунд от серверного времени, поэтому держите часы синхронизированными.
  2. Соберите строку сообщения, конкатенируя в строго таком порядке, без разделителей:
    message = Timestamp + Id + WebApiKey
    где Timestamp - значение миллисекунд как строка, Id - идентификатор вашего запроса, WebApiKey - ваш ключ.
  3. Подпишите сообщение алгоритмом HMAC-SHA256, используя ваш Secret в качестве ключа.
  4. Закодируйте в Base64 сырой дайджест HMAC. Эта строка и есть Signature.
Формула Signature = Base64( HMAC_SHA256( Timestamp + Id + WebApiKey, key = Secret ) )

Пример расчёта

Для входных значений:

Timestamp1700000000000
Id"1"
WebApiKey"YOUR_WEB_API_ID_KEY"
Secret"YOUR_SECRET"

Строка сообщения - 17000000000001YOUR_WEB_API_ID_KEY, а итоговая подпись:

signature
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=

Используйте этот вектор для проверки своей реализации до выхода в боевой режим.

Эталонные реализации

javascript
const crypto = require("crypto");

function sign(timestamp, id, webApiKey, secret) {
  const message = String(timestamp) + id + webApiKey;
  return crypto.createHmac("sha256", secret)
    .update(message)
    .digest("base64");
}

// Пример
const sig = sign(1700000000000, "1", "YOUR_WEB_API_ID_KEY", "YOUR_SECRET");
console.log(sig); // dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
python
import hmac, hashlib, base64

def sign(timestamp, id, web_api_key, secret):
    message = str(timestamp) + id + web_api_key
    digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
    return base64.b64encode(digest).decode()

# Пример
sig = sign(1700000000000, "1", "YOUR_WEB_API_ID_KEY", "YOUR_SECRET")
print(sig)  # dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=

Успех и ошибка

При успехе сервер отвечает Authenticated: true и затем автоматически шлёт SessionInfo:

json
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }

При ошибке сервер возвращает ошибку и закрывает соединение:

json
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }
Ограничение попыток Неудачные попытки входа ограничены по частоте для защиты от перебора. Не зацикливайте Login при ошибке подписи - сначала исправьте параметры (см. Решение проблем).

Connect

Connect to the endpoint over a secure WebSocket. Plain ws:// is not accepted.

EndpointTransport
wss://finaquotes.com/feedWebSocket (wss only)

The connection lifecycle is:

  1. open - the WebSocket connection is established.
  2. send Login - your first frame should be the Login request.
  3. SessionInfo arrives - right after a successful Login, the server pushes a SessionInfo message on its own. You can also request it any time with {"Request":"SessionInfo"}.

SessionInfo

json
{
  "Response": "SessionInfo",
  "Result": {
    "PlatformName": "FinaQuotes Feed",
    "PlatformCompany": "FinaQuotes",
    "PlatformTimezoneOffset": 0,
    "SessionId": "550e8400-e29b-41d4-a716-446655440000",
    "SessionStatus": "Opened",
    "SessionStartTime": 1700000000000
  }
}

Подключение

Подключайтесь к точке входа по защищённому WebSocket. Обычный ws:// не принимается.

Точка входаТранспорт
wss://finaquotes.com/feedWebSocket (только wss)

Жизненный цикл соединения:

  1. open - WebSocket-соединение установлено.
  2. send Login - первым кадром отправьте запрос Login.
  3. придёт SessionInfo - сразу после успешного Login сервер сам присылает сообщение SessionInfo. Его также можно запросить в любой момент через {"Request":"SessionInfo"}.

SessionInfo

json
{
  "Response": "SessionInfo",
  "Result": {
    "PlatformName": "FinaQuotes Feed",
    "PlatformCompany": "FinaQuotes",
    "PlatformTimezoneOffset": 0,
    "SessionId": "550e8400-e29b-41d4-a716-446655440000",
    "SessionStatus": "Opened",
    "SessionStartTime": 1700000000000
  }
}

Get symbols

Use the Symbols request to retrieve the list of available instruments - either all of them, or a single one.

Request - all symbols

json
{ "Id": "2", "Request": "Symbols" }

Request - a single symbol

json
{ "Request": "Symbols", "Params": { "Symbol": "AAPL" } }

Response

json
{
  "Id": "2",
  "Response": "Symbols",
  "Result": {
    "Symbols": [
      {
        "Symbol": "AAPL",
        "Precision": 2,
        "Description": "Apple Inc",
        "ContractSize": 1,
        "MarginCurrency": "USD",
        "ProfitCurrency": "USD",
        "TradeAmountStep": 1,
        "MinTradeAmount": 1
      }
    ]
  }
}

Fields

FieldTypeMeaning
SymbolstringThe ticker, e.g. AAPL.
PrecisionintNumber of decimal places in the price (cents = 2).
DescriptionstringHuman-readable instrument name.
ContractSizenumberNominal default.
MarginCurrencystringNominal default.
ProfitCurrencystringNominal default.
TradeAmountStepnumberNominal default.
MinTradeAmountnumberNominal default.
The ContractSize, MarginCurrency, ProfitCurrency, TradeAmountStep and MinTradeAmount fields carry nominal default values. For a quotes-only integration the fields that matter are Symbol, Precision and Description.

Получение символов

Запрос Symbols возвращает список доступных инструментов - все сразу либо один.

Запрос - все символы

json
{ "Id": "2", "Request": "Symbols" }

Запрос - один символ

json
{ "Request": "Symbols", "Params": { "Symbol": "AAPL" } }

Ответ

json
{
  "Id": "2",
  "Response": "Symbols",
  "Result": {
    "Symbols": [
      {
        "Symbol": "AAPL",
        "Precision": 2,
        "Description": "Apple Inc",
        "ContractSize": 1,
        "MarginCurrency": "USD",
        "ProfitCurrency": "USD",
        "TradeAmountStep": 1,
        "MinTradeAmount": 1
      }
    ]
  }
}

Поля

ПолеТипЗначение
SymbolstringТикер, например AAPL.
PrecisionintЧисло знаков после запятой в цене (центы = 2).
DescriptionstringЧитаемое название инструмента.
ContractSizenumberНоминальный дефолт.
MarginCurrencystringНоминальный дефолт.
ProfitCurrencystringНоминальный дефолт.
TradeAmountStepnumberНоминальный дефолт.
MinTradeAmountnumberНоминальный дефолт.
Поля ContractSize, MarginCurrency, ProfitCurrency, TradeAmountStep и MinTradeAmount несут номинальные значения по умолчанию. Для интеграции «только котировки» важны поля Symbol, Precision и Description.

Subscribe

Subscribe to one or more symbols with FeedSubscribe. The response delivers an immediate snapshot of the last price for each recognized symbol, plus a list of any that could not be matched. After that, FeedTick notifications stream in as prices change.

Request

json
{
  "Id": "3",
  "Request": "FeedSubscribe",
  "Params": {
    "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ]
  }
}

Response - snapshot + fails

json
{
  "Id": "3",
  "Response": "FeedSubscribe",
  "Result": {
    "Snapshot": [
      {
        "Symbol": "AAPL",
        "Timestamp": 1700000000000,
        "BestBid": { "Type": "Bid", "Price": 307.39, "Volume": 0 },
        "BestAsk": { "Type": "Ask", "Price": 307.39, "Volume": 0 }
      }
    ],
    "Fails": [ "UNKNOWN1" ]
  }
}
  • Snapshot - the current last price for each recognized symbol, returned immediately.
  • Fails - symbols from your request that were not recognized.
You may include a BookDepth field in the subscription, but it is ignored - the feed always delivers top-of-book (a single level).

FeedTick notifications

For every subscribed symbol, a FeedTick notification is pushed whenever its price changes. These have no Id:

json
{
  "Response": "FeedTick",
  "Result": {
    "Symbol": "AAPL",
    "Timestamp": 1700000000500,
    "BestBid": { "Type": "Bid", "Price": 307.5, "Volume": 0 },
    "BestAsk": { "Type": "Ask", "Price": 307.5, "Volume": 0 }
  }
}

Unsubscribe

FeedUnsubscribe removes symbols. The response returns Symbols - the subscriptions that remain active.

json
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
json
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }

Подписка

Подпишитесь на один или несколько символов через FeedSubscribe. В ответ сразу приходит снапшот последней цены по каждому распознанному символу плюс список нераспознанных. Далее по подписанным символам идут уведомления FeedTick по мере изменения цены.

Запрос

json
{
  "Id": "3",
  "Request": "FeedSubscribe",
  "Params": {
    "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ]
  }
}

Ответ - снапшот + нераспознанные

json
{
  "Id": "3",
  "Response": "FeedSubscribe",
  "Result": {
    "Snapshot": [
      {
        "Symbol": "AAPL",
        "Timestamp": 1700000000000,
        "BestBid": { "Type": "Bid", "Price": 307.39, "Volume": 0 },
        "BestAsk": { "Type": "Ask", "Price": 307.39, "Volume": 0 }
      }
    ],
    "Fails": [ "UNKNOWN1" ]
  }
}
  • Snapshot - текущая последняя цена по каждому распознанному символу, возвращается сразу.
  • Fails - символы из запроса, которые не были распознаны.
В подписку можно передать поле BookDepth, но оно игнорируется - фид всегда отдаёт top-of-book (один уровень).

Уведомления FeedTick

По каждому подписанному символу при изменении цены приходит уведомление FeedTick. Поля Id в нём нет:

json
{
  "Response": "FeedTick",
  "Result": {
    "Symbol": "AAPL",
    "Timestamp": 1700000000500,
    "BestBid": { "Type": "Bid", "Price": 307.5, "Volume": 0 },
    "BestAsk": { "Type": "Ask", "Price": 307.5, "Volume": 0 }
  }
}

Отписка

FeedUnsubscribe убирает символы. В ответе возвращается Symbols - оставшиеся активные подписки.

json
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
json
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }

Price format

The feed is designed for simplicity. Three things to know:

  • bid = ask = last. Each tick carries a single price, exposed as both BestBid.Price and BestAsk.Price with the same value. If your application needs a spread, apply it yourself on top of this last price.
  • Top-of-book only. One price level is provided - there is no order book depth. Any BookDepth in a subscription is ignored.
  • Volume is always 0. Per-level volume is not provided, so Volume is reported as 0 on both sides.
Treat BestBid.Price (equivalently BestAsk.Price) as the current last price of the instrument. Timestamp is the price time in milliseconds since the Unix epoch.

Формат цены

Фид сделан максимально простым. Три момента, которые нужно знать:

  • bid = ask = last. Каждый тик несёт одну цену, отдаваемую и как BestBid.Price, и как BestAsk.Price с одинаковым значением. Если приложению нужен спред - наложите его сами поверх этой последней цены.
  • Только top-of-book. Предоставляется один ценовой уровень - глубины стакана нет. Любой BookDepth в подписке игнорируется.
  • Volume всегда 0. Объём по уровню не предоставляется, поэтому Volume равен 0 с обеих сторон.
Считайте BestBid.Price (равно как BestAsk.Price) текущей последней ценой инструмента. Timestamp - время цены в миллисекундах от Unix-эпохи.

Symbols list

  • Categories. The current universe is equities (stocks).
  • Getting the current list. Always pull the live list with the Symbols request rather than hardcoding it - see Get symbols.
  • Automatic expansion. The instrument list grows automatically; new symbols appear on their own without any action on your side.
  • Ticker normalization. Tickers are clean (e.g. AAPL). Tickers that contain a dot are passed without it - for example BRK.B becomes BRKB.

Full instruments list - what we deliver now and the broader catalogue available on request.

Список инструментов

  • Категории. Текущий набор - акции (equities).
  • Актуальный список. Всегда запрашивайте живой список запросом Symbols, а не зашивайте его в код - см. Получение символов.
  • Автоматическое расширение. Список инструментов растёт автоматически; новые символы появляются сами, без действий с вашей стороны.
  • Нормализация тикеров. Тикеры чистые (например AAPL). Тикеры с точкой передаются без неё - например BRK.B становится BRKB.

Полный список инструментов - что отдаём сейчас и какой каталог доступен дополнительно.

Message reference

All messages share a consistent envelope.

DirectionShape
request{ "Id", "Request", "Params" }
response{ "Id", "Response", "Result" }
error{ "Id", "Response": "Error", "Error": { "Code", "Message" } }
notification{ "Response", "Result" }  (no Id)

Login

request
{ "Id": "1", "Request": "Login", "Params": { "AuthType": "HMAC", "WebApiId": "YOUR_WEB_API_ID", "WebApiKey": "YOUR_WEB_API_KEY", "Timestamp": 1700000000000, "Signature": "<base64>" } }
response
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }

SessionInfo

request (optional)
{ "Request": "SessionInfo" }
response
{ "Response": "SessionInfo", "Result": { "PlatformName": "FinaQuotes Feed", "PlatformCompany": "FinaQuotes", "PlatformTimezoneOffset": 0, "SessionId": "<uuid>", "SessionStatus": "Opened", "SessionStartTime": 1700000000000 } }

Symbols

request
{ "Id": "2", "Request": "Symbols" }
response
{ "Id": "2", "Response": "Symbols", "Result": { "Symbols": [ { "Symbol": "AAPL", "Precision": 2, "Description": "Apple Inc", "ContractSize": 1, "MarginCurrency": "USD", "ProfitCurrency": "USD", "TradeAmountStep": 1, "MinTradeAmount": 1 } ] } }

FeedSubscribe

request
{ "Id": "3", "Request": "FeedSubscribe", "Params": { "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ] } }
response
{ "Id": "3", "Response": "FeedSubscribe", "Result": { "Snapshot": [ { "Symbol": "AAPL", "Timestamp": 1700000000000, "BestBid": { "Type": "Bid", "Price": 307.39, "Volume": 0 }, "BestAsk": { "Type": "Ask", "Price": 307.39, "Volume": 0 } } ], "Fails": [ "UNKNOWN1" ] } }

FeedTick (notification)

notification
{ "Response": "FeedTick", "Result": { "Symbol": "AAPL", "Timestamp": 1700000000500, "BestBid": { "Type": "Bid", "Price": 307.5, "Volume": 0 }, "BestAsk": { "Type": "Ask", "Price": 307.5, "Volume": 0 } } }

FeedUnsubscribe

request
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
response
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }

Ping / Pong

request
{ "Request": "Ping" }
response
{ "Response": "Pong" }

Error

response
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }

Справочник сообщений

Все сообщения используют единый конверт.

НаправлениеФорма
запрос{ "Id", "Request", "Params" }
ответ{ "Id", "Response", "Result" }
ошибка{ "Id", "Response": "Error", "Error": { "Code", "Message" } }
уведомление{ "Response", "Result" }  (без Id)

Login

запрос
{ "Id": "1", "Request": "Login", "Params": { "AuthType": "HMAC", "WebApiId": "YOUR_WEB_API_ID", "WebApiKey": "YOUR_WEB_API_KEY", "Timestamp": 1700000000000, "Signature": "<base64>" } }
ответ
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }

SessionInfo

запрос (опц.)
{ "Request": "SessionInfo" }
ответ
{ "Response": "SessionInfo", "Result": { "PlatformName": "FinaQuotes Feed", "PlatformCompany": "FinaQuotes", "PlatformTimezoneOffset": 0, "SessionId": "<uuid>", "SessionStatus": "Opened", "SessionStartTime": 1700000000000 } }

Symbols

запрос
{ "Id": "2", "Request": "Symbols" }
ответ
{ "Id": "2", "Response": "Symbols", "Result": { "Symbols": [ { "Symbol": "AAPL", "Precision": 2, "Description": "Apple Inc", "ContractSize": 1, "MarginCurrency": "USD", "ProfitCurrency": "USD", "TradeAmountStep": 1, "MinTradeAmount": 1 } ] } }

FeedSubscribe

запрос
{ "Id": "3", "Request": "FeedSubscribe", "Params": { "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ] } }
ответ
{ "Id": "3", "Response": "FeedSubscribe", "Result": { "Snapshot": [ { "Symbol": "AAPL", "Timestamp": 1700000000000, "BestBid": { "Type": "Bid", "Price": 307.39, "Volume": 0 }, "BestAsk": { "Type": "Ask", "Price": 307.39, "Volume": 0 } } ], "Fails": [ "UNKNOWN1" ] } }

FeedTick (уведомление)

уведомление
{ "Response": "FeedTick", "Result": { "Symbol": "AAPL", "Timestamp": 1700000000500, "BestBid": { "Type": "Bid", "Price": 307.5, "Volume": 0 }, "BestAsk": { "Type": "Ask", "Price": 307.5, "Volume": 0 } } }

FeedUnsubscribe

запрос
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
ответ
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }

Ping / Pong

запрос
{ "Request": "Ping" }
ответ
{ "Response": "Pong" }

Error

ответ
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }

Rules

One connection per token

A token may hold a single active connection. Opening a new connection with the same token disconnects the previous one.

Heartbeat

  • If the server sees no activity from the client for more than 60 seconds, it drops the connection.
  • Send a Ping (or a WebSocket ping frame) periodically - every 20-30 seconds is recommended.
  • The server also sends a WebSocket ping every 30 seconds.

Reconnect

On disconnect, reconnect with exponential backoff (for example 1s, 2s, 4s … capped at 30s) and perform Login again.

Outside market hours

When the market is closed, subscribing still returns a snapshot (the last price), but no FeedTick stream arrives until the market opens. This is expected.

Login attempts

Failed login attempts are rate-limited (brute-force protection). Do not loop Login on a signature error - verify your parameters first.

Правила

Одно подключение на токен

На токен допустимо одно активное соединение. Новое подключение тем же токеном разрывает предыдущее.

Heartbeat

  • Если сервер не видит активности от клиента дольше 60 секунд, он разрывает соединение.
  • Периодически шлите Ping (или WS-ping-кадр) - рекомендуется каждые 20-30 секунд.
  • Сервер также сам шлёт WebSocket-ping каждые 30 секунд.

Реконнект

При разрыве переподключайтесь с экспоненциальным backoff (например 1с, 2с, 4с … до 30с) и выполняйте Login повторно.

Вне торговой сессии

При закрытом рынке подписка всё равно возвращает снапшот (последнюю цену), но потока FeedTick нет до открытия рынка. Это нормально.

Попытки входа

Неудачные попытки входа ограничены по частоте (защита от перебора). Не зацикливайте Login при ошибке подписи - сначала проверьте параметры.

Full client example

A complete, copy-paste runnable client. It connects, logs in with a correct HMAC signature, subscribes, prints incoming ticks, sends a Ping every 25 seconds as a heartbeat, and reconnects with exponential backoff.

Requires the ws package: npm install ws. The crypto module is built in.
client.js
const WebSocket = require("ws");
const crypto = require("crypto");

const URL = "wss://finaquotes.com/feed";
const WEB_API_ID = "YOUR_WEB_API_ID";
const WEB_API_KEY = "YOUR_WEB_API_KEY";
const SECRET = "YOUR_SECRET";
const SYMBOLS = ["AAPL", "MSFT"];

let ws;
let pingTimer = null;
let backoff = 1000; // start at 1s, cap at 30s

function sign(timestamp, id, webApiKey, secret) {
  const message = String(timestamp) + id + webApiKey;
  return crypto.createHmac("sha256", secret).update(message).digest("base64");
}

function send(obj) {
  ws.send(JSON.stringify(obj));
}

function login() {
  const id = "1";
  const ts = Date.now();
  const signature = sign(ts, id, WEB_API_KEY, SECRET);
  send({
    Id: id,
    Request: "Login",
    Params: {
      AuthType: "HMAC",
      WebApiId: WEB_API_ID,
      WebApiKey: WEB_API_KEY,
      Timestamp: ts,
      Signature: signature
    }
  });
}

function subscribe() {
  send({
    Id: "3",
    Request: "FeedSubscribe",
    Params: { Subscribe: SYMBOLS.map((s) => ({ Symbol: s })) }
  });
}

function startHeartbeat() {
  stopHeartbeat();
  pingTimer = setInterval(() => send({ Request: "Ping" }), 25000);
}
function stopHeartbeat() {
  if (pingTimer) clearInterval(pingTimer);
  pingTimer = null;
}

function connect() {
  ws = new WebSocket(URL);

  ws.on("open", () => {
    console.log("connected, logging in");
    login();
  });

  ws.on("message", (data) => {
    let msg;
    try { msg = JSON.parse(data.toString()); } catch (e) { return; }

    if (msg.Response === "Login" && msg.Result && msg.Result.Authenticated) {
      console.log("authenticated");
      backoff = 1000; // reset backoff on success
      startHeartbeat();
      subscribe();
    } else if (msg.Response === "SessionInfo") {
      console.log("session:", msg.Result.SessionId);
    } else if (msg.Response === "FeedSubscribe") {
      console.log("snapshot:", msg.Result.Snapshot);
      if (msg.Result.Fails && msg.Result.Fails.length) {
        console.log("unrecognized:", msg.Result.Fails);
      }
    } else if (msg.Response === "FeedTick") {
      const r = msg.Result;
      console.log(r.Symbol, r.BestBid.Price, "@", r.Timestamp);
    } else if (msg.Response === "Pong") {
      // heartbeat acknowledged
    } else if (msg.Response === "Error") {
      console.error("error:", msg.Error.Code, msg.Error.Message);
    }
  });

  ws.on("close", () => {
    console.log("disconnected, reconnecting in", backoff, "ms");
    stopHeartbeat();
    setTimeout(connect, backoff);
    backoff = Math.min(backoff * 2, 30000);
  });

  ws.on("error", (err) => {
    console.error("socket error:", err.message);
    // close will fire next and trigger reconnect
  });
}

connect();
Requires websocket-client: pip install websocket-client. The hmac, hashlib and base64 modules are built in.
client.py
import json
import time
import hmac
import hashlib
import base64
import threading
import websocket  # pip install websocket-client

URL = "wss://finaquotes.com/feed"
WEB_API_ID = "YOUR_WEB_API_ID"
WEB_API_KEY = "YOUR_WEB_API_KEY"
SECRET = "YOUR_SECRET"
SYMBOLS = ["AAPL", "MSFT"]

backoff = 1.0  # seconds, cap at 30
_ping_stop = None


def sign(timestamp, id, web_api_key, secret):
    message = str(timestamp) + id + web_api_key
    digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
    return base64.b64encode(digest).decode()


def send(ws, obj):
    ws.send(json.dumps(obj))


def login(ws):
    id = "1"
    ts = int(time.time() * 1000)
    signature = sign(ts, id, WEB_API_KEY, SECRET)
    send(ws, {
        "Id": id,
        "Request": "Login",
        "Params": {
            "AuthType": "HMAC",
            "WebApiId": WEB_API_ID,
            "WebApiKey": WEB_API_KEY,
            "Timestamp": ts,
            "Signature": signature,
        },
    })


def subscribe(ws):
    send(ws, {
        "Id": "3",
        "Request": "FeedSubscribe",
        "Params": {"Subscribe": [{"Symbol": s} for s in SYMBOLS]},
    })


def start_heartbeat(ws):
    global _ping_stop
    _ping_stop = threading.Event()

    def loop():
        while not _ping_stop.wait(25):
            try:
                send(ws, {"Request": "Ping"})
            except Exception:
                break

    threading.Thread(target=loop, daemon=True).start()


def stop_heartbeat():
    global _ping_stop
    if _ping_stop:
        _ping_stop.set()
        _ping_stop = None


def on_open(ws):
    print("connected, logging in")
    login(ws)


def on_message(ws, message):
    global backoff
    try:
        msg = json.loads(message)
    except ValueError:
        return

    resp = msg.get("Response")
    if resp == "Login" and msg.get("Result", {}).get("Authenticated"):
        print("authenticated")
        backoff = 1.0  # reset backoff on success
        start_heartbeat(ws)
        subscribe(ws)
    elif resp == "SessionInfo":
        print("session:", msg["Result"]["SessionId"])
    elif resp == "FeedSubscribe":
        print("snapshot:", msg["Result"]["Snapshot"])
        fails = msg["Result"].get("Fails")
        if fails:
            print("unrecognized:", fails)
    elif resp == "FeedTick":
        r = msg["Result"]
        print(r["Symbol"], r["BestBid"]["Price"], "@", r["Timestamp"])
    elif resp == "Pong":
        pass  # heartbeat acknowledged
    elif resp == "Error":
        print("error:", msg["Error"]["Code"], msg["Error"]["Message"])


def on_close(ws, status, reason):
    print("disconnected:", status, reason)
    stop_heartbeat()


def run():
    global backoff
    while True:
        ws = websocket.WebSocketApp(
            URL,
            on_open=on_open,
            on_message=on_message,
            on_close=on_close,
        )
        # ping_interval keeps a WS-level heartbeat as well
        ws.run_forever(ping_interval=25)
        print("reconnecting in", backoff, "s")
        time.sleep(backoff)
        backoff = min(backoff * 2, 30.0)


if __name__ == "__main__":
    run()

Полный клиент

Полный, готовый к запуску клиент. Он подключается, входит с корректной HMAC-подписью, подписывается, печатает входящие тики, шлёт Ping каждые 25 секунд как heartbeat и переподключается с экспоненциальным backoff.

Нужен пакет ws: npm install ws. Модуль crypto встроенный.
client.js
const WebSocket = require("ws");
const crypto = require("crypto");

const URL = "wss://finaquotes.com/feed";
const WEB_API_ID = "YOUR_WEB_API_ID";
const WEB_API_KEY = "YOUR_WEB_API_KEY";
const SECRET = "YOUR_SECRET";
const SYMBOLS = ["AAPL", "MSFT"];

let ws;
let pingTimer = null;
let backoff = 1000; // старт 1с, потолок 30с

function sign(timestamp, id, webApiKey, secret) {
  const message = String(timestamp) + id + webApiKey;
  return crypto.createHmac("sha256", secret).update(message).digest("base64");
}

function send(obj) {
  ws.send(JSON.stringify(obj));
}

function login() {
  const id = "1";
  const ts = Date.now();
  const signature = sign(ts, id, WEB_API_KEY, SECRET);
  send({
    Id: id,
    Request: "Login",
    Params: {
      AuthType: "HMAC",
      WebApiId: WEB_API_ID,
      WebApiKey: WEB_API_KEY,
      Timestamp: ts,
      Signature: signature
    }
  });
}

function subscribe() {
  send({
    Id: "3",
    Request: "FeedSubscribe",
    Params: { Subscribe: SYMBOLS.map((s) => ({ Symbol: s })) }
  });
}

function startHeartbeat() {
  stopHeartbeat();
  pingTimer = setInterval(() => send({ Request: "Ping" }), 25000);
}
function stopHeartbeat() {
  if (pingTimer) clearInterval(pingTimer);
  pingTimer = null;
}

function connect() {
  ws = new WebSocket(URL);

  ws.on("open", () => {
    console.log("connected, logging in");
    login();
  });

  ws.on("message", (data) => {
    let msg;
    try { msg = JSON.parse(data.toString()); } catch (e) { return; }

    if (msg.Response === "Login" && msg.Result && msg.Result.Authenticated) {
      console.log("authenticated");
      backoff = 1000; // сброс backoff при успехе
      startHeartbeat();
      subscribe();
    } else if (msg.Response === "SessionInfo") {
      console.log("session:", msg.Result.SessionId);
    } else if (msg.Response === "FeedSubscribe") {
      console.log("snapshot:", msg.Result.Snapshot);
      if (msg.Result.Fails && msg.Result.Fails.length) {
        console.log("unrecognized:", msg.Result.Fails);
      }
    } else if (msg.Response === "FeedTick") {
      const r = msg.Result;
      console.log(r.Symbol, r.BestBid.Price, "@", r.Timestamp);
    } else if (msg.Response === "Pong") {
      // heartbeat подтверждён
    } else if (msg.Response === "Error") {
      console.error("error:", msg.Error.Code, msg.Error.Message);
    }
  });

  ws.on("close", () => {
    console.log("disconnected, reconnecting in", backoff, "ms");
    stopHeartbeat();
    setTimeout(connect, backoff);
    backoff = Math.min(backoff * 2, 30000);
  });

  ws.on("error", (err) => {
    console.error("socket error:", err.message);
    // следом сработает close и запустит реконнект
  });
}

connect();
Нужен websocket-client: pip install websocket-client. Модули hmac, hashlib и base64 встроенные.
client.py
import json
import time
import hmac
import hashlib
import base64
import threading
import websocket  # pip install websocket-client

URL = "wss://finaquotes.com/feed"
WEB_API_ID = "YOUR_WEB_API_ID"
WEB_API_KEY = "YOUR_WEB_API_KEY"
SECRET = "YOUR_SECRET"
SYMBOLS = ["AAPL", "MSFT"]

backoff = 1.0  # секунды, потолок 30
_ping_stop = None


def sign(timestamp, id, web_api_key, secret):
    message = str(timestamp) + id + web_api_key
    digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
    return base64.b64encode(digest).decode()


def send(ws, obj):
    ws.send(json.dumps(obj))


def login(ws):
    id = "1"
    ts = int(time.time() * 1000)
    signature = sign(ts, id, WEB_API_KEY, SECRET)
    send(ws, {
        "Id": id,
        "Request": "Login",
        "Params": {
            "AuthType": "HMAC",
            "WebApiId": WEB_API_ID,
            "WebApiKey": WEB_API_KEY,
            "Timestamp": ts,
            "Signature": signature,
        },
    })


def subscribe(ws):
    send(ws, {
        "Id": "3",
        "Request": "FeedSubscribe",
        "Params": {"Subscribe": [{"Symbol": s} for s in SYMBOLS]},
    })


def start_heartbeat(ws):
    global _ping_stop
    _ping_stop = threading.Event()

    def loop():
        while not _ping_stop.wait(25):
            try:
                send(ws, {"Request": "Ping"})
            except Exception:
                break

    threading.Thread(target=loop, daemon=True).start()


def stop_heartbeat():
    global _ping_stop
    if _ping_stop:
        _ping_stop.set()
        _ping_stop = None


def on_open(ws):
    print("connected, logging in")
    login(ws)


def on_message(ws, message):
    global backoff
    try:
        msg = json.loads(message)
    except ValueError:
        return

    resp = msg.get("Response")
    if resp == "Login" and msg.get("Result", {}).get("Authenticated"):
        print("authenticated")
        backoff = 1.0  # сброс backoff при успехе
        start_heartbeat(ws)
        subscribe(ws)
    elif resp == "SessionInfo":
        print("session:", msg["Result"]["SessionId"])
    elif resp == "FeedSubscribe":
        print("snapshot:", msg["Result"]["Snapshot"])
        fails = msg["Result"].get("Fails")
        if fails:
            print("unrecognized:", fails)
    elif resp == "FeedTick":
        r = msg["Result"]
        print(r["Symbol"], r["BestBid"]["Price"], "@", r["Timestamp"])
    elif resp == "Pong":
        pass  # heartbeat подтверждён
    elif resp == "Error":
        print("error:", msg["Error"]["Code"], msg["Error"]["Message"])


def on_close(ws, status, reason):
    print("disconnected:", status, reason)
    stop_heartbeat()


def run():
    global backoff
    while True:
        ws = websocket.WebSocketApp(
            URL,
            on_open=on_open,
            on_message=on_message,
            on_close=on_close,
        )
        # ping_interval держит WS-heartbeat на уровне протокола
        ws.run_forever(ping_interval=25)
        print("reconnecting in", backoff, "s")
        time.sleep(backoff)
        backoff = min(backoff * 2, 30.0)


if __name__ == "__main__":
    run()

FAQ / Troubleshooting

Login fails with "Authentication failed"

Almost always a signature mismatch. Check, in order:

  • The message is built as Timestamp + Id + WebApiKey in exactly that order, concatenated with no separators, Timestamp as a string.
  • The HMAC key is your Secret - not the WebApiKey.
  • The digest is Base64-encoded (not hex).
  • Compare against the published test vector - it should produce dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=.

Login rejected even though the signature looks right

Your Timestamp is probably stale. It must be within ±60 seconds of server time. Synchronize your machine clock (NTP) and generate the timestamp fresh at send time.

The connection drops after ~60 seconds of silence

You are not sending heartbeats. Send a Ping every 20-30 seconds (the examples use 25s). Without activity the server closes idle connections.

I subscribed but no ticks arrive

The market is likely closed. You will still receive the snapshot (last price) on subscription, but FeedTick notifications only flow while the market is open. They resume automatically when it opens.

The connection drops right after a second Login

This is the one-connection-per-token rule. A new connection with the same token disconnects the older one. Make sure you are not running two clients with the same credentials.

A symbol shows up in "Fails"

It was not recognized. Check the spelling and remember ticker normalization: dotted tickers are passed without the dot (e.g. BRK.BBRKB). Pull the live list with Symbols to confirm the exact name.

FAQ / Решение проблем

Login падает с «Authentication failed»

Почти всегда это несовпадение подписи. Проверьте по порядку:

  • Сообщение собирается как Timestamp + Id + WebApiKey в строго таком порядке, конкатенацией без разделителей, Timestamp как строка.
  • Ключ HMAC - это ваш Secret, а не WebApiKey.
  • Дайджест кодируется в Base64 (не hex).
  • Сверьтесь с опубликованным тест-вектором - он должен дать dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=.

Login отклоняется, хотя подпись выглядит верной

Скорее всего устарел Timestamp. Он должен быть в пределах ±60 секунд от серверного времени. Синхронизируйте часы машины (NTP) и формируйте timestamp заново в момент отправки.

Соединение рвётся после ~60 секунд тишины

Вы не шлёте heartbeat. Отправляйте Ping каждые 20-30 секунд (в примерах - 25с). Без активности сервер закрывает простаивающие соединения.

Подписался, но тиков нет

Вероятно, рынок закрыт. Снапшот (последнюю цену) при подписке вы всё равно получите, но уведомления FeedTick идут только при открытом рынке. Они возобновятся автоматически при открытии.

Соединение рвётся сразу после второго Login

Это правило «одно подключение на токен». Новое подключение тем же токеном разрывает старое. Убедитесь, что не запущены два клиента с одними и теми же учётными данными.

Символ попадает в «Fails»

Он не был распознан. Проверьте написание и помните про нормализацию тикеров: тикеры с точкой передаются без неё (например BRK.BBRKB). Запросите живой список через Symbols, чтобы уточнить точное имя.