# Build 011 — Instrument Feed Registry

**Статус:** Завершён  
**Подсистема:** `market_data/acquisition`  
**Область:** Instrument Reference Data  
**Тип изменения:** Изолированное добавление Registry для регистрации и получения Instrument Feed без подключения к production runtime  
**Результат полного набора тестов:** `167 passed`

---

## 1. Цель Build 011

Цель Build 011 — реализовать Registry для регистрации и получения доступных Instrument Feed по идентификатору источника.

Реализован класс:

```text
InstrumentFeedRegistry
```

Его архитектурная граница:

```text
source_name
    ↓
InstrumentFeedRegistry
    ↓
InstrumentFeedProtocol
```

Registry позволяет:

```text
зарегистрировать Feed;
получить Feed по имени источника;
запретить неявную повторную регистрацию;
явно сообщить об отсутствии запрошенного Feed.
```

Build 011 не хранит модели `Instrument`, не выполняет acquisition, не создаёт кэш и не подключается к существующему `ExchangeService`.

---

## 2. Почему Build 011 выполняется именно сейчас

До начала Build 011 были завершены:

```text
Build 001 — внутренняя модель Instrument Reference Data
Build 002 — raw-модели ответа Dzengi
Build 003 — структурная валидация exchangeInfo
Build 004 — parser exchangeInfo
Build 005 — value validation
Build 006 — mapper Dzengi → Instrument
Build 007 — Protocol и Exceptions
Build 008 — Dzengi REST Adapter
Build 009 — Instrument Handler
Build 010 — Instrument Feed
```

После Build 010 уже существовала полная техническая цепочка:

```text
Dzengi REST API
    ↓
DzengiInstrumentDocumentSource
    ↓
InstrumentFeed
    ↓
DzengiInstrumentDocumentHandler
    ↓
tuple[Instrument, ...]
```

Однако будущему `Acquisition Service` ещё требовался механизм получения нужного Feed без прямой зависимости от конкретной реализации.

Build 011 создаёт эту границу:

```text
Acquisition Service
    ↓
source_name
    ↓
InstrumentFeedRegistry
    ↓
InstrumentFeedProtocol
```

---

## 3. Архитектурное уточнение ответственности Registry

Registry в Build 011 не является хранилищем актуального справочника инструментов.

Он не хранит:

```text
tuple[Instrument, ...];
последний успешный справочник;
предыдущий snapshot;
TTL;
время обновления;
возраст данных;
последнюю ошибку;
индекс символов;
legacy ExchangeSymbol.
```

Его единственная предметная ответственность:

```text
source_name
    →
InstrumentFeedProtocol
```

Хранение и кэширование актуальных данных относятся к другим архитектурным слоям и будущим Build:

```text
Build 019 — подготовка переноса кэша в Storage
Build 020 — перенос кэша
```

---

## 4. Изменённые файлы

В рамках Build 011 изменены:

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

Создан тестовый файл:

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

Другие production-файлы не изменялись.

---

## 5. Реализованный InstrumentFeedRegistry

В файле:

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

реализован класс:

```python
class InstrumentFeedRegistry:
    ...
```

Registry предоставляет два публичных метода:

```python
def register(
    self,
    source_name: str,
    feed: InstrumentFeedProtocol,
) -> None:
    ...
```

и:

```python
def get(
    self,
    source_name: str,
) -> InstrumentFeedProtocol:
    ...
```

Минимальный публичный контракт:

```text
register(source_name, feed)
    ↓
регистрация Feed

get(source_name)
    ↓
получение зарегистрированного Feed
```

---

## 6. Внутреннее хранение

Registry использует внутренний типизированный словарь:

```python
dict[str, InstrumentFeedProtocol]
```

Архитектурная схема:

```text
"dzengi"
    ↓
InstrumentFeedProtocol
```

Пример:

```text
{
    "dzengi": <InstrumentFeedProtocol>
}
```

Внутренний словарь:

```text
не возвращается наружу;
не содержит Instrument;
не является предметным кэшем;
не содержит результатов Feed;
используется только как индекс зарегистрированных Feed.
```

---

## 7. Регистрация Feed

Метод:

```python
register(
    source_name: str,
    feed: InstrumentFeedProtocol,
) -> None
```

выполняет следующую последовательность:

