Files
dzentra_bot/docs/migrations/build_035.md

898 lines
20 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 035 — Перевод market runtime на Quotes Feed
**Статус:** Завершён
**Подсистема:** Market Data
**Контур:** Market Data Acquisition / Quotes Feed / Market Runtime
**Проект:** Dzentra
**Язык документации:** Русский
---
## 1. Цель Build
Цель Build 035 — перевести существующий WebSocket market runtime с самостоятельного legacy parsing рыночных сообщений на канонический контур обработки котировок, созданный в предыдущих Build.
До Build 035 runtime самостоятельно извлекал цены из WebSocket-сообщений Dzengi и передавал примитивные значения в legacy facade:
```text
Dzengi WebSocket message
legacy runtime parsing
float price / bid / ask
MarketPriceCache.set_price()
Quote Store
```
После Build 035 рабочий runtime-путь использует специализированный WebSocket-адаптер и каноническую модель `Quote`:
```text
Dzengi WebSocket message
ExchangeWebSocketClient
DzengiWebSocketQuoteAdapter
canonical Quote
MarketPriceCache.set_quote()
Quote Store
```
Таким образом, WebSocket runtime больше не выполняет собственное преобразование транспортного формата Dzengi в набор примитивных ценовых значений.
---
## 2. Предпосылки
Build 035 опирается на результаты предыдущих этапов миграции.
### Build 027
Создана каноническая модель:
```text
Quote
```
### Build 028
Созданы:
```text
REST quote schema validation
REST quote parser
REST quote value validation
```
### Build 029
Созданы:
```text
REST quote mapper
DzengiQuoteDocumentHandler
```
### Build 030
Создан полный REST Quotes Feed:
```text
Dzengi REST ticker/24hr
DzengiQuoteDocumentSource
QuotesFeed
QuoteAcquisitionService
canonical Quote
```
### Build 031
Новый REST Quotes Feed подключён под legacy `ExchangeService` facade.
### Build 032
Создан канонический:
```text
Quote Store
```
### Build 033
`MarketPriceCache` переведён на использование `Quote Store` как внутреннего хранилища.
### Build 034
Создан специализированный контур обработки WebSocket-котировок:
```text
Dzengi WebSocket message
schema validation
parser
value validation
mapper
DzengiWebSocketQuoteAdapter
canonical Quote
```
Build 035 подключает этот контур к существующему market runtime.
---
## 3. Архитектурная проблема до Build 035
До Build 035 существовало несколько независимых путей обработки котировок.
REST-контур уже использовал каноническую модель:
```text
REST ticker/24hr
Quotes Feed
Quote
Quote Store
```
Но WebSocket runtime продолжал самостоятельно разбирать транспортные сообщения.
### `market_stream.py`
Рабочий путь имел вид:
```text
WebSocket message
_payload_from_message()
_extract_market_event()
float price / bid / ask
MarketPriceCache.set_price()
```
### `market_data_runner.py`
Рабочий путь имел вид:
```text
WebSocket message
_extract_depth_payload()
_extract_best_price()
float midpoint
MarketPriceCache.set_price()
```
Таким образом, логика понимания формата Dzengi WebSocket существовала одновременно:
```text
в новом DzengiWebSocketQuoteAdapter
в market_stream.py
в market_data_runner.py
```
Это нарушало архитектурную границу:
```text
transport-specific parsing
только adapter layer
```
---
## 4. Архитектурный результат
После Build 035 оба WebSocket runtime-пути используют единый канонический адаптер:
```text
ExchangeWebSocketClient
decoded WebSocket message
DzengiWebSocketQuoteAdapter
canonical Quote
MarketPriceCache compatibility facade
Quote Store
```
Runtime больше не должен самостоятельно знать:
```text
как устроены payload / Payload
как извлекается symbolName
как извлекаются bids / asks
какие варианты depth item поддерживает Dzengi
как вычисляется canonical last_price
как преобразуется timestamp
```
Эта ответственность принадлежит:
```text
src/market_data/acquisition/adapters/dzengi/websocket.py
```
и связанному с ним контуру:
```text
schema validation
parser
value validation
mapper
```
---
## 5. Изменённые production-файлы
В рамках Build 035 изменены:
```text
src/integrations/exchange/market_cache.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```
---
## 6. Изменённые тестовые файлы
Расширены существующие тесты:
```text
tests/unit/integrations/exchange/test_market_cache.py
tests/unit/integrations/exchange/test_market_stream.py
tests/unit/integrations/exchange/test_market_data_runner.py
```
Новые тестовые файлы не создавались.
---
## 7. Расширение MarketPriceCache
В `MarketPriceCache` добавлен новый метод:
```python
@classmethod
def set_quote(
cls,
quote: Quote,
*,
runtime_key: str = "default",
) -> None:
...
```
Его задача — принять уже готовый канонический объект:
```text
Quote
```
и записать его в существующее внутреннее хранилище через compatibility facade.
Цепочка:
```text
canonical Quote
MarketPriceCache.set_quote()
Quote Store
```
---
## 8. Сохранение канонического Quote без повторного mapping
До Build 035 при наличии уже готового `Quote` потенциально мог возникнуть лишний цикл:
```text
Quote
float values
MarketPriceCache.set_price()
создание нового Quote
Quote Store
```
После Build 035 используется прямой путь:
```text
Quote
MarketPriceCache.set_quote()
Quote Store
```
При этом не требуется:
```text
преобразовывать Decimal в float
повторно создавать Quote
повторно вычислять received_at
повторно преобразовывать exchange_timestamp
изменять source
```
Таким образом, сохраняется исходный канонический объект.
---
## 9. Сохранение legacy set_price()
Существующий публичный метод:
```text
MarketPriceCache.set_price()
```
не удалён.
Это необходимо для сохранения обратной совместимости существующего бота и legacy-потребителей.
После Build 035 его архитектурная роль:
```text
legacy primitive values
MarketPriceCache.set_price()
canonical Quote
MarketPriceCache.set_quote()
Quote Store
```
Таким образом, `set_price()` остаётся compatibility entry point, а непосредственная запись готового канонического объекта выполняется через:
```text
set_quote()
```
---
## 10. Перевод market_stream.py
Рабочий WebSocket-путь `market_stream.py` переведён на:
```text
ExchangeWebSocketClient.stream_depth()
DzengiWebSocketQuoteAdapter.map_message()
Quote
MarketPriceCache.set_quote()
```
Новый runtime-путь больше не использует legacy-функцию:
```text
_extract_market_event()
```
для обработки рабочих WebSocket-сообщений.
---
## 11. Проверка символа в market_stream.py
После получения канонического `Quote` выполняется проверка соответствия символа ожидаемому инструменту.
Концептуально:
```text
requested symbol
Quote.symbol
```
Если сообщение относится к другому инструменту, оно не должно записываться в runtime namespace.
Это предотвращает сохранение чужой котировки в контексте текущего WebSocket-потока.
---
## 12. Обработка невалидных сообщений в market_stream.py
Ошибка обработки отдельного WebSocket-сообщения не должна немедленно завершать весь market stream.
Ошибки канонического Acquisition-контура отдельного сообщения обрабатываются внутри цикла:
```text
invalid WebSocket message
DzengiWebSocketQuoteAdapter
MarketDataAcquisitionError
сообщение пропускается
stream продолжает работу
```
При этом сетевые, transport и connection errors не маскируются этим механизмом и продолжают обрабатываться существующим reconnect-контуром.
---
## 13. Перевод MarketDataRunner
Основной WebSocket runtime в:
```text
MarketDataRunner._run_websocket()
```
переведён с самостоятельного извлечения:
```text
best_bid
best_ask
```
на канонический путь:
```text
raw WebSocket payload
DzengiWebSocketQuoteAdapter.map_message()
Quote
```
После успешного mapping готовый объект записывается:
```text
Quote
MarketPriceCache.set_quote(
runtime_key=context.runtime_key
)
Quote Store
```
---
## 14. Runtime isolation
Сохраняется существующая изоляция runtime-контекстов через:
```text
runtime_key
```
Примеры существующих runtime:
```text
auto
debug_auto
default
```
При записи через:
```text
MarketPriceCache.set_quote()
```
используется соответствующий:
```text
context.runtime_key
```
Таким образом, котировки разных runtime не смешиваются.
---
## 15. Bid и ask после перехода на Quote
До Build 035 `MarketDataRunner` самостоятельно извлекал:
```text
best_bid
best_ask
```
из сырого WebSocket payload.
После Build 035 эти значения берутся из уже проверенного канонического объекта:
```text
Quote.bid_price
Quote.ask_price
```
Для legacy logging boundary при необходимости допускается преобразование:
```text
Decimal → float
```
Но внутренний канонический объект остаётся основанным на:
```text
Decimal
```
---
## 16. Last price для depth-сообщений
Согласно контракту Build 034 для WebSocket depth-сообщений:
```text
last_price = midpoint(best_bid, best_ask)
```
После Build 035 runtime больше не должен самостоятельно повторять этот расчёт.
Он получает готовое значение:
```text
Quote.last_price
```
из `DzengiWebSocketQuoteAdapter`.
Таким образом, правило определения `last_price` имеет одну каноническую реализацию.
---
## 17. Сохранение invalid payload semantics
До Build 035 `MarketDataRunner` поддерживал счётчик последовательных невалидных WebSocket-сообщений.
Существующая семантика сохранена:
```text
invalid message
invalid_payload_count += 1
```
Успешный `Quote`:
```text
valid Quote
invalid_payload_count = 0
```
После пяти последовательных невалидных сообщений:
```text
5 consecutive invalid messages
RuntimeError
существующий fallback-контур
```
Таким образом, Build 035 не изменяет существующую политику деградации runtime.
---
## 18. REST fallback
REST fallback не потребовал изменения.
К моменту Build 035 REST-путь уже использует новый Quotes Feed:
```text
Dzengi REST ticker/24hr
DzengiQuoteDocumentSource
QuotesFeed
QuoteAcquisitionService
canonical Quote
MarketPriceCache facade
Quote Store
```
После Build 035 WebSocket и REST-пути сходятся на одной канонической модели:
```text
WebSocket
DzengiWebSocketQuoteAdapter
Quote
Quote Store
Quote
REST Quotes Feed
REST
```
---
## 19. Что намеренно не изменялось
В Build 035 не изменялись:
```text
src/integrations/exchange/ws_client.py
src/market_data/acquisition/adapters/dzengi/websocket.py
src/market_data/acquisition/models/quote.py
src/storage/quote_store.py
```
Причина:
- `ws_client.py` уже имеет достаточный transport-only контракт;
- WebSocket-адаптер завершён в Build 034;
- каноническая модель `Quote` не требует расширения;
- `Quote Store` уже предоставляет необходимый контракт хранения.
---
## 20. Legacy parsing helpers
После перевода рабочего runtime-пути некоторые legacy helper-функции больше не являются частью основного пути обработки котировок.
В частности:
```text
market_stream.py:
_extract_market_event()
market_data_runner.py:
_extract_best_price()
```
Они не удалены в Build 035.
Это намеренное решение.
Окончательная очистка legacy parsing относится к последующему этапу:
```text
Build 039 — Удаление legacy quote parsing и MarketPriceCache
```
Build 035 меняет рабочий runtime-путь, но не выполняет преждевременную очистку compatibility layer.
---
## 21. Обратная совместимость
Build 035 сохраняет работоспособность существующего бота.
Не удалены:
```text
MarketPriceCache
MarketPriceCache.set_price()
legacy read APIs
runtime_key isolation
REST fallback
существующие runtime lifecycle contracts
```
Это соответствует принятому принципу миграции Dzentra:
```text
новый контур создаётся
существующие потребители постепенно переключаются
legacy удаляется только после завершения миграции
```
---
## 22. Проверка компиляции
Выполнена команда:
```bash
python -m py_compile \
src/integrations/exchange/market_cache.py \
src/integrations/exchange/market_stream.py \
src/integrations/exchange/market_data_runner.py \
tests/unit/integrations/exchange/test_market_cache.py \
tests/unit/integrations/exchange/test_market_stream.py \
tests/unit/integrations/exchange/test_market_data_runner.py
```
Результат:
```text
успешно
```
---
## 23. Специализированные тесты
Выполнена команда:
```bash
python -m pytest \
tests/unit/integrations/exchange/test_market_cache.py \
tests/unit/integrations/exchange/test_market_stream.py \
tests/unit/integrations/exchange/test_market_data_runner.py \
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py \
-q
```
Результат:
```text
33 passed in 0.13s
```
---
## 24. Полная регрессия
Выполнена команда:
```bash
python -m pytest -q
```
Результат:
```text
608 passed in 0.29s
```
Количество тестов до Build 035:
```text
602 passed
```
Количество тестов после Build 035:
```text
608 passed
```
Добавлено:
```text
6 тестов
```
Полная регрессия подтверждает отсутствие обнаруженных регрессий в существующем коде проекта.
---
## 25. Итог Build
Build 035 завершён полностью.
Реализованы:
```text
подключение DzengiWebSocketQuoteAdapter к market_stream
подключение DzengiWebSocketQuoteAdapter к MarketDataRunner
передача canonical Quote непосредственно в compatibility facade
добавление MarketPriceCache.set_quote()
сохранение legacy MarketPriceCache.set_price()
сохранение runtime_key isolation
сохранение invalid payload semantics
сохранение REST fallback
сохранение обратной совместимости
```
Подтверждён новый рабочий WebSocket-путь:
```text
ExchangeWebSocketClient
DzengiWebSocketQuoteAdapter
canonical Quote
MarketPriceCache.set_quote()
Quote Store
```
---
## 26. Архитектурное состояние после Build 035
После завершения Build 035 Dzentra имеет два канонических пути получения текущей котировки.
### WebSocket
```text
Dzengi WebSocket
ExchangeWebSocketClient
DzengiWebSocketQuoteAdapter
Quote
MarketPriceCache compatibility facade
Quote Store
```
### REST
```text
Dzengi REST ticker/24hr
DzengiQuoteDocumentSource
QuotesFeed
QuoteAcquisitionService
Quote
MarketPriceCache compatibility facade
Quote Store
```
Таким образом, оба transport-пути приводят данные к единой канонической модели:
```text
Quote
```
до передачи их потребителям.
---
## 27. Следующий Build
Следующий этап:
```text
Build 036 — Перевод read-only и UI-потребителей
```
Его задача — определить и перевести read-only и UI-потребителей текущей рыночной котировки с legacy-представлений на канонический `Quote` и новый контур хранения там, где это архитектурно обосновано.
Build 036 должен выполняться без изменения execution semantics и без преждевременного удаления legacy compatibility layer.
Execution-потребители остаются отдельным последующим этапом миграции.