# Build 046 — Интеграция канонического Candles Feed в сервисный слой Market Data Acquisition

## Статус

**Завершён**

---

## Цель Build

Интегрировать канонический `Candles Feed` в существующий сервисный слой подсистемы:

```text
src/market_data/acquisition/
```

без переключения legacy-потребителей `ExchangeService.get_klines()` и без изменения существующего поведения работающего торгового бота.

Build продолжает поэтапную миграцию получения OHLCV-данных из legacy-подсистемы:

```text
src/integrations/exchange/
```

в каноническую подсистему:

```text
src/market_data/acquisition/
```

---

## Исходное состояние

До начала Build 046 канонический OHLCV-контур уже включал:

```text
Candle
    ↓
DzengiCandlesDocumentSource
    ↓
schema validation
    ↓
parser
    ↓
value validation
    ↓
mapper
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
```

Однако сервисный слой `Market Data Acquisition` ещё не предоставлял application-level сервис для получения канонического набора свечей.

В файле:

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

существовали отдельные сервисы для:

```text
InstrumentAcquisitionService
QuoteAcquisitionService
```

При этом `CandlesAcquisitionService` отсутствовал.

---

## Принятое архитектурное решение

В рамках Build 046 не создавался новый объединяющий класс `MarketDataAcquisitionService`.

Существующая архитектура сервисного слоя использует отдельный application-level сервис для каждого типа рыночных данных:

```text
InstrumentAcquisitionService
QuoteAcquisitionService
```

Поэтому для OHLCV-данных добавлен отдельный:

```text
CandlesAcquisitionService
```

Это сохраняет существующий архитектурный паттерн и не требует изменения уже работающих контрактов Instrument Reference Data и Quotes Feed.

---

## Границы Build

В рамках Build 046 изменены только:

```text
src/market_data/acquisition/service.py
tests/unit/market_data/acquisition/test_service.py
docs/migrations/build_046.md
```

Не изменялись:

```text
src/market_data/acquisition/registry.py
src/market_data/acquisition/protocol.py
src/market_data/acquisition/feeds/candles_feed.py
src/market_data/acquisition/handlers/candles_handler.py
src/market_data/acquisition/adapters/dzengi/
src/integrations/exchange/service.py
src/integrations/exchange/models.py
src/trading/market_analysis/
```

---

## Реализованные изменения

### 1. Добавлен `CandlesAcquisitionService`

В файл:

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

добавлен класс:

```python
class CandlesAcquisitionService:
    def __init__(
        self,
        *,
        registry: CandlesFeedRegistry,
    ) -> None:
        self._registry = registry

    def load_candles(
        self,
        source_name: str,
        symbol: str,
        *,
        interval: str,
        limit: int,
        price_type: str,
    ) -> tuple[Candle, ...]:
        feed = self._registry.get(source_name)

        return feed.load_candles(
            symbol,
            interval=interval,
            limit=limit,
            price_type=price_type,
        )
```

---

### 2. Добавлены зависимости сервисного слоя

В `service.py` добавлены импорты:

```python
from src.market_data.acquisition.models.candle import Candle
```

и:

```python
from src.market_data.acquisition.registry import CandlesFeedRegistry
```

---

### 3. Зафиксирована ответственность `CandlesAcquisitionService`

`CandlesAcquisitionService` выполняет только две операции:

1. получает зарегистрированный `Candles Feed` по имени источника;
2. делегирует ему загрузку свечей.

Каноническая цепочка:

```text
source_name
    ↓
CandlesFeedRegistry.get()
    ↓
CandlesFeedProtocol
    ↓
load_candles()
    ↓
tuple[Candle, ...]
```

Сервис не выполняет:

```text
нормализацию source_name
нормализацию symbol
валидацию interval
ограничение limit
нормализацию price_type
schema validation
parsing
value validation
mapping
REST-запросы
retry
кэширование
сортировку свечей
копирование результата
оборачивание исключений
```

Это сохраняет строгие границы ответственности между слоями.

---

## Контракт `CandlesAcquisitionService`

Метод:

```python
load_candles(
    source_name: str,
    symbol: str,
    *,
    interval: str,
    limit: int,
    price_type: str,
) -> tuple[Candle, ...]
```

гарантирует следующую последовательность:

```text
1. Получить Feed:
   registry.get(source_name)

2. Передать Feed исходные параметры:
   symbol
   interval
   limit
   price_type

3. Вернуть результат Feed без преобразований.
```

Сервис не изменяет входные параметры.

Сервис не изменяет порядок свечей.

Сервис сохраняет identity возвращённого immutable `tuple`.

Сервис не выполняет автоматический retry при ошибке.

Исключения нижележащих слоёв пробрасываются без оборачивания.

---

## Добавленные тесты

В:

```text
tests/unit/market_data/acquisition/test_service.py
```

добавлено тестовое покрытие `CandlesAcquisitionService`.

Проверяются следующие свойства:

1. сервис возвращает результат зарегистрированного Feed;
2. сохраняется identity возвращённого `tuple`;
3. `source_name` передаётся в registry без изменений;
4. registry вызывается ровно один раз;
5. `symbol` передаётся в Feed без изменений;
6. `interval` передаётся без изменений;
7. `limit` передаётся без изменений;
8. `price_type` передаётся без изменений;
9. Feed вызывается ровно один раз;
10. пустой `tuple` возвращается без изменения;
11. порядок свечей сохраняется;
12. `CandleFeedRegistryError` пробрасывается без оборачивания;
13. после ошибки registry Feed не вызывается;
14. ошибки Feed пробрасываются без оборачивания;
15. после ошибки Feed автоматический retry не выполняется.

Для проверки ошибок Feed используются:

```text
CandleTransportError
CandleValueError
CandleMappingError
```

---

## Результаты проверок

### Компиляция

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

```bash
python -m compileall \
  src/market_data/acquisition/service.py \
  tests/unit/market_data/acquisition/test_service.py
```

Результат:

```text
Compiling 'src/market_data/acquisition/service.py'...
Compiling 'tests/unit/market_data/acquisition/test_service.py'...
```

Ошибок компиляции нет.

---

### Targeted tests

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

```bash
python -m pytest -q \
  tests/unit/market_data/acquisition/test_service.py
```

Результат:

```text
40 passed in 0.03s
```

---

### Полный test suite

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

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

Результат:

```text
724 passed in 2.43s
```

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

---

### Проверка Git diff

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

```bash
git diff --check
```

Результат:

```text
пустой вывод
```

Whitespace errors отсутствуют.

---

## Архитектурный результат

После Build 046 канонический OHLCV-контур имеет следующую структуру:

```text
External Dzengi API
        ↓
DzengiCandlesDocumentSource
        ↓
raw transport document
        ↓
validate_candles_schema()
        ↓
parse_candles()
        ↓
validate_candles_values()
        ↓
map_candles()
        ↓
DzengiCandlesDocumentHandler
        ↓
CandlesFeed
        ↓
CandlesFeedRegistry
        ↓
CandlesAcquisitionService
        ↓
tuple[Candle, ...]
```

Таким образом, канонический Candles Feed теперь имеет полный application-level путь от внешнего REST API до сервисного слоя `Market Data Acquisition`.

---

## Состояние legacy-контура

В рамках Build 046 legacy-метод:

```text
ExchangeService.get_klines()
```

не изменялся и не удалялся.

Legacy-потребители:

```text
src/trading/market_analysis/service.py
src/trading/market_analysis/htf.py
```

продолжают использовать существующий API.

Это соответствует основному правилу миграции Dzentra:

> Сначала строится и проверяется новый канонический путь, затем потребители переключаются на него поэтапно, и только после подтверждения полной совместимости удаляется legacy-код.

---

## Что намеренно не выполнялось

Build 046 не включает:

```text
переключение ExchangeService.get_klines()
переключение trading/market_analysis
удаление legacy KlineBatch
удаление legacy Kline
удаление legacy parser
удаление legacy endpoint /api/v1/klines
регистрацию production Candles Feed на composition root
изменение runtime
изменение scheduler
кэширование свечей
хранение свечей
проверку последовательности свечей
```

Эти задачи должны выполняться отдельными Build с собственными границами и проверками.

---

## Критерии завершения

Build 046 считается завершённым, поскольку:

- `CandlesAcquisitionService` реализован;
- сервис использует `CandlesFeedRegistry`;
- сервис возвращает канонический `tuple[Candle, ...]`;
- входные параметры передаются без изменений;
- результат не копируется и не сортируется;
- ошибки не оборачиваются;
- retry отсутствует;
- существующие Instrument и Quote сервисы не нарушены;
- targeted tests успешно проходят;
- полный test suite успешно проходит;
- `git diff --check` не обнаруживает ошибок;
- legacy-потребители не переключались;
- работающий торговый бот сохраняет обратную совместимость.

---

## Итог

**Build 046 завершён успешно.**

Канонический `Candles Feed` интегрирован в сервисный слой `Market Data Acquisition`.

Текущий завершённый путь:

```text
Dzengi REST API
    ↓
DzengiCandlesDocumentSource
    ↓
Schema Validation
    ↓
Parser
    ↓
Value Validation
    ↓
Mapper
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
    ↓
CandlesAcquisitionService
    ↓
tuple[Candle, ...]
```

Следующий Build должен продолжить миграцию OHLCV-контура без преждевременного удаления legacy-реализации и без нарушения работы существующего торгового бота.