# Build 047 — Переключение `ExchangeService.get_klines()` на канонический Candles Feed

**Статус:** Completed  
**Дата:** 2026-07-15  
**Подсистема:** Market Data Acquisition / OHLCV Feed  
**Тип изменения:** Миграция legacy-потребителя на канонический Acquisition pipeline

---

## 1. Цель Build

Цель Build 047 — переключить существующий публичный метод:

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

с прямого получения и самостоятельной обработки ответа Dzengi `/api/v1/klines` на канонический pipeline подсистемы:

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

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

---

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

До Build 047 метод:

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

самостоятельно:

1. выполнял прямой REST-запрос к `/api/v1/klines`;
2. извлекал элементы ответа;
3. разбирал отдельные свечи;
4. создавал legacy-модели `Kline`;
5. формировал `KlineBatch`.

В `ExchangeService` существовали собственные legacy-функции обработки свечей:

```text
_parse_klines_payload()
_extract_klines_items()
_parse_kline_item()
```

Таким образом, после появления канонического Candles Feed в системе существовали два независимых пути получения и обработки OHLCV-данных.

Это создавало дублирование ответственности между:

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

и:

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

---

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

В Build 047 выполнено:

1. переключение внутренней реализации `ExchangeService.get_klines()` на канонический Candles Feed;
2. сохранение публичного контракта `ExchangeService.get_klines()`;
3. сохранение legacy-моделей `Kline` и `KlineBatch`;
4. добавление внутреннего адаптационного преобразования `Candle -> Kline`;
5. удаление прямого REST-запроса `/api/v1/klines` из `ExchangeService`;
6. удаление legacy-функций:
   - `_parse_klines_payload()`;
   - `_extract_klines_items()`;
   - `_parse_kline_item()`;
7. добавление специализированных unit-тестов для `ExchangeService.get_klines()` и его интеграции с каноническим Acquisition pipeline.

В Build 047 не выполнялось:

- изменение публичного контракта `ExchangeService.get_klines()`;
- изменение legacy-потребителей в `trading/market_analysis`;
- удаление моделей `Kline` и `KlineBatch`;
- прямое переключение `trading/market_analysis` на `CandlesAcquisitionService`;
- изменение архитектуры других Market Data feeds;
- изменение runtime-компонентов Acquisition.

---

## 4. Изменённые production-файлы

### 4.1. `src/integrations/exchange/service.py`

Метод:

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

сохранён как публичный compatibility-контракт.

Внутренний путь получения данных изменён.

До Build 047:

```text
ExchangeService.get_klines()
    ↓
прямой REST-запрос /api/v1/klines
    ↓
_parse_klines_payload()
    ↓
_extract_klines_items()
    ↓
_parse_kline_item()
    ↓
Kline
    ↓
KlineBatch
```

После Build 047:

```text
ExchangeService.get_klines()
    ↓
_load_candles_via_acquisition()
    ↓
DzengiCandlesDocumentSource
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
    ↓
CandlesAcquisitionService
    ↓
Candle
    ↓
_kline_from_candle()
    ↓
Kline
    ↓
KlineBatch
```

---

## 5. Канонический путь получения свечей

После Build 047 единственный production-путь прямого обращения к endpoint:

```text
/api/v1/klines
```

расположен в:

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

Это соответствует архитектурному разделению ответственности:

```text
Exchange-specific transport
    ↓
Market Data Acquisition
    ↓
Canonical Candle
    ↓
Legacy compatibility boundary
    ↓
Kline / KlineBatch
```

`ExchangeService` больше не владеет транспортной логикой получения OHLCV-данных от Dzengi.

---

## 6. Добавленные внутренние методы

### 6.1. `_load_candles_via_acquisition()`

Добавлен внутренний compatibility-helper:

```text
ExchangeService._load_candles_via_acquisition()
```

Его ответственность:

1. создать `DzengiCandlesDocumentSource`;
2. создать `DzengiCandlesDocumentHandler`;
3. создать `CandlesFeed`;
4. зарегистрировать feed в `CandlesFeedRegistry`;
5. создать `CandlesAcquisitionService`;
6. вызвать канонический `load_candles()`;
7. вернуть immutable-набор моделей `Candle`.

Метод является внутренней границей между legacy `ExchangeService` и канонической подсистемой Market Data Acquisition.

### 6.2. `_kline_from_candle()`

Добавлен внутренний адаптер:

```text
ExchangeService._kline_from_candle()
```

Преобразование выполняется по следующему правилу:

```text
Candle.symbol       -> Kline.symbol
Candle.interval     -> Kline.interval
Candle.open_time    -> Kline.open_time
Candle.open_price   -> Kline.open_price
Candle.high_price   -> Kline.high_price
Candle.low_price    -> Kline.low_price
Candle.close_price  -> Kline.close_price
Candle.volume       -> Kline.volume
```

