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,WebApiKeyand aSecret) - provided by the operator. - The endpoint URL:
wss://finaquotes.com/feed(secure WebSocket only). - Any standard WebSocket client in your language of choice.
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:
- Open a WebSocket connection to
wss://finaquotes.com/feed. - Send a Login request with an HMAC signature.
- Receive
Login → Authenticated: true, then a SessionInfo message arrives automatically. - Send FeedSubscribe with the symbols you want. You get an immediate snapshot of the last prices.
- 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.
Быстрый старт
Минимальный путь от нуля до живого потока цен - пять шагов:
- Откройте WebSocket-соединение к
wss://finaquotes.com/feed. - Отправьте запрос Login с HMAC-подписью.
- Получите
Login → Authenticated: true, затем автоматически придёт сообщение SessionInfo. - Отправьте FeedSubscribe с нужными символами. В ответ сразу придёт снапшот последних цен.
- Принимайте уведомления FeedTick по мере обновления цен.
Все сообщения - JSON. Запросы несут выбранный вами Id; соответствующий ответ возвращает тот же Id. Уведомления (например FeedTick) поля Id не содержат.
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
{
"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:
- 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.
- Build the message string by concatenating, in this exact order, with no separators:
message = Timestamp + Id + WebApiKey
whereTimestampis the millisecond value as a string,Idis your request id, andWebApiKeyis your key. - Sign the message with HMAC-SHA256 using your
Secretas the key. - Base64-encode the raw HMAC digest. That string is the
Signature.
Signature = Base64( HMAC_SHA256( Timestamp + Id + WebApiKey, key = Secret ) )Example calculation
For these inputs:
| Timestamp | 1700000000000 |
| 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:
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
Use this vector to verify your own implementation before going live.
Reference implementations
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=
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:
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }
On failure the server returns an error and closes the connection:
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }
Аутентификация
Аутентификация использует подпись HMAC-SHA256. Сам Secret никогда не передаётся по сети - вы подписываете им короткое сообщение, а сервер проверяет подпись своей копией вашего секрета.
Запрос Login
{
"Id": "1",
"Request": "Login",
"Params": {
"AuthType": "HMAC",
"WebApiId": "YOUR_WEB_API_ID",
"WebApiKey": "YOUR_WEB_API_KEY",
"Timestamp": 1700000000000,
"Signature": "<base64 HMAC-SHA256>"
}
}
Как вычислить подпись
Подпись вычисляется пошагово из четырёх значений, которые у вас уже есть:
- Timestamp - текущее время в миллисекундах от Unix-эпохи (ms). Оно должно быть в пределах ±60 секунд от серверного времени, поэтому держите часы синхронизированными.
- Соберите строку сообщения, конкатенируя в строго таком порядке, без разделителей:
message = Timestamp + Id + WebApiKey
гдеTimestamp- значение миллисекунд как строка,Id- идентификатор вашего запроса,WebApiKey- ваш ключ. - Подпишите сообщение алгоритмом HMAC-SHA256, используя ваш
Secretв качестве ключа. - Закодируйте в Base64 сырой дайджест HMAC. Эта строка и есть
Signature.
Signature = Base64( HMAC_SHA256( Timestamp + Id + WebApiKey, key = Secret ) )Пример расчёта
Для входных значений:
| Timestamp | 1700000000000 |
| Id | "1" |
| WebApiKey | "YOUR_WEB_API_ID_KEY" |
| Secret | "YOUR_SECRET" |
Строка сообщения - 17000000000001YOUR_WEB_API_ID_KEY, а итоговая подпись:
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
Используйте этот вектор для проверки своей реализации до выхода в боевой режим.
Эталонные реализации
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=
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:
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }
При ошибке сервер возвращает ошибку и закрывает соединение:
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }
Connect
Connect to the endpoint over a secure WebSocket. Plain ws:// is not accepted.
| Endpoint | Transport |
|---|---|
| wss://finaquotes.com/feed | WebSocket (wss only) |
The connection lifecycle is:
- open - the WebSocket connection is established.
- send Login - your first frame should be the Login request.
- SessionInfo arrives - right after a successful Login, the server pushes a
SessionInfomessage on its own. You can also request it any time with{"Request":"SessionInfo"}.
SessionInfo
{
"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/feed | WebSocket (только wss) |
Жизненный цикл соединения:
- open - WebSocket-соединение установлено.
- send Login - первым кадром отправьте запрос Login.
- придёт SessionInfo - сразу после успешного Login сервер сам присылает сообщение
SessionInfo. Его также можно запросить в любой момент через{"Request":"SessionInfo"}.
SessionInfo
{
"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
{ "Id": "2", "Request": "Symbols" }
Request - a single symbol
{ "Request": "Symbols", "Params": { "Symbol": "AAPL" } }
Response
{
"Id": "2",
"Response": "Symbols",
"Result": {
"Symbols": [
{
"Symbol": "AAPL",
"Precision": 2,
"Description": "Apple Inc",
"ContractSize": 1,
"MarginCurrency": "USD",
"ProfitCurrency": "USD",
"TradeAmountStep": 1,
"MinTradeAmount": 1
}
]
}
}
Fields
| Field | Type | Meaning |
|---|---|---|
| Symbol | string | The ticker, e.g. AAPL. |
| Precision | int | Number of decimal places in the price (cents = 2). |
| Description | string | Human-readable instrument name. |
| ContractSize | number | Nominal default. |
| MarginCurrency | string | Nominal default. |
| ProfitCurrency | string | Nominal default. |
| TradeAmountStep | number | Nominal default. |
| MinTradeAmount | number | Nominal default. |
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 возвращает список доступных инструментов - все сразу либо один.
Запрос - все символы
{ "Id": "2", "Request": "Symbols" }
Запрос - один символ
{ "Request": "Symbols", "Params": { "Symbol": "AAPL" } }
Ответ
{
"Id": "2",
"Response": "Symbols",
"Result": {
"Symbols": [
{
"Symbol": "AAPL",
"Precision": 2,
"Description": "Apple Inc",
"ContractSize": 1,
"MarginCurrency": "USD",
"ProfitCurrency": "USD",
"TradeAmountStep": 1,
"MinTradeAmount": 1
}
]
}
}
Поля
| Поле | Тип | Значение |
|---|---|---|
| Symbol | string | Тикер, например AAPL. |
| Precision | int | Число знаков после запятой в цене (центы = 2). |
| Description | string | Читаемое название инструмента. |
| ContractSize | number | Номинальный дефолт. |
| MarginCurrency | string | Номинальный дефолт. |
| ProfitCurrency | string | Номинальный дефолт. |
| TradeAmountStep | number | Номинальный дефолт. |
| MinTradeAmount | number | Номинальный дефолт. |
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
{
"Id": "3",
"Request": "FeedSubscribe",
"Params": {
"Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ]
}
}
Response - snapshot + fails
{
"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.
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:
{
"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.
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }
Подписка
Подпишитесь на один или несколько символов через FeedSubscribe. В ответ сразу приходит снапшот последней цены по каждому распознанному символу плюс список нераспознанных. Далее по подписанным символам идут уведомления FeedTick по мере изменения цены.
Запрос
{
"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" ]
}
}
- Snapshot - текущая последняя цена по каждому распознанному символу, возвращается сразу.
- Fails - символы из запроса, которые не были распознаны.
BookDepth, но оно игнорируется - фид всегда отдаёт top-of-book (один уровень).Уведомления FeedTick
По каждому подписанному символу при изменении цены приходит уведомление FeedTick. Поля Id в нём нет:
{
"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 - оставшиеся активные подписки.
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
{ "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.PriceandBestAsk.Pricewith 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
BookDepthin a subscription is ignored. - Volume is always 0. Per-level volume is not provided, so
Volumeis reported as0on both sides.
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
Symbolsrequest 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 exampleBRK.BbecomesBRKB.
Список инструментов
- Категории. Текущий набор - акции (equities).
- Актуальный список. Всегда запрашивайте живой список запросом
Symbols, а не зашивайте его в код - см. Получение символов. - Автоматическое расширение. Список инструментов растёт автоматически; новые символы появляются сами, без действий с вашей стороны.
- Нормализация тикеров. Тикеры чистые (например
AAPL). Тикеры с точкой передаются без неё - напримерBRK.BстановитсяBRKB.
Message reference
All messages share a consistent envelope.
| Direction | Shape |
|---|---|
| request | { "Id", "Request", "Params" } |
| response | { "Id", "Response", "Result" } |
| error | { "Id", "Response": "Error", "Error": { "Code", "Message" } } |
| notification | { "Response", "Result" } (no 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 (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
{ "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" } }Справочник сообщений
Все сообщения используют единый конверт.
| Направление | Форма |
|---|---|
| запрос | { "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.
ws package: npm install ws. The crypto module is built in.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();
websocket-client: pip install websocket-client. The hmac, hashlib and base64 modules are built in.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 встроенный.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 встроенные.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 + WebApiKeyin exactly that order, concatenated with no separators,Timestampas a string. - The HMAC key is your
Secret- not theWebApiKey. - 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.B → BRKB). 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.B → BRKB). Запросите живой список через Symbols, чтобы уточнить точное имя.