build 049: expose canonical candles through ExchangeService

This commit is contained in:
2026-07-15 20:54:24 +03:00
parent 3a253d89a9
commit 16ed64f7c6
3 changed files with 1010 additions and 7 deletions

View File

@@ -0,0 +1,569 @@
# 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-контракта.