FinaQuotes Feed: документация WebSocket API
Котировочный фид реального времени, доставляемый по одному WebSocket-соединению. Аутентифицируйтесь один раз, подпишитесь на нужные инструменты и получайте живой поток ценовых тиков.
Что это
Фид - это потоковый API: цены приходят уведомлениями в момент изменения, опрос (polling) не нужен. Котировки собираются из нескольких институциональных и биржевых источников данных, нормализуются FinaQuotes и доставляются вам по единому простому JSON-протоколу.
Что нужно для подключения
- Токен доступа (
WebApiId,WebApiKeyиSecret) - предоставляется оператором. - URL точки входа:
wss://feed.finaquotes.com/feed(только защищённый WebSocket). - Любой стандартный WebSocket-клиент на вашем языке.
Secret в секрете - он никогда не покидает вашу сторону и используется только для вычисления подписи запросов.
Быстрый старт
Минимальный путь от нуля до живого потока цен - пять шагов:
- Откройте WebSocket-соединение к
wss://feed.finaquotes.com/feed. - Отправьте запрос Login с HMAC-подписью.
- Получите
Login → Authenticated: true, затем автоматически придёт сообщение SessionInfo. - Отправьте FeedSubscribe с нужными символами. В ответ сразу придёт снапшот последних цен.
- Принимайте уведомления FeedTick по мере обновления цен.
Все сообщения - JSON. Запросы несут выбранный вами Id; соответствующий ответ возвращает тот же Id. Уведомления (например FeedTick) поля Id не содержат.
Аутентификация
Аутентификация использует подпись 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" } }
Подключение
Подключайтесь к точке входа по защищённому WebSocket. Обычный ws:// не принимается.
| Точка входа | Транспорт |
|---|---|
| wss://feed.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
}
}
Получение символов
Запрос 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.Подписка
Подпишитесь на один или несколько символов через 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.36, "Volume": 100 },
"BestAsk": { "Type": "Ask", "Price": 307.42, "Volume": 200 },
"SessionOpen": true
}
],
"Fails": [ "UNKNOWN1" ]
}
}
- Snapshot - текущая котировка (лучшие bid/ask) по каждому распознанному символу, возвращается сразу.
- SessionOpen - торгуется ли рынок инструмента в момент снапшота. Если
false, это последняя котировка перед закрытием, еёTimestampможет быть давним; потокаFeedTickдо открытия рынка не будет. - Fails - символы из запроса, которые не были распознаны.
BookDepth, но оно игнорируется - фид всегда отдаёт top-of-book (один уровень).Уведомления FeedTick
По каждому подписанному символу при изменении цены приходит уведомление FeedTick. Поля Id в нём нет:
{
"Response": "FeedTick",
"Result": {
"Symbol": "AAPL",
"Timestamp": 1700000000500,
"BestBid": { "Type": "Bid", "Price": 307.47, "Volume": 300 },
"BestAsk": { "Type": "Ask", "Price": 307.53, "Volume": 100 }
}
}
Расширенные часы (акции США)
Акции США торгуются и до, и после основной сессии. Если для вашего аккаунта включены расширенные часы, фид покрывает и их:
FeedTickпо акциям США продолжают приходить в премаркет, 04:00-09:30, и в постмаркет, 16:00-20:00 по нью-йоркскому времени (с учётом перехода на летнее время). Формат сообщений тот же, что в основной сессии.- Снапшот при
FeedSubscribeотдаёт последнюю котировку с учётом расширенной сессии, аSessionOpenв эти часы равенtrue. - История цен расширенной сессии доступна по REST с параметром
session=extended- см. GET /api/v1/candles.
Если расширенные часы для аккаунта выключены, акции США идут только в основную сессию, 09:30-16:00 по нью-йоркскому времени, а вне её SessionOpen равен false. Расширенные часы есть только у акций США, остальные классы активов торгуются в свои обычные часы. Чтобы включить или выключить расширенные часы для аккаунта, напишите в поддержку через вашего менеджера.
Отписка
FeedUnsubscribe убирает символы. В ответе возвращается Symbols - оставшиеся активные подписки.
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }
Формат цены
Каждый тик несёт лучший bid и лучший ask - по одному ценовому уровню на сторону. Содержимое полей зависит от инструмента:
- Реальный спред там, где рынок его котирует. Для акций, форекса и спот-металлов
BestBid.PriceиBestAsk.Priceнесут фактические лучшие bid и ask - спред между ними является реальными рыночными данными. - Реальные размеры уровней для акций. Для акций США и ряда международных листингов
Volumeнесёт фактический размер уровня в единицах инструмента (штуках акций). Там, где размеров нет (форекс, индексы и другие),Volumeравен0. - Инструменты с одной ценой. Там, где рынок не даёт котировочных данных (криптовалюты, индексы, сырьё, облигации), обе стороны несут одну и ту же последнюю цену -
bid = ask = last- иVolumeравен0. Если приложению нужен спред по таким инструментам - наложите его сами поверх последней цены. - Только top-of-book. Предоставляется один ценовой уровень - глубины стакана нет. Любой
BookDepthв подписке игнорируется.
BestBid.Price и BestAsk.Price; у инструментов с одной ценой обе стороны равны. Volume: 0 означает, что размер недоступен, а не пустой уровень. Timestamp - время цены в миллисекундах от Unix-эпохи.Список инструментов
- Категории. Текущий набор охватывает акции, форекс, криптовалюты, металлы, сырьё, облигации и индексы.
- Актуальный список. Всегда запрашивайте живой список запросом
Symbols, а не зашивайте его в код - см. Получение символов. - Автоматическое расширение. Список инструментов растёт автоматически; новые символы появляются сами, без действий с вашей стороны.
- Нормализация тикеров. Тикеры чистые (например
AAPL). Тикеры с точкой передаются без неё - напримерBRK.BстановитсяBRKB.
Полный список инструментов - что отдаём сейчас и какой каталог доступен дополнительно.
REST API
Помимо WebSocket-фида, справочные данные и история цен доступны по обычному HTTPS. Все эндпоинты живут под https://feed.finaquotes.com/api/v1/ и аутентифицируются одним заголовком:
x-history-token: YOUR_HISTORY_TOKEN
Токен выдаёт оператор вместе с учётными данными фида. Запросы без валидного токена возвращают 404.
Полный список параметров, коды ошибок, лимиты и примеры на curl и Python - в справочнике REST API.
GET /api/v1/instruments
Каталог инструментов, раздаваемых фидом, - тот же набор, что возвращает запрос Symbols по WebSocket. Необязательный query-параметр category фильтрует по классу активов; неизвестная категория возвращает 400.
curl -H "x-history-token: YOUR_HISTORY_TOKEN" \
"https://feed.finaquotes.com/api/v1/instruments?category=forex"
{
"count": 70,
"instruments": [
{
"symbol": "AUDCAD",
"name": "Australian Dollar / Canadian Dollar",
"category": "forex",
"currency": "CAD",
"digits": 5,
"session": "forex_24_5"
}
]
}
Поля
| Поле | Тип | Значение |
|---|---|---|
| symbol | string | Тикер в том виде, как его использует фид, например AAPL, EURUSD, BTCUSDT. |
| name | string | Читаемое название инструмента. |
| category | string | Класс актива: crypto, stocks, forex, metals, commodities, indices или bonds. |
| currency | string | Валюта котировки. |
| digits | int | Число знаков после запятой в цене - совпадает с Precision в фиде. |
| session | string | Тип торговой сессии, например 24_7, nasdaq, forex_24_5. |
category возвращается полный список, отсортированный по категории и символу. Цен в ответе нет - живые цены идут из фида, история - из /api/v1/candles.GET /api/v1/categories
Классы активов с количеством активных инструментов в каждом:
{
"count": 7,
"categories": [
{ "category": "crypto", "count": 292 },
{ "category": "stocks", "count": 819 },
{ "category": "forex", "count": 70 },
{ "category": "metals", "count": 19 },
{ "category": "commodities", "count": 38 },
{ "category": "indices", "count": 38 },
{ "category": "bonds", "count": 6 }
]
}
GET /api/v1/candles
История цен OHLCV. Параметры: symbol (обязателен), tf - один из 1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W, 1MN, from/to - миллисекунды от Unix-эпохи (необязательны), limit - число баров (по умолчанию 1000, максимум 5000), session - regular (по умолчанию) или extended, чтобы включить премаркет и постмаркет акций США (только если для аккаунта включены расширенные часы).
curl -H "x-history-token: YOUR_HISTORY_TOKEN" \
"https://feed.finaquotes.com/api/v1/candles?symbol=AAPL&tf=1H&limit=2"
{
"symbol": "AAPL",
"tf": "1H",
"candles": [
{ "ts": 1782928800000, "open": 293.725, "high": 295.53, "low": 292.7, "close": 294.72, "volume": 4190956, "trades": 43640 },
{ "ts": 1782932400000, "open": 294.73, "high": 295.55, "low": 293.7501, "close": 294.25, "volume": 5363332, "trades": 59130 }
]
}
ts - время открытия бара (UTC, мс). Неверный tf или отсутствующий symbol возвращают 400; неизвестный символ - 404.
Справочник сообщений
Все сообщения используют единый конверт.
| Направление | Форма |
|---|---|
| запрос | { "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.36, "Volume": 100 }, "BestAsk": { "Type": "Ask", "Price": 307.42, "Volume": 200 }, "SessionOpen": true } ], "Fails": [ "UNKNOWN1" ] } }FeedTick (уведомление)
{ "Response": "FeedTick", "Result": { "Symbol": "AAPL", "Timestamp": 1700000000500, "BestBid": { "Type": "Bid", "Price": 307.47, "Volume": 300 }, "BestAsk": { "Type": "Ask", "Price": 307.53, "Volume": 100 } } }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" } }Правила
Одно подключение на токен
На токен допустимо одно активное соединение. Новое подключение тем же токеном разрывает предыдущее.
Heartbeat
- Если сервер не видит активности от клиента дольше 60 секунд, он разрывает соединение.
- Периодически шлите
Ping(или WS-ping-кадр) - рекомендуется каждые 20-30 секунд. - Сервер также сам шлёт WebSocket-ping каждые 30 секунд.
Реконнект
При разрыве переподключайтесь с экспоненциальным backoff (например 1с, 2с, 4с … до 30с) и выполняйте Login повторно.
Вне торговой сессии
При закрытом рынке подписка всё равно возвращает снапшот (последнюю известную котировку), но потока FeedTick нет до открытия рынка. Это нормально. Для акций США с включёнными расширенными часами премаркет и постмаркет считаются открытым рынком - см. Расширенные часы.
Попытки входа
Неудачные попытки входа ограничены по частоте (защита от перебора). Не зацикливайте Login при ошибке подписи - сначала проверьте параметры.
Полный клиент
Полный, готовый к запуску клиент. Он подключается, входит с корректной HMAC-подписью, подписывается, печатает входящие тики, шлёт Ping каждые 25 секунд как heartbeat и переподключается с экспоненциальным backoff.
ws: npm install ws. Модуль crypto встроенный.const WebSocket = require("ws");
const crypto = require("crypto");
const URL = "wss://feed.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://feed.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 / Решение проблем
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, чтобы уточнить точное имя.