```text
получить source_name
    ↓
удалить внешние пробелы
    ↓
проверить непустое имя
    ↓
проверить соответствие InstrumentFeedProtocol
    ↓
проверить отсутствие существующей регистрации
    ↓
сохранить Feed
```

Пример:

```python
registry.register(
    "dzengi",
    feed,
)
```

После этого:

```python
registry.get("dzengi")
```

возвращает тот же объект `feed`.

---

## 8. Получение Feed

Метод:

```python
get(
    source_name: str,
) -> InstrumentFeedProtocol
```

выполняет:

```text
получить source_name
    ↓
удалить внешние пробелы
    ↓
проверить непустое имя
    ↓
найти зарегистрированный Feed
    ↓
вернуть тот же объект Feed
```

Если Feed отсутствует, выбрасывается:

```text
InstrumentFeedRegistryError
```

Registry не создаёт Feed автоматически и не пытается использовать default source.

---

## 9. Сохранение identity Feed

Registry сохраняет и возвращает исходный объект Feed без:

```text
копирования;
оборачивания;
создания proxy;
создания нового Feed;
изменения объекта.
```

Если зарегистрирован:

```python
feed = StubInstrumentFeed()

registry.register("dzengi", feed)
```

то:

```python
registry.get("dzengi") is feed
```

равно:

```text
True
```

Это подтверждено unit-тестами.

---

## 10. Проверка InstrumentFeedProtocol

Перед регистрацией выполняется:

```python
isinstance(feed, InstrumentFeedProtocol)
```

Это возможно благодаря тому, что `InstrumentFeedProtocol` объявлен как runtime-checkable Protocol.

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

```text
объект
    ↓
соответствует InstrumentFeedProtocol?
    ├── да → регистрация разрешена
    └── нет → InstrumentFeedRegistryError
```

Registry не требует явного наследования от Protocol.

Используется structural typing:

```text
если объект реализует требуемый контракт,
он соответствует Protocol.
```

---

## 11. Registry не вызывает Feed

При выполнении:

```python
registry.register("dzengi", feed)
```

не вызывается:

```python
feed.load_instruments()
```

При выполнении:

```python
registry.get("dzengi")
```

также не вызывается:

```python
feed.load_instruments()
```

Registry только хранит и возвращает Feed.

Цепочка:

```text
register()
    ↓
сохранить ссылку на Feed

get()
    ↓
вернуть ссылку на Feed
```

Отсутствует:

```text
load_instruments()
```

Это подтверждено отдельными unit-тестами.

---

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

Registry выполняет только:

```python
source_name.strip()
```

Пример:

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

Поэтому после:

```python
registry.register("  dzengi  ", feed)
```

оба вызова:

```python
registry.get("dzengi")
registry.get("  dzengi  ")
```

возвращают тот же Feed.

---

## 13. Что Registry не делает с именем источника

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

```text
lower()
casefold()
replace()
alias resolution
automatic source mapping
```

Поэтому:

```text
"dzengi"
```

и:

```text
"DZENGI"
```

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

Можно одновременно зарегистрировать:

```python
registry.register("dzengi", first_feed)
registry.register("DZENGI", second_feed)
```

После этого:

```text
get("dzengi")
    ↓
first_feed

get("DZENGI")
    ↓
second_feed
```

Такое поведение исключает скрытую нормализацию без утверждённого контракта.

---

## 14. Запрет пустого имени источника

Registry отклоняет:

```text
""
" "
"   "
"\t"
"\n"
```

После:

```python
source_name.strip()
```

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

Выбрасывается:

```text
InstrumentFeedRegistryError
```

с сообщением:

```text
Имя источника Instrument Feed не должно быть пустым.
```

Проверка действует как для:

```text
register()
```

так и для:

```text
get()
```

---

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

Следующая последовательность запрещена:

```python
registry.register("dzengi", first_feed)
registry.register("dzengi", second_feed)
```

Вторая операция выбрасывает:

```text
InstrumentFeedRegistryError
```

с диагностикой:

```text
Instrument Feed для источника 'dzengi' уже зарегистрирован.
```

Registry не выполняет молчаливую замену.

---

## 16. Почему молчаливая замена запрещена

Молчаливая операция:

```text
existing Feed
    ↓
register same source name
    ↓
new Feed silently replaces old Feed
```

могла бы незаметно изменить production acquisition pipeline.

Поэтому используется fail-fast поведение:

```text
duplicate source name
    ↓
InstrumentFeedRegistryError
```

Если в будущем понадобится контролируемая замена Feed, она должна быть реализована отдельным явно определённым контрактом.

В Build 011 такой контракт не нужен.

---

## 17. Повторная регистрация после нормализации

Проверка duplicate выполняется после:

```python
source_name.strip()
```

Поэтому:

```python
registry.register("dzengi", first_feed)
registry.register("  dzengi  ", second_feed)
```

считается повторной регистрацией одного и того же источника.

Вторая операция завершается:

```text
InstrumentFeedRegistryError
```

Исходный Feed при этом сохраняется.

---

## 18. Отсутствие молчаливой замены Feed

После:

```python
registry.register("dzengi", first_feed)
```

и неуспешной попытки:

```python
registry.register("dzengi", second_feed)
```

результат:

```python
registry.get("dzengi") is first_feed
```

остаётся:

```text
True
```

Таким образом, ошибочная повторная регистрация не изменяет состояние Registry.

---

## 19. Поведение для отсутствующего источника

Если выполнить:

```python
registry.get("dzengi")
```

до регистрации Feed, выбрасывается:

```text
InstrumentFeedRegistryError
```

с диагностикой:

```text
Instrument Feed для источника 'dzengi' не зарегистрирован.
```

Registry не:

```text
возвращает None;
создаёт Feed автоматически;
выбирает default Feed;
выполняет fallback на другой источник.
```

Отсутствие Feed является явной ошибкой Registry.

---

## 20. Новая ошибка InstrumentFeedRegistryError

В файле:

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

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

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

Иерархия acquisition-ошибок после Build 011:

```text
MarketDataAcquisitionError
├── InstrumentReferenceTransportError
├── InstrumentReferenceSchemaError
├── InstrumentReferenceParseError
├── InstrumentReferenceValueError
├── InstrumentReferenceMappingError
└── InstrumentFeedRegistryError
```

Ошибка используется для:

```text
пустого имени источника;
объекта, не соответствующего InstrumentFeedProtocol;
повторной регистрации;
запроса отсутствующего Feed.
```

---

## 21. Почему не созданы отдельные Registry exceptions

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

```text
DuplicateInstrumentFeedError
InstrumentFeedNotFoundError
InvalidInstrumentFeedNameError
InvalidInstrumentFeedError
```

На текущем этапе для них нет отдельных алгоритмов обработки.

Все ошибки относятся к одной архитектурной категории:

```text
Instrument Feed Registry error
```

Поэтому используется один класс:

```text
InstrumentFeedRegistryError
```

Дополнительная детализация исключений без реальной необходимости только усложнила бы контракт.

---

## 22. Dependency Injection

Registry не создаёт Feed самостоятельно.

Отсутствует:

```python
self._feed = InstrumentFeed(...)
```

Также Registry не создаёт:

```text
DzengiInstrumentDocumentSource
DzengiInstrumentDocumentHandler
ExchangeRestClient
```

Правильная будущая композиция:

```text
DzengiInstrumentDocumentSource
    +
DzengiInstrumentDocumentHandler
    ↓
InstrumentFeed
    ↓
registry.register("dzengi", feed)
```

Composition остаётся явной и находится за пределами Registry.

---

## 23. Почему Registry хранит Feed, а не Source и Handler отдельно

К моменту Build 011 уже существует архитектурная capability:

```text
InstrumentFeedProtocol
```

Feed инкапсулирует взаимодействие:

```text
InstrumentDocumentSource
    ↓
InstrumentDocumentHandler
```

Если Registry начал бы отдельно хранить:

```text
Source
Handler
```

ему пришлось бы знать, как их соединять.

Это нарушило бы границу Build 010.

Правильная схема:

```text
Source
    +
Handler
    ↓
InstrumentFeed
    ↓
InstrumentFeedRegistry
```

Registry работает только с готовым:

```text
InstrumentFeedProtocol
```

---

## 24. Что Registry не хранит

Внутри Registry отсутствуют:

