# Build 026 — Аудит текущего контура Quotes Feed **Статус:** Завершён **Подсистема:** Market Data Acquisition **Функциональный модуль:** Quotes Feed **Проект:** Dzentra --- ## 1. Цель Build Провести полный аудит существующего контура получения, обработки, кэширования и потребления текущих рыночных котировок перед началом миграции в целевую подсистему: ```text src/market_data/acquisition/ ``` Основная задача Build — определить: - где сейчас реализовано получение котировок; - какие REST- и WebSocket-источники используются; - какие модели представляют котировку; - где выполняются parsing, validation и mapping; - как работает оперативный кэш котировок; - какие компоненты являются фактическими потребителями ценовых данных; - какие обязанности относятся непосредственно к Quotes Feed; - какие обязанности должны остаться за пределами Acquisition; - в какой последовательности выполнять безопасную миграцию без нарушения работы существующего бота. --- ## 2. Итог аудита Текущий бот уже имеет функционально работающий контур получения и использования котировок. Котировки поступают из двух источников: 1. REST API; 2. WebSocket depth stream. При этом архитектурно логика распределена между: ```text src/integrations/exchange/service.py src/integrations/exchange/rest_client.py src/integrations/exchange/ws_client.py src/integrations/exchange/market_stream.py src/integrations/exchange/market_data_runner.py src/integrations/exchange/market_cache.py src/integrations/exchange/models.py ``` Единой канонической модели `Quote` в production-контуре пока нет. Одна и та же концепция текущей рыночной котировки представлена несколькими различными контрактами: ```text TickerPrice ExecutionPriceSnapshot MarketPriceSnapshot dict[str, object] ``` Это подтверждает необходимость поэтапной миграции в каноническую модель Quotes Feed. --- ## 3. Текущий REST-контур котировок Основная реализация находится в: ```text src/integrations/exchange/service.py ``` Используются следующие методы: ```python refresh_price_cache() refresh_market_snapshot_cache() get_price() get_market_snapshot() get_execution_snapshot() get_fresh_market_snapshot() _get_real_price() ``` Основной источник данных: ```text GET /api/v1/ticker/24hr ``` Из ответа используются поля: ```text lastPrice bidPrice askPrice ``` Текущая цепочка выглядит следующим образом: ```text ExchangeService.get_fresh_market_snapshot() ↓ ExchangeRestClient.get_json() ↓ GET /api/v1/ticker/24hr ↓ lastPrice / bidPrice / askPrice ↓ legacy dict snapshot ``` REST-транспорт реализован в: ```text src/integrations/exchange/rest_client.py ``` --- ## 4. Текущий WebSocket-контур котировок В проекте существуют две реализации обработки WebSocket/depth-данных: ```text src/integrations/exchange/market_stream.py src/integrations/exchange/market_data_runner.py ``` WebSocket-транспорт находится в: ```text src/integrations/exchange/ws_client.py ``` Для получения данных используется: ```python ExchangeWebSocketClient.stream_depth() ``` Из depth payload извлекаются: ```text best bid best ask ``` После чего рассчитывается: ```text midpoint = (best_bid + best_ask) / 2 ``` Результат записывается в: ```text MarketPriceCache ``` --- ## 5. Текущие модели котировок ### 5.1. TickerPrice Находится в: ```text src/integrations/exchange/models.py ``` Текущий контракт: ```python @dataclass(slots=True) class TickerPrice: symbol: str price: float source: str updated_at: str ``` Используется как упрощённое представление текущей цены инструмента. --- ### 5.2. ExecutionPriceSnapshot Находится в: ```text src/integrations/exchange/models.py ``` Содержит: ```text symbol last_price bid_price ask_price updated_at source is_fresh age_seconds freshness_status spread_percent ``` Эта модель относится прежде всего к execution layer и не должна становиться канонической моделью Quotes Feed. --- ### 5.3. MarketPriceSnapshot Находится в: ```text src/integrations/exchange/market_cache.py ``` Содержит: ```text symbol price bid_price ask_price updated_at source runtime_key received_monotonic ``` Одновременно выполняет роль: - модели записи кэша; - контейнера рыночной цены; - источника информации о возрасте записи. --- ### 5.4. Словарные snapshot-контракты Ряд методов `ExchangeService` возвращает: ```python dict[str, object] ``` с ключами: ```text symbol last_price bid_price ask_price updated_at source age_seconds ``` Такие словарные контракты используются многими существующими потребителями и должны быть удалены только после их полного перевода на новые типизированные контракты. --- ## 6. Основные архитектурные проблемы ### 6.1. Отсутствует единая каноническая модель Quote Файл: ```text src/market_data/acquisition/models/quote.py ``` существует в целевой структуре, но текущий production-контур ещё не использует единую каноническую модель `Quote`. Вместо неё используются: ```text TickerPrice ExecutionPriceSnapshot MarketPriceSnapshot dict[str, object] ``` Целевая архитектура должна иметь одну внутреннюю каноническую модель котировки. --- ### 6.2. ExchangeService перегружен обязанностями В текущем состоянии `ExchangeService` одновременно: - вызывает REST API; - получает ticker response; - разбирает поля ответа; - проверяет значения; - создаёт snapshot; - читает кэш; - обновляет кэш; - оценивает freshness; - создаёт execution snapshot; - поддерживает legacy API для существующих потребителей. Эти обязанности должны быть постепенно разделены между: ```text adapters/dzengi/ validation/ models/ handlers/ feeds/ service.py storage/ execution/ ``` --- ### 6.3. WebSocket parsing дублируется Сходная логика присутствует одновременно в: ```text src/integrations/exchange/market_stream.py src/integrations/exchange/market_data_runner.py ``` Дублируются следующие операции: - извлечение вложенного payload; - извлечение `bids`; - извлечение `asks`; - получение первой цены; - преобразование значения в `float`; - проверка положительности цены; - расчёт midpoint. Эта логика должна быть централизована в Dzengi adapter: ```text src/market_data/acquisition/adapters/dzengi/parser.py src/market_data/acquisition/adapters/dzengi/mapper.py ``` --- ### 6.4. Quotes Feed и Order Book Feed частично смешаны Метод: ```python stream_depth() ``` получает depth-сообщение, относящееся к данным стакана. Однако текущие потребители используют из него только: ```text best bid best ask ``` Для Quotes Feed это допустимый источник Level I quote. При этом полный depth не должен переноситься в Quotes Feed, поскольку полный стакан относится к отдельной будущей подсистеме: ```text Order Book Feed ``` Таким образом, Quotes Feed должен получать из depth только необходимую информацию верхнего уровня: ```text best bid best ask ``` и формировать из неё канонический `Quote`. --- ### 6.5. Кэш расположен в integration layer Текущий кэш находится в: ```text src/integrations/exchange/market_cache.py ``` Он отвечает одновременно за: - модель snapshot; - хранение; - runtime partitioning; - возраст записи; - форматирование локального времени. В целевой архитектуре хранение котировок не должно принадлежать Acquisition или exchange integration layer. Канонический Quote Store должен находиться в storage layer. --- ### 6.6. Внутреннее время представлено UI-строкой Текущее представление: ```text DD.MM.YYYY HH:MM:SS ``` например: ```text 10.07.2026 12:00:00 ``` является человекочитаемым UI-представлением, а не подходящим внутренним временным контрактом. Каноническая модель должна хранить машинное время, например: ```text exchange_timestamp_ms received_timestamp_ms ``` или timezone-aware `datetime`. Форматирование времени для пользователя должно происходить только на UI-границе. --- ### 6.7. REST client содержит дублирование В: ```text src/integrations/exchange/rest_client.py ``` существуют два метода: ```python get_payload() get_json() ``` которые в значительной степени дублируют транспортную реализацию. Исправление этого дублирования не является задачей первого этапа Quotes Feed. Однако при дальнейшем развитии Dzengi REST adapter не следует создавать дополнительное дублирование транспорта. --- ### 6.8. Текущий WebSocket не является обычной push-subscription Метод: ```python stream_depth() ``` работает следующим образом: ```text открыть постоянное WebSocket-соединение ↓ отправить новый request ↓ получить один response ↓ сделать sleep ↓ повторить request ``` Таким образом, текущая реализация ближе к polling поверх постоянного WebSocket-соединения, чем к классической push-subscription. Кроме того, запуск WebSocket stream из: ```text src/main.py ``` временно отключён, поскольку runtime probe не подтвердил рабочий endpoint с WebSocket Upgrade 101. Поэтому на текущем этапе архитектурно зафиксировано: ```text REST — рабочий основной источник котировок WebSocket — сохраняемый экспериментальный или резервный транспорт ``` Первая версия нового Quotes Feed не должна зависеть от гарантированной доступности WebSocket. --- ## 7. Граница ответственности канонической модели Quote Каноническая модель должна представлять непосредственно полученную рыночную котировку. В неё должны входить данные уровня: ```text symbol last_price bid_price ask_price exchange_timestamp received_timestamp source ``` Дополнительно могут быть предусмотрены: ```text sequence_id event_id ``` но только если соответствующий источник Dzengi действительно предоставляет такие значения. --- ## 8. Что не должно входить в базовую модель Quote В каноническую модель не следует помещать: ```text runtime_key age_seconds is_fresh freshness_status spread_percent execution side entry price UI-formatted updated_at ``` Причины: | Поле | Правильная ответственность | |---|---| | `runtime_key` | Storage | | `age_seconds` | Storage / Access layer | | `is_fresh` | Политика конкретного потребителя | | `freshness_status` | Runtime / consumer policy | | `spread_percent` | Производная метрика | | `execution side` | Execution layer | | `entry price` | Execution layer | | `updated_at` в UI-формате | UI formatting | --- ## 9. Фактические потребители котировок ### 9.1. Потребители `get_price()` ```text src/telegram/ui/currency_ui.py src/telegram/handlers/auto/ui.py src/trading/auto/execution_quality.py ``` --- ### 9.2. Потребители `get_market_snapshot()` ```text src/telegram/handlers/auto/ui.py src/telegram/handlers/debug_auto/ui.py src/trading/auto/signal_runtime.py src/trading/auto/execution_quality.py src/trading/strategies/trend.py src/trading/strategies/scalp.py src/trading/diagnostics/snapshot.py ``` --- ### 9.3. Потребители `get_execution_snapshot()` ```text src/trading/execution/pricing.py src/telegram/handlers/debug_auto/ui.py ``` --- ### 9.4. Потребители `get_fresh_market_snapshot()` ```text src/integrations/exchange/service.py src/trading/debug/execution.py ``` Кроме того, этот метод используется внутри runtime-проверки статуса инструмента. --- ## 10. Текущий MarketPriceCache Реализация находится в: ```text src/integrations/exchange/market_cache.py ``` Основные операции: ```python MarketPriceCache.set_price() MarketPriceCache.get_price() MarketPriceCache.clear() ``` Ключ записи: ```text (runtime_key, symbol) ``` Кэш используется из: ```text src/integrations/exchange/service.py src/integrations/exchange/market_stream.py src/integrations/exchange/market_data_runner.py ``` На текущем этапе `MarketPriceCache` нельзя удалять, поскольку он является частью рабочего production-контура. Он будет заменён только после появления канонического Quote Store и перевода всех производителей и потребителей. --- ## 11. Целевая архитектурная цепочка REST Quotes Feed ```text Dzengi GET /api/v1/ticker/24hr ↓ adapters/dzengi/rest.py ↓ adapters/dzengi/models.py ↓ adapters/dzengi/parser.py ↓ validation/schema.py ↓ validation/values.py ↓ adapters/dzengi/mapper.py ↓ models/quote.py ↓ handlers/quotes_handler.py ↓ feeds/quotes_feed.py ↓ acquisition/service.py ↓ legacy ExchangeService facade ↓ существующие потребители бота ``` --- ## 12. Целевая архитектурная цепочка WebSocket Quotes Feed ```text Dzengi WebSocket depth message ↓ adapters/dzengi/websocket.py ↓ adapters/dzengi/parser.py ↓ извлечение best bid / best ask ↓ validation/ ↓ adapters/dzengi/mapper.py ↓ models/quote.py ↓ handlers/quotes_handler.py ↓ feeds/quotes_feed.py ↓ Quote Store ↓ runtime consumers ``` Полный order book при этом не является частью Quotes Feed и должен в будущем обрабатываться отдельной подсистемой: ```text Order Book Feed ``` --- ## 13. Принцип безопасной миграции Миграция должна выполняться без одномоментной замены рабочего контура. Основной принцип: ```text новая реализация создаётся параллельно ↓ покрывается тестами ↓ подключается под существующий facade ↓ потребители переводятся поэтапно ↓ legacy удаляется только после подтверждения отсутствия потребителей ``` На переходном этапе сохраняются: ```text ExchangeService.get_price() ExchangeService.get_market_snapshot() ExchangeService.get_execution_snapshot() ExchangeService.get_fresh_market_snapshot() MarketPriceCache TickerPrice ExecutionPriceSnapshot ``` Удаление допускается только в соответствующих поздних Build после полного перевода потребителей. --- ## 14. Утверждённый план миграции Quotes Feed ```text Build 026 — Аудит текущего контура Quotes Feed Build 027 — Каноническая модель Quote и специализированные контракты Build 028 — Dzengi REST quote models, parser и validation Build 029 — Dzengi mapper и Quotes Handler Build 030 — Quotes Feed и регистрация в Acquisition Service Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade Build 032 — Канонический Quote Store Build 033 — Перенос MarketPriceCache на Quote Store Build 034 — Dzengi WebSocket quote parsing и адаптер Build 035 — Перевод market runtime на Quotes Feed Build 036 — Перевод read-only и UI-потребителей Build 037 — Перевод execution-потребителей Build 038 — Удаление legacy TickerPrice и market snapshot dict layer Build 039 — Удаление legacy quote parsing и MarketPriceCache Build 040 — Финальная архитектурная проверка Quotes Feed ``` Положение WebSocket-этапа после рабочего REST-контура является намеренным. Бот должен сохранить гарантированный рабочий способ получения котировок даже при отсутствии подтверждённого production WebSocket endpoint. --- ## 15. Результат Build 026 В результате Build 026: - полностью определён существующий REST-контур котировок; - полностью определён существующий WebSocket/depth-контур; - найдены все текущие модели ценовых данных; - определены прямые производители и потребители котировок; - проанализирован `MarketPriceCache`; - обнаружено дублирование WebSocket parsing; - определена граница между Quotes Feed и Order Book Feed; - определена граница между Acquisition, Storage, Execution и UI; - подтверждена необходимость сохранения legacy facade на время миграции; - определена безопасная последовательность Build 027–040. --- ## 16. Статус завершения **Build 026 завершён полностью.** Дополнительных изменений кода в рамках Build 026 не требуется. Следующий этап: ```text Build 027 — Каноническая модель Quote и специализированные контракты ```