# Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade ## Статус **Завершён.** --- ## Цель Подключить новый канонический контур **Quotes Feed** как внутренний источник свежих REST-котировок для существующего `ExchangeService`, сохранив без изменений его публичные legacy-контракты и поведение существующих потребителей. Основная архитектурная цель Build 031: ```text Dzengi GET /api/v1/ticker/24hr ↓ DzengiQuoteDocumentSource ↓ DzengiQuoteDocumentHandler ↓ QuotesFeed ↓ QuoteAcquisitionService ↓ Quote ↓ ExchangeService legacy facade ↓ существующие потребители ``` После Build 031 `ExchangeService` больше не должен самостоятельно: - выполнять прямой REST-запрос к `/api/v1/ticker/24hr`; - знать транспортные поля `lastPrice`, `bidPrice`, `askPrice`, `closeTime`; - разбирать сырой ответ ticker endpoint; - выполнять собственный parsing котировки Dzengi. Эти обязанности переданы специализированной подсистеме `market_data/acquisition`. --- ## Исходное состояние До Build 031 метод: ```python ExchangeService.get_fresh_market_snapshot() ``` самостоятельно выполнял полный legacy-процесс: ```text ExchangeService ↓ ExchangeRestClient ↓ GET /api/v1/ticker/24hr ↓ ручное чтение lastPrice / bidPrice / askPrice / closeTime ↓ legacy dict snapshot ``` В результате `ExchangeService` одновременно отвечал за: - транспорт; - знание конкретного endpoint Dzengi; - знание транспортной схемы Dzengi; - parsing значений; - формирование внутреннего представления котировки; - формирование legacy snapshot; - обработку freshness. Это нарушало архитектурное разделение ответственности. К моменту начала Build 031 новый канонический Quotes Feed уже был реализован: ```text DzengiQuoteDocumentSource ↓ DzengiQuoteDocumentHandler ↓ QuotesFeed ↓ QuoteFeedRegistry ↓ QuoteAcquisitionService ↓ Quote ``` Задачей Build 031 стало подключение этого контура под существующий `ExchangeService` facade. --- ## Объём изменений ### Изменён production-файл ```text src/integrations/exchange/service.py ``` ### Добавлен тестовый файл ```text tests/unit/integrations/exchange/test_service_quotes_facade.py ``` ### Не изменялись ```text src/integrations/exchange/models.py src/integrations/exchange/market_cache.py src/integrations/exchange/mock_data.py src/market_data/acquisition/adapters/dzengi/rest.py src/market_data/acquisition/feeds/quotes_feed.py src/market_data/acquisition/handlers/quotes_handler.py src/market_data/acquisition/models/quote.py src/market_data/acquisition/registry.py src/market_data/acquisition/service.py src/market_data/acquisition/exceptions.py ``` Новый Acquisition-контур уже содержал всю необходимую функциональность и не потребовал дополнительных изменений. --- ## Реализованная архитектура После Build 031 получение свежей REST-котировки выполняется по следующей цепочке: ```text ExchangeService.get_fresh_market_snapshot() ↓ QuoteAcquisitionService ↓ QuoteFeedRegistry ↓ QuotesFeed ↓ DzengiQuoteDocumentSource ↓ GET /api/v1/ticker/24hr ↓ DzengiQuoteDocumentHandler ↓ schema validation ↓ parser ↓ value validation ↓ mapper ↓ canonical Quote ↓ legacy-compatible snapshot dict ``` Таким образом, граница ответственности теперь выглядит следующим образом: ```text market_data/acquisition │ │ отвечает за получение, проверку, │ parsing и mapping котировки ↓ canonical Quote │ │ временная compatibility boundary ↓ ExchangeService facade │ │ сохраняет старые публичные контракты ↓ legacy consumers ``` --- ## Подключение Quote Acquisition pipeline В `ExchangeService` добавлен внутренний путь получения канонической котировки через уже реализованные компоненты Quotes Feed. Используемая цепочка: ```text DzengiQuoteDocumentSource ↓ DzengiQuoteDocumentHandler ↓ QuotesFeed ↓ QuoteFeedRegistry ↓ QuoteAcquisitionService ↓ Quote ``` `ExchangeService` теперь получает готовую каноническую модель: ```python Quote ``` вместо сырого ответа Dzengi: ```python dict[str, object] ``` Это устраняет зависимость facade от транспортной схемы `ticker/24hr`. --- ## Изменение get_fresh_market_snapshot() До Build 031 метод самостоятельно выполнял: ```text создание ExchangeRestClient ↓ вызов /api/v1/ticker/24hr ↓ чтение lastPrice ↓ чтение bidPrice ↓ чтение askPrice ↓ чтение closeTime / eventTime ↓ преобразование значений ↓ формирование snapshot ``` После Build 031 метод получает: ```python quote = self._load_quote_via_acquisition( validation.normalized_symbol, ) ``` После чего выполняет только временную legacy-проекцию: ```text Quote ↓ legacy-compatible dict snapshot ``` Таким образом, `get_fresh_market_snapshot()` больше не является parser транспортного ответа Dzengi. --- ## Удалённый legacy parsing Из REST quote-пути `ExchangeService` удалено прямое знание следующих транспортных полей: ```text lastPrice bidPrice askPrice closeTime eventTime ``` Также удалён прямой вызов: ```text GET /api/v1/ticker/24hr ``` из `ExchangeService`. Теперь endpoint и его транспортная схема принадлежат исключительно адаптеру: ```text src/market_data/acquisition/adapters/dzengi/ ``` Это соответствует утверждённой архитектуре Acquisition. --- ## Legacy-compatible projection Build 031 намеренно не удаляет legacy snapshot layer. Каноническая модель: ```python Quote ``` временно преобразуется обратно в: ```python dict[str, object] ``` с сохранением прежней структуры: ```python { "symbol": ..., "last_price": ..., "bid_price": ..., "ask_price": ..., "updated_at": ..., "source": "fresh_rest", "age_seconds": ..., "is_fresh": ..., } ``` Это необходимо для безопасной поэтапной миграции существующего работающего бота. Удаление этого compatibility layer запланировано на: ```text Build 038 — Удаление legacy TickerPrice и market snapshot dict layer ``` --- ## Сохранение числового контракта Каноническая модель `Quote` использует точные числовые значения, представленные через `Decimal`. Legacy-потребители ожидают `float`. Поэтому на временной границе совместимости выполняется преобразование: ```text Quote Decimal ↓ ExchangeService compatibility boundary ↓ legacy float ``` То есть точность сохраняется внутри новой канонической подсистемы, а преобразование выполняется только при передаче данных старым потребителям. Это временное решение до полного перевода потребителей на канонический `Quote`. --- ## Сохранение symbol contract Перед получением котировки сохраняется существующая проверка символа: ```python validation = self.validate_symbol(symbol_to_use) ``` В новый Acquisition pipeline передаётся: ```python validation.normalized_symbol ``` Таким образом: - невалидный символ не передаётся в Quotes Feed; - используется канонически нормализованный символ; - существующее поведение `ExchangeService` сохраняется. --- ## Сохранение source contract Канонический `Quote` содержит источник Acquisition: ```text dzengi ``` Однако существующий legacy snapshot использует: ```text fresh_rest ``` В Build 031 сохранено прежнее значение: ```python "source": "fresh_rest" ``` Это исключает непреднамеренное изменение поведения: - UI; - журналирования; - диагностики; - runtime; - существующих потребителей, потенциально зависящих от значения `source`. Переход на каноническую семантику источника должен выполняться отдельно при удалении legacy snapshot layer. --- ## Сохранение timestamp contract Канонический `Quote` содержит timezone-aware timestamp. На legacy-границе сохраняется прежнее представление: ```text Quote.exchange_timestamp ↓ timestamp в миллисекундах ↓ существующие ExchangeService helpers ↓ updated_at age_seconds is_fresh ``` Благодаря этому существующие потребители не получают изменения временной семантики. --- ## Сохранение freshness contract Сохранена существующая логика определения свежести REST-котировки. Порог: ```text 60 секунд ``` Результат продолжает содержать: ```python "age_seconds": ... "is_fresh": ... ``` Условие остаётся эквивалентным прежнему: ```python is_fresh = ( age_seconds is not None and age_seconds <= 60 ) ``` Build 031 не меняет политику freshness. --- ## Сохранение публичных контрактов ExchangeService После Build 031 сохранены без изменения следующие публичные методы: ```text get_price() -> TickerPrice get_fresh_market_snapshot() -> dict[str, object] refresh_price_cache() -> TickerPrice refresh_market_snapshot_cache() -> dict[str, object] get_market_snapshot() -> dict[str, object] get_execution_snapshot() -> ExecutionPriceSnapshot ``` Это позволяет существующим потребителям продолжать работу без изменений. В частности, не потребовалось изменять: ```text src/telegram/ui/currency_ui.py 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/execution/pricing.py src/trading/diagnostics/snapshot.py src/trading/debug/execution.py ``` --- ## Влияние на существующие методы ExchangeService Методы: ```text get_price() get_market_snapshot() get_execution_snapshot() refresh_market_snapshot_cache() refresh_price_cache() _get_real_price() get_symbol_runtime_status() ``` продолжают работать через существующие публичные и внутренние контракты. Поскольку свежая REST-котировка теперь поступает через: ```text get_fresh_market_snapshot() ↓ Quote Acquisition pipeline ``` существующие методы автоматически используют новый канонический REST Quotes Feed без прямого перевода каждого потребителя. --- ## Обработка ошибок Новый Acquisition-контур использует специализированные ошибки quote-подсистемы. Внешний контракт `ExchangeService` продолжает использовать: ```python ExchangeError ``` Поэтому на границе facade сохраняется адаптация: ```text Acquisition error ↓ ExchangeService compatibility boundary ↓ ExchangeError ``` При этом исходная ошибка сохраняется как: ```python __cause__ ``` Это обеспечивает одновременно: - совместимость существующих потребителей; - сохранение исходного контекста ошибки; - возможность диагностики первопричины; - отсутствие утечки новой модели исключений в legacy-код раньше запланированного этапа миграции. --- ## Сохранение mock-режима При отключённой реальной биржевой интеграции: ```python exchange_enabled = False ``` новый Acquisition pipeline не вызывается. Сохраняется прежний mock-контур: ```text ExchangeService ↓ mock_ticker_price() ↓ legacy snapshot ``` Build 031 не изменяет поведение mock-режима. --- ## Тестовое покрытие Добавлен специализированный тестовый файл: ```text tests/unit/integrations/exchange/test_service_quotes_facade.py ``` Тесты проверяют границу между: ```text canonical Quote Acquisition ``` и: ```text legacy ExchangeService facade ``` Проверяемые свойства включают: - использование нового Acquisition pipeline; - передачу нормализованного символа; - сохранение legacy snapshot contract; - преобразование канонических числовых значений в legacy-compatible значения; - сохранение `source="fresh_rest"`; - сохранение timestamp contract; - сохранение freshness contract; - адаптацию ошибок в `ExchangeError`; - сохранение исходной ошибки в `__cause__`; - отсутствие вызова нового Acquisition pipeline в mock-режиме; - отсутствие прямого ticker REST parsing в новом facade-пути. --- ## Регрессионная проверка runtime status Дополнительно выполнен существующий набор тестов: ```text tests/unit/integrations/exchange/test_service_symbol_runtime_status.py ``` Он подтверждает сохранение внешнего runtime-контракта после переключения внутреннего источника REST-котировки. --- ## Выполненные проверки ### Проверка синтаксиса ```bash python -m py_compile \ src/integrations/exchange/service.py \ tests/unit/integrations/exchange/test_service_quotes_facade.py ``` Результат: ```text успешно ``` ### Специализированные и регрессионные тесты ```bash python -m pytest \ tests/unit/integrations/exchange/test_service_quotes_facade.py \ tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ -q ``` Результат: ```text 38 passed in 0.08s ``` ### Полная регрессия проекта ```bash python -m pytest -q ``` Результат: ```text 502 passed in 0.25s ``` --- ## Рост тестового покрытия После предыдущего этапа: ```text Build 030 498 passed ``` После завершения Build 031: ```text Build 031 502 passed ``` Добавлено: ```text 4 новых теста ``` Полная регрессия остаётся зелёной. --- ## Архитектурный результат ### До Build 031 ```text ExchangeService ↓ ExchangeRestClient ↓ GET /api/v1/ticker/24hr ↓ ручной parsing полей Dzengi ↓ legacy dict snapshot ↓ потребители ``` ### После Build 031 ```text ExchangeService legacy facade ↓ QuoteAcquisitionService ↓ QuoteFeedRegistry ↓ QuotesFeed ↓ DzengiQuoteDocumentSource ↓ DzengiQuoteDocumentHandler ↓ schema validation ↓ parser ↓ value validation ↓ mapper ↓ canonical Quote ↓ legacy-compatible dict projection ↓ существующие потребители ``` --- ## Архитектурные гарантии после Build 031 После завершения этапа выполняются следующие гарантии: 1. `ExchangeService` больше не выполняет прямой REST-запрос к `/api/v1/ticker/24hr`. 2. `ExchangeService` больше не знает транспортные поля: ```text lastPrice bidPrice askPrice closeTime eventTime ``` 3. REST-котировка проходит через канонический Quotes Feed. 4. Внутренним результатом Acquisition является: ```python Quote ``` 5. Legacy snapshot создаётся только как временная compatibility projection. 6. Существующие публичные контракты `ExchangeService` сохранены. 7. `MarketPriceCache` пока не изменён. 8. WebSocket-контур пока не изменён. 9. UI-потребители пока не переведены напрямую на `Quote`. 10. Execution-потребители пока не переведены напрямую на `Quote`. 11. Полная регрессия проекта проходит успешно: ```text 502 passed ``` --- ## Что намеренно не входит в Build 031 Build 031 не реализует: ```text Quote Store перенос MarketPriceCache удаление MarketPriceCache изменение MarketPriceSnapshot WebSocket quote parsing WebSocket quote adapter перевод market runtime на Quotes Feed прямой перевод UI на Quote прямой перевод execution на Quote удаление TickerPrice удаление ExecutionPriceSnapshot удаление legacy market snapshot dict layer удаление legacy quote compatibility layer ``` Эти изменения выполняются последующими Build по утверждённому плану. --- ## Следующий этап ```text Build 032 — Канонический Quote Store ``` Его задача — создать канонический слой хранения текущих котировок, который станет основой для последующего переноса существующего: ```text MarketPriceCache ``` на новую архитектуру. Последовательность дальнейшей миграции: ```text 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 ``` --- ## Итог **Build 031 завершён полностью.** Новый канонический REST Quotes Feed стал внутренним источником свежих котировок для `ExchangeService`, при этом существующий работающий бот сохранил прежние публичные контракты. Ключевой результат: ```text Было: ExchangeService ↓ прямой REST ticker/24hr ↓ ручной parsing ↓ legacy snapshot ``` ```text Стало: ExchangeService facade ↓ canonical Quotes Feed ↓ Quote ↓ временная legacy projection ↓ существующие потребители ``` Это создаёт безопасную архитектурную основу для следующего этапа: ```text Build 032 — Канонический Quote Store ```