# Build 049 — Предоставление канонических Candle через публичный API ExchangeService

## Статус

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

---

## Цель Build

Предоставить существующим потребителям публичный канонический API получения рыночных свечей:

```text
ExchangeService.get_candles()
    ↓
tuple[Candle, ...]
```

без прямого доступа слоя Trading к деталям сборки `Market Data Acquisition` и без удаления действующего compatibility-контракта:

```text
ExchangeService.get_klines()
    ↓
KlineBatch
```

Build 049 подготавливает orchestration-компоненты `Market Analysis` к последующему поэтапному переключению с legacy-моделей `Kline` и `KlineBatch` на каноническую модель `Candle`.

---

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

До Build 049 `ExchangeService` уже использовал канонический Candles pipeline:

```text
DzengiCandlesDocumentSource
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
    ↓
CandlesAcquisitionService
    ↓
tuple[Candle, ...]
```

Однако публично сервис предоставлял только legacy-метод:

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

который:

1. выполнял проверку параметров;
2. вызывал канонический Acquisition pipeline;
3. преобразовывал `Candle` в `Kline`;
4. возвращал `KlineBatch`.

Из-за отсутствия публичного метода `get_candles()` непосредственное переключение `Market Analysis` на канонические модели потребовало бы либо использования внутреннего helper, либо дублирования composition chain в слое Trading.

---

## Объём изменений

В Build 049 изменены:

```text
src/integrations/exchange/service.py
tests/unit/integrations/exchange/test_service_candles.py
docs/migrations/build_049.md
```

Существующий файл:

```text
tests/unit/integrations/exchange/test_service_klines.py
```

не изменялся. Он использован как regression-контракт сохранения поведения legacy API.

Build 049 не изменяет:

```text
src/trading/market_analysis/service.py
src/trading/market_analysis/htf.py
src/market_data/acquisition/
src/integrations/exchange/models.py
```

Build не удаляет:

```text
ExchangeService.get_klines()
Kline
KlineBatch
_kline_from_candle()
```

---

## Новый публичный метод get_candles()

В `ExchangeService` добавлен метод:

```python
def get_candles(
    self,
    symbol: str | None = None,
    *,
    interval: str = "1m",
    limit: int = 200,
    price_type: str = "bid",
) -> tuple[Candle, ...]:
    ...
```

Метод возвращает immutable-набор канонических моделей:

```text
tuple[Candle, ...]
```

без преобразования в legacy-модели, без копирования и без сортировки результата.

---

## Ответственность get_candles()

`get_candles()` выполняет:

1. выбор `default_symbol`, если symbol не передан;
2. нормализацию `limit`;
3. проверку поддерживаемого interval;
4. нормализацию `price_type`;
5. проверку mock mode;
6. валидацию symbol;
7. вызов `_load_candles_via_acquisition()`;
8. логирование ошибок Acquisition pipeline;
9. преобразование ошибки в `ExchangeError`.

Нормализация limit:

```text
limit <= 0  → 200
limit > 200 → 200
иначе       → исходное значение
```

Поддерживаемые intervals:

```text
1m
5m
15m
1h
```

Поддерживаемые значения `price_type`:

```text
bid
ask
```

Неизвестное значение нормализуется в:

```text
bid
```

---

## Канонический путь получения данных

После Build 049 публичный канонический путь выглядит так:

```text
ExchangeService.get_candles()
    ↓
_load_candles_via_acquisition()
    ↓
DzengiCandlesDocumentSource
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
    ↓
CandlesAcquisitionService
    ↓
tuple[Candle, ...]
```

Слой Trading получает стабильную публичную точку доступа к каноническим свечам и не знает о:

```text
DzengiCandlesDocumentSource
DzengiCandlesDocumentHandler
CandlesFeedRegistry
CandlesAcquisitionService composition
```

---

## Изменение get_klines()

`ExchangeService.get_klines()` сохранён как legacy compatibility-фасад.

После Build 049 его цепочка:

```text
ExchangeService.get_klines()
    ↓
ExchangeService.get_candles()
    ↓
tuple[Candle, ...]
    ↓
_kline_from_candle()
    ↓
list[Kline]
    ↓
KlineBatch
```

`get_klines()` больше не вызывает `_load_candles_via_acquisition()` напрямую.

Таким образом, логика получения, проверки и обработки Acquisition-ошибок сосредоточена в одном публичном каноническом методе:

```text
get_candles()
```

---

## Сохранение legacy-контракта

Для `get_klines()` сохранены:

- публичная сигнатура;
- возвращаемый тип `KlineBatch`;
- нормализация limit;
- нормализация `price_type`;
- compatibility mapping `Candle → Kline`;
- источник вида `rest_klines:<price_type>`;
- прежнее сообщение mock mode:

```text
Klines are not available in mock exchange mode.
```

Для нового канонического API используется отдельное сообщение:

```text
Candles are not available in mock exchange mode.
```

Это сохраняет прежний контракт legacy-метода и одновременно вводит семантически корректный контракт нового API.

---

## Исключение повторной валидации symbol

При пустом наборе канонических свечей `get_klines()` не выполняет повторный вызов:

```text
validate_symbol()
```

Для заполнения symbol в пустом `KlineBatch` используется локальная нормализация:

```text
normalize_symbol(symbol_to_use)
```

Это сохраняет:

