594 lines
15 KiB
Markdown
594 lines
15 KiB
Markdown
# Build 034 — Dzengi WebSocket quote parsing и адаптер
|
||
|
||
**Статус:** Завершён
|
||
**Подсистема:** Market Data
|
||
**Контур:** Market Data Acquisition / Quotes Feed
|
||
**Проект:** Dzentra
|
||
**Язык документации:** Русский
|
||
|
||
---
|
||
|
||
## 1. Цель Build
|
||
|
||
Цель Build 034 — создать специализированный контур обработки WebSocket-сообщений котировок Dzengi и преобразования их в каноническую модель `Quote`.
|
||
|
||
Build должен был изолировать знание транспортных форматов Dzengi WebSocket от канонического слоя Market Data и подготовить архитектурную основу для последующего перевода market runtime на новый Quotes Feed.
|
||
|
||
В рамках Build реализована цепочка:
|
||
|
||
```text
|
||
Dzengi WebSocket message
|
||
↓
|
||
schema validation
|
||
↓
|
||
parser
|
||
↓
|
||
DzengiWebSocketQuoteResponse
|
||
↓
|
||
value validation
|
||
↓
|
||
mapper
|
||
↓
|
||
DzengiWebSocketQuoteAdapter
|
||
↓
|
||
canonical Quote
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Архитектурный результат
|
||
|
||
После Build 034 обработка WebSocket-котировок Dzengi получила специализированный адаптерный контур внутри:
|
||
|
||
```text
|
||
src/market_data/acquisition/
|
||
```
|
||
|
||
Транспортные особенности Dzengi WebSocket больше не должны распространяться на каноническую модель `Quote` и будущих потребителей Quotes Feed.
|
||
|
||
Архитектурная граница имеет следующий вид:
|
||
|
||
```text
|
||
Dzengi-specific transport formats
|
||
↓
|
||
adapters/dzengi
|
||
↓
|
||
canonical Quote
|
||
↓
|
||
Quotes Feed / Quote Store / consumers
|
||
```
|
||
|
||
Каноническая модель:
|
||
|
||
```text
|
||
src/market_data/acquisition/models/quote.py
|
||
```
|
||
|
||
остаётся независимой от:
|
||
|
||
```text
|
||
payload
|
||
Payload
|
||
symbolName
|
||
bid
|
||
ask
|
||
ofr
|
||
bids
|
||
asks
|
||
price
|
||
p
|
||
bidPrice
|
||
askPrice
|
||
```
|
||
|
||
Эти имена являются особенностями внешнего транспорта Dzengi и обрабатываются внутри адаптерного слоя.
|
||
|
||
---
|
||
|
||
## 3. Изменённые файлы
|
||
|
||
В рамках Build 034 изменены следующие файлы:
|
||
|
||
```text
|
||
src/market_data/acquisition/adapters/dzengi/models.py
|
||
src/market_data/acquisition/adapters/dzengi/parser.py
|
||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||
src/market_data/acquisition/validation/schema.py
|
||
src/market_data/acquisition/validation/values.py
|
||
```
|
||
|
||
Добавлен специализированный WebSocket-адаптер:
|
||
|
||
```text
|
||
src/market_data/acquisition/adapters/dzengi/websocket.py
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Добавленные тесты
|
||
|
||
Добавлены следующие специализированные тестовые файлы:
|
||
|
||
```text
|
||
tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py
|
||
tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Поддерживаемые WebSocket-форматы
|
||
|
||
Новый адаптерный контур поддерживает транспортные варианты, существовавшие в legacy-реализации Dzengi WebSocket.
|
||
|
||
### 5.1. Оболочки сообщения
|
||
|
||
Поддерживаются:
|
||
|
||
```text
|
||
payload
|
||
Payload
|
||
```
|
||
|
||
а также вложенная двойная оболочка.
|
||
|
||
Примеры допустимой структуры:
|
||
|
||
```json
|
||
{
|
||
"payload": {
|
||
"symbolName": "BTC/USD_LEVERAGE",
|
||
"bid": "64159.45",
|
||
"ask": "64159.55"
|
||
}
|
||
}
|
||
```
|
||
|
||
и:
|
||
|
||
```json
|
||
{
|
||
"Payload": {
|
||
"Payload": {
|
||
"symbolName": "BTC/USD_LEVERAGE",
|
||
"bids": [
|
||
["64159.45", "1.0"]
|
||
],
|
||
"asks": [
|
||
["64159.55", "1.0"]
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Поддерживаемые поля символа
|
||
|
||
Адаптер поддерживает следующие транспортные имена символа:
|
||
|
||
```text
|
||
symbolName
|
||
symbol
|
||
```
|
||
|
||
После обработки внешнее представление преобразуется в каноническое поле:
|
||
|
||
```text
|
||
Quote.symbol
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Поддерживаемые представления bid и ask
|
||
|
||
Поддерживаются прямые поля:
|
||
|
||
```text
|
||
bid
|
||
ask
|
||
```
|
||
|
||
вариант Dzengi:
|
||
|
||
```text
|
||
bid
|
||
ofr
|
||
```
|
||
|
||
а также depth-представление:
|
||
|
||
```text
|
||
bids
|
||
asks
|
||
```
|
||
|
||
Для элементов depth поддерживаются представления в виде:
|
||
|
||
```text
|
||
list
|
||
dict
|
||
```
|
||
|
||
Поддерживаемые имена поля цены внутри depth-элементов:
|
||
|
||
```text
|
||
price
|
||
p
|
||
bidPrice
|
||
askPrice
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Семантика last_price для depth-сообщений
|
||
|
||
Для WebSocket depth-сообщений, содержащих лучшие цены bid и ask, сохранена legacy-семантика:
|
||
|
||
```text
|
||
last_price = midpoint(best_bid, best_ask)
|
||
```
|
||
|
||
То есть каноническое значение `Quote.last_price` определяется как середина между лучшей ценой покупки и лучшей ценой продажи.
|
||
|
||
Это решение сохраняет обратную совместимость с существующим поведением market runtime до его последующего архитектурного перевода.
|
||
|
||
---
|
||
|
||
## 9. Обработка timestamp
|
||
|
||
WebSocket timestamp является необязательным.
|
||
|
||
Если транспортное сообщение содержит допустимый timestamp биржи, он преобразуется в:
|
||
|
||
```text
|
||
Quote.exchange_timestamp
|
||
```
|
||
|
||
Если timestamp отсутствует, каноническая модель допускает:
|
||
|
||
```text
|
||
exchange_timestamp = None
|
||
```
|
||
|
||
Время фактического получения и обработки котировки фиксируется отдельно:
|
||
|
||
```text
|
||
Quote.received_at
|
||
```
|
||
|
||
Таким образом, сохраняется разделение двух временных характеристик:
|
||
|
||
```text
|
||
exchange_timestamp
|
||
время события по данным биржи
|
||
|
||
received_at
|
||
время получения котировки системой Dzentra
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Schema validation
|
||
|
||
Schema validation отвечает исключительно за структурную корректность WebSocket-документа.
|
||
|
||
На этом этапе проверяется возможность извлечения необходимых частей сообщения без переноса бизнес-логики в транспортный слой.
|
||
|
||
Schema validation не должна:
|
||
|
||
```text
|
||
создавать canonical Quote
|
||
выполнять mapping
|
||
управлять runtime
|
||
записывать данные в Quote Store
|
||
обращаться к MarketPriceCache
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Parser
|
||
|
||
Parser преобразует структурно проверенный WebSocket-документ в специализированную raw-модель Dzengi:
|
||
|
||
```text
|
||
DzengiWebSocketQuoteResponse
|
||
```
|
||
|
||
Parser сохраняет границу между:
|
||
|
||
```text
|
||
сырой внешний документ
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
типизированное транспортное представление Dzengi
|
||
```
|
||
|
||
Parser не создаёт канонический `Quote`.
|
||
|
||
---
|
||
|
||
## 12. Value validation
|
||
|
||
Value validation проверяет семантическую допустимость извлечённых значений.
|
||
|
||
В частности, контур должен обеспечивать корректность значений, необходимых для построения канонической котировки:
|
||
|
||
```text
|
||
symbol
|
||
bid price
|
||
ask price
|
||
timestamp, если присутствует
|
||
```
|
||
|
||
Проверка значений выполняется до mapping в каноническую модель.
|
||
|
||
---
|
||
|
||
## 13. Mapper
|
||
|
||
Mapper преобразует проверенную raw-модель Dzengi WebSocket в:
|
||
|
||
```text
|
||
Quote
|
||
```
|
||
|
||
На этой границе происходит переход:
|
||
|
||
```text
|
||
Dzengi-specific representation
|
||
↓
|
||
canonical Dzentra representation
|
||
```
|
||
|
||
После mapping потребитель не должен зависеть от исходного формата WebSocket-сообщения.
|
||
|
||
---
|
||
|
||
## 14. DzengiWebSocketQuoteAdapter
|
||
|
||
Специализированный адаптер инкапсулирует полный конвейер обработки одного WebSocket-сообщения:
|
||
|
||
```text
|
||
raw document
|
||
↓
|
||
schema validation
|
||
↓
|
||
parsing
|
||
↓
|
||
value validation
|
||
↓
|
||
mapping
|
||
↓
|
||
Quote
|
||
```
|
||
|
||
Результатом успешной обработки является канонический объект:
|
||
|
||
```text
|
||
Quote
|
||
```
|
||
|
||
Адаптер не отвечает за:
|
||
|
||
```text
|
||
поддержание WebSocket-соединения
|
||
reconnect
|
||
runtime lifecycle
|
||
регистрацию market runtime
|
||
запись в Quote Store
|
||
legacy MarketPriceCache facade
|
||
```
|
||
|
||
Эти обязанности принадлежат другим архитектурным слоям.
|
||
|
||
---
|
||
|
||
## 15. Что намеренно не изменялось
|
||
|
||
В Build 034 не изменялись runtime-файлы:
|
||
|
||
```text
|
||
src/integrations/exchange/ws_client.py
|
||
src/integrations/exchange/market_stream.py
|
||
src/integrations/exchange/market_data_runner.py
|
||
```
|
||
|
||
Также Build 034 не выполнял переключение:
|
||
|
||
```text
|
||
market runtime → Quotes Feed
|
||
```
|
||
|
||
и не удалял legacy-механизмы.
|
||
|
||
Это принципиальная граница Build.
|
||
|
||
Build 034 создаёт новый специализированный адаптерный контур, но не переключает на него существующий runtime.
|
||
|
||
Перевод runtime предусмотрен следующим этапом:
|
||
|
||
```text
|
||
Build 035 — Перевод market runtime на Quotes Feed
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Обратная совместимость
|
||
|
||
В Build 034 сохранены существующие транспортные варианты legacy WebSocket-контура:
|
||
|
||
```text
|
||
payload / Payload
|
||
двойная оболочка
|
||
symbolName / symbol
|
||
bid + ask
|
||
bid + ofr
|
||
bids + asks
|
||
depth item list
|
||
depth item dict
|
||
price / p / bidPrice / askPrice
|
||
необязательный timestamp
|
||
```
|
||
|
||
Для depth-сообщений сохранено существующее правило:
|
||
|
||
```text
|
||
last_price = midpoint(best_bid, best_ask)
|
||
```
|
||
|
||
Таким образом, Build не требует одномоментного удаления legacy runtime и подготавливает безопасный переход к новой архитектуре.
|
||
|
||
---
|
||
|
||
## 17. Проверка компиляции
|
||
|
||
Выполнена проверка:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/market_data/acquisition/adapters/dzengi/models.py \
|
||
src/market_data/acquisition/adapters/dzengi/parser.py \
|
||
src/market_data/acquisition/adapters/dzengi/mapper.py \
|
||
src/market_data/acquisition/adapters/dzengi/websocket.py \
|
||
src/market_data/acquisition/validation/schema.py \
|
||
src/market_data/acquisition/validation/values.py \
|
||
tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py \
|
||
tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
успешно
|
||
```
|
||
|
||
---
|
||
|
||
## 18. Специализированные тесты
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py \
|
||
tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
24 passed in 0.04s
|
||
```
|
||
|
||
---
|
||
|
||
## 19. Полная регрессия
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
602 passed in 0.30s
|
||
```
|
||
|
||
Количество тестов до Build 034:
|
||
|
||
```text
|
||
578 passed
|
||
```
|
||
|
||
Количество тестов после Build 034:
|
||
|
||
```text
|
||
602 passed
|
||
```
|
||
|
||
Добавлено:
|
||
|
||
```text
|
||
24 теста
|
||
```
|
||
|
||
Полная регрессия подтверждает отсутствие обнаруженных регрессий в существующем коде проекта.
|
||
|
||
---
|
||
|
||
## 20. Итог Build
|
||
|
||
Build 034 завершён полностью.
|
||
|
||
Реализованы:
|
||
|
||
```text
|
||
специализированная WebSocket raw-модель Dzengi
|
||
WebSocket schema validation
|
||
WebSocket parser
|
||
WebSocket value validation
|
||
WebSocket mapper
|
||
DzengiWebSocketQuoteAdapter
|
||
преобразование WebSocket-сообщения в canonical Quote
|
||
поддержка legacy-вариантов формата Dzengi
|
||
24 специализированных теста
|
||
```
|
||
|
||
Не выполнялись:
|
||
|
||
```text
|
||
переключение market runtime
|
||
изменение ws_client.py
|
||
изменение market_stream.py
|
||
изменение market_data_runner.py
|
||
удаление legacy WebSocket parsing
|
||
удаление MarketPriceCache
|
||
```
|
||
|
||
Архитектурный результат:
|
||
|
||
```text
|
||
Dzengi WebSocket transport
|
||
↓
|
||
Dzengi-specific validation / parsing / mapping
|
||
↓
|
||
canonical Quote
|
||
```
|
||
|
||
---
|
||
|
||
## 21. Следующий Build
|
||
|
||
Следующий этап:
|
||
|
||
```text
|
||
Build 035 — Перевод market runtime на Quotes Feed
|
||
```
|
||
|
||
Его задача — подключить существующий market runtime к новому каноническому контуру котировок, используя созданные ранее:
|
||
|
||
```text
|
||
Quote
|
||
Quote Store
|
||
Quotes Feed
|
||
Dzengi REST Quotes Feed
|
||
Dzengi WebSocket quote adapter
|
||
```
|
||
|
||
При этом переход должен выполняться без преждевременного удаления legacy-механизмов и с сохранением работоспособности существующего бота до завершения последующих этапов миграции. |