Files
dzentra_bot/docs/migrations/build_060_16.md

1283 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 060.16 — Trade Subscription Layer
**Engineering Migration Report**
---
# Контроль документа
| Свойство | Значение |
|----------|----------|
| Build | 060.16 |
| Название | Trade Subscription Layer |
| Статус | Completed |
| Проект | Dzentra |
| Подсистема | Market Data Acquisition |
| Компонент | Trades Feed |
| Версия | 1.0 |
---
# Цель Build
После завершения Build 060.15 система получила полностью интегрированный механизм маршрутизации входящих WebSocket-сообщений.
К этому моменту архитектура уже обеспечивала:
- единый WebSocket Runtime;
- единый механизм транспортной маршрутизации;
- полноценный Trade Adapter;
- преобразование транспортных сообщений в каноническую модель `Trade`.
Однако один важный уровень инфраструктуры всё ещё отсутствовал.
Система уже умела принимать сообщения о сделках, но ещё не обладала единым механизмом формирования исходящих WebSocket-подписок.
Подписка на поток Trade оставалась частью низкоуровневого протокола биржи и не была представлена отдельным архитектурным уровнем.
Это приводило к нескольким ограничениям.
Во-первых, детали протокола Dzengi могли начать проникать в Runtime.
Во-вторых, отсутствовал единый механизм построения подписок для различных типов рыночных данных.
В-третьих, будущие Feed были бы вынуждены самостоятельно формировать JSON-документы подписки.
Подобная архитектура противоречила одному из базовых принципов Dzentra — строгому разделению ответственности между слоями системы.
Основная задача Build заключается в создании отдельного уровня **Trade Subscription Layer**, который полностью инкапсулирует знания о протоколе подписки Dzengi и предоставляет Runtime исключительно универсальные транспортные команды.
После завершения Build система должна уметь:
- построить стабильный идентификатор подписки;
- сформировать корректный WebSocket-документ `trades.subscribe`;
- преобразовать транспортное сообщение в универсальную `SubscribeCommand`;
- полностью скрыть детали протокола Dzengi от Runtime.
При этом Build принципиально не затрагивает:
- WebSocket Runtime;
- WebSocket Protocol;
- Unified Router;
- Parser;
- Value Validation;
- Mapper;
- Trade Adapter;
- Trades Feed;
- Registry подписок;
- механизм восстановления подписок после reconnect.
---
# Предпосылки
К началу Build архитектура Market Data Acquisition уже содержала полностью реализованный транспортный уровень Runtime.
Он обеспечивал выполнение всех операций, связанных с жизненным циклом WebSocket-соединения.
Конвейер управления соединением выглядел следующим образом.
```text
SubscribeCommand
Runtime
WebSocket Transport
```
Runtime уже обладал универсальной инфраструктурой транспортных команд.
В частности, были реализованы:
```text
ConnectCommand
DisconnectCommand
SubscribeCommand
UnsubscribeCommand
SendTextCommand
SendBinaryCommand
```
При этом Runtime принципиально не содержал информации о конкретной бирже.
Он не должен был знать:
- структуру JSON-документов;
- названия WebSocket destination;
- формат сообщений подписки;
- особенности протокола Dzengi.
Одновременно с этим Build 060.15 завершил интеграцию Trade Pipeline в Unified WebSocket Routing.
Таким образом система уже умела получать входящие сообщения:
```text
internal.trade
```
и преобразовывать их в каноническую модель:
```text
Trade
```
Однако обратная часть жизненного цикла — формирование исходящей подписки — всё ещё отсутствовала как самостоятельный архитектурный уровень.
В результате знания о протоколе подписки неизбежно начали бы распространяться по нескольким компонентам системы.
---
# Архитектурное основание
Одним из фундаментальных принципов архитектуры Dzentra является изоляция транспортного протокола биржи от внутренних компонентов системы.
Каждый уровень должен знать исключительно ту информацию, которая относится к его собственной зоне ответственности.
Runtime отвечает исключительно за передачу транспортных сообщений.
Feed отвечает исключительно за организацию потока данных.
Parser отвечает исключительно за разбор транспортных моделей.
Mapper отвечает исключительно за построение канонических моделей.
Следовательно, знания о формате WebSocket-подписки также должны быть сосредоточены в отдельном специализированном слое.
После завершения Build архитектурная схема формирования подписки принимает следующий вид.
```text
Trade Symbols
Trade Subscription Builder
TransportTextMessage
SubscribeCommand
Runtime
WebSocket
```
Каждый уровень отвечает только за собственную задачу.
Builder знает исключительно протокол Dzengi.
Runtime знает исключительно транспортные команды.
WebSocket знает исключительно способ передачи сообщения.
Подобное разделение полностью соответствует общей архитектуре подсистемы Market Data Acquisition.
---
# Результаты архитектурного аудита
Перед началом реализации Build был выполнен полный аудит существующей инфраструктуры Runtime.
Анализ подтвердил наличие полностью сформированного транспортного уровня.
В частности, компонент
```text
runtime/runtime_commands.py
```
уже содержал универсальную команду
```text
SubscribeCommand
```
которая полностью отделяет вызывающий код от конкретного способа передачи сообщения.
Дополнительно был выполнен аудит транспортных сообщений.
Компонент
```text
runtime/transport_messages.py
```
уже содержал универсальную модель
```text
TransportTextMessage
```
предназначенную для передачи текстовых WebSocket-документов.
Также был выполнен аудит протокола Runtime.
Интерфейс
```text
WebSocketSubscriptionManagerProtocol
```
уже предусматривал поддержку восстановления подписок после переподключения посредством методов:
```text
restore_subscriptions()
clear_subscriptions()
```
Тем самым было подтверждено, что архитектура Runtime уже подготовлена к будущей реализации Registry подписок.
Вмешательство в Runtime в рамках Build 060.16 не требуется.
Единственным отсутствующим уровнем являлся специализированный механизм построения транспортных подписок.
Именно этот уровень и становится предметом настоящего Build.
---
# Исследование протокола Dzengi
Перед реализацией Subscription Layer был выполнен отдельный анализ публичного WebSocket-протокола Dzengi.
Изучение Swagger и существующей производственной интеграции подтвердило использование следующего формата подписки.
```json
{
"correlationId": "...",
"destination": "trades.subscribe",
"payload": {
"symbols": [
"BTC/USD_LEVERAGE"
]
}
}
```
После успешной обработки сервер возвращает подтверждение подписки.
```json
{
"status": "OK",
"destination": "trades.subscribe",
"payload": {
"subscriptions": {
"BTC/USD_LEVERAGE": "PROCESSED"
}
}
}
```
После этого сервер начинает передавать сообщения
```text
destination = "internal.trade"
```
которые поступают в ранее реализованный Trade Pipeline.
Проведённый анализ также подтвердил отсутствие документированной операции выборочной отмены подписки.
В Swagger отсутствует описание команды
```text
trades.unsubscribe
```
что потребовало проведения дополнительного инженерного исследования.
---
# Экспериментальная проверка `trades.unsubscribe`
В рамках Build был проведён отдельный эксперимент, целью которого являлась проверка существования недокументированной операции выборочной отмены подписки.
Для этого был подготовлен диагностический сценарий, выполняющий следующую последовательность действий.
```text
Открытие WebSocket
trades.subscribe
Получение ACK
trades.unsubscribe
Анализ ответа сервера
```
В ходе эксперимента сервер успешно принял команду
```text
trades.subscribe
```
и подтвердил регистрацию подписки.
После этого была отправлена команда
```text
destination = "trades.unsubscribe"
```
Ответ сервера оказался следующим.
```json
{
"status": "ERROR",
"payload": {
"errorCode": "BAD_REQUEST"
}
}
```
Таким образом было подтверждено:
- сервер распознаёт транспортный запрос;
- предложенный формат команды не поддерживается;
- документированного механизма выборочной отмены подписки не существует.
Полученный результат стал одним из ключевых архитектурных оснований настоящего Build.
Dzentra не реализует неподтверждённые возможности внешнего API.
Subscription Layer поддерживает исключительно подтверждённую операцию
```text
trades.subscribe
```
а прекращение получения потока данных остаётся частью жизненного цикла самого WebSocket-соединения.
---
# Архитектурное решение
По результатам проведённого аудита было принято решение не изменять существующую инфраструктуру Runtime.
Вместо добавления новой логики в транспортный уровень был реализован отдельный слой построения подписок.
Данный слой полностью изолирует знания о протоколе Dzengi от остальных компонентов системы.
Новая архитектура принимает следующий вид.
```text
Trade Symbols
Trade Subscription Layer
├────────► build_trade_subscription_key()
├────────► build_trade_subscribe_message()
└────────► build_trade_subscribe_command()
SubscribeCommand
Runtime
WebSocket
```
Таким образом Runtime продолжает работать исключительно с универсальными транспортными командами.
Ни один компонент Runtime не знает:
- название WebSocket destination;
- структуру JSON-документа;
- список символов;
- особенности протокола Dzengi.
Все эти знания полностью сосредоточены внутри нового Subscription Layer.
Подобное решение сохраняет существующее разделение ответственности и обеспечивает возможность дальнейшего расширения системы без изменения транспортной инфраструктуры.
---
# Новый пакет Subscription Layer
Для реализации нового архитектурного уровня создан отдельный пакет.
```text
src/
└── market_data/
└── acquisition/
└── subscriptions/
├── __init__.py
└── trades.py
```
Появление отдельного пакета является осознанным архитектурным решением.
Во время проектирования рассматривались несколько альтернатив.
Размещение Builder внутри Runtime было отклонено, поскольку Runtime не должен содержать знания о конкретной бирже.
Размещение Builder внутри Adapter также было признано неудачным.
Adapter отвечает исключительно за преобразование входящих транспортных документов.
Построение исходящих подписок относится к другой области ответственности.
В результате был выбран самостоятельный пакет
```text
acquisition/subscriptions
```
который становится специализированным уровнем формирования транспортных подписок.
Данная архитектура обеспечивает единое место хранения логики построения подписок для всех будущих потоков рыночных данных.
В дальнейшем аналогичным образом могут быть реализованы:
```text
quotes.py
candles.py
orderbook.py
status.py
```
Каждый модуль будет отвечать исключительно за построение транспортных подписок собственного типа данных.
---
# Реализованные Builder-функции
В рамках Build реализованы три чистые функции.
Они полностью покрывают процесс формирования универсальной Runtime-команды.
---
## Построение ключа подписки
Функция
```text
build_trade_subscription_key()
```
формирует стабильный идентификатор подписки.
При построении ключа выполняется:
- удаление пустых элементов;
- удаление дубликатов;
- нормализация пробелов;
- каноническая сортировка символов.
Например
```text
BTC/USD_LEVERAGE
```
преобразуется в
```text
trades:BTC/USD_LEVERAGE
```
а набор
```text
ETH/USD_LEVERAGE
BTC/USD_LEVERAGE
ETH/USD_LEVERAGE
```
преобразуется в
```text
trades:BTC/USD_LEVERAGE,ETH/USD_LEVERAGE
```
Использование стабильного идентификатора позволяет в дальнейшем однозначно регистрировать подписки внутри Runtime Registry.
---
## Построение транспортного сообщения
Функция
```text
build_trade_subscribe_message()
```
формирует полностью готовый WebSocket-документ протокола Dzengi.
Результатом работы функции является универсальная транспортная модель
```text
TransportTextMessage
```
содержащая сериализованный JSON-документ.
При отсутствии внешнего
```text
correlationId
```
Builder автоматически создаёт новый UUID.
При передаче значения извне оно сохраняется без изменений.
Подобный механизм обеспечивает возможность детерминированного тестирования и сопоставления серверного ACK.
---
## Построение Runtime-команды
Функция
```text
build_trade_subscribe_command()
```
является верхним уровнем Subscription Layer.
Она объединяет результаты двух предыдущих функций.
В результате вызывающая сторона получает полностью сформированную
```text
SubscribeCommand
```
которая уже содержит:
- стабильный идентификатор подписки;
- готовое транспортное сообщение.
После построения команды никакая дополнительная обработка более не требуется.
Runtime получает полностью готовую транспортную команду.
---
# Последовательность формирования SubscribeCommand
После завершения Build полный процесс построения подписки выглядит следующим образом.
```text
Symbols
Normalization
Subscription Key
Trade JSON Builder
TransportTextMessage
SubscribeCommand
Runtime
WebSocket
```
Каждый этап отвечает исключительно за собственную задачу.
Нормализация символов не знает о Runtime.
Runtime не знает о JSON.
JSON Builder не знает о транспортном соединении.
Подобное разделение обеспечивает высокую повторную используемость компонентов.
---
# Использование существующей Runtime-инфраструктуры
Одним из ключевых требований Build являлось максимальное повторное использование уже реализованных компонентов.
Subscription Layer не вводит новых транспортных моделей.
Не создаёт новых Runtime-команд.
Не изменяет существующий протокол передачи сообщений.
Вместо этого используются уже реализованные компоненты проекта.
Для передачи транспортного документа применяется
```text
TransportTextMessage
```
Для взаимодействия с Runtime используется
```text
SubscribeCommand
```
Передача данных продолжает выполняться существующим WebSocket Runtime.
Таким образом новый Build полностью повторно использует уже реализованную инфраструктуру проекта.
---
# Почему Subscription Layer реализован функциями
Во время проектирования рассматривался вариант реализации отдельного класса
```text
TradeSubscriptionManager
```
Однако после проведения архитектурного аудита данный вариант был отклонён.
Причина заключается в отсутствии собственного состояния.
Subscription Layer:
- не хранит активные подписки;
- не выполняет регистрацию;
- не взаимодействует с Runtime Registry;
- не выполняет восстановление после reconnect.
Единственной задачей слоя является построение транспортных объектов.
Подобная логика является полностью детерминированной.
Она зависит исключительно от входных параметров.
Поэтому реализация в виде чистых функций полностью соответствует принятому в проекте архитектурному правилу.
Если компонент не хранит состояние, он должен быть реализован функциями.
Создание класса в данном случае привело бы лишь к искусственному усложнению архитектуры.
---
# Почему Runtime не знает о `trades.subscribe`
Во время проектирования отдельно рассматривался вариант переноса формирования JSON-документа непосредственно в Runtime.
После анализа архитектуры данный вариант был отклонён.
Основные причины:
- Runtime не должен зависеть от конкретной биржи;
- Runtime не должен знать транспортный протокол;
- Runtime не должен содержать названия WebSocket destination;
- Runtime должен работать исключительно с универсальными командами.
После завершения Build Runtime продолжает получать только
```text
SubscribeCommand
```
которая уже содержит полностью сформированное транспортное сообщение.
Таким образом достигается полная независимость Runtime от конкретной реализации протокола Dzengi.
---
# Почему `trades.unsubscribe` не реализован
Во время подготовки Build отдельно рассматривалась возможность реализации симметричной операции отмены подписки.
На первый взгляд подобное решение выглядело естественным продолжением механизма
```text
trades.subscribe
```
Однако архитектура Dzentra строится исключительно на подтверждённых возможностях внешнего API.
Поэтому перед реализацией был проведён отдельный инженерный эксперимент.
В результате сервер вернул ответ
```text
status = ERROR
errorCode = BAD_REQUEST
```
Тем самым было подтверждено, что используемый формат
```text
trades.unsubscribe
```
не поддерживается.
Отсутствие документированного контракта означает невозможность гарантировать корректную работу подобной функции.
Поэтому Build сознательно ограничен исключительно подтверждённой возможностью
```text
trades.subscribe
```
Данное решение полностью соответствует одному из инженерных принципов проекта.
Dzentra никогда не реализует функциональность, существование которой не подтверждено документацией либо экспериментально.
Удаление подписки будет реализовано исключительно посредством управления жизненным циклом WebSocket-соединения и будущего Runtime Registry.
---
# Изменённые файлы
В рамках Build добавлены два новых файла.
## Subscription Layer
```text
src/market_data/acquisition/subscriptions/__init__.py
```
Создан новый пакет формирования транспортных подписок.
Пакет становится единым местом хранения Builder-функций для различных типов рыночных данных.
---
## Trade Subscription Builder
```text
src/market_data/acquisition/subscriptions/trades.py
```
Реализованы:
```text
build_trade_subscription_key()
build_trade_subscribe_message()
build_trade_subscribe_command()
```
Никакие существующие компоненты Runtime изменены не были.
---
## Unit-тесты
Добавлен новый набор тестов.
```text
tests/unit/market_data/acquisition/subscriptions/test_trades.py
```
Все существующие тесты Runtime сохранены без изменений.
---
# Добавленные тесты
В рамках Build реализирован полный набор unit-тестов нового Subscription Layer.
Проверяются следующие сценарии.
---
## Построение ключа подписки
Подтверждается корректная генерация:
```text
trades:BTC/USD_LEVERAGE
```
для одиночного символа.
---
## Нормализация символов
Подтверждается:
- удаление дубликатов;
- удаление пустых строк;
- удаление пробелов;
- стабильная сортировка.
Получаемый ключ не зависит от порядка входных данных.
---
## Проверка пустого списка
Подтверждается генерация
```text
ValueError
```
при отсутствии корректных символов.
---
## Построение транспортного сообщения
Подтверждается возврат объекта
```text
TransportTextMessage
```
с корректным JSON-документом Dzengi.
---
## Генерация UUID
Подтверждается автоматическое создание
```text
correlationId
```
при отсутствии значения.
---
## Использование внешнего correlationId
Подтверждается сохранение значения,
переданного вызывающей стороной.
Это обеспечивает возможность детерминированного тестирования.
---
## Построение SubscribeCommand
Подтверждается возврат корректной
```text
SubscribeCommand
```
с правильным:
- subscription key;
- TransportTextMessage.
---
## Согласованность Subscription Builder
Подтверждается использование одинакового набора нормализованных символов как при построении ключа подписки, так и при построении JSON-документа.
Это гарантирует отсутствие рассогласования между Runtime Registry и транспортным протоколом.
---
# Результаты тестирования
После завершения реализации выполнен запуск нового набора unit-тестов.
```bash
PYTHONPATH="$PWD" python -m pytest \
tests/unit/market_data/acquisition/subscriptions/test_trades.py -v
```
Результат:
```text
11 passed
```
Подтверждена корректная работа всех Builder-функций.
---
# Регрессионное тестирование Runtime
После завершения реализации выполнен запуск полного набора Runtime-тестов.
```bash
PYTHONPATH="$PWD" python -m pytest \
tests/unit/market_data/acquisition/runtime
```
Результат:
```text
30 passed
```
Подтверждено отсутствие регрессий существующей транспортной инфраструктуры.
Ни один существующий Runtime-компонент не изменился.
---
# Проверка компиляции
После завершения реализации выполнена проверка компиляции новых компонентов.
```bash
python -m compileall \
src/market_data/acquisition/subscriptions \
tests/unit/market_data/acquisition/subscriptions
```
Компиляция завершилась успешно.
Ошибок синтаксиса не обнаружено.
Все новые файлы успешно компилируются.
---
# Проверка Git diff
После завершения реализации выполнена финальная проверка изменений.
```bash
git diff --check
```
Результат:
```text
без замечаний
```
Проверка подтвердила отсутствие:
- trailing whitespace;
- ошибок окончания строк;
- конфликтов diff;
- нарушений форматирования.
---
# Scope Build 060.16
В рамках настоящего Build реализован исключительно уровень
```text
Trade Subscription Layer
```
Build **не включает**:
- Trades Feed;
- Runtime Registry;
- Runtime Integration;
- Reconnect;
- восстановление подписок;
- обработку входящих Trade;
- сортировку сделок;
- дедупликацию;
- REST Backfill.
Подобное ограничение полностью соответствует принятому принципу атомарной реализации Build.
Каждый этап дорожной карты реализует только один самостоятельный архитектурный уровень.
---
# Архитектурный результат
После завершения Build система получила полностью самостоятельный слой формирования Trade-подписок.
Архитектура приобретает следующий вид.
```text
Trade Symbols
Trade Subscription Builder
├────────► Subscription Key
├────────► TransportTextMessage
└────────► SubscribeCommand
Runtime
WebSocket
```
Subscription Layer полностью изолирует знания о протоколе Dzengi.
Runtime продолжает работать исключительно с универсальными транспортными командами.
Это позволяет в дальнейшем добавлять новые типы подписок без каких-либо изменений транспортной инфраструктуры.
---
# Соблюдение архитектурных принципов
В рамках Build полностью сохранены архитектурные инварианты Dzentra.
## Локальность изменений
Добавлены только:
- `src/market_data/acquisition/subscriptions/__init__.py`;
- `src/market_data/acquisition/subscriptions/trades.py`;
- `tests/unit/market_data/acquisition/subscriptions/test_trades.py`.
Существующие Runtime-компоненты не изменялись.
---
## Повторное использование инфраструктуры
Subscription Layer полностью использует уже существующие компоненты:
- `TransportTextMessage`;
- `SubscribeCommand`;
- WebSocket Runtime.
Новая транспортная инфраструктура не создавалась.
---
## Разделение ответственности
Subscription Builder отвечает исключительно за построение транспортных подписок.
Runtime отвечает исключительно за передачу сообщений.
Feed будет отвечать исключительно за организацию потока данных.
Подобное разделение полностью соответствует архитектуре Market Data Acquisition.
---
## Повторное использование архитектуры
Build не вводит нового транспортного уровня.
Не изменяет существующий Runtime.
Не изменяет WebSocket Protocol.
Не изменяет механизм передачи сообщений.
Вместо этого новый уровень полностью использует уже существующую архитектуру Market Data Acquisition.
Фактически Build является логическим продолжением ранее реализованной Runtime-инфраструктуры.
Это позволяет последующим этапам дорожной карты использовать единый механизм формирования подписок независимо от конкретного типа рыночных данных.
---
## Минимальность изменений
Одним из ключевых требований Build являлось минимальное вмешательство в существующий код.
По результатам архитектурного аудита было принято решение отказаться от изменения Runtime.
Все изменения локализованы внутри нового пакета
```text
acquisition/subscriptions
```
Подобная локализация существенно снижает риск возникновения регрессий и позволяет независимо развивать Subscription Layer без влияния на остальные компоненты системы.
---
## Функциональный подход
Все компоненты Subscription Layer реализованы в виде чистых функций.
Каждая функция:
- имеет одну область ответственности;
- не зависит от внешнего состояния;
- не хранит собственных данных;
- детерминированно преобразует входные параметры в результат.
Подобный подход полностью соответствует принятому в проекте правилу:
> Stateless-компоненты реализуются функциями. Stateful-компоненты реализуются классами.
Subscription Layer относится к первой категории.
---
## Подготовка к масштабированию
Несмотря на то что Build реализует только Trade Subscription Layer, выбранная архитектура изначально ориентирована на дальнейшее расширение.
В дальнейшем аналогичный механизм может использоваться для:
```text
Quote Subscription Layer
Candles Subscription Layer
Order Book Subscription Layer
Market Status Subscription Layer
```
Каждый новый Builder будет полностью независимым.
При этом Runtime останется неизменным.
Таким образом уже на этапе Build 060.16 закладывается единый архитектурный шаблон для всех будущих потоков рыночных данных.
---
# Архитектурные решения Build (ADR)
## ADR-060.16-001
**Runtime остаётся полностью транспортно-независимым.**
Runtime не знает:
- структуру JSON;
- `destination`;
- особенности протокола Dzengi;
- типы рыночных данных.
Он работает исключительно с универсальными транспортными командами.
---
## ADR-060.16-002
**Построение подписок выполняется специализированным Subscription Layer.**
Все знания о WebSocket-протоколе Dzengi сосредоточены внутри Builder-функций.
Feed и Runtime не взаимодействуют с транспортным протоколом напрямую.
---
## ADR-060.16-003
**Неподтверждённые возможности внешнего API не реализуются.**
Во время Build выполнена экспериментальная проверка команды
```text
trades.unsubscribe
```
Полученный ответ
```text
BAD_REQUEST
```
подтвердил отсутствие поддерживаемого контракта.
В результате Subscription Layer реализует исключительно документированную операцию
```text
trades.subscribe
```
---
## ADR-060.16-004
**Subscription Builder не хранит собственного состояния.**
Все функции являются полностью детерминированными.
Registry подписок, управление жизненным циклом подписок и механизм восстановления после reconnect относятся к следующим Build и не входят в область ответственности настоящего слоя.
---
# Критерии завершения Build
Build 060.16 считается завершённым, поскольку выполнены все поставленные задачи.
- ✔ создан отдельный пакет `acquisition/subscriptions`;
- ✔ реализована функция `build_trade_subscription_key()`;
- ✔ реализована функция `build_trade_subscribe_message()`;
- ✔ реализована функция `build_trade_subscribe_command()`;
- ✔ выполнена нормализация списка символов;
- ✔ реализована генерация стабильного ключа подписки;
- ✔ реализовано построение `TransportTextMessage`;
- ✔ реализовано построение `SubscribeCommand`;
- ✔ Runtime не изменён;
- ✔ подтверждено отсутствие поддержки `trades.unsubscribe`;
- ✔ реализованы unit-тесты нового Subscription Layer;
- ✔ успешно пройдены новые тесты (`11 passed`);
- ✔ успешно пройдена регрессия Runtime (`30 passed`);
- ✔ проект успешно компилируется;
-`git diff --check` не выявил замечаний;
- ✔ изменения полностью укладываются в согласованный scope Build.
---
# Следующий этап
Следующим этапом дорожной карты является
```text
Build 060.17 — Trades Feed Core
```
Цель следующего Build:
- реализовать специализированный Feed обработки сделок;
- подключить поток `Trade` к инфраструктуре Market Data Acquisition;
- организовать приём канонических моделей `Trade`;
- подготовить основу для последующих этапов:
- упорядочивания сделок;
- дедупликации;
- восстановления истории после переподключения;
- интеграции с Runtime.
Subscription Layer, реализованный в Build 060.16, станет источником формирования подписок для нового Feed.
---
# Итог
Build 060.16 завершил формирование самостоятельного уровня **Trade Subscription Layer** в подсистеме Market Data Acquisition.
Новая реализация полностью повторно использует существующую инфраструктуру Runtime, не изменяет транспортный протокол и не нарушает архитектурные инварианты проекта.
Все знания о WebSocket-протоколе Dzengi теперь сосредоточены в одном специализированном пакете, тогда как Runtime продолжает работать исключительно с универсальными транспортными командами.
В ходе Build была не только реализована новая функциональность, но и проведено инженерное исследование публичного WebSocket API Dzengi, подтвердившее отсутствие поддерживаемой операции `trades.unsubscribe`. Полученные результаты легли в основу принятых архитектурных решений и закреплены в ADR настоящего Build.
Реализация ограничена согласованным scope, успешно прошла целевое и регрессионное тестирование, не потребовала изменений существующей Runtime-инфраструктуры и сформировала единый архитектурный шаблон для будущих Subscription Layer всех типов рыночных данных.
После завершения Build система получила завершённый механизм формирования подписок `Trade`, который станет фундаментом для реализации **Trades Feed Core** в следующем этапе дорожной карты.