# Build 045 — регистрация канонического Candles Feed

## Статус

**Завершён**

---

## Цель Build

Продолжить поэтапную миграцию **OHLCV Feed** в утверждённую архитектуру `Market Data Acquisition` после завершения фундаментального слоя Candles Feed в Build 044.

Build 045 добавляет реестр канонических источников свечей и завершает следующий минимальный архитектурный шаг:

```text
Candles Feed
    ↓
CandlesFeedRegistry
```

При этом:

- существующий runtime не переключается;
- `ExchangeService.get_klines()` не изменяется;
- legacy-путь получения свечей продолжает работать;
- новая архитектура остаётся изолированной от существующего торгового runtime;
- обратная совместимость полностью сохраняется.

---

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

До Build 045 проект уже содержал фундамент канонического Candles Feed, созданный в Build 044:

```text
REST /api/v1/klines
    ↓
DzengiCandlesDocumentSource
    ↓
validate_candles_schema()
    ↓
parse_candles()
    ↓
validate_candles_values()
    ↓
map_candles()
    ↓
Candle
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
```

Также существовали:

- каноническая модель `Candle`;
- транспортная raw-модель Dzengi для свечей;
- REST document source;
- schema validation;
- parser;
- value validation;
- mapper;
- document handler;
- `CandlesFeed`;
- протоколы Candles Feed;
- специализированные исключения;
- unit-тесты фундаментального слоя.

Однако отсутствовал реестр, позволяющий регистрировать и выбирать конкретный `CandlesFeed` по имени источника.

---

## Объём изменений

Build 045 изменяет только следующие файлы:

```text
src/market_data/acquisition/exceptions.py
src/market_data/acquisition/registry.py
tests/unit/market_data/acquisition/test_registry.py
docs/migrations/build_045.md
```

Build не изменяет:

```text
src/market_data/acquisition/service.py
src/integrations/exchange/service.py
src/integrations/exchange/models.py
src/trading/
src/telegram/
```

---

## 1. Исключение CandleFeedRegistryError

В файл:

```text
src/market_data/acquisition/exceptions.py
```

добавлено специализированное исключение:

```python
class CandleFeedRegistryError(MarketDataAcquisitionError):
    pass
```

Назначение исключения — изолировать ошибки регистрации и получения Candles Feed от других ошибок подсистемы `Market Data Acquisition`.

Иерархия:

```text
MarketDataAcquisitionError
    ↓
CandleFeedRegistryError
```

---

## 2. Реестр CandlesFeedRegistry

В файл:

```text
src/market_data/acquisition/registry.py
```

добавлен класс:

```text
CandlesFeedRegistry
```

Реестр отвечает исключительно за регистрацию и получение реализаций:

```text
CandlesFeedProtocol
```

Реестр не выполняет:

- REST-запросы;
- schema validation;
- parsing;
- value validation;
- mapping;
- хранение свечей;
- управление runtime;
- переключение legacy-потребителей.

---

## 3. Контракт реестра

`CandlesFeedRegistry` предоставляет только операции регистрации и получения Candles Feed.

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

```text
register()
get()
```

Внутренняя нормализация имени источника выполняется методом:

```text
_normalize_source_name()
```

Концептуально:

```text
source name
    ↓
CandlesFeedRegistry
    ↓
CandlesFeedProtocol
```

Пример:

```text
"dzengi"
    ↓
CandlesFeedRegistry
    ↓
CandlesFeed
```

Реестр принимает только объекты, соответствующие каноническому контракту:

```text
CandlesFeedProtocol
```

---

## 4. Нормализация имени источника

Имена источников нормализуются перед использованием.

Поддерживается удаление внешних пробелов:

```text
"dzengi"
"  dzengi  "
```

После нормализации оба значения соответствуют одному ключу:

```text
dzengi
```

Пустые имена источников не допускаются.

Регистр символов сохраняется. Имена источников являются case-sensitive:

```text
"dzengi"
```

и:

```text
"DZENGI"
```

считаются разными именами источников.

---

## 5. Защита от некорректной регистрации

`CandlesFeedRegistry` отклоняет:

- пустое имя источника;
- имя, состоящее только из пробелов;
- объект, не соответствующий `CandlesFeedProtocol`;
- повторную регистрацию уже существующего нормализованного имени источника.

Ошибки представлены через:

```text
CandleFeedRegistryError
```

---

## 6. Защита от повторной регистрации

Повторная регистрация одного и того же нормализованного имени источника запрещена.

Пример:

```text
register("dzengi", feed_a)
    ↓
dzengi → feed_a
```

Повторная регистрация:

```text
register("dzengi", feed_b)
    ↓
CandleFeedRegistryError
```

Исходный Feed при ошибке повторной регистрации не заменяется.

Для замены зарегистрированного Feed требуется отдельное явное изменение архитектурного контракта. В Build 045 такая возможность не добавлялась.

---

## 7. Получение зарегистрированного Feed

Зарегистрированный Candles Feed может быть получен по имени источника.

Концептуально:

```text
registry.get("dzengi")
    ↓
CandlesFeedProtocol
```

При успешном получении реестр возвращает тот же объект Feed, который был ранее зарегистрирован:

```text
feed = CandlesFeed(...)

registry.register("dzengi", feed)

registry.get("dzengi") is feed
    ↓
True
```

Попытка получить неизвестный источник приводит к:

```text
CandleFeedRegistryError
```

---

## 8. Ограниченный публичный контракт

Публичный контракт `CandlesFeedRegistry` в Build 045 ограничен двумя операциями:

```text
register()
get()
```

В Build 045 намеренно не добавлены:

```text
contains()
remove()
replace()
clear()
```

Реестр не предоставляет управление жизненным циклом зарегистрированных Feed и не выполняет их запуск или остановку.

Такое ограничение соответствует принципу минимального безопасного Build: добавляется только функциональность, необходимая для последующей интеграции Candles Feed в сервисный слой.

---

## 9. Изоляция от Acquisition Service

В рамках Build 045 намеренно не добавлялись:

```text
CandlesAcquisitionService
```

или метод:

```text
load_candles()
```

в существующий:

```text
src/market_data/acquisition/service.py
```

Архитектурная проверка:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  "CandlesAcquisitionService\|load_candles" \
  src/market_data/acquisition/service.py
```

Результат:

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

Это подтверждает, что Build 045 ограничен добавлением реестра и не выполняет преждевременную интеграцию сервисного слоя.

---

## 10. Изоляция от legacy runtime

Build 045 не изменяет существующий runtime-путь получения свечей:

```text
Trading / Market Analysis
    ↓
ExchangeService.get_klines()
    ↓
legacy parsing
    ↓
KlineBatch
```

После Build 045 этот путь продолжает работать без изменений.

Новый канонический путь пока существует параллельно:

```text
DzengiCandlesDocumentSource
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
```

Таким образом, в системе временно существуют два пути:

```text
Legacy runtime path
    +
Canonical Candles Feed path
```

Это ожидаемое переходное состояние безопасной поэтапной миграции.

---

## 11. Unit-тесты

Расширен файл:

```text
tests/unit/market_data/acquisition/test_registry.py
```

Добавлено покрытие для:

- создания `CandlesFeedRegistry`;
- регистрации Candles Feed;
- получения зарегистрированного Feed;
- нормализации внешних пробелов имени источника;
- case-sensitive поведения имён источников;
- отклонения пустого имени;
- отклонения имени, состоящего только из пробелов;
- отклонения объекта, не соответствующего `CandlesFeedProtocol`;
- защиты от повторной регистрации;
- проверки, что повторная регистрация не заменяет исходный Feed;
- получения неизвестного источника;
- сохранения identity зарегистрированного Feed;
- отсутствия вызова `load_candles()` при регистрации Feed;
- отсутствия вызова `load_candles()` при получении Feed;
- соответствия зарегистрированного объекта `CandlesFeedProtocol`;
- корректной работы специализированного `CandleFeedRegistryError`.

---

## 12. Результаты targeted-тестов

Выполнена команда:

```bash
python -m pytest -q \
  tests/unit/market_data/acquisition/test_registry.py
```

Результат:

```text
61 passed in 0.04s
```

---

## 13. Результаты полного набора тестов

Выполнена команда:

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

Результат:

```text
707 passed in 2.87s
```

Все unit-тесты проекта проходят успешно.

---

## 14. Архитектурная проверка реестра

Выполнена команда:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  "CandlesFeedRegistry\|CandleFeedRegistryError" \
  src tests
```

Результат подтверждает, что новые сущности находятся только в ожидаемых областях:

```text
src/market_data/acquisition/registry.py
src/market_data/acquisition/exceptions.py
tests/unit/market_data/acquisition/test_registry.py
```

Неожиданных зависимостей не обнаружено.

---

## 15. Проверка отсутствия преждевременной сервисной интеграции

Выполнена команда:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  "CandlesAcquisitionService\|load_candles" \
  src/market_data/acquisition/service.py
```

Результат:

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

Это подтверждает, что Build 045 не расширяет `MarketDataAcquisitionService` и не переключает runtime.

---

## 16. Проверка синтаксиса

Выполнена команда:

```bash
python -m compileall \
  src/market_data/acquisition/exceptions.py \
  src/market_data/acquisition/registry.py \
  tests/unit/market_data/acquisition/test_registry.py
```

Результат:

```text
успешно
```

Ошибок синтаксиса не обнаружено.

---

## 17. Проверка форматирования diff

Первоначальная проверка:

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

обнаружила одну лишнюю пустую строку в конце:

```text
app/tests/unit/market_data/acquisition/test_registry.py:691: new blank line at EOF.
```

После удаления лишней пустой строки необходимо повторно выполнить:

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

Финальный ожидаемый результат:

```text
пустой вывод
```

До создания commit эта проверка должна быть подтверждена.

---

## 18. Итоговая архитектура после Build 045

После завершения Build 045 канонический Candles Feed имеет следующую структуру:

```text
Dzengi REST /api/v1/klines
    ↓
DzengiCandlesDocumentSource
    ↓
validate_candles_schema()
    ↓
parse_candles()
    ↓
validate_candles_values()
    ↓
map_candles()
    ↓
Candle
    ↓
DzengiCandlesDocumentHandler
    ↓
CandlesFeed
    ↓
CandlesFeedRegistry
```

При этом существующий production/runtime-путь остаётся неизменным:

```text
Trading / Market Analysis
    ↓
ExchangeService.get_klines()
    ↓
legacy parsing
    ↓
KlineBatch
```

---

## 19. Что намеренно не входит в Build 045

Build 045 намеренно не выполняет:

- интеграцию Candles Feed в `MarketDataAcquisitionService`;
- изменение `ExchangeService.get_klines()`;
- изменение `Kline`;
- изменение `KlineBatch`;
- переключение `trading/market_analysis`;
- переключение HTF-анализа;
- удаление legacy parser свечей;
- добавление постоянного Candle Store;
- изменение runtime;
- изменение Telegram UI;
- изменение торговой логики.

Эти ограничения являются частью стратегии безопасной миграции.

---

## 20. Следующий Build

Следующий минимальный безопасный этап:

```text
Build 046 — интеграция канонического Candles Feed в MarketDataAcquisitionService
```

Предполагаемая цель:

```text
CandlesFeedRegistry
    ↓
MarketDataAcquisitionService
    ↓
load_candles(...)
    ↓
tuple[Candle, ...]
```

Build 046 должен:

- добавить поддержку `CandlesFeedRegistry` в сервисный слой `Market Data Acquisition`;
- добавить публичный метод загрузки свечей через канонический сервис;
- сохранить существующие Instrument Reference Data и Quotes Feed без изменений;
- не переключать `ExchangeService.get_klines()`;
- не удалять legacy `Kline` и `KlineBatch`;
- не изменять торговую логику;
- сохранить полную обратную совместимость.

---

## Итог

Build 045 завершает регистрацию канонического Candles Feed.

Достигнуто состояние:

```text
OHLCV Feed foundation
    ✓ Candle model
    ✓ REST document source
    ✓ schema validation
    ✓ parser
    ✓ value validation
    ✓ mapper
    ✓ document handler
    ✓ Candles Feed
    ✓ CandlesFeedRegistry
    ☐ Acquisition Service integration
    ☐ ExchangeService compatibility bridge
    ☐ Consumer migration
    ☐ Legacy Kline removal
```

Build 045 является небольшим изолированным архитектурным шагом и не изменяет поведение существующего рабочего торгового бота.