# 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:`; - прежнее сообщение 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-контракта.