# 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-потребители остаются отдельным последующим этапом миграции.