Это позволяет сохранить существующий legacy-контракт без переноса legacy-моделей внутрь `market_data/acquisition`.

---

## 7. Удалённая legacy-логика

Из:

```text
src/integrations/exchange/service.py
```

удалены:

```text
_parse_klines_payload()
_extract_klines_items()
_parse_kline_item()
```

Также удалён прямой вызов:

```text
/api/v1/klines
```

из `ExchangeService`.

После Build 047 parsing, schema validation, value validation и mapping OHLCV-данных выполняются только канонической подсистемой:

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

---

## 8. Сохранённая обратная совместимость

Публичный контракт:

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

сохранён.

Существующие production-потребители не изменялись:

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

Они продолжают работать через:

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

и получать:

```text
KlineBatch
```

Таким образом, Build 047 меняет внутренний источник данных, но не требует одновременного переписывания legacy-потребителей.

---

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

Добавлен специализированный файл:

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

Тесты покрывают:

- использование `default_symbol`;
- нормализацию `limit`;
- нормализацию `interval`;
- отклонение неподдерживаемого interval;
- передачу `price_type`;
- обработку выключенной биржи;
- обработку неизвестного символа;
- передачу параметров в Acquisition pipeline;
- преобразование `Candle` в `Kline`;
- формирование `KlineBatch`;
- обработку пустого набора свечей;
- обработку исключений канонического Acquisition pipeline;
- построение полного composition pipeline;
- сохранение публичного legacy-контракта.

Результат targeted-проверки:

```text
26 passed in 0.17s
```

---

## 10. Полная регрессионная проверка

Выполнено:

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

Результат:

```text
750 passed in 2.83s
```

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

---

## 11. Контроль прямого использования `/api/v1/klines`

Команда:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  '"/api/v1/klines"' \
  src
```

Результат:

```text
src/market_data/acquisition/adapters/dzengi/rest.py:17:_KLINES_PATH = "/api/v1/klines"
```

Вывод:

Прямое знание endpoint `/api/v1/klines` находится только в каноническом Dzengi REST-адаптере.

---

## 12. Контроль удаления legacy-парсеров

Команда:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  "_parse_klines_payload\|_extract_klines_items\|_parse_kline_item" \
  src tests
```

Результат:

```text
пусто
```

Вывод:

Legacy-функции обработки kline payload полностью удалены.

---

## 13. Контроль оставшихся потребителей `ExchangeService.get_klines()`

Команда:

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

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

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

Дополнительно присутствуют специализированные вызовы в:

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

Вывод:

Публичный compatibility-контракт `ExchangeService.get_klines()` продолжает обслуживать существующие legacy-потребители.

---

## 14. Контроль интеграции с Acquisition pipeline

Команда:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  "CandlesAcquisitionService\|CandlesFeedRegistry\|DzengiCandlesDocumentSource" \
  src/integrations/exchange/service.py
```

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

```text
DzengiCandlesDocumentSource
CandlesFeedRegistry
CandlesAcquisitionService
```

внутри compatibility boundary `ExchangeService`.

---

## 15. Контроль качества diff

Выполнено:

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

Результат:

```text
пусто
```

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

---

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

После Build 047 путь OHLCV-данных имеет следующую структуру:

```text
Dzengi REST API
    ↓
DzengiCandlesDocumentSource
    ↓
schema validation
    ↓
parser
    ↓
value validation
    ↓
mapper
    ↓
Candle
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
    ↓
CandlesAcquisitionService
    ↓
ExchangeService compatibility boundary
    ↓
Kline / KlineBatch
    ↓
legacy trading consumers
```

Главный архитектурный результат:

```text
ExchangeService больше не получает и не разбирает
сырой ответ /api/v1/klines самостоятельно.
```

Владение получением и канонической обработкой OHLCV-данных теперь принадлежит:

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

---

## 17. Состояние после Build 047

На момент завершения Build:

```text
Targeted tests: 26 passed
Full test suite: 750 passed
git diff --check: clean
```

Прямой endpoint:

```text
/api/v1/klines
```

остался только в:

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

Legacy parsing helpers:

```text
_parse_klines_payload
_extract_klines_items
_parse_kline_item
```

полностью отсутствуют.

Публичный контракт:

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

сохранён.

---

## 18. Итог Build

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

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

Дублирующая legacy-логика получения и разбора `/api/v1/klines` удалена.

Существующие потребители продолжают работать без изменений.

Полная регрессионная проверка подтверждает отсутствие нарушений существующего поведения:

```text
750 passed
```