# Build 030 — Quotes Feed и регистрация в Acquisition Service

**Статус:** Завершён  
**Подсистема:** Market Data Acquisition  
**Вертикаль:** Quotes Feed  
**Проект:** Dzentra  
**Тип изменения:** Архитектурная миграция без изменения поведения legacy runtime

---

## 1. Цель Build

Цель Build 030 — собрать ранее реализованные компоненты Quotes Feed в завершённую прикладную цепочку получения канонической котировки и зарегистрировать эту цепочку в слое Acquisition Service.

Build должен обеспечить следующий поток данных:

```text
Dzengi REST /api/v1/ticker/24hr
        ↓
DzengiQuoteDocumentSource
        ↓
DzengiQuoteDocumentHandler
        ↓
QuotesFeed
        ↓
QuoteFeedRegistry
        ↓
QuoteAcquisitionService
        ↓
Quote
```

На данном этапе новый Quotes Feed существует параллельно с legacy-контуром и ещё не подключается к `ExchangeService`, `MarketPriceCache`, market runtime, UI или Execution.

---

## 2. Предпосылки

К началу Build 030 были завершены предыдущие этапы:

```text
Build 027 — Каноническая модель Quote и специализированные контракты
Build 028 — Dzengi REST quote models, parser и validation
Build 029 — Dzengi mapper и Quotes Handler
```

В результате уже существовали:

- каноническая модель `Quote`;
- контракт `QuoteDocumentSource`;
- контракт `QuoteDocumentHandler`;
- контракт `QuoteFeedProtocol`;
- транспортная модель ответа Dzengi;
- schema validation;
- parser;
- value validation;
- mapper;
- `DzengiQuoteDocumentHandler`;
- специализированные исключения Quotes Feed.

Не хватало orchestration-слоя, связывающего эти компоненты в завершённый pipeline.

---

## 3. Границы Build

В Build 030 изменены следующие production-файлы:

```text
src/market_data/acquisition/adapters/dzengi/rest.py
src/market_data/acquisition/feeds/quotes_feed.py
src/market_data/acquisition/registry.py
src/market_data/acquisition/service.py
```

Добавлен новый файл тестов:

```text
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py
```

Расширены существующие тесты:

```text
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
tests/unit/market_data/acquisition/test_registry.py
tests/unit/market_data/acquisition/test_service.py
```

Следующие компоненты намеренно не изменялись:

```text
src/integrations/exchange/service.py
src/integrations/exchange/market_cache.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```

Также не изменялись:

- UI-потребители;
- Execution-потребители;
- торговые стратегии;
- runtime-контур;
- Quote Store;
- legacy market snapshot dict layer.

Эти изменения относятся к следующим Build.

---

## 4. Реализованная архитектура

### 4.1. REST source

В файле:

```text
src/market_data/acquisition/adapters/dzengi/rest.py
```

реализован специализированный источник:

```python
DzengiQuoteDocumentSource
```

Его ответственность ограничена получением сырого транспортного документа текущей котировки.

Целевая операция:

```python
fetch_quote_document(symbol: str) -> object
```

Источник выполняет запрос:

```text
GET /api/v1/ticker/24hr
```

с параметрами:

```python
{
    "symbol": symbol,
}
```

REST source:

- принимает торговый символ;
- передаёт его REST-клиенту без изменения;
- получает декодированный транспортный документ;
- возвращает исходный payload;
- преобразует транспортные ошибки в специализированную ошибку Quotes Feed.

REST source не выполняет:

- schema validation;
- parsing;
- value validation;
- mapping;
- кэширование;
- retry;
- нормализацию торгового символа.

---

## 5. Quotes Feed

В файле:

```text
src/market_data/acquisition/feeds/quotes_feed.py
```

реализован:

```python
QuotesFeed
```

Основная операция:

```python
load_quote(symbol: str) -> Quote
```

Внутренняя последовательность:

```text
symbol
    ↓
QuoteDocumentSource.fetch_quote_document(symbol)
    ↓
raw document
    ↓
QuoteDocumentHandler.handle_quote_document(document)
    ↓
Quote
```

`QuotesFeed` является orchestration-компонентом и не дублирует обязанности других слоёв.

Он не выполняет:

- транспортные запросы самостоятельно;
- schema validation;
- parsing;
- value validation;
- mapping;
- нормализацию символа;
- retry;
- кэширование;
- сохранение в Store;
- обращение к `ExchangeService`.

