build 049: expose canonical candles through ExchangeService
This commit is contained in:
569
docs/migrations/build_049.md
Normal file
569
docs/migrations/build_049.md
Normal 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-контракта.
|
||||
Reference in New Issue
Block a user