```text
Instrument;
tuple[Instrument, ...];
результат load_instruments();
последний успешный справочник;
предыдущий справочник;
snapshot;
timestamp;
TTL;
cache age;
последняя ошибка Feed;
legacy ExchangeSymbol.
```

Registry хранит только:

```text
dict[str, InstrumentFeedProtocol]
```

---

## 25. Что Registry не делает

Build 011 сознательно не выполняет:

```text
REST-запросы;
получение exchangeInfo;
schema validation;
parsing;
value validation;
mapping;
вызов load_instruments();
retry;
backoff;
кэширование;
хранение Instrument;
сравнение snapshot;
фильтрацию инструментов;
сортировку инструментов;
дедупликацию инструментов;
нормализацию торговых символов;
создание Source;
создание Handler;
создание Feed;
создание Acquisition Service;
создание compatibility mapper;
изменение ExchangeService;
изменение AutoTrade;
изменение Telegram UI;
изменение trading runtime.
```

Единственная ответственность:

```text
source_name
    ↔
InstrumentFeedProtocol
```

---

## 26. Реализованные тестовые сценарии

Создан файл:

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

Фактически выполнено:

```text
25 tests
```

Проверены следующие сценарии:

1. регистрация и получение Feed;
2. сохранение identity Feed;
3. соответствие объекта `InstrumentFeedProtocol`;
4. несколько Feed под разными именами;
5. удаление внешних пробелов из имени;
6. запрет пустой строки при регистрации;
7. запрет строки из пробелов при регистрации;
8. запрет tab/newline при регистрации;
9. запрет пустой строки при `get()`;
10. запрет строки из пробелов при `get()`;
11. запрет tab/newline при `get()`;
12. запрет повторной регистрации;
13. отсутствие замены исходного Feed при duplicate;
14. duplicate после нормализации внешних пробелов;
15. сохранение case-sensitive поведения;
16. ошибка при запросе отсутствующего Feed;
17. запрет объекта без `InstrumentFeedProtocol`;
18. отсутствие вызова Feed при регистрации;
19. отсутствие вызова Feed при получении;
20. хранение Feed, а не результата `Instrument`;
21. наследование `InstrumentFeedRegistryError` от `MarketDataAcquisitionError`.

Часть сценариев реализована через параметризацию, поэтому фактическое количество выполненных тестовых случаев составляет:

```text
25
```

---

## 27. Проверка регистрации и получения Feed

Тест подтверждает:

```python
registry = InstrumentFeedRegistry()
feed = StubInstrumentFeed()

registry.register("dzengi", feed)

result = registry.get("dzengi")

assert result is feed
```

Это доказывает:

```text
Feed зарегистрирован;
Feed доступен по source_name;
identity объекта сохранена.
```

---

## 28. Проверка нескольких источников

Registry поддерживает несколько независимых записей:

```text
"dzengi"
    ↓
dzengi_feed

"secondary"
    ↓
secondary_feed
```

Тест подтверждает:

```python
assert registry.get("dzengi") is dzengi_feed
assert registry.get("secondary") is secondary_feed
```

Registry не имеет встроенного ограничения на один источник.

---

## 29. Проверка отсутствия вызова Feed

Тестовый Feed содержит счётчик:

```python
self.load_call_count = 0
```

Метод:

```python
load_instruments()
```

увеличивает счётчик.

После:

```python
registry.register("dzengi", feed)
```

подтверждено:

```python
assert feed.load_call_count == 0
```

После:

```python
registry.get("dzengi")
```

также подтверждено:

```python
assert feed.load_call_count == 0
```

Таким образом, Registry не запускает acquisition pipeline.

---

## 30. Проверка хранения Feed, а не результата Feed

Тест создаёт:

```python
instruments = (
    _instrument(),
)
```

и Feed:

```python
feed = StubInstrumentFeed(
    instruments=instruments,
)
```

После регистрации:

```python
registered_feed = registry.get("dzengi")
```

подтверждено:

```python
assert registered_feed is feed
assert registered_feed is not instruments
```

Registry хранит сам Feed, а не:

```text
tuple[Instrument, ...]
```

---

## 31. Выполненные проверки

### Проверка 1 — unit-тесты Registry

Команда:

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

Результат:

```text
......................... [100%]
25 passed in 0.02s
```

Статус:

```text
PASSED
```

---

### Проверка 2 — Python compilation

Команда:

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

Результат:

```text
Команда завершилась без ошибок и без вывода.
```

Статус:

```text
PASSED
```

---

### Проверка 3 — полный набор тестов проекта

Команда:

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

Результат:

```text
....................................................................................................................................................................... [100%]
167 passed in 0.09s
```

Статус:

```text
PASSED
```

---

### Проверка 4 — отсутствие преждевременной production-интеграции

Команда:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  -E "InstrumentFeedRegistry|InstrumentFeedRegistryError" \
  src tests
```

Полученные production-использования находятся только в:

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

Остальные использования находятся исключительно в:

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

Не обнаружено подключения к:

```text
Acquisition Service
ExchangeService
Telegram UI
AutoTrade
Trading runtime
другим production-потребителям
```

Статус:

```text
PASSED
```

---

## 32. Архитектура после Build 011

После завершения Build 011 новая часть подсистемы имеет следующую структуру:

```text
market_data/
└── acquisition/
    ├── exceptions.py
    │   ├── MarketDataAcquisitionError
    │   ├── InstrumentReferenceTransportError
    │   ├── InstrumentReferenceSchemaError
    │   ├── InstrumentReferenceParseError
    │   ├── InstrumentReferenceValueError
    │   ├── InstrumentReferenceMappingError
    │   └── InstrumentFeedRegistryError
    │
    ├── protocol.py
    │   ├── InstrumentDocumentSource
    │   ├── InstrumentDocumentHandler
    │   └── InstrumentFeedProtocol
    │
    ├── registry.py
    │   └── InstrumentFeedRegistry
    │
    ├── models/
    │   └── instrument.py
    │       └── Instrument
    │
    ├── validation/
    │   ├── schema.py
    │   └── values.py
    │
    ├── handlers/
    │   └── instrument_handler.py
    │       └── DzengiInstrumentDocumentHandler
    │
    ├── feeds/
    │   └── instrument_feed.py
    │       └── InstrumentFeed
    │
    └── adapters/
        └── dzengi/
            ├── models.py
            ├── parser.py
            ├── mapper.py
            └── rest.py
                ├── _PayloadRestClient
                └── DzengiInstrumentDocumentSource
```

На текущем этапе ещё не реализован:

```text
service.py
```

---

## 33. Полная архитектурная цепочка после Build 011

После Build 011 существуют две связанные архитектурные части.

### Регистрация capability

```text
DzengiInstrumentDocumentSource
    +
DzengiInstrumentDocumentHandler
    ↓
InstrumentFeed
    ↓
InstrumentFeedRegistry.register(
    "dzengi",
    feed,
)
```

### Получение capability будущим сервисом

```text
Acquisition Service
    ↓
source_name = "dzengi"
    ↓
InstrumentFeedRegistry.get("dzengi")
    ↓
InstrumentFeedProtocol
```

После получения Feed будущий `Acquisition Service` сможет выполнить:

```text
InstrumentFeedProtocol.load_instruments()
    ↓
tuple[Instrument, ...]
```

Эта orchestration logic относится к следующему:

```text
Build 012 — Acquisition Service
```

---

## 34. Полный acquisition pipeline после Build 011

Технически уже реализована следующая цепочка:

```text
source_name
    ↓
InstrumentFeedRegistry
    ↓
InstrumentFeedProtocol
    ↓
InstrumentFeed
    ↓
InstrumentDocumentSource
    ↓
DzengiInstrumentDocumentSource
    ↓
ExchangeRestClient.get_payload()
    ↓
Dzengi REST API
```

После получения документа:

```text
object
    ↓
InstrumentDocumentHandler
    ↓
DzengiInstrumentDocumentHandler
    ↓
validate_exchange_info_schema()
    ↓
ValidatedExchangeInfoDocument
    ↓
parse_exchange_info()
    ↓
DzengiExchangeInfoResponse
    ↓
validate_exchange_info_values()
    ↓
map_dzengi_exchange_info_to_instruments()
    ↓
tuple[Instrument, ...]
```

Однако Registry сам эту цепочку не запускает.

---

## 35. Что ещё не реализовано

После Build 011 отсутствуют:

```text
Acquisition Service;
официальная production composition;
equivalence verification с legacy implementation;
compatibility mapper Instrument → ExchangeSymbol;
переключение get_exchange_symbols();
перевод normalize_symbol()/symbol_candidates();
переключение validate_symbol();
переключение get_symbol_runtime_status();
перенос кэша.
```

Поэтому новая acquisition pipeline всё ещё остаётся изолированной от существующего production runtime.

---

## 36. Влияние на legacy-систему

Build 011 не подключён к существующим компонентам:

```text
ExchangeService
ExchangeSymbol
SymbolValidationResult
Telegram UI
AutoTrade
Market Stream
Market Data Runner
Execution Quality
Trading runtime
```

Не изменены:

```text
ExchangeService.get_exchange_symbols()
ExchangeService.validate_symbol()
ExchangeService.get_symbol_runtime_status()
normalize_symbol()
symbol_candidates()
```

Старый production-путь продолжает работать без изменений.

---

## 37. Обратная совместимость

Подтверждено сохранение:

```text
сигнатур существующих legacy-методов;
старых импортов;
существующего формата legacy-ошибок;
Telegram UI;
автоторговли;
runtime-поведения;
legacy ExchangeSymbol;
legacy-кэша.
```

Build 011 имеет полную обратную совместимость.

---

## 38. Классификация изменений

| Изменение | Классификация |
|---|---|
| `InstrumentFeedRegistry` | Обязательное архитектурное изменение |
| Индексация Feed по `source_name` | Обязательное архитектурное изменение |
| Проверка `InstrumentFeedProtocol` | Улучшение надёжности |
| Запрет повторной регистрации | Улучшение надёжности |
| `InstrumentFeedRegistryError` | Обязательная диагностическая граница |
| Удаление внешних пробелов из имени | Улучшение надёжности |
| Сохранение case-sensitive ключей | Отсутствие скрытого изменения поведения |
| Хранение `Instrument` | Не выполняется |
| Вызов Feed | Не выполняется |
| Кэширование | Не выполняется |
| Изменение legacy-кода | Отсутствует |
| Изменение production-поведения | Отсутствует |

---

## 39. Условие завершения Build 011

Все условия выполнены:

```text
InstrumentFeedRegistry
    реализован;

InstrumentFeedProtocol
    может быть зарегистрирован;

Feed
    возвращается по source_name;

identity Feed
    сохраняется;

несколько источников
    поддерживаются;

внешние пробелы имени
    удаляются;

регистр имени
    сохраняется;

пустые ключи
    отклоняются;

повторная регистрация
    отклоняется;

неуспешная повторная регистрация
    не заменяет исходный Feed;

отсутствующий Feed
    вызывает InstrumentFeedRegistryError;

невалидный Feed
    отклоняется;

Registry
    не вызывает load_instruments();

Registry
    не хранит Instrument;

Registry
    не выполняет кэширование;

unit-тесты
    проходят;

полный pytest
    проходит;

production runtime
    не затронут.
```

---

## 40. Итог Build 011

Build 011 завершён успешно.

Реализовано:

```text
InstrumentFeedRegistry

source_name
    ↓
register()
    ↓
InstrumentFeedProtocol
```

и:

```text
source_name
    ↓
get()
    ↓
тот же InstrumentFeedProtocol
```

Подтверждено:

```text
25 Registry tests passed
167 total project tests passed
Python compilation passed
No premature production integration detected
Legacy bot behavior unchanged
```

Итоговый статус:

```text
BUILD 011 — COMPLETE
```

---

## 41. Следующий этап

Следующий этап утверждённого плана:

```text
Build 012 — Acquisition Service
```

Его задача — реализовать application-level orchestration над Registry и Feed:

```text
source_name
    ↓
Acquisition Service
    ↓
InstrumentFeedRegistry
    ↓
InstrumentFeedProtocol
    ↓
load_instruments()
    ↓
tuple[Instrument, ...]
```

Build 012 не должен:

```text
изменять ExchangeService;
переключать legacy get_exchange_symbols();
создавать compatibility mapper;
переносить legacy-кэш;
подключаться к Telegram UI;
изменять AutoTrade;
изменять trading runtime.
```

После успешного завершения Build 012 будет закончена изолированная новая acquisition pipeline, после чего можно будет перейти к:

```text
Build 013 — проверка эквивалентности старой и новой реализации.
```