Files
dzentra_bot/docs/migrations/build_031.md

22 KiB
Raw Permalink Blame History

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

После завершения этапа выполняются следующие гарантии:

  1. ExchangeService больше не выполняет прямой REST-запрос к /api/v1/ticker/24hr.

  2. ExchangeService больше не знает транспортные поля:

    lastPrice
    bidPrice
    askPrice
    closeTime
    eventTime
    
  3. REST-котировка проходит через канонический Quotes Feed.

  4. Внутренним результатом Acquisition является:

    Quote
    
  5. Legacy snapshot создаётся только как временная compatibility projection.

  6. Существующие публичные контракты ExchangeService сохранены.

  7. MarketPriceCache пока не изменён.

  8. WebSocket-контур пока не изменён.

  9. UI-потребители пока не переведены напрямую на Quote.

  10. Execution-потребители пока не переведены напрямую на Quote.

  11. Полная регрессия проекта проходит успешно:

    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