# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.18.0] - 2026-07-20 — Признак isTradifi: токенизированные акции и сырьё распознаются и на Bybit ### Added - **Нормализованный признак `isTradifi: boolean` в `TradeSymbol`.** Токенизированные TradFi-перпы (акции, ETF, сырьё/металлы) теперь помечаются на обеих биржах единым полем. Binance помечает их `contractType: 'TRADIFI_PERPETUAL'`; Bybit держит обычный `contractType: 'LinearPerpetual'`, и единственный маркер — `symbolType: 'stock'` (96 токенизированных акций/ETF на 15.07.2026, первые с 21.04.2026) или `'commodity'` (нефть BZ/CL, золото, серебро). `symbolType: 'innovation'` — обычная крипта из Innovation Zone, не TradFi. До этого фильтр `excludeTradifi` в trade-engine ловил только Binance-признак, и все акции Bybit (включая IBMUSDT) проходили в фид и детекцию стратегий. `buildStreamName` Binance-потока переведён с локальной проверки `contractType` на новое поле — единая точка правды. (`src/types/common.ts`, `src/normalizers/bybitNormalizer.ts`, `src/normalizers/binanceNormalizer.ts`, `src/ws/BinanceFuturesPublicStream.ts`) ### Fixed - **Bybit ticker delta затирала `indexPrice` в `0`.** Bybit шлёт тикер-дельты только с изменившимися полями — кадр с `markPrice`, но без `indexPrice`, раньше приводил к `indexPrice: 0` на выходе `extractMarkPriceUpdates`. Теперь per-symbol хранится последний известный индекс (`lastIndexPriceBySymbol`), и дельта без индекса несёт последнее известное значение; `0` отдаётся только пока индекс ни разу не приходил. Обнаружено при разборе инцидента OPGUSDT 2026-07-07 (coin-listing): нулевой индекс отключал компонент `Max(Index, Mark×(1+X))` price-limit формулы. (`src/ws/BybitPublicStream.ts`) - **Market-ордер Bybit (WS) нёс `price`, если он был передан в `args`.** `buildBybitOrderParams` теперь прикладывает `price` только к limit-like типам (`Limit`/`StopLimit`/`TakeProfitLimit`) — market-запрос никогда не несёт price на wire, даже если вызывающий код передал стрей/устаревшее значение (например переиспользуя limit-параметры для market-piece). (`src/exchanges/BybitBaseClient.ts`) - **Stale-watcher Binance Futures public держал процесс Node живым.** Фоновый `setInterval`, проверяющий залипшие соединения, теперь `unref`-нут — housekeeping-таймер больше не мешает процессу-потребителю завершиться после того, как тот закрыл все свои соединения (в рабочем приложении event loop держат сами открытые WebSocket-соединения, поведение не меняется). Устраняет также утечку таймеров в тестах (Jest force-exit worker). (`src/ws/BinanceFuturesPublicStream.ts`) ### Changed - **BREAKING — минимальная версия Node поднята `>=18.0.0` → `>=22.0.0`** (`package.json` → `engines.node`). Библиотека (через `@solncebro/websocket-engine`) полагается на глобальный `WebSocket`, стабильный только с Node 22 — прежняя граница `18` была неточной и могла падать в рантайме на Node 18/20 (`ReferenceError: WebSocket is not defined`). `engines` теперь отражает фактически поддерживаемый рантайм. - **Зависимость `@solncebro/websocket-engine` обновлена `^0.3.0` → `^0.4.0`.** Обратно совместимо: публичный API пакета не изменился (потребляются `ReliableWebSocket`, `WebSocketStatus` + типы, все без изменений). 0.4.0 несёт внутренний фикс переподключения (отсоединение мёртвого сокета, чтобы пара событий `error`+`close` не сбрасывала только что взведённый reconnect-таймер) и убирает неиспользуемую транзитивную зависимость `ws`. (`package.json`) ## [0.16.0] - 2026-07-06 — Public-stream efficiency: ленивый стакан, bid/ask тикера, Binance aggTrade Эффективность публичных потоков и поддержка расчёта premium из тикера. Все находки подтверждены по коду (`файл:строка`). Раздел не дублирует Binance-feeder-объём 0.15.0 (авто-шардинг, залп `SUBSCRIBE`, учёт REST-веса, хуки `beforeRequest`/`onResponse`) — он ниже. ### Added - **Ленивый парсинг стакана (opt-in `ExchangeConfig.lazyOrderbookParse`, только Bybit).** При включённом флаге orderbook-кадры не проходят полный `JSON.parse`: символ дёшево достаётся из головы кадра (`ORDERBOOK_TOPIC_HEAD_REGEX`, первые 200 байт), а весь сырой кадр складывается как строка. Полностью парсятся только символы из live-набора (реактивный вход). Экономит CPU, когда публичный поток стакана слишком тяжёл для одного Node-потока. Публичное API: `setOrderbookLiveSymbol({ symbol, isLive })`, `subscribeRawOrderbook(handler)` / `unsubscribeRawOrderbook(handler)` в `ExchangeClient`; on-demand парсер сырого кадра `parseBybitOrderbookRawFrame({ raw, symbol, receivedTimestamp })` (экспортируется из `index`); типы `RawOrderbookFrame` / `RawOrderbookHandler`. Тикеры/klines/publicTrade не затронуты. (`src/ws/BybitPublicStream.ts`, `src/normalizers/bybitNormalizer.ts`, `src/types/common.ts`) - **Best bid/ask (уровень 1) в тикере + живой поток тикеров.** `Ticker` несёт `bid1Price` / `bid1Size` / `ask1Price` / `ask1Size` (`src/types/common.ts`); `normalizeBybitTickers` заполняет их, когда Bybit прислал (snapshot всегда, delta — при изменении стороны) (`src/normalizers/bybitNormalizer.ts`). `ExchangeClient.subscribeAllTickers(handler, symbolList)` / `unsubscribeAllTickers(handler)` дают поток `TickerBySymbol` на каждое обновление — потребитель считает premium (mid − mark) прямо из тикера, не трогая стакан. Топик `tickers.{symbol}` делится с mark-price по ref-count — без лишнего соединения. (`src/types/exchange.ts`, `src/exchanges/BaseExchangeClient.ts`) - **Публичные сделки Binance Futures через `@aggTrade`.** `subscribePublicTrades` / `unsubscribePublicTrades` / `resubscribePublicTrades` теперь работают и на Binance Futures (раньше публичные сделки были только у Bybit) — стрим `{symbol}@aggTrade` на отдельном соединении (`groupKey = 'aggTrades'`). Нормализатор `normalizeBinanceAggTradeFromStream` + raw-тип `BinanceAggTradeWebSocketRaw` (`src/normalizers/binanceNormalizer.ts`, `src/ws/BinanceFuturesPublicStream.ts`). - **`fetchOpenConditionalOrders(settleCoin)` в `ExchangeClient`.** Открытые условные (stop / take-profit-trigger) ордера, которые обычный `fetchOpenOrders` не отдаёт. Bybit: `/v5/order/realtime` с `orderFilter=StopOrder` + `settleCoin`. Binance: `/fapi/v1/openOrders` без символа с фильтром `stopPrice > 0`. Добавлен параметр `orderFilter` в `buildCategoryParams` / `SymbolLimitFilterArgs`. (`src/exchanges/BybitBaseClient.ts`, `src/exchanges/BinanceBaseClient.ts`, `src/http/BybitHttpClient.ts`) - **`getMaxOrderQty(symbol)` / `getMarketMaxOrderQty(symbol)` в `ExchangeClient`.** Верхняя граница объёма из `filter.maxQty` и `filter.marketMaxQty` (с fallback на `maxQty`, если у рынка нет отдельного market-cap). (`src/exchanges/BaseExchangeClient.ts`) - **Subpath-экспорт `@solncebro/exchange-engine/public` (`src/public.ts`).** Лёгкая точка входа только для публичных стримов (`BybitPublicStream`, `BinanceFuturesPublicStream`), нормализаторов klines / tradeSymbols и нужных констант/типов — не тянет весь клиент. `package.json` → `exports["./public"]`. - **Application-level heartbeat Binance Futures public.** Ping через `{ method: 'LIST_SUBSCRIPTIONS', id }` с детекцией ответа по `id` + `Array.isArray(result)` вместо дефолтного ping `ReliableWebSocket`. (`src/ws/BinanceFuturesPublicStream.ts`) - **Тесты-детекторы полноты ответа биржи.** `bybitResponseCompleteness`, `bybitTickerCompleteness`, `binanceAggTrade` — падают, если биржа прислала поле, которое ни смоделировано в raw-типе, ни явно проигнорировано (защита от тихой потери данных при изменениях API). ### Changed - **`authenticateBybitWebSocket` вынесен в отдельный модуль `src/ws/bybitWebSocketAuth.ts`** (из `bybitWebSocketUtils.ts`) — убирает импорт `crypto` из barrel-утилит; потребители (`BybitPrivateStream`, `BybitTradeStream`) импортируют из нового файла. Тело функции (подпись, обработка ошибок, логирование) перенесено без изменений. - **`parseWebSocketMessage` берёт тип `RawData` из `@solncebro/websocket-engine`** вместо прямого импорта из `ws` (гигиена зависимостей — тип реэкспортируется движком). ## [0.15.0] - 2026-06-22 — Binance feeder multi-app readiness Усиление для работы market-data-feeder на Binance USD-M futures с несколькими приложениями за одним IP. Кодовые находки подтверждены `файл:строка`, лимиты Binance сверены с живым `fapi/v1/exchangeInfo` и developers.binance.com. ### Fixed — High - **Залп `SUBSCRIBE` при подключении 2-го/3-го интервала (Binance public stream).** Когда соединение группы интервала ещё не открыто, инкрементальный путь слал по одному `SUBSCRIBE` на каждый из сотен символов (`sendDynamicSubscribe` через отложенные `readyPromise.then`) — залпом, превышая лимит Binance 10 входящих сообщений/сек на соединение → разрыв (и риск бана IP при повторах). Теперь на ещё-не-открытое соединение поштучные кадры не шлются: `onOpen` подписывает весь `streamList` пакетами (50 / 200мс ≈ 5 сообщений/сек), а `sendSubscribeBatches` читает длину списка вживую, поэтому символы, добавленные во время открытия, не теряются. (`src/ws/BinanceFuturesPublicStream.ts`) ### Added - **Авто-шардинг соединений под лимит 1024 стрима.** Один интервал дробится на несколько соединений с общим `groupKey`, когда число стримов приближается к потолку Binance (1024); лимит на соединение настраивается (`maxStreamsPerConnection`, дефолт 900). `createConnections` чанкует через `chunkList`, `addStreamToIntervalGroup` кладёт стрим на шард с местом либо открывает новый. Раньше превышение вело к тихой потере подписок. (`src/ws/BinanceFuturesPublicStream.ts`) - **Учёт REST-веса и обработка HTTP 418 (Binance).** HTTP-клиент Binance парсит `x-mbx-used-weight-1m`, упреждающе тормозит чтения около ~80% от лимита 2400/мин на IP и делает back-off на 418 (с учётом `Retry-After`, ограничен разумным максимумом). Логика вынесена в чистые функции (`src/http/binanceRateLimit.ts`) и подключена двумя no-op хуками `beforeRequest`/`onResponse` в `BaseHttpClient` (Bybit и прочие не затронуты). Покрывает все чтения через единый клиент — дозагрузку, опрос тикеров и переподписку kline-watchdog. (`src/http/BaseHttpClient.ts`, `src/http/BinanceBaseHttpClient.ts`) ## [0.14.0] - 2026-05-30 — Reliability & API-compliance audit Комплексный аудит надёжности и соответствия текущему API Binance/Bybit. Кодовые находки подтверждены `файл:строка`; API-находки сверены с официальными доками бирж. Включает базовый объём 0.14.0 (см. history note ниже). ### Fixed — Critical - **Подпись Binance batch-ордеров.** HMAC считался над строкой `URLSearchParams`, а axios сериализовал параметры своим дефолтным сериализатором (оставляет `,`/`:` без кодирования) → байты на проводе ≠ подписанные → Binance `-1022` на всех batch-операциях (create/cancel/modify). Введён общий `serializeQueryParams` (`src/utils/serializeQueryParams.ts`), используемый и при подписи, и как `paramsSerializer` axios-инстанса (`BaseHttpClient`) — wire-строка байт-в-байт равна подписанной. Покрыто wire-level тестом. - **Binance Futures user-data WS URL.** Legacy `wss://fstream.binance.com/ws/` выведен биржей из эксплуатации 2026-04-23 → переведён на `wss://fstream.binance.com/private/ws/`. ### Fixed — High - **Binance Spot user-data stream перестроен на WebSocket API** (listenKey REST `/api/v3/userDataStream` удалён биржей 2026-02-20). Новый класс `BinanceSpotUserDataStream` подписывается через `userDataStream.subscribe` на `ws-api/v3` (подпись HMAC-SHA256 — Ed25519/`session.logon` НЕ требуются, сверено по `binance-spot-api-docs/web-socket-api.md`), переподписывается на reconnect, шлёт `userDataStream.unsubscribe` на close. События маппятся: `executionReport → onOrderUpdate`, `outboundAccountPosition → onBalanceUpdate`. Добавлены тип `BalanceUpdateEvent` и опциональный `UserDataStreamHandlerArgs.onBalanceUpdate`. ⚠️ Требует live-проверки на testnet: точный формат event-фрейма (голый vs `{event:...}`) и поведение подписки при долгой сессии. - **Demo (testnet) Binance Futures UDS** использовал prod-хост → теперь testnet-хост в demo-режиме. - **Bybit demo публичные стримы** (`stream-demo.bybit.com` не отдаёт публичные данные) → публичные подписки всегда на mainnet-хост. - **Binance `timeInForce`**: `PostOnly` уходил сырым (невалидно) → futures маппится в `GTX`, spot post-only — в тип `LIMIT_MAKER`. - **Статус ордеров в user-data**: устранены локальные дубли карт статусов, расходившиеся с нормализаторами (Binance `REJECTED` и Bybit `PartiallyFilledCanceled` падали в `'open'`). Используется единый канон `BINANCE_ORDER_STATUS` / `BYBIT_ORDER_STATUS`. - **Precision без `loadTradeSymbols`**: `amountToPrecision` больше не «флорит» sub-1 объём в `0` (риск ордера с `qty: 0`). - **Bybit дедуп WS-топиков**: `addTopicToConnection` не дублирует уже подписанный топик (исключает утечку при cleanup и спам на reconnect). ### Fixed — Medium / Low - Bybit private WS учитывает demo-режим (`stream-demo.bybit.com/v5/private`). - Защита от двойного `connectUserDataStream` (утечка стрима + keepalive-таймера). - `isConnected()` user-data/private стримов сверяется с реальным статусом сокета (`getStatus()`), а не «объект существует». - `BybitBaseClient.fetchKlines('1s')` бросает понятную ошибку (Bybit REST не отдаёт 1s). - Bybit `cancelOrder` возвращает реальный ордер (`getOrder`) вместо фабрикованного stub с `side: Buy`. - `submitOrder` (Bybit single WS) пробрасывает `Order.rateLimit` snapshot. - Нормализаторы: `side` с fallback (`?? Buy`), `Triggered → 'open'`, отсечение `NaN`-балансов. - `BaseTradeStream.ensureConnected` — порядок проверок исправлен (нет ложного `WebSocket is not connected` при гонке). - `TradeToKlineAggregator` не эмитит вырожденную свечу `volume=0`. - `loadKlinesInChunks` и `Exchange.close` — `Promise.allSettled` (сбой одного не рушит остальное). - `Retry-After` с HTTP-датой → экспоненциальный backoff (не мгновенный ретрай). - `modifyOrder`/`modifyBatchOrders` валидируют ответ (`signedPut`). - Bybit REST `recvWindow` = 7000 (константа `BYBIT_RECV_WINDOW` задействована). - `BinanceSpotPublicStream` — убрана двойная переподписка на reconnect. - `roundToStep` корректно обрабатывает научную нотацию step (`1e-8`). - `src/types/index.ts` — runtime-энумы экспортируются как значения (`export {}`), не `export type`. - Bybit spot Market + `quoteOrderQty` явно ставит `marketUnit=quoteCoin`. - **Bybit `getOrderRateLimit`** теперь зависит от категории (сверено с https://bybit-exchange.github.io/docs/v5/rate-limit): **spot = 20 RPS**, **linear = 10 RPS** (документированный create-лимит per-UID; у linear все order-эндпоинты 10/s, у spot create/cancel/batch 20/s, amend 10/s). ### Changed - Удалены неиспользуемые demo-public Bybit константы; добавлена `BYBIT_DEMO_PRIVATE_WEBSOCKET_URL`. - `BybitPrivateStreamArgs` получил обязательное поле `url`. ### Notes - **Не вошло (требует отдельной работы):** точечные WS-reconnect-края (`BinanceFuturesPublicStream` cleanup пустых соединений, connect-timeout, Bybit resubscribe 1s / double-subscribe). closePosition+quantity — требует подтверждения на живом Binance. ## [0.14.0-base] - 2026-05-18 (history note — изначальный объём 0.14.0 до аудита) Базовый объём релиза 0.14.0: новые публичные подписки Bybit V5 (orderbook + publicTrade), визуализация rate-limit'ов на двух уровнях (per-UID + per-order runtime snapshot), полный спектр batch-операций над ордерами (create/modify/cancel через REST + WebSocket) с автоматическим chunking, и BREAKING-изменение возвращаемого типа `cancelBatchOrders`. ### Added #### Bybit V5 public subscriptions - Публичные подписки на orderbook updates и сырые publicTrade-сделки на Bybit V5 public stream (`linear` и `spot`). - В `BybitPublicStream`: `subscribeOrderbook(symbol, depth, handler)` / `unsubscribeOrderbook(...)` для topic `orderbook.{depth}.{symbol}` и `subscribePublicTrades(symbol, handler)` / `unsubscribePublicTrades(...)` для topic `publicTrade.{symbol}`. Новый custom-subscriber `publicTrade` работает рядом с существующим `TradeToKlineAggregator` — оба consumer'а получают сообщения независимо; topic снимается только когда отписались оба. - В `BaseExchangeClient`: делегирующие методы `subscribeOrderbook` / `unsubscribeOrderbook` / `subscribePublicTrades` / `unsubscribePublicTrades`. Если `PublicStreamLike` не реализует эти методы (Binance) — `subscribe*` бросает `Not supported`, `unsubscribe*` — no-op. - Типы: `OrderBookUpdate`, `OrderBookUpdateType` (`'snapshot' | 'delta'`), `OrderBookRawLevel` (`[string, string]` — точные decimal-строки без потери точности), `OrderBookHandler`, `PublicTradeHandler`, `SubscribeOrderbookArgs`, `SubscribePublicTradesArgs`. Экспортируются из корневого `index.ts`. - Нормализаторы: `normalizeBybitOrderBookUpdate(raw, symbol, updateType, receivedTimestamp)` — возвращает `OrderBookUpdate` с сохранением `bidList`/`askList` как сырых пар decimal-строк; `normalizeBybitPublicTradeFromStream(rawList)` — конвертирует WS-payload (`T`/`s`/`p`/`v`/`S`/`i`/`BT`) в `PublicTrade[]` с маппингом side через `BYBIT_ORDER_SIDE`. - `BybitOrderBookRaw` расширен опциональными полями `s` (symbol из WS payload) и `seq` (cross-sequence). `BybitPublicTradeDataRaw` расширен опциональными `S` (side), `i` (trade id), `BT` (block trade flag), `L` (price direction tick). - `PublicStreamLike` расширен optional методами для orderbook/publicTrade подписок, чтобы Binance public stream продолжал ему соответствовать без переопределения. #### Rate-limit visibility (per-UID + per-order runtime) - `ExchangeClient.getOrderRateLimit(): Promise` — возвращает per-UID write rate-limit для order endpoints (статический снимок из exchange-info). Тип `OrderRateLimit { writeRequestsPerSecond: number; source: OrderRateLimitSource }`, где `OrderRateLimitSource = 'binance-exchange-info' | 'bybit-documented' | 'fallback'`. - Binance Futures/Spot реализация: `parseBinanceOrderRateLimit(raw: BinanceExchangeInfoRaw)` (`src/normalizers/binanceNormalizer.ts`) фильтрует `rateLimits[].rateLimitType === 'ORDERS'`, конвертирует все интервалы в RPS, берёт `Math.min(...)` (наиболее строгий лимит). Если в `/fapi/v1/exchangeInfo` или `/api/v3/exchangeInfo` нет `rateLimits` — возвращает `{ writeRequestsPerSecond: BINANCE_ORDER_RATE_LIMIT_FALLBACK_RPS, source: 'fallback' }` (15 RPS). Новый raw-тип `BinanceRateLimitRaw { rateLimitType, interval: 'SECOND'|'MINUTE'|'HOUR'|'DAY', intervalNum, limit }`; `BinanceExchangeInfoRaw` расширен опциональным `rateLimits?`. - Bybit Linear/Spot реализация: hardcoded `{ writeRequestsPerSecond: 20, source: 'bybit-documented' }` — Bybit не отдаёт rate-limits через GET endpoint, только через headers на actual order calls. Документированный лимит для `/v5/order/create`, `/v5/order/cancel`, `/v5/order/create-batch`, `/v5/order/cancel-batch` = 20 RPS per-UID (источник: https://bybit-exchange.github.io/docs/v5/rate-limit). - Default throw `getOrderRateLimit()` в `BaseExchangeClient`; реализация на уровне `BinanceBaseClient` + `BybitBaseClient`. - `OrderRateLimitSnapshot { limit, remaining, resetMs }` в `src/types/common.ts` — per-response runtime snapshot из заголовков ответа на одиночные order-операции через WebSocket Trade Stream. Опциональное поле `Order.rateLimit?` заполняется автоматически. - `BybitTradeStream.handleSingleOrderResponse` парсит `message.header` через `parseBybitHeaderRateLimit` (`X-Bapi-Limit`, `X-Bapi-Limit-Status`, `X-Bapi-Limit-Reset-Timestamp` → `{ limit, remaining, resetMs }`). Новый тип `BybitTradeResponseHeader` в `BybitTradeStream.types.ts`. - `BinanceTradeStream.handleMessage` парсит `message.rateLimits` через `parseBinanceRateLimitArray` (ищет entry с `rateLimitType === 'ORDERS' && interval === 'SECOND'` → `{ limit, remaining: limit - count, resetMs: now + intervalNum * 1000 }`). Новый тип `BinanceWebSocketRateLimitEntry` в `BinanceTradeStream.types.ts`. #### Batch order operations (REST + WebSocket) - `ExchangeClient.modifyBatchOrders(orderList: ModifyBatchOrderArgs[]): Promise` — массовая модификация ордеров с per-order success/error результатом (`ModifyBatchOrdersResult = ModifyOrderItemResult[]`). - **Binance Futures REST**: `modifyBatchOrders` через `PUT /fapi/v1/batchOrders` (max 5 ордеров на batch). `BinanceFuturesHttpClient.modifyBatchOrders(orderList)` использует ручной signing через `buildBinanceSignedParams` (паттерн как у `modifyOrder`). Тип возврата HTTP-слоя — `Array>`: Binance возвращает смесь успешных ответов (`{orderId, ...}`) и ошибок (`{code, msg}`) per-order. На exchange-слое смесь разворачивается в `ModifyOrderItemResult[]`. - **Bybit Linear/Spot REST**: `modifyBatchOrders` через `POST /v5/order/amend-batch` (max 20 для linear/inverse/option, 10 для spot). `BybitHttpClient.amendBatchOrders(category, requestList)` + новые типы `BybitAmendBatchOrderRaw`, `BybitAmendBatchOrderExtInfo`, `BybitAmendBatchOrdersResponse`. На exchange-слое `BybitBaseClient.modifyBatchOrders` маппит `retExtInfo.list[i].code === 0 → isSuccess`. - **Bybit Trade Stream WebSocket**: три новых публичных метода в `BybitTradeStream` — `createBatchOrders(category, originalArgsList, requestPayloadList)`, `modifyBatchOrders(category, originalOrderIdList, requestPayloadList)`, `cancelBatchOrders(category, originalOrderIdList, requestPayloadList)`. Используют ops `order.create-batch`, `order.amend-batch`, `order.cancel-batch` со структурой args `[{ category, request: [...] }]`. Хранение pending запросов через `pendingBatchContextByReqId: Map` (discriminated union по kind), очищается при resolve/reject/orphan/kind-mismatch/timeout. - **WS-routing** в `BybitBaseClient`: для всех трёх batch-методов (`createBatchOrders`, `modifyBatchOrders`, `cancelBatchOrders`) — если `tradeStream !== null && tradeStream.isConnected()` маршрут через WebSocket, иначе REST. В demo mode (`tradeStream === null`) — всегда REST. Chunking применяется снаружи на уровне `BybitBaseClient`, не нарушает лимит per-batch. - Утилита `chunkList(list, chunkSize): T[][]` в `src/utils/chunkList.ts` + 8 тестов покрытия (split, empty, edge cases, validation). - Константы chunk-size: `BINANCE_FUTURES_CREATE_BATCH_CHUNK_SIZE=5`, `BINANCE_FUTURES_MODIFY_BATCH_CHUNK_SIZE=5`, `BINANCE_FUTURES_CANCEL_BATCH_CHUNK_SIZE=10`, `BYBIT_LINEAR_BATCH_CHUNK_SIZE=20`, `BYBIT_SPOT_BATCH_CHUNK_SIZE=10`. - В `BybitBaseClient` добавлено абстрактное `protected abstract readonly batchChunkSize: number`; в `BybitLinear` = 20, в `BybitSpot` = 10. - Маппинг `BINANCE_ORDER_SIDE_REVERSE: Record` (Buy → 'BUY', Sell → 'SELL') в `src/constants/mappings.ts` — используется в `buildBinanceModifyBatchOrderParams` для конвертации `ModifyBatchOrderArgs.side` в wire-формат Binance. - Типы `ModifyBatchOrderArgs` (с обязательным `side: OrderSideEnum` для Binance Futures PUT batch), `ModifyOrderItemResult` (симметричен `CancelOrderItemResult`), `ModifyBatchOrdersResult` в `src/types/exchange.ts`; реэкспортированы из `src/index.ts`. - В `BaseTradeStream` добавлены protected `sendRequest(request, requestId, kind)`, `ensureConnectedHook()`, `generateReqId()` — обобщённая инфраструктура для отправки batch-запросов и матчинга ответов в подклассах. - В `BybitTradeStream.types.ts` добавлены `BybitBatchOrderItem`, `BybitBatchOrderExtInfo`, `BybitBatchTradeMessage`, `BybitTradeAnyMessage` (union для базы), `BybitPendingBatchContext` (discriminated union). - Тесты: `test/utils/chunkList.test.ts` (новый, 8 тестов), `test/ws/BybitTradeStream.batch.test.ts` (новый, 15 тестов: send/receive batch, top-level retCode != 0, WS disconnected, timeout, "Unknown error" fallback, ignore non-batch ops, orphan context cleanup, kind mismatch protection). Расширены `BinanceFuturesHttpClient.test.ts`, `BybitHttpClient.test.ts`, `BinanceFutures.test.ts`, `BybitLinear.test.ts`, `BybitSpot.test.ts`. - Фикстуры: `BINANCE_RAW_MODIFY_BATCH_RESPONSE_MIXED`, `BYBIT_RAW_AMEND_BATCH_RESPONSE`, `BYBIT_WS_RAW_CREATE_BATCH_RESPONSE`, `BYBIT_WS_RAW_AMEND_BATCH_RESPONSE`, `BYBIT_WS_RAW_CANCEL_BATCH_RESPONSE`. ### Changed - `BinanceFutures.createBatchOrders` теперь чанкает входной массив по 5 ордеров (`BINANCE_FUTURES_CREATE_BATCH_CHUNK_SIZE`). Раньше отправлял всё одним запросом — при >5 ордеров Binance отвергал запрос. Кроме того, per-order ошибки в ответе Binance (`{code, msg}`) теперь логируются как `warning` и пропускаются — раньше падали на `normalizeBinanceOrder` со сломанным объектом. - `BybitBaseClient.createBatchOrders` и `cancelBatchOrders` теперь чанкают по `batchChunkSize` (linear=20, spot=10). Раньше отправляли всё одним запросом — при превышении Bybit отвергал. - `BybitBaseClient.cancelBatchOrders`: добавлен early-return `[]` для пустого input до debug-лога. Маппинг success/error через `retExtInfo.list[i].code === 0` сохранён байт-в-байт. - `BaseTradeStream.PendingRequest` теперь kind-tagged: enum `PendingRequestKind` (`SingleOrder | CreateBatch | ModifyBatch | CancelBatch`), поле `kind: PendingRequestKind` и `resolve: (data: unknown) => void`. Старый приватный `sendOrderRequest` стал wrapper'ом над новым `sendRequest(..., SingleOrder)`. Сигнатура публичного `createOrder` не изменилась. - `BinanceTradeStream.handleMessage` и `BybitTradeStream.handleSingleOrderResponse` добавили защиту от mismatched `pending.kind !== SingleOrder` — reject вместо silent corruption. - `BybitTradeStream.handleMessage` маршрутизирует по `op`: `order.create` → single, `order.create-batch/amend-batch/cancel-batch` → соответствующие batch handler'ы. Контекст `pendingBatchContextByReqId` удаляется во всех ветках (resolve/reject/orphan/kind-mismatch) — утечка предотвращена. - `BybitPublicStream.handleMessage`: ветка `topic.startsWith('orderbook.')` обрабатывается ДО проверки `Array.isArray(message.data)` — Bybit отдаёт `data` как объект (не массив) для orderbook-сообщений. - `BybitPublicStream.unsubscribeKlines` (интервал `1s`): topic `publicTrade.{symbol}` снимается только если нет ни одного custom `publicTrade`-subscriber'а (защита от удаления topic при работающем capture-сервисе). - `BybitPublicStream.resubscribeStream` / `BinanceFuturesPublicStream.resubscribeStream`: вместо безусловного `recreateConnection` теперь используется conditional light-resubscribe — `unsubscribe` + `subscribe` для подключённых сокетов, `recreateConnection` как fallback при ошибке отправки или `WebSocketStatus !== CONNECTED`. Снижает overhead на принудительной переподписке к одному topic, сохраняет recreate-семантику для отказных соединений. - `BaseHttpClient.get`: в финальном `throw` (catch-блок retry-цикла) ошибка обогащается через `this.enrichError(axiosError)` — единый формат `Error("[code] msg")` или `Error("HTTP {status}: {message}")` вместо raw `AxiosError`. ### Changed (BREAKING) - `ExchangeClient.cancelBatchOrders(symbol, orderIdList)` — сигнатура возврата изменена с `Promise` на `Promise` (`= CancelOrderItemResult[]`). Каждый элемент содержит `{ orderId, isSuccess, errorCode: number | null, errorText: string | null }` для per-order трассировки результата. Consumer'ам, которые игнорировали возврат, миграция не требуется; те, кто ожидал `void` в type-checking — пометьте возврат явно или используйте `await` без присваивания. ### Notes - **Binance Spot batch API не существует** на стороне биржи — `modifyBatchOrders`, `createBatchOrders`, `cancelBatchOrders` на Binance Spot бросают `Not supported for spot market` (наследуется default throw из `BaseExchangeClient`). - **Binance Futures WebSocket API не имеет batch-методов** — только одиночные `order.place`, `order.modify`, `order.cancel`. Verified: URLs `Place-Multiple-Orders` / `Modify-Multiple-Orders` / `Cancel-Multiple-Orders` в Binance WebSocket API docs возвращают 404. Batch на Binance идёт только через REST. - **Per-order `rateLimit` snapshot заполняется** только для одиночных order-операций через WebSocket Trade Stream (`createOrder` → `order.create` / `order.place`). REST endpoints и batch-методы не возвращают эту структуру в текущей реализации. ## [0.13.1] - 2026-05-11 ### Changed - `BybitPublicStream.resubscribeStream()`: вместо повторной отправки `{ op: 'subscribe' }` на уже открытый сокет вызывается пересоздание соединения, которое держит topic (`recreateConnection` с причиной `manual resubscribe of …`). Снижает риск «тихих» сбоев при несовпадении состояния сокета и повторяет паттерн жёсткой переподписки, как у Binance Futures public. ### Added - Внутренний `recreateConnection(connectionIndex, reason)` у `BybitPublicStream`: закрытие текущего `ReliableWebSocket`, замена на новый через `buildWebSocket`, дедуп повторных recreate в окне `2000ms` (`RECREATE_DEDUP_WINDOW_MS`), перед recreate для топиков `publicTrade.{symbol}` вызывается `tradeAggregator.clearSymbol(symbol)`. - Поле `lastRecreateTimestamp` в `BybitConnection` (`BybitPublicStream.types.ts`) для дедупа recreate. ## [0.13.0] - 2026-04-30 Сводный релиз, объединяющий все локальные эксперименты после 0.12.1: WebSocket-отчёт, Bybit Linear position-mode detection, и spot/futures order-params матрицу для нового `PositionManager` API в trade-engine. ### Added - `OrderTypeEnum` расширен: `StopLimit`, `TakeProfitLimit` — для атомарных STOP_LOSS_LIMIT / TAKE_PROFIT_LIMIT на Binance Spot и Bybit Spot. - `TriggerByEnum` (`MarkPrice` | `LastPrice` | `IndexPrice`) — для conditional/stop ордеров на Bybit Linear. - `OrderFilterEnum` (`Order` | `tpslOrder` | `StopOrder`) — спецификация Bybit Spot conditional/TPSL ордеров. - `MarketUnitEnum` (`baseCoin` | `quoteCoin`) — выбор единицы количества для Market ордеров на Bybit Spot. - `CreateOrderWebSocketArgs` дополнен полями: `triggerBy`, `closeOnTrigger`, `orderFilter`, `marketUnit`, `trailingDelta`, `quoteOrderQty`. Все опциональные, обратно-совместимы. - `BINANCE_FUTURES_ORDER_TYPE_REVERSE` и `BINANCE_SPOT_ORDER_TYPE_REVERSE` — раздельные mapping таблицы. На spot `OrderTypeEnum.StopMarket → 'STOP_LOSS'`, `StopLimit → 'STOP_LOSS_LIMIT'`, `TakeProfitMarket → 'TAKE_PROFIT'`, `TakeProfitLimit → 'TAKE_PROFIT_LIMIT'`. На futures сохранён прежний `STOP_MARKET`/`TAKE_PROFIT_MARKET` контракт. - `BINANCE_ORDER_TYPE` mapping расширен значениями `STOP_LOSS`, `STOP_LOSS_LIMIT`, `TAKE_PROFIT_LIMIT` для парсинга входящих spot-ордеров. - `BybitLinear.fetchPositionMode()` — реализация через `GET /v5/position/list?category=linear&settleCoin=USDT`. Логика: Hedge если есть позиция с `positionIdx ∈ {1, 2}`; OneWay если все `positionIdx === 0`; `undefined` если позиций нет (Bybit V5 не даёт API для чтения mode без позиций). - `SymbolLimitFilterArgs`: опциональные `settleCoin?: string`, `baseCoin?: string` для Bybit V5 position-list endpoint. - `BybitHttpClient.buildCategoryParams()` поддерживает `settleCoin`/`baseCoin` в query-параметрах. - `formatWebSocketConnectionsReport` — публичная утилита для plain-text отчёта о WebSocket-соединениях (`exchangeClient.getWebSocketConnectionInfoList()` → human-readable summary). - `ExchangeClient.awaitWebSocketConnectionsReady()` — ожидание готовности публичных WS после подписок (Binance Futures: после SUBSCRIBE-батчей; у стримов без `awaitConnectionsReady` — немедленный resolve). - `PublicStreamLike.awaitConnectionsReady?()` — опциональный хук на уровне публичного стрима. - `WebSocketConnectionInfo.messageCount?` / `lastMessageTimestamp?` — per-connection диагностика (заполняет Binance Futures public). ### Changed (BREAKING) - `ExchangeClient.fetchPositionMode()` теперь возвращает `Promise`. Раньше — `Promise`. Реализации без надёжного API-способа определить режим без открытых позиций (Bybit Linear) возвращают `undefined`. ### Changed - `BinanceFuturesPublicStream`: endpoint `wss://fstream.binance.com/market/ws` (и demo-аналог); подписка через JSON `SUBSCRIBE` батчами после `onOpen`; группировка соединений — отдельное на `!miniTicker@arr` / `!markPrice@arr@1s` и по одному на каждый kline interval; stale-watcher с пересозданием «зависших» соединений; настраиваемые `pauseBetweenConnectionsMs`, `staleThresholdMs`, `staleCheckIntervalMs`, `subscribeBatchSize`, `pauseBetweenSubscribeBatchesMs`. - `normalizeBybitTradeSymbols`: инструменты со `status === 'PreLaunch'` получают `isActive: true` (наряду с `Trading`). - `BinanceBaseClient.buildBinanceOrderParams()` различает spot и futures по `marketLabel`. На spot НЕ выставляются `closePosition`, `workingType`, `positionSide`, `reduceOnly`. Поддерживаются `quoteOrderQty` (USDT-сумма для Market Buy на spot) и `trailingDelta` (STOP_LOSS/TAKE_PROFIT trailing). `timeInForce` ставится для всех Limit-like типов (`Limit`, `StopLimit`, `TakeProfitLimit`). - `BybitBaseClient.buildBybitOrderParams()` различает spot и linear. На spot НЕ выставляются `triggerDirection`, `triggerBy`, `reduceOnly`, `closeOnTrigger` (валидны только linear/inverse). Добавлены ветки `orderFilter` и `marketUnit` (только spot). Поддерживается `quoteOrderQty` для Market Buy на spot. - `BybitSpot.createOrderWebSocket()` форсирует `marketUnit='baseCoin'` только если поле НЕ задано явно через `args.marketUnit` И не используется `quoteOrderQty`. Default остаётся `baseCoin` для backward-compat. ### Notes - `BINANCE_ORDER_TYPE_REVERSE` оставлен как алиас `BINANCE_FUTURES_ORDER_TYPE_REVERSE` для обратной совместимости с внешними импортами. ## [0.13.0-prev] - 2026-04-27 (history note) Раньше существовали отдельные локальные релизы 0.13.0 (2026-04-27, formatWebSocketConnectionsReport), 0.14.0 (2026-04-29, BybitLinear.fetchPositionMode + BREAKING) и uncommitted 0.15.0. Они объединены в выпускной 0.13.0 от 2026-04-30 выше. Историческая запись (полные технические подробности WebSocket-отчёта) сохранена ниже для справки. ### Added #### `formatWebSocketConnectionsReport` — заготовка отчёта о WebSocket-соединениях Новая публичная утилита (`src/utils/formatWebSocketConnectionsReport.ts`), доступная через `import { formatWebSocketConnectionsReport } from '@solncebro/exchange-engine'`. Принимает `WebSocketConnectionInfo[]` (полученный через `exchangeClient.getWebSocketConnectionInfoList()`), возвращает многострочную plain-text строку с компактным форматом: ``` 🌐 WebSocket Connections now: 2026-04-27 14:02:30 total: 54 | source: Binance Futures Public WebSocket ✅ symbols-001 | 40 streams | 2481 msgs | 0s ago 1000BONKUSDT, 1000FLOKIUSDT, 1000LUNCUSDT, ... ``` Опции: - `formatTimestamp?: (epochMs: number) => string` — кастомный форматтер времени (default: ISO-подобный) - `nowTimestamp?: number` — переопределение текущего времени для тестов (default: `Date.now()`) - `headerLine?: string` — кастомная шапка (default: `🌐 WebSocket Connections`) Без зависимостей от Telegram/dayjs — приложение само эскейпит markdown и форматирует timestamp в нужном для своей домены формате. #### Публичный API ExchangeClient - `ExchangeClient.awaitWebSocketConnectionsReady(): Promise` — позволяет дождаться полной готовности всех WS-соединений (handshake + SUBSCRIBE батчи) после вызовов `subscribeKlines`/`subscribeAllTickers`/`subscribeMarkPrices`. Используется для sequential init: подписался → дождался → REST-загрузка. - `WebSocketConnectionInfo.messageCount?: number` и `lastMessageTimestamp?: number` — per-connection диагностика (только Binance Futures сейчас). - `PublicStreamLike.awaitConnectionsReady?(): Promise` — опциональный метод в интерфейсе stream (Bybit/Spot не реализуют — возвращается `Promise.resolve()` сразу). ### Changed #### `BinanceFuturesPublicStream` — детали реализации в релизной ветке Актуальное поведение (группировка по типу стримов и kline interval, endpoint `/market/ws`, SUBSCRIBE-батчи, stale-recreate, `readyPromise`) описано в выпускной секции **[0.13.0] → Changed** выше и в `.claude/rules/websocket-layer.md`. Ранний черновик этой записи описывал другую схему (`symbolsPerConnection`); он не соответствует финальному коду и оставлен только как напоминание о эволюции задачи. ### Added (BinanceFuturesPublicStreamArgs) - `staleThresholdMs?`, `staleCheckIntervalMs?`, `subscribeBatchSize?`, `pauseBetweenSubscribeBatchesMs?`, `pauseBetweenConnectionsMs?` — см. фактические default-значения в `BinanceFuturesPublicStream.ts` ### Notes - **Bybit** и **Binance Spot** streams не затронуты этой работой — у них своя архитектура (Bybit использует `MAX_TOPICS_PER_CONNECTION = 200` + heartbeat, Spot single connection). - Backward-compatibility: новые методы добавлены к интерфейсам как обязательные (`awaitWebSocketConnectionsReady`) или опциональные (`awaitConnectionsReady?`). Поля `messageCount?`/`lastMessageTimestamp?` опциональны, поэтому non-Futures клиенты возвращают `undefined`. ## [0.12.1] - 2026-04-24 ### Fixed - `BinanceBaseClient.createOrderWebSocket()`: `reduceOnly` больше не отправляется в hedge mode, если задан `positionSide`; параметр остаётся доступен для one-way mode. - `BybitBaseClient.createOrderWebSocket()`: для linear-рынка добавлено корректное сопоставление `positionSide` → `positionIdx` (`Long` → `1`, `Short` → `2`), что делает хедж-ордера валидными для Bybit API. ### Tests - `BinanceFutures.test.ts`: добавлена проверка, что `reduceOnly` отбрасывается при наличии `positionSide`. - `BybitLinear.test.ts`: добавлены сценарии для `positionIdx` в hedge/one-way режимах и совместимости с `reduceOnly`. - `BybitSpot.test.ts`: добавлена проверка, что `positionIdx` не добавляется для spot-ордеров. ## [0.12.0] - 2026-04-19 ### Added **Новые унифицированные типы** (`src/types/common.ts`): - `LeverageFilter` — `{ minLeverage, maxLeverage, leverageStep }` (все string) - `PriceLimitRisk` — дискриминированное объединение с тегом `source`: - `'binancePercentPrice'` — `multiplierUp`, `multiplierDown`, `multiplierDecimal` - `'binancePercentPriceBySide'` — `bidMultiplierUp/Down`, `askMultiplierUp/Down`, `avgPriceMins` - `'bybitRiskParameters'` — `priceLimitRatioX`, `priceLimitRatioY` - `TradingFunding` — `{ fundingIntervalMinutes?, upperFundingRate?, lowerFundingRate? }` **Расширение существующих типов** (все поля опциональные, обратно-совместимо): - `Ticker`: `markPrice?`, `indexPrice?`, `fundingRate?`, `nextFundingTime?` — Bybit tickers теперь отдают данные mark price и funding в одном запросе - `TradeSymbolFilter`: `minPrice?`, `maxPrice?`, `maxNotional?`, `marketMinQty?`, `marketMaxQty?`, `marketStepSize?`, `postOnlyMaxQty?` - `TradeSymbol`: `leverageFilter?`, `priceLimitRisk?`, `pricePrecision?`, `quantityPrecision?`, `funding?`, `launchTimestamp?`, `triggerProtect?`, `liquidationFee?`, `orderTypeList?`, `timeInForceList?`, `info?` - `Balance`: `walletBalance?`, `availableToWithdraw?`, `totalOrderInitialMargin?`, `totalPositionInitialMargin?` - `AccountBalances`: `accountType?`, `totalMarginBalance?`, `totalInitialMargin?` - `OrderBook`: `updateId?`, `eventTimestamp?` (Binance: `lastUpdateId` и `E`) - `PublicTrade`: `isBlockTrade?`, `side?: OrderSideEnum` - `Income`: `quantity?` — проставляется при наличии поля в ответе биржи (Bybit transaction log) **Публичные экспорты** (`src/index.ts`): - `PriceLimitRisk`, `LeverageFilter`, `TradingFunding` - `MarkPriceUpdate`, `MarkPriceHandler` **WebSocket mark/index price** (`ExchangeClient`, публичные стримы): - `subscribeMarkPrices(handler)` / `unsubscribeMarkPrices(handler)` — поток обновлений mark/index; хендлер получает `MarkPriceUpdate[]` (`symbol`, `markPrice`, `indexPrice`, `timestamp`) - **Binance Futures**: подписка на combined-stream `!markPrice@arr@1s`, разбор через `normalizeBinanceMarkPriceWebSocketList()` (`BinanceMarkPriceWebSocketRaw`) - **Bybit** (linear и spot): те же сообщения `tickers.*`, что и у all-tickers; при наличии подписчиков mark price из сырого тикера собирается в `MarkPriceUpdate` (записи без валидного `markPrice` отбрасываются) - **Binance Spot**: вызов `subscribeMarkPrices` не открывает стрим — в лог пишется предупреждение, хендлер не вызывается (`unsubscribeMarkPrices` — no-op) **Контракт стрима** (`src/types/stream.ts`): - Обязательные `subscribeMarkPrices` / `unsubscribeMarkPrices` на `PublicStreamLike` - Опциональный `unsubscribeAllTickers` — реализован у Binance/Bybit публичных стримов для снятия хендлера all-tickers ### Changed **Нормализаторы**: - `normalizeBinanceMarkPriceWebSocketList()` — raw события mark price c фьючерсного WebSocket → `MarkPriceUpdate[]` - `normalizeBinanceTradeSymbols()` — извлекает `PERCENT_PRICE` / `PERCENT_PRICE_BY_SIDE` в `priceLimitRisk`, `MARKET_LOT_SIZE` в `filter.marketMinQty/marketMaxQty/marketStepSize`, `PRICE_FILTER.minPrice/maxPrice` в `filter`, а также `pricePrecision`, `quantityPrecision`, `onboardDate → launchTimestamp`, `triggerProtect`, `liquidationFee`, `orderTypes → orderTypeList`, `timeInForce → timeInForceList`; сохраняет сырой payload в `info` - `normalizeBybitTradeSymbols()` — строит `leverageFilter`, `priceLimitRisk` (из `riskParameters`), `funding` (из `fundingInterval`/`upperFundingRate`/`lowerFundingRate`), `launchTimestamp` (из `launchTime`); извлекает `priceFilter.minPrice/maxPrice`, `lotSizeFilter.maxMktOrderQty → marketMaxQty`, `lotSizeFilter.postOnlyMaxOrderQty → postOnlyMaxQty`; сохраняет сырой payload в `info` - `normalizeBybitTickers()` — пробрасывает `markPrice`, `indexPrice`, `fundingRate`, `nextFundingTime`, если присутствуют в raw-ответе - `normalizeBybitBalances()` — заполняет `walletBalance`, `availableToWithdraw`, `totalOrderInitialMargin`, `totalPositionInitialMargin` на каждый asset; возвращает `accountType`, `totalMarginBalance`, `totalInitialMargin` на уровне `AccountBalances` - `normalizeBinanceOrderBook()` — пробрасывает `updateId` (из `lastUpdateId`) и `eventTimestamp` (из `E`) - `normalizeBybitOrderBook()` — пробрасывает `updateId` (из `u`) - `normalizeBybitPublicTradeList()` — пробрасывает `isBlockTrade` и маппит `side` через `BYBIT_ORDER_SIDE` - `normalizeBybitIncomeList()` — пробрасывает `qty → quantity` **Типизация фильтров Binance** (внутренняя): - `BinanceFilterRaw` разбит на дискриминированное объединение `BinanceKnownFilterRaw` по `filterType` (`PRICE_FILTER`, `LOT_SIZE`, `MARKET_LOT_SIZE`, `MIN_NOTIONAL`, `NOTIONAL`, `PERCENT_PRICE`, `PERCENT_PRICE_BY_SIDE`, `MAX_NUM_ORDERS`, `POSITION_RISK_CONTROL`, `ICEBERG_PARTS`, `TRAILING_DELTA`, `MAX_NUM_ORDER_LISTS`, `MAX_NUM_ALGO_ORDERS`, `MAX_NUM_ORDER_AMENDS`) + fallback `BinanceUnknownFilterRaw` - `extractFilter()` теперь возвращает точный тип фильтра по `filterType` ### Internal - `normalizeBinanceTradeSymbols()`: извлечение `minNotional` из `MIN_NOTIONAL` и `NOTIONAL` разнесено на отдельные переменные — поведение не меняется (фильтры не сосуществуют в одном символе), упрощает дальнейшее чтение `NOTIONAL.maxNotional` для `filter.maxNotional` ## [0.11.0] - 2026-04-17 ### Added - `connectUserDataStream(handler: UserDataStreamHandlerArgs): Promise` — устанавливает WebSocket-соединение для приёма приватных событий (ордера, позиции) - `disconnectUserDataStream(): void` — закрывает соединение и освобождает ресурсы - `isUserDataStreamConnected(): boolean` — проверяет наличие активного соединения - Новые типы в публичном API: `OrderUpdateEvent`, `PositionUpdateEvent`, `OrderUpdateHandler`, `PositionUpdateHandler`, `UserDataStreamHandlerArgs` **Binance-реализация** (`BinanceFutures`, `BinanceSpot`): - Создаёт listenKey через REST, каждые 30 минут продлевает (`keepAliveListenKey`), при отключении удаляет (`deleteListenKey`) - Обрабатывает события `ORDER_TRADE_UPDATE` → `onOrderUpdate` и `ACCOUNT_UPDATE` → `onPositionUpdate` - Нормализация статусов ордеров через `BINANCE_ORDER_STATUS_MAP` (NEW/PARTIALLY_FILLED → open, FILLED → closed, CANCELED → canceled, EXPIRED/EXPIRED_IN_MATCH → expired) **Bybit-реализация** (`BybitLinear`, `BybitSpot`): - Использует `BybitPrivateStream` с подпиской на топики `['order', 'position']` - Обрабатывает события `order` → `onOrderUpdate` и `position` → `onPositionUpdate` - Нормализация статусов через `BYBIT_ORDER_STATUS_MAP` (New/PartiallyFilled/Untriggered → open, Filled → closed, Cancelled/Rejected → canceled, Deactivated/Expired → expired) - `getWebSocketConnectionInfoList()` теперь включает private stream info - `close()` теперь вызывает `disconnectUserDataStream()` ### Changed - `BybitPrivateStream`: добавлена поддержка `topicList?: string[]` в `BybitPrivateStreamArgs` — топики автоматически подписываются после успешной аутентификации ## [0.10.0] - 2026-04-14 ### Breaking Changes - `BybitLinear.setMarginMode()` is now a no-op — Bybit Unified Account manages margin mode at account level, not per-symbol. Method logs a message and returns without calling the API ### Added - `BybitPublicStream`: multi-connection support with topic chunking (max 200 topics per connection) — automatically creates additional connections when topic count exceeds the limit - `BybitPublicStream`: batched subscribe messages (max 10 topics per SUBSCRIBE request) to comply with Bybit WebSocket API limits - `BybitPublicStream`: deferred connection via `queueMicrotask()` for batching initial subscriptions (matching Binance Futures pattern) - `BybitPublicStream.resubscribeStream(symbol, interval)` — explicit topic resubscription on a specific connection - `BybitConnection` type — internal connection state tracking (webSocket, label, topicList, dynamicTopicList, url) ### Fixed - `BybitBaseClient.getOrder()`: now checks realtime (open orders) first, then falls back to order history — previously only queried history, missing recently placed orders - `BybitBaseClient.submitOrder()`: checks `tradeStream.isConnected()` before sending via WebSocket — previously only checked `tradeStream !== null`, causing failures when stream existed but was not connected - `BybitLinear.setLeverage()`: handles Bybit error code `110043` (leverage not modified) as no-op instead of throwing - `normalizeBybitKlines()`: reverses API response from descending to ascending chronological order — Bybit returns klines newest-first, consumers expect oldest-first ### Internal - `BybitPublicStream`: refactored from single `webSocket` to `connectionList: BybitConnection[]` architecture - `BybitPublicStream`: subscription success messages no longer logged at debug level (reduced log noise) - `BybitPublicStream.getConnectionInfoList()` now returns per-connection info with numbered labels when multiple connections exist - `BybitPublicStream.buildSubscriptionList()` extracts human-readable subscription names from topic strings ## [0.9.1] - 2026-04-13 ### Fixed - `BaseHttpClient`: non-GET HTTP errors now throw readable `Error` messages instead of raw `AxiosError` — Binance API errors formatted as `[code] msg`, other HTTP errors as `HTTP {status}: {message}` ### Changed - `BinanceFuturesPublicStream`: dynamic streams are now subscribed on initial WebSocket open via `onOpen` callback (previously only on reconnect via `onReconnectSuccess`) ## [0.9.0] - 2026-04-08 ### Breaking Changes - `TradeSymbol.contractType: string` — new field added to identify contract type (PERPETUAL, TRADIFI_PERPETUAL, etc.). While backward compatible for existing code, consumers should update type expectations - `BaseExchangeClient.createNotifyHandler()` behavior simplified: CRITICAL message handling no longer automatically calls `process.exit(1)`. Consumers relying on automatic termination must implement their own shutdown logic ### Added - Support for TRADIFI_PERPETUAL contracts in Binance Futures: - `TradeSymbol.contractType` field now included in normalized data (exported in public API) - `BinanceFuturesPublicStream` automatically selects correct kline stream format based on contract type - TRADIFI contracts use `{symbol}@kline_{interval}` format instead of perpetual `{symbol}_perpetual@continuousKline_{interval}` - `setTradeSymbols()` method on `BinanceFuturesPublicStream` for dynamic stream name resolution ### Changed - `BinanceFuturesPublicStream` now requires trade symbols context to properly differentiate between PERPETUAL and TRADIFI_PERPETUAL contracts - `BaseExchangeClient.createNotifyHandler()` now delegates entirely to user's `onNotify` callback without automatic process termination ### Internal - Both `binanceNormalizer` and `bybitNormalizer` extract and preserve raw `contractType` field from exchange API ## [0.8.0] - 2026-03-26 ### Added - WebSocket resubscription methods: `resubscribeStream(symbol, interval)` on `BinanceFuturesPublicStream` and `BinanceSpotPublicStream` - `resubscribeKlines(symbol, interval)` on `ExchangeClient` for explicit stream reconnection - Dynamic stream tracking in `BinanceFuturesPublicStream` to optimize reconnection behavior ## [0.6.2] - 2026-03-26 ### Fixed - Binance futures no-op responses (`-4046` margin type unchanged, `-4059` position side unchanged) are treated as successful no-ops: - `setMarginMode()` / `setPositionMode()` do not throw on these codes - `BaseHttpClient` logs `info` using the exchange `msg` text without additional prefixes ## [0.6.1] - 2026-03-23 ### Changed - Standardized WebSocket connection labels across all exchange clients: - `Binance Futures Public WebSocket`, `Binance Spot Public WebSocket` - `Binance Futures Order WebSocket`, `Binance Spot Order WebSocket` - `Bybit Linear Public WebSocket`, `Bybit Spot Public WebSocket` - `Bybit Linear Order WebSocket`, `Bybit Spot Order WebSocket` ### Fixed - `authenticateBybitWebSocket()` now includes stream label context in error logs and thrown errors to simplify troubleshooting: - log format: `[