515 lines
13 KiB
Markdown
515 lines
13 KiB
Markdown
# Build 046 — Интеграция канонического Candles Feed в сервисный слой Market Data Acquisition
|
||
|
||
## Статус
|
||
|
||
**Завершён**
|
||
|
||
---
|
||
|
||
## Цель Build
|
||
|
||
Интегрировать канонический `Candles Feed` в существующий сервисный слой подсистемы:
|
||
|
||
```text
|
||
src/market_data/acquisition/
|
||
```
|
||
|
||
без переключения legacy-потребителей `ExchangeService.get_klines()` и без изменения существующего поведения работающего торгового бота.
|
||
|
||
Build продолжает поэтапную миграцию получения OHLCV-данных из legacy-подсистемы:
|
||
|
||
```text
|
||
src/integrations/exchange/
|
||
```
|
||
|
||
в каноническую подсистему:
|
||
|
||
```text
|
||
src/market_data/acquisition/
|
||
```
|
||
|
||
---
|
||
|
||
## Исходное состояние
|
||
|
||
До начала Build 046 канонический OHLCV-контур уже включал:
|
||
|
||
```text
|
||
Candle
|
||
↓
|
||
DzengiCandlesDocumentSource
|
||
↓
|
||
schema validation
|
||
↓
|
||
parser
|
||
↓
|
||
value validation
|
||
↓
|
||
mapper
|
||
↓
|
||
DzengiCandlesDocumentHandler
|
||
↓
|
||
CandlesFeed
|
||
↓
|
||
CandlesFeedRegistry
|
||
```
|
||
|
||
Однако сервисный слой `Market Data Acquisition` ещё не предоставлял application-level сервис для получения канонического набора свечей.
|
||
|
||
В файле:
|
||
|
||
```text
|
||
src/market_data/acquisition/service.py
|
||
```
|
||
|
||
существовали отдельные сервисы для:
|
||
|
||
```text
|
||
InstrumentAcquisitionService
|
||
QuoteAcquisitionService
|
||
```
|
||
|
||
При этом `CandlesAcquisitionService` отсутствовал.
|
||
|
||
---
|
||
|
||
## Принятое архитектурное решение
|
||
|
||
В рамках Build 046 не создавался новый объединяющий класс `MarketDataAcquisitionService`.
|
||
|
||
Существующая архитектура сервисного слоя использует отдельный application-level сервис для каждого типа рыночных данных:
|
||
|
||
```text
|
||
InstrumentAcquisitionService
|
||
QuoteAcquisitionService
|
||
```
|
||
|
||
Поэтому для OHLCV-данных добавлен отдельный:
|
||
|
||
```text
|
||
CandlesAcquisitionService
|
||
```
|
||
|
||
Это сохраняет существующий архитектурный паттерн и не требует изменения уже работающих контрактов Instrument Reference Data и Quotes Feed.
|
||
|
||
---
|
||
|
||
## Границы Build
|
||
|
||
В рамках Build 046 изменены только:
|
||
|
||
```text
|
||
src/market_data/acquisition/service.py
|
||
tests/unit/market_data/acquisition/test_service.py
|
||
docs/migrations/build_046.md
|
||
```
|
||
|
||
Не изменялись:
|
||
|
||
```text
|
||
src/market_data/acquisition/registry.py
|
||
src/market_data/acquisition/protocol.py
|
||
src/market_data/acquisition/feeds/candles_feed.py
|
||
src/market_data/acquisition/handlers/candles_handler.py
|
||
src/market_data/acquisition/adapters/dzengi/
|
||
src/integrations/exchange/service.py
|
||
src/integrations/exchange/models.py
|
||
src/trading/market_analysis/
|
||
```
|
||
|
||
---
|
||
|
||
## Реализованные изменения
|
||
|
||
### 1. Добавлен `CandlesAcquisitionService`
|
||
|
||
В файл:
|
||
|
||
```text
|
||
src/market_data/acquisition/service.py
|
||
```
|
||
|
||
добавлен класс:
|
||
|
||
```python
|
||
class CandlesAcquisitionService:
|
||
def __init__(
|
||
self,
|
||
*,
|
||
registry: CandlesFeedRegistry,
|
||
) -> None:
|
||
self._registry = registry
|
||
|
||
def load_candles(
|
||
self,
|
||
source_name: str,
|
||
symbol: str,
|
||
*,
|
||
interval: str,
|
||
limit: int,
|
||
price_type: str,
|
||
) -> tuple[Candle, ...]:
|
||
feed = self._registry.get(source_name)
|
||
|
||
return feed.load_candles(
|
||
symbol,
|
||
interval=interval,
|
||
limit=limit,
|
||
price_type=price_type,
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Добавлены зависимости сервисного слоя
|
||
|
||
В `service.py` добавлены импорты:
|
||
|
||
```python
|
||
from src.market_data.acquisition.models.candle import Candle
|
||
```
|
||
|
||
и:
|
||
|
||
```python
|
||
from src.market_data.acquisition.registry import CandlesFeedRegistry
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Зафиксирована ответственность `CandlesAcquisitionService`
|
||
|
||
`CandlesAcquisitionService` выполняет только две операции:
|
||
|
||
1. получает зарегистрированный `Candles Feed` по имени источника;
|
||
2. делегирует ему загрузку свечей.
|
||
|
||
Каноническая цепочка:
|
||
|
||
```text
|
||
source_name
|
||
↓
|
||
CandlesFeedRegistry.get()
|
||
↓
|
||
CandlesFeedProtocol
|
||
↓
|
||
load_candles()
|
||
↓
|
||
tuple[Candle, ...]
|
||
```
|
||
|
||
Сервис не выполняет:
|
||
|
||
```text
|
||
нормализацию source_name
|
||
нормализацию symbol
|
||
валидацию interval
|
||
ограничение limit
|
||
нормализацию price_type
|
||
schema validation
|
||
parsing
|
||
value validation
|
||
mapping
|
||
REST-запросы
|
||
retry
|
||
кэширование
|
||
сортировку свечей
|
||
копирование результата
|
||
оборачивание исключений
|
||
```
|
||
|
||
Это сохраняет строгие границы ответственности между слоями.
|
||
|
||
---
|
||
|
||
## Контракт `CandlesAcquisitionService`
|
||
|
||
Метод:
|
||
|
||
```python
|
||
load_candles(
|
||
source_name: str,
|
||
symbol: str,
|
||
*,
|
||
interval: str,
|
||
limit: int,
|
||
price_type: str,
|
||
) -> tuple[Candle, ...]
|
||
```
|
||
|
||
гарантирует следующую последовательность:
|
||
|
||
```text
|
||
1. Получить Feed:
|
||
registry.get(source_name)
|
||
|
||
2. Передать Feed исходные параметры:
|
||
symbol
|
||
interval
|
||
limit
|
||
price_type
|
||
|
||
3. Вернуть результат Feed без преобразований.
|
||
```
|
||
|
||
Сервис не изменяет входные параметры.
|
||
|
||
Сервис не изменяет порядок свечей.
|
||
|
||
Сервис сохраняет identity возвращённого immutable `tuple`.
|
||
|
||
Сервис не выполняет автоматический retry при ошибке.
|
||
|
||
Исключения нижележащих слоёв пробрасываются без оборачивания.
|
||
|
||
---
|
||
|
||
## Добавленные тесты
|
||
|
||
В:
|
||
|
||
```text
|
||
tests/unit/market_data/acquisition/test_service.py
|
||
```
|
||
|
||
добавлено тестовое покрытие `CandlesAcquisitionService`.
|
||
|
||
Проверяются следующие свойства:
|
||
|
||
1. сервис возвращает результат зарегистрированного Feed;
|
||
2. сохраняется identity возвращённого `tuple`;
|
||
3. `source_name` передаётся в registry без изменений;
|
||
4. registry вызывается ровно один раз;
|
||
5. `symbol` передаётся в Feed без изменений;
|
||
6. `interval` передаётся без изменений;
|
||
7. `limit` передаётся без изменений;
|
||
8. `price_type` передаётся без изменений;
|
||
9. Feed вызывается ровно один раз;
|
||
10. пустой `tuple` возвращается без изменения;
|
||
11. порядок свечей сохраняется;
|
||
12. `CandleFeedRegistryError` пробрасывается без оборачивания;
|
||
13. после ошибки registry Feed не вызывается;
|
||
14. ошибки Feed пробрасываются без оборачивания;
|
||
15. после ошибки Feed автоматический retry не выполняется.
|
||
|
||
Для проверки ошибок Feed используются:
|
||
|
||
```text
|
||
CandleTransportError
|
||
CandleValueError
|
||
CandleMappingError
|
||
```
|
||
|
||
---
|
||
|
||
## Результаты проверок
|
||
|
||
### Компиляция
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m compileall \
|
||
src/market_data/acquisition/service.py \
|
||
tests/unit/market_data/acquisition/test_service.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
Compiling 'src/market_data/acquisition/service.py'...
|
||
Compiling 'tests/unit/market_data/acquisition/test_service.py'...
|
||
```
|
||
|
||
Ошибок компиляции нет.
|
||
|
||
---
|
||
|
||
### Targeted tests
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q \
|
||
tests/unit/market_data/acquisition/test_service.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
40 passed in 0.03s
|
||
```
|
||
|
||
---
|
||
|
||
### Полный test suite
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
724 passed in 2.43s
|
||
```
|
||
|
||
Регрессий не обнаружено.
|
||
|
||
---
|
||
|
||
### Проверка Git diff
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
git diff --check
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
пустой вывод
|
||
```
|
||
|
||
Whitespace errors отсутствуют.
|
||
|
||
---
|
||
|
||
## Архитектурный результат
|
||
|
||
После Build 046 канонический OHLCV-контур имеет следующую структуру:
|
||
|
||
```text
|
||
External Dzengi API
|
||
↓
|
||
DzengiCandlesDocumentSource
|
||
↓
|
||
raw transport document
|
||
↓
|
||
validate_candles_schema()
|
||
↓
|
||
parse_candles()
|
||
↓
|
||
validate_candles_values()
|
||
↓
|
||
map_candles()
|
||
↓
|
||
DzengiCandlesDocumentHandler
|
||
↓
|
||
CandlesFeed
|
||
↓
|
||
CandlesFeedRegistry
|
||
↓
|
||
CandlesAcquisitionService
|
||
↓
|
||
tuple[Candle, ...]
|
||
```
|
||
|
||
Таким образом, канонический Candles Feed теперь имеет полный application-level путь от внешнего REST API до сервисного слоя `Market Data Acquisition`.
|
||
|
||
---
|
||
|
||
## Состояние legacy-контура
|
||
|
||
В рамках Build 046 legacy-метод:
|
||
|
||
```text
|
||
ExchangeService.get_klines()
|
||
```
|
||
|
||
не изменялся и не удалялся.
|
||
|
||
Legacy-потребители:
|
||
|
||
```text
|
||
src/trading/market_analysis/service.py
|
||
src/trading/market_analysis/htf.py
|
||
```
|
||
|
||
продолжают использовать существующий API.
|
||
|
||
Это соответствует основному правилу миграции Dzentra:
|
||
|
||
> Сначала строится и проверяется новый канонический путь, затем потребители переключаются на него поэтапно, и только после подтверждения полной совместимости удаляется legacy-код.
|
||
|
||
---
|
||
|
||
## Что намеренно не выполнялось
|
||
|
||
Build 046 не включает:
|
||
|
||
```text
|
||
переключение ExchangeService.get_klines()
|
||
переключение trading/market_analysis
|
||
удаление legacy KlineBatch
|
||
удаление legacy Kline
|
||
удаление legacy parser
|
||
удаление legacy endpoint /api/v1/klines
|
||
регистрацию production Candles Feed на composition root
|
||
изменение runtime
|
||
изменение scheduler
|
||
кэширование свечей
|
||
хранение свечей
|
||
проверку последовательности свечей
|
||
```
|
||
|
||
Эти задачи должны выполняться отдельными Build с собственными границами и проверками.
|
||
|
||
---
|
||
|
||
## Критерии завершения
|
||
|
||
Build 046 считается завершённым, поскольку:
|
||
|
||
- `CandlesAcquisitionService` реализован;
|
||
- сервис использует `CandlesFeedRegistry`;
|
||
- сервис возвращает канонический `tuple[Candle, ...]`;
|
||
- входные параметры передаются без изменений;
|
||
- результат не копируется и не сортируется;
|
||
- ошибки не оборачиваются;
|
||
- retry отсутствует;
|
||
- существующие Instrument и Quote сервисы не нарушены;
|
||
- targeted tests успешно проходят;
|
||
- полный test suite успешно проходит;
|
||
- `git diff --check` не обнаруживает ошибок;
|
||
- legacy-потребители не переключались;
|
||
- работающий торговый бот сохраняет обратную совместимость.
|
||
|
||
---
|
||
|
||
## Итог
|
||
|
||
**Build 046 завершён успешно.**
|
||
|
||
Канонический `Candles Feed` интегрирован в сервисный слой `Market Data Acquisition`.
|
||
|
||
Текущий завершённый путь:
|
||
|
||
```text
|
||
Dzengi REST API
|
||
↓
|
||
DzengiCandlesDocumentSource
|
||
↓
|
||
Schema Validation
|
||
↓
|
||
Parser
|
||
↓
|
||
Value Validation
|
||
↓
|
||
Mapper
|
||
↓
|
||
DzengiCandlesDocumentHandler
|
||
↓
|
||
CandlesFeed
|
||
↓
|
||
CandlesFeedRegistry
|
||
↓
|
||
CandlesAcquisitionService
|
||
↓
|
||
tuple[Candle, ...]
|
||
```
|
||
|
||
Следующий Build должен продолжить миграцию OHLCV-контура без преждевременного удаления legacy-реализации и без нарушения работы существующего торгового бота. |