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