Ошибки source и handler не переоборачиваются повторно.

---

## 6. Quote Feed Registry

В файле:

```text
src/market_data/acquisition/registry.py
```

добавлен отдельный реестр:

```python
QuoteFeedRegistry
```

Существующий:

```python
InstrumentFeedRegistry
```

сохранён без архитектурного объединения с Quotes Feed.

Это позволяет:

- не изменять стабильный Instrument Reference Data contour;
- сохранить изоляцию вертикалей Acquisition;
- минимизировать область регрессии;
- избежать преждевременной универсализации registry.

`QuoteFeedRegistry` обеспечивает:

- регистрацию `QuoteFeedProtocol`;
- получение зарегистрированного Feed по имени источника;
- нормализацию внешних пробелов имени источника;
- запрет пустого имени;
- запрет повторной регистрации;
- runtime-проверку соответствия `QuoteFeedProtocol`;
- сохранение identity зарегистрированного объекта.

Ошибки registry представлены специализированным типом:

```python
QuoteFeedRegistryError
```

---

## 7. Quote Acquisition Service

В файле:

```text
src/market_data/acquisition/service.py
```

добавлен отдельный прикладной сервис:

```python
QuoteAcquisitionService
```

Основная операция:

```python
load_quote(
    source_name: str,
    symbol: str,
) -> Quote
```

Внутренняя последовательность:

```text
source_name
    ↓
QuoteFeedRegistry.get(source_name)
    ↓
QuoteFeedProtocol
    ↓
load_quote(symbol)
    ↓
Quote
```

Сервис:

- выбирает Feed через registry;
- передаёт `symbol` выбранному Feed без изменения;
- возвращает канонический `Quote`;
- не копирует полученную модель;
- не выполняет retry;
- не перехватывает и не переоборачивает ошибки registry или Feed.

Существующий:

```python
InstrumentAcquisitionService
```

не изменяет свою ответственность и продолжает обслуживать Instrument Reference Data.

---

## 8. Dependency Injection

В Build 030 сохранён уже применяемый в Instrument Reference Data подход явной сборки зависимостей.

Пример архитектурной сборки:

```python
source = DzengiQuoteDocumentSource(...)
handler = DzengiQuoteDocumentHandler(...)
feed = QuotesFeed(
    source=source,
    handler=handler,
)

registry = QuoteFeedRegistry()
registry.register("dzengi", feed)

service = QuoteAcquisitionService(
    registry=registry,
)
```

В Build намеренно не добавлены:

- глобальный singleton registry;
- автоматическая регистрация при импорте;
- скрытая сборка production pipeline внутри `QuoteAcquisitionService`;
- глобальное mutable-состояние для Feed.

Такое решение сохраняет:

- dependency injection;
- тестируемость;
- явные зависимости;
- изоляцию composition root от application service.

Фактическое подключение production pipeline к legacy facade отложено до Build 031.

---

## 9. Ответственности компонентов

| Компонент | Ответственность |
|---|---|
| `DzengiQuoteDocumentSource` | Получение сырого REST-документа котировки |
| `DzengiQuoteDocumentHandler` | Полная обработка документа до канонической модели |
| `QuotesFeed` | Оркестрация source → handler |
| `QuoteFeedRegistry` | Регистрация и выбор Quotes Feed |
| `QuoteAcquisitionService` | Прикладная точка получения `Quote` через выбранный Feed |
| `Quote` | Каноническое внутреннее представление текущей котировки |

---

## 10. Полная цепочка обработки

После завершения Build 030 REST Quotes Feed имеет следующую структуру:

```text
GET /api/v1/ticker/24hr
        ↓
DzengiQuoteDocumentSource
        ↓
raw object
        ↓
DzengiQuoteDocumentHandler
        ↓
validate_dzengi_quote_schema()
        ↓
parse_dzengi_quote_document()
        ↓
DzengiQuotePayload
        ↓
validate_dzengi_quote_values()
        ↓
map_dzengi_quote()
        ↓
Quote
        ↓
QuotesFeed
        ↓
QuoteFeedRegistry
        ↓
QuoteAcquisitionService
```

Таким образом, транспортный формат Dzengi полностью изолирован от внешних потребителей Acquisition.

---

## 11. Архитектурные ограничения

Build 030 намеренно не реализует следующие функции:

```text
ExchangeService facade integration
Quote Store
MarketPriceCache migration
WebSocket quote parsing
market runtime migration
read-only consumer migration
UI consumer migration
Execution consumer migration
legacy TickerPrice removal
legacy market snapshot dict removal
MarketPriceCache removal
```

Они относятся к следующим этапам:

```text
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
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
```

---

## 12. Тестовое покрытие

Build 030 покрывает следующие сценарии.

### 12.1. REST source

Проверяется:

- использование endpoint `/api/v1/ticker/24hr`;
- передача `symbol` в query parameters;
- возврат исходного payload;
- однократный вызов REST-клиента;
- поддержка dependency injection REST-клиента;
- создание стандартного REST-клиента при отсутствии injected client;
- преобразование транспортной ошибки в `QuoteTransportError`;
- сохранение исходной ошибки через `__cause__`.

### 12.2. Quotes Feed

Проверяется:

- соответствие `QuoteFeedProtocol`;
- однократный вызов source;
- передача `symbol` без изменения;
- однократный вызов handler;
- передача исходного документа handler без изменения;
- возврат `Quote` без копирования;
- отсутствие retry;
- отсутствие повторного переоборачивания ошибок.

### 12.3. Quote Feed Registry

Проверяется:

- регистрация корректного Feed;
- получение Feed по имени;
- нормализация внешних пробелов имени;
- запрет пустого имени;
- запрет повторной регистрации;
- проверка соответствия `QuoteFeedProtocol`;
- сохранение identity объекта;
- специализированные ошибки registry.

### 12.4. Quote Acquisition Service

Проверяется:

- передача `source_name` registry;
- передача `symbol` Feed без изменения;
- однократное обращение к registry;
- однократный вызов Feed;
- возврат `Quote` без копирования;
- сохранение ошибок registry;
- сохранение ошибок Feed;
- отсутствие retry.

---

## 13. Проверка компиляции

Выполнена команда:

```bash
python -m py_compile \
  src/market_data/acquisition/adapters/dzengi/rest.py \
  src/market_data/acquisition/feeds/quotes_feed.py \
  src/market_data/acquisition/registry.py \
  src/market_data/acquisition/service.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
  tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \
  tests/unit/market_data/acquisition/test_registry.py \
  tests/unit/market_data/acquisition/test_service.py
```

Результат:

```text
Успешно.
Ошибок компиляции нет.
```

---

## 14. Специализированные тесты

Выполнена команда:

```bash
python -m pytest \
  tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
  tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \
  tests/unit/market_data/acquisition/test_registry.py \
  tests/unit/market_data/acquisition/test_service.py \
  -q
```

Результат:

```text
89 passed in 0.06s
```

---

## 15. Полная регрессия

Выполнена команда:

```bash
python -m pytest -q
```

Результат:

```text
498 passed in 0.25s
```

Регрессий не обнаружено.

---

## 16. Результат Build

Build 030 завершён полностью.

Создана завершённая и протестированная вертикаль REST Quotes Feed:

```text
Dzengi REST API
        ↓
DzengiQuoteDocumentSource
        ↓
DzengiQuoteDocumentHandler
        ↓
QuotesFeed
        ↓
QuoteFeedRegistry
        ↓
QuoteAcquisitionService
        ↓
Quote
```

Новая вертикаль пока работает независимо от legacy runtime, что обеспечивает безопасную поэтапную миграцию без изменения поведения работающего торгового бота.

---

## 17. Следующий этап

Следующий этап утверждённого плана:

```text
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
```

Его цель — переключить REST-получение текущей котировки внутри существующего `ExchangeService` на новый канонический Quotes Feed, сохранив текущие публичные интерфейсы и поведение legacy-потребителей.

Целевая переходная схема:

```text
Legacy consumer
        ↓
ExchangeService facade
        ↓
QuoteAcquisitionService
        ↓
QuotesFeed
        ↓
DzengiQuoteDocumentSource
        ↓
Dzengi /api/v1/ticker/24hr
        ↓
Quote
        ↓
legacy-compatible projection
        ↓
Legacy consumer
```

До завершения последующих этапов `ExchangeService` остаётся совместимым фасадом между новой архитектурой Market Data Acquisition и существующими потребителями работающего бота.