# 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
```