# 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": } ``` Внутренний словарь: ```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 — проверка эквивалентности старой и новой реализации. ```