build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

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