Files
dzentra_bot/docs/migrations/build_031.md

894 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```