- один validation flow;
- один вызов `validate_symbol()`;
- корректный symbol пустого `KlineBatch`;
- отсутствие повторной загрузки Instrument Reference Data.

---

## Новый тестовый файл

Добавлен:

```text
tests/unit/integrations/exchange/test_service_candles.py
```

Тесты проверяют:

- использование `default_symbol`;
- нормализацию неположительного limit;
- ограничение limit значением 200;
- допустимые intervals;
- отклонение неподдерживаемого interval;
- нормализацию `price_type`;
- ошибку mock mode;
- ошибку invalid symbol;
- точные параметры вызова Acquisition helper;
- сохранение identity возвращённого tuple;
- сохранение порядка свечей;
- сохранение identity пустого tuple;
- логирование Acquisition-ошибок;
- преобразование ошибки в `ExchangeError`;
- сохранение исходного исключения как `__cause__`.

---

## Результаты targeted-тестов get_candles()

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

```bash
python -m pytest -q   tests/unit/integrations/exchange/test_service_candles.py
```

Результат:

```text
23 passed in 0.13s
```

---

## Совместная проверка canonical и legacy API

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

```bash
python -m pytest -q   tests/unit/integrations/exchange/test_service_candles.py   tests/unit/integrations/exchange/test_service_klines.py
```

Результат:

```text
49 passed in 0.13s
```

Это подтверждает одновременно:

```text
get_candles() → новый канонический контракт
get_klines()  → сохранённый legacy compatibility-контракт
```

---

## Полный regression suite

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

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

Результат:

```text
789 passed in 2.80s
```

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

---

## Архитектурная проверка get_candles()

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

```bash
grep -RIn   --exclude-dir="__pycache__"   --exclude="*.pyc"   "def get_candles\|\.get_candles("   src tests
```

Результат подтверждает:

- определение `get_candles()` находится в `ExchangeService`;
- `get_klines()` вызывает `get_candles()`;
- специализированные вызовы находятся в `test_service_candles.py`;
- production-компоненты `Market Analysis` пока не переключены.

---

## Контроль legacy-потребителей

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

```bash
grep -RIn   --exclude-dir="__pycache__"   --exclude="*.pyc"   "\.get_klines("   src/trading/market_analysis   tests/unit/integrations/exchange
```

Production-вызовы сохранены в:

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

Всего остаётся три production-вызова:

```text
MarketAnalysisService — один вызов
HTF orchestration — два вызова
```

Это ожидаемое переходное состояние.

---

## Контроль вызова Acquisition helper

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

```bash
grep -RIn   --exclude-dir="__pycache__"   --exclude="*.pyc"   "_load_candles_via_acquisition("   src/integrations/exchange/service.py   tests/unit/integrations/exchange
```

В production обнаружены:

```text
один вызов внутри get_candles()
одно определение _load_candles_via_acquisition()
```

`get_klines()` больше не обращается к Acquisition helper напрямую.

---

## Контроль composition и compatibility bridge

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

```bash
grep -nE   "def get_candles|def get_klines|self\.get_candles|_load_candles_via_acquisition|_kline_from_candle"   src/integrations/exchange/service.py
```

Результат подтверждает ожидаемую последовательность:

```text
get_candles()
    ↓
_load_candles_via_acquisition()

get_klines()
    ↓
self.get_candles()
    ↓
_kline_from_candle()
```

---

## Проверка форматирования

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

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

Вывод отсутствует.

Whitespace-ошибок не обнаружено.

---

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

После Build 049 доступны два явно разделённых публичных контракта.

Канонический API:

```text
ExchangeService.get_candles()
    ↓
tuple[Candle, ...]
```

Legacy compatibility API:

```text
ExchangeService.get_klines()
    ↓
get_candles()
    ↓
Candle → Kline
    ↓
KlineBatch
```

Получение данных и обработка ошибок больше не дублируются между двумя публичными методами.

---

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

Build 049 намеренно не включает:

- переключение `MarketAnalysisService` на `get_candles()`;
- переключение HTF orchestration на `get_candles()`;
- удаление `get_klines()`;
- удаление `Kline`;
- удаление `KlineBatch`;
- удаление `_kline_from_candle()`;
- изменение торговой логики;
- изменение алгоритмов Market Analysis;
- изменение `Market Data Acquisition`;
- изменение структуры каталогов;
- удаление рабочего compatibility-кода.

---

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

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

- добавлен публичный `ExchangeService.get_candles()`;
- метод возвращает канонический `tuple[Candle, ...]`;
- `get_klines()` использует `get_candles()` как единственный источник свечей;
- прямой вызов Acquisition helper из `get_klines()` устранён;
- legacy-контракт `KlineBatch` сохранён;
- повторная symbol validation устранена;
- новый canonical API покрыт специализированными тестами;
- legacy API проходит существующие regression-тесты;
- полный suite проходит;
- архитектурные grep соответствуют переходному состоянию;
- `git diff --check` чистый.

---

## Итог

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

Публичный канонический API:

```text
ExchangeService.get_candles()
```

готов для поэтапного переключения orchestration-компонентов `Market Analysis`.

Текущее состояние:

```text
Targeted canonical + legacy tests: 49 passed
Full test suite: 789 passed
git diff --check: clean
```

Следующий безопасный этап — отдельное переключение непосредственных production-потребителей с:

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

на:

```text
ExchangeService.get_candles()
```

без одновременного удаления legacy compatibility-контракта.
