1283 lines
46 KiB
Markdown
1283 lines
46 KiB
Markdown
# 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** в следующем этапе дорожной карты. |