# Build 058 — Runtime исследование Dzengi WebSocket OHLC Market Data --- # Контроль документа | Свойство | Значение | |----------|-----------| | Build | 058 | | Название | Runtime исследование Dzengi WebSocket OHLC Market Data | | Статус | Completed | | Проект | Dzentra | | Подсистема | Market Data Acquisition | | Источник | Dzengi Exchange | | Дата | 2026-07-16 | --- # Цель Build Провести полное runtime-исследование контракта WebSocket ``` OHLCMarketData.subscribe ``` и определить возможность его использования в качестве канонического источника Candles Feed. Необходимо было подтвердить: - соответствие Swagger реальному API; - формат подписки; - формат ACK; - формат событий; - поддерживаемые параметры; - поддержку нескольких символов; - поддержку нескольких интервалов; - наличие или отсутствие Volume; - соответствие REST API; - возможность использования в Build 059. --- # Исходные материалы Были исследованы: REST ``` GET /api/v1/klines ``` WebSocket ``` OHLCMarketData.subscribe ``` Swagger WebSocket ``` wss://.../connect ``` Также были выполнены реальные подключения к: ``` demo-api-adapter.dzengi.com ``` и ``` api-adapter.dzengi.com ``` --- # Вывод по Demo Во время исследования обнаружено: Demo WebSocket корректно принимает подписку: ``` OHLCMarketData.subscribe ``` но поток данных практически отсутствует. Поэтому дальнейшее исследование проводилось исключительно на Production API. --- # Production WebSocket Используемый URL ``` wss://api-adapter.dzengi.com/connect ``` Подписка выполняется сообщением ```json { "correlationId": "", "destination": "OHLCMarketData.subscribe", "payload": { "intervals": [ "1m" ], "symbols": [ "BTC/USD_LEVERAGE" ], "type": "classic" } } ``` После отправки сервер возвращает ACK. --- # ACK Фактический ответ сервера ```json { "correlationId": "...", "destination": "OHLCMarketData.subscribe", "payload": { "subscriptions": { "BTC/USD_LEVERAGE:1m": "PROCESSED" } }, "status": "OK" } ``` Подтверждено: - destination совпадает со Swagger; - status = OK; - подписки перечислены внутри payload.subscriptions; - ключ имеет вид ``` symbol:interval ``` например ``` BTC/USD_LEVERAGE:1m ``` --- # Поддержка нескольких интервалов Проверена подписка ```json { "payload": { "symbols":[ "BTC/USD_LEVERAGE" ], "intervals":[ "1m", "5m" ], "type":"classic" } } ``` Полученный ACK ```json { "subscriptions": { "BTC/USD_LEVERAGE:1m":"PROCESSED", "BTC/USD_LEVERAGE:5m":"PROCESSED" } } ``` Таким образом подтверждено, что одна подписка может одновременно обслуживать несколько таймфреймов. Каждая комбинация ``` symbol + interval ``` имеет собственный статус. --- # Формат события После закрытия свечи сервер публикует ```json { "destination":"ohlc.event", "payload":{ "symbol":"BTC/USD_LEVERAGE", "interval":"1m", "type":"classic", "t":1784224740000, "o":63992.0, "h":64032.55, "l":63984.0, "c":64032.55 }, "status":"OK" } ``` Полученные поля | Поле | Значение | |------|----------| | symbol | инструмент | | interval | таймфрейм | | type | тип свечи | | t | open time | | o | Open | | h | High | | l | Low | | c | Close | Volume отсутствует. Это подтверждено всеми полученными событиями. --- # Исследование типа свечей Swagger указывает, что поле ``` type ``` используется для выбора разновидности свечей. Во время runtime-исследования были проверены все возможные значения. --- # Попытка использовать bid Подписка ```json { "payload":{ "symbols":[ "BTC/USD_LEVERAGE" ], "intervals":[ "1m" ], "type":"bid" } } ``` Ответ сервера ```json { "subscriptions":{ "bid":"ERROR: Invalid type. Valid: classic, heikin-ashi" } } ``` Таким образом подтверждено, что WebSocket не использует параметр ``` priceType ``` аналогичный REST. --- # Поддерживаемые значения type Во время исследования подтверждены два допустимых значения ``` classic ``` и ``` heikin-ashi ``` Других поддерживаемых вариантов обнаружено не было. --- # Исследование classic Подписка ```json { "payload":{ "symbols":[ "BTC/USD_LEVERAGE" ], "intervals":[ "1m" ], "type":"classic" } } ``` События поступают регулярно после закрытия каждой минутной свечи. Пример ```json { "destination":"ohlc.event", "payload":{ "symbol":"BTC/USD_LEVERAGE", "interval":"1m", "type":"classic", "t":1784224860000, "o":64047.45, "h":64099.10, "l":64042.25, "c":64098.00 } } ``` --- # Сравнение с REST Для той же свечи был выполнен запрос ``` GET /api/v1/klines ``` ``` priceType=bid ``` Получен ответ ```json [ [ 1784224860000, "64047.45", "64099.10", "64042.25", "64098.00", 165 ] ] ``` Полное сравнение | Поле | REST | WebSocket | |------|------|-----------| | Open Time | ✔ | ✔ | | Open | ✔ | ✔ | | High | ✔ | ✔ | | Low | ✔ | ✔ | | Close | ✔ | ✔ | | Volume | ✔ | ✖ | OHLC совпадают полностью. --- # Вывод Runtime подтвердил, что ``` classic ``` представляет собой классические рыночные свечи, идентичные ``` REST /klines priceType=bid ``` за исключением отсутствующего объёма. --- # Исследование Heikin-Ashi Подписка ```json { "payload":{ "symbols":[ "BTC/USD_LEVERAGE" ], "intervals":[ "1m" ], "type":"heikin-ashi" } } ``` Получен ACK ```json { "subscriptions":{ "BTC/USD_LEVERAGE:1m":"PROCESSED" } } ``` После этого сервер начал публиковать события ```json { "destination":"ohlc.event", "payload":{ "symbol":"BTC/USD_LEVERAGE", "interval":"1m", "type":"heikin-ashi", "t":1784225280000, "o":64068.18, "h":64128.80, "l":64068.18, "c":64100.85 } } ``` Получены несколько последовательных свечей. --- # Вывод по Heikin-Ashi Контракт полностью совпадает с форматом ``` classic ``` Изменяется только способ расчёта OHLC. Следовательно ``` classic ``` и ``` heikin-ashi ``` представляют две различные серии свечей, имеющие одинаковый транспортный контракт. --- # Исследование времени публикации Во время наблюдения было подтверждено, что событие публикуется только после завершения интервала. Например ``` t = 1784224860000 ``` соответствует времени открытия свечи. Само событие было получено примерно через одну минуту, после её закрытия. Таким образом ``` t ``` является временем открытия свечи, а не временем публикации события. --- # Исследование нескольких интервалов Одновременно были подписаны ``` 1m ``` и ``` 5m ``` После закрытия пятиминутного интервала сервер практически одновременно прислал ``` 1m ``` и ``` 5m ``` события. Полученное пятиминутное событие ```json { "payload":{ "interval":"5m", "type":"classic", "t":1784225400000, "o":64130.80, "h":64184.70, "l":64118.20, "c":64151.00 } } ``` Тем самым подтверждено, что сервер самостоятельно агрегирует свечи для всех запрошенных интервалов. Клиенту не требуется самостоятельно строить 5m, 15m или другие свечи из минутных данных. --- # Сравнение REST и WebSocket Во время исследования одновременно выполнялись: - периодические запросы REST; - постоянная подписка WebSocket. Это позволило определить различия поведения. --- # Поведение REST Запрос ``` GET /api/v1/klines ``` возвращает последние свечи. Последняя свеча является текущей, поэтому её значения изменяются по мере поступления новых сделок. Например 21:01:41 ```text Open 64047.45 High 64074.90 Low 64042.25 Close 64074.90 Volume 111 ``` Через некоторое время 21:01:56 ```text High 64095.45 Close 64095.45 Volume 153 ``` Ещё позже 21:02:11 ```text High 64099.10 Close 64098.00 Volume 165 ``` Таким образом REST показывает формирующуюся свечу. --- # Поведение WebSocket Для этой же свечи был получен ```json { "t":1784224860000, "o":64047.45, "h":64099.10, "l":64042.25, "c":64098.00 } ``` WebSocket опубликовал событие только после окончания интервала. Таким образом опубликована уже окончательная свеча. --- # Главное различие REST ``` может вернуть ещё незавершённую свечу ``` WebSocket ``` публикует только закрытую свечу ``` Именно это делает WebSocket очень полезным источником событий. --- # Исследование Volume Во всех событиях WebSocket отсутствует ``` volume ``` Например ```json { "payload":{ "o":64096.20, "h":64110.60, "l":64045.45, "c":64052.50 } } ``` Поле ``` volume ``` не передаётся. Однако REST возвращает ```text 389 ``` для этой же свечи. Следовательно WebSocket публикует ``` OHLC ``` но не ``` OHLCV ``` --- # Ограничения использования Текущая каноническая модель проекта ``` Candle ``` содержит ```python volume: Decimal ``` Следовательно WebSocket-событие не может напрямую быть преобразовано в канонический Candle. Заполнение ``` volume = 0 ``` является архитектурно неверным, поскольку означает "объём равен нулю", хотя на самом деле объём неизвестен. Поэтому WebSocket не должен непосредственно создавать канонический Candle. --- # Архитектурное решение На основании проведённого исследования для Build 059 принято решение использовать гибридную схему. ``` OHLCMarketData.subscribe │ ▼ получено событие закрытия свечи │ ▼ REST GET /klines (symbol + interval + open_time) │ ▼ получена окончательная OHLCV свеча │ ▼ Validation │ ▼ Parser │ ▼ Mapper │ ▼ Canonical Candle ``` Таким образом WebSocket становится ``` источником события о закрытии свечи ``` а REST остаётся ``` авторитетным источником полной OHLCV модели. ``` --- # Новая транспортная модель Для Build 059 рекомендуется создать отдельную transport-модель ``` DzengiWebSocketOhlcEvent ``` например ```python @dataclass(frozen=True, slots=True) class DzengiWebSocketOhlcEvent: symbol: str interval: str candle_type: str open_time: int open_price: DzengiRawNumeric high_price: DzengiRawNumeric low_price: DzengiRawNumeric close_price: DzengiRawNumeric ``` Она не должна заменять каноническую модель Candle. Её назначение — представление транспортного WebSocket-события. --- # Итоги исследования Подтверждено ✓ WebSocket полностью работоспособен. ✓ Поддерживает несколько инструментов. ✓ Поддерживает несколько интервалов. ✓ Поддерживает ``` classic ``` и ``` heikin-ashi ``` ✓ Публикует только завершённые свечи. ✓ Формат classic совпадает с REST bid OHLC. ✓ Передаёт только OHLC. ✓ Не содержит Volume. ✓ Может использоваться как триггер появления новой закрытой свечи. --- # Вывод Build 058 Исследование показало, что ``` OHLCMarketData.subscribe ``` не является заменой REST Candles Feed. Однако данный поток является идеальным механизмом для своевременного обнаружения момента закрытия новой свечи. Именно такая схема будет реализована в следующем этапе проекта. --- # Следующий Build ``` Build 059 Интеграция WebSocket OHLC в Candles Feed ``` Основные задачи Build 059 - создать транспортную модель DzengiWebSocketOhlcEvent; - реализовать schema validation; - реализовать parser; - реализовать value validation; - реализовать mapper; - добавить WebSocket Adapter; - интегрировать новый источник в Candles Feed; - использовать WebSocket как источник события закрытия свечи; - использовать REST для получения окончательной OHLCV свечи; - сохранить существующую каноническую модель Candle без изменений. --- # Работа с диагностическим скриптом В рамках Build 058 был разработан отдельный диагностический инструмент ``` scripts/check_ohlc_websocket.py ``` Назначение скрипта: - проверка доступности WebSocket; - проверка подписки OHLCMarketData.subscribe; - получение ACK; - получение реальных OHLC-событий; - проверка нескольких символов; - проверка нескольких интервалов; - проверка типа свечей; - исследование поведения API после изменений на стороне биржи. Скрипт не является частью Production-кода и предназначен исключительно для инженерной диагностики. --- # Проверка синтаксиса ```bash python -m compileall \ scripts/check_ohlc_websocket.py ``` --- # Проверка оформления ```bash git diff --check ``` --- # Запуск ```bash EXCHANGE_BASE_URL=https://api-adapter.dzengi.com \ EXCHANGE_WS_URL=wss://api-adapter.dzengi.com \ EXCHANGE_API_KEY="" \ PYTHONPATH=. \ python scripts/check_ohlc_websocket.py ``` --- # Настраиваемые параметры В начале файла можно изменять ```python SYMBOLS = ( "BTC/USD_LEVERAGE", ) ``` для исследования нескольких инструментов. Например ```python SYMBOLS = ( "BTC/USD_LEVERAGE", "ETH/USD_LEVERAGE", ) ``` --- Можно изменять интервалы ```python INTERVALS = ( "1m", ) ``` например ```python INTERVALS = ( "1m", "5m", ) ``` --- Можно выбирать тип свечей ```python CANDLE_TYPE = "classic" ``` или ```python CANDLE_TYPE = "heikin-ashi" ``` --- Также можно изменять ```python MAX_MESSAGES ``` и ```python TOTAL_TIMEOUT_SECONDS ``` для длительного наблюдения. --- # Что выводит скрипт После запуска отображаются - URL подключения; - список инструментов; - интервалы; - тип свечей; - параметры исследования. Затем выводится отправляемый JSON подписки. После подключения отображается ACK. Например ```json { "subscriptions": { "BTC/USD_LEVERAGE:1m": "PROCESSED" } } ``` После этого выводятся все события ``` ohlc.event ``` с указанием - номера сообщения; - времени от начала подключения; - полного JSON. --- # Проверка нескольких интервалов При использовании ```python INTERVALS = ( "1m", "5m", ) ``` ожидается ACK ```json { "subscriptions": { "BTC/USD_LEVERAGE:1m": "PROCESSED", "BTC/USD_LEVERAGE:5m": "PROCESSED" } } ``` и последующее получение событий ```text interval = 1m ``` и ```text interval = 5m ``` в одном WebSocket-соединении. --- # Проверка Heikin-Ashi Для исследования синтетических свечей достаточно изменить ```python CANDLE_TYPE = "heikin-ashi" ``` После этого ожидаются события ```json { "type":"heikin-ashi" } ``` Контракт полностью совпадает с классическими свечами, однако значения OHLC рассчитываются по алгоритму Heikin-Ashi. --- # Использование в дальнейшем Скрипт рекомендуется сохранять в репозитории проекта. Он может использоваться - после изменений API Dzengi; - после обновления WebSocket; - при регрессионном тестировании; - при диагностике Production; - при разработке новых адаптеров; - при интеграции новых таймфреймов. Наличие отдельного диагностического инструмента значительно упрощает проверку реального контракта биржи без запуска всего приложения Dzentra. --- # Итог Build 058 Build 058 полностью подтвердил контракт WebSocket OHLCMarketData.subscribe на реальном Production API Dzengi. Полученные результаты являются основанием для перехода к следующему этапу проекта — ``` Build 059 Интеграция WebSocket OHLC в канонический Candles Feed. ```