# Build 010 — Instrument Feed **Статус:** Завершён **Подсистема:** `market_data/acquisition` **Область:** Instrument Reference Data **Тип изменения:** Изолированное добавление orchestration-компонента Feed без подключения к production runtime **Результат полного набора тестов:** `142 passed` --- ## 1. Цель Build 010 Цель Build 010 — реализовать общий Instrument Feed, соединяющий уже существующие контракты: ```text InstrumentDocumentSource ↓ InstrumentDocumentHandler ``` и предоставляющий единый интерфейс: ```text InstrumentFeedProtocol ``` Реализован класс: ```text InstrumentFeed ``` Архитектурная граница Build 010: ```text InstrumentFeed.load_instruments() ↓ InstrumentDocumentSource.fetch_instrument_document() ↓ object ↓ InstrumentDocumentHandler.handle_instrument_document() ↓ tuple[Instrument, ...] ``` Build 010 не создаёт Registry, Acquisition Service, compatibility mapper и не подключается к существующему `ExchangeService`. --- ## 2. Почему Build 010 выполняется именно сейчас До начала Build 010 были завершены: ```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 009 существовали две независимые части acquisition pipeline. ### Получение документа ```text Dzengi REST API ↓ ExchangeRestClient.get_payload() ↓ DzengiInstrumentDocumentSource ↓ object ``` ### Обработка документа ```text object ↓ DzengiInstrumentDocumentHandler ↓ validate_exchange_info_schema() ↓ parse_exchange_info() ↓ validate_exchange_info_values() ↓ map_dzengi_exchange_info_to_instruments() ↓ tuple[Instrument, ...] ``` До Build 010 эти две части не были соединены общим orchestration-компонентом. Build 010 реализовал эту связь через абстрактные контракты: ```text InstrumentDocumentSource InstrumentDocumentHandler ``` без зависимости самого Feed от конкретной биржи или формата документа. --- ## 3. Изменённые файлы В рамках Build 010 реализован production-файл: ```text app/src/market_data/acquisition/feeds/instrument_feed.py ``` Создан тестовый файл: ```text app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py ``` Другие production-файлы не изменялись. --- ## 4. Реализованный Instrument Feed В файле: ```text app/src/market_data/acquisition/feeds/instrument_feed.py ``` реализован класс: ```python class InstrumentFeed: ... ``` Его публичный метод: ```python def load_instruments(self) -> tuple[Instrument, ...]: ... ``` соответствует контракту: ```text InstrumentFeedProtocol ``` --- ## 5. Зависимости Instrument Feed `InstrumentFeed` получает две обязательные зависимости: ```python def __init__( self, *, source: InstrumentDocumentSource, handler: InstrumentDocumentHandler, ) -> None: ... ``` Первая зависимость: ```text InstrumentDocumentSource ``` отвечает за получение исходного документа. Вторая зависимость: ```text InstrumentDocumentHandler ``` отвечает за преобразование исходного документа во внутренние модели: ```text tuple[Instrument, ...] ``` Обе зависимости передаются явно через конструктор. --- ## 6. Реализованный orchestration pipeline Метод: ```python load_instruments() ``` выполняет только две операции: ```python document = self._source.fetch_instrument_document() return self._handler.handle_instrument_document(document) ``` Полная последовательность: ```text InstrumentFeed.load_instruments() ↓ source.fetch_instrument_document() ↓ object ↓ handler.handle_instrument_document(document) ↓ tuple[Instrument, ...] ``` Feed не реализует самостоятельно transport, parsing, validation или mapping. --- ## 7. Разделение ответственности После Build 010 обязанности компонентов разделены следующим образом. ### InstrumentDocumentSource Отвечает за: ```text получение документа от внешнего источника. ``` Конкретная реализация: ```text DzengiInstrumentDocumentSource ``` --- ### InstrumentDocumentHandler Отвечает за: ```text преобразование исходного документа во внутренние модели Instrument. ``` Конкретная реализация: ```text DzengiInstrumentDocumentHandler ``` --- ### InstrumentFeed Отвечает только за: ```text получить документ через Source; передать документ Handler; вернуть результат Handler. ``` Feed не знает внутренней реализации Source и Handler. --- ## 8. Независимость Instrument Feed от Dzengi В `instrument_feed.py` отсутствуют прямые зависимости от: ```text DzengiInstrumentDocumentSource DzengiInstrumentDocumentHandler ExchangeRestClient DzengiExchangeInfoResponse DzengiExchangeInfoSymbol Dzengi parser Dzengi mapper Dzengi validation pipeline ``` Feed зависит только от общих контрактов: ```text InstrumentDocumentSource InstrumentDocumentHandler ``` и внутренней модели: ```text Instrument ``` Архитектурная зависимость: ```text InstrumentFeed ↓ InstrumentDocumentSource Protocol ↓ конкретная реализация определяется снаружи ``` ```text InstrumentFeed ↓ InstrumentDocumentHandler Protocol ↓ конкретная реализация определяется снаружи ``` Таким образом, Feed остаётся source-independent orchestration-компонентом. --- ## 9. Почему Feed не создаёт зависимости самостоятельно В Build 010 намеренно не используется: ```python self._source = DzengiInstrumentDocumentSource() self._handler = DzengiInstrumentDocumentHandler() ``` Такое решение напрямую связало бы общий Feed с конкретным источником Dzengi. Вместо этого используется явная dependency injection: ```python InstrumentFeed( source=source, handler=handler, ) ``` Это обеспечивает: ```text независимость от конкретной биржи; явные архитектурные зависимости; тестируемость без сети; возможность другой реализации Source; возможность другой реализации Handler; отсутствие скрытой composition logic. ``` Создание конкретной production-композиции не относится к ответственности Feed. --- ## 10. Почему аргументы конструктора keyword-only Конструктор объявлен как: ```python def __init__( self, *, source: InstrumentDocumentSource, handler: InstrumentDocumentHandler, ) -> None: ... ``` Символ: ```text * ``` делает аргументы keyword-only. Корректное создание: ```python InstrumentFeed( source=source, handler=handler, ) ``` Это явно показывает роли зависимостей и исключает позиционную неоднозначность. --- ## 11. Отсутствие нового Feed exception В Build 010 намеренно не создавалась ошибка: ```text InstrumentReferenceFeedError ``` Feed сохраняет без дополнительного wrapping специализированные ошибки нижележащих компонентов: ```text InstrumentReferenceTransportError InstrumentReferenceSchemaError InstrumentReferenceParseError InstrumentReferenceValueError InstrumentReferenceMappingError ``` Это сохраняет точную диагностическую классификацию отказов. --- ## 12. Отсутствие try/except в Instrument Feed В `load_instruments()` намеренно отсутствует: ```python try: ... except Exception: ... ``` Если Source выбрасывает: ```text InstrumentReferenceTransportError ``` Feed передаёт тот же объект исключения вызывающему коду. Если Handler выбрасывает: ```text InstrumentReferenceSchemaError InstrumentReferenceParseError InstrumentReferenceValueError InstrumentReferenceMappingError ``` Feed также передаёт исходное исключение без изменения. Цепочка: ```text нижний компонент ↓ специализированное исключение ↓ InstrumentFeed ↓ то же исключение ↓ внешний потребитель ``` --- ## 13. Поведение при transport error Если: ```text source.fetch_instrument_document() ``` выбрасывает: ```text InstrumentReferenceTransportError ``` то: ```text ошибка передаётся без wrapping; handler не вызывается; повторный запрос не выполняется. ``` Последовательность: ```text InstrumentFeed.load_instruments() ↓ source.fetch_instrument_document() ↓ InstrumentReferenceTransportError ↓ выполнение прекращается ``` Handler в этом случае не получает документ. --- ## 14. Поведение при processing error Если Source успешно возвращает документ: ```text object ``` но Handler выбрасывает специализированную processing-ошибку, например: ```text InstrumentReferenceValueError ``` то Feed: ```text не перехватывает ошибку; не заменяет её другим типом; не повторяет Source; не повторяет Handler. ``` Последовательность: ```text source ↓ document ↓ handler ↓ InstrumentReferenceValueError ↓ внешний потребитель ``` --- ## 15. Отсутствие retry Build 010 не реализует retry. Если Source завершился ошибкой: ```text InstrumentReferenceTransportError ``` Feed не выполняет: ```text повторный REST-запрос; задержку; backoff; повторный вызов Source. ``` В рамках одного вызова: ```python feed.load_instruments() ``` Source вызывается ровно один раз. Retry policy, если она потребуется, должна находиться на другом архитектурном уровне и не должна скрыто появляться внутри базового Feed. --- ## 16. Передача документа без изменения Документ, возвращённый Source: ```python document = self._source.fetch_instrument_document() ``` передаётся непосредственно Handler: ```python self._handler.handle_instrument_document(document) ``` Feed не выполняет: ```text копирование; преобразование; нормализацию; фильтрацию; валидацию; оборачивание; изменение структуры. ``` Тестом подтверждено сохранение identity: ```python handler.documents[0] is document ``` --- ## 17. Возврат результата без изменения Если Handler возвращает: ```python instruments: tuple[Instrument, ...] ``` Feed возвращает непосредственно тот же объект: ```python return self._handler.handle_instrument_document(document) ``` Feed не выполняет: ```python tuple(instruments) ``` или другое копирование коллекции. Тестом подтверждено: ```python result is instruments ``` Это гарантирует отсутствие скрытого преобразования результата. --- ## 18. Сохранение порядка инструментов Feed не выполняет: ```text сортировку; фильтрацию; дедупликацию; перегруппировку. ``` Если Handler возвращает: ```text BTC/USD_LEVERAGE ETH/USD_LEVERAGE XRP/USD_LEVERAGE ``` Feed возвращает инструменты в том же порядке: ```text BTC/USD_LEVERAGE ETH/USD_LEVERAGE XRP/USD_LEVERAGE ``` Ответственность за порядок данных не переносится в Feed. --- ## 19. Поведение при пустом результате Если Handler возвращает: ```python () ``` Feed возвращает: ```python () ``` без ошибки. Feed не определяет, является ли пустой справочник: ```text допустимым состоянием; временной ошибкой; критическим отказом; условием для сохранения предыдущего snapshot. ``` Такая policy logic не относится к ответственности Feed. --- ## 20. Что Instrument Feed не делает Build 010 сознательно не выполняет: ```text REST-запросы самостоятельно; парсинг JSON; schema validation; value validation; mapping; retry; backoff; кэширование; фильтрацию инструментов; сортировку инструментов; удаление дубликатов; нормализацию символов; проверку exchange_enabled; создание Registry; создание Acquisition Service; создание legacy ExchangeSymbol; создание compatibility mapper; логирование; формирование Telegram-сообщений; изменение ExchangeService; изменение AutoTrade; изменение trading runtime. ``` Единственная ответственность Feed: ```text Source ↓ Handler ↓ tuple[Instrument, ...] ``` --- ## 21. Реализованные тестовые сценарии Создан файл: ```text app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py ``` Реализовано 10 тестов. Проверены следующие сценарии: 1. `InstrumentFeed` соответствует `InstrumentFeedProtocol`; 2. Source вызывается один раз; 3. Handler вызывается один раз; 4. документ передаётся Handler без изменения; 5. результат Handler возвращается без изменения; 6. порядок инструментов сохраняется; 7. пустой `tuple` возвращается без ошибки; 8. `InstrumentReferenceTransportError` сохраняется без wrapping; 9. processing error сохраняется без wrapping; 10. после transport error Source не вызывается повторно. --- ## 22. Проверка соответствия InstrumentFeedProtocol Тестом подтверждено: ```python assert isinstance(feed, InstrumentFeedProtocol) ``` Это возможно благодаря: ```text @runtime_checkable ``` в определении Protocol и структурному соответствию метода: ```python load_instruments() -> tuple[Instrument, ...] ``` Feed не наследуется явно от Protocol. Используется structural typing: ```text если объект реализует требуемый контракт, он соответствует Protocol. ``` --- ## 23. Проверка вызова Source Тестовый Source ведёт счётчик: ```python self.call_count = 0 ``` При каждом вызове: ```python fetch_instrument_document() ``` значение увеличивается. После: ```python feed.load_instruments() ``` подтверждено: ```python assert source.call_count == 1 ``` Таким образом, Feed не выполняет скрытых повторных запросов. --- ## 24. Проверка вызова Handler Тестовый Handler сохраняет полученные документы: ```python self.documents: list[object] = [] ``` При вызове: ```python handle_instrument_document(document) ``` документ добавляется в список. После: ```python feed.load_instruments() ``` подтверждено: ```python assert len(handler.documents) == 1 ``` Handler вызывается ровно один раз. --- ## 25. Проверка transport error Создан исходный объект ошибки: ```python original_error = InstrumentReferenceTransportError( "Не удалось получить exchangeInfo." ) ``` Source выбрасывает именно этот объект. Тест подтверждает: ```python assert exc_info.value is original_error ``` Это доказывает отсутствие: ```text wrapping; замены исключения; создания нового объекта ошибки. ``` Дополнительно подтверждено: ```python assert source.call_count == 1 assert handler.documents == [] ``` То есть: ```text Source вызван один раз; Handler не вызван. ``` --- ## 26. Проверка processing error Создан исходный объект: ```python original_error = InstrumentReferenceValueError( "Некорректное значение." ) ``` Handler выбрасывает именно этот объект. Тест подтверждает: ```python assert exc_info.value is original_error ``` Дополнительно: ```python assert source.call_count == 1 assert handler.documents == [document] ``` Это подтверждает корректную последовательность: ```text Source успешно вызван один раз ↓ документ передан Handler ↓ Handler выбросил processing error ↓ ошибка вышла из Feed без изменения ``` --- ## 27. Выполненные проверки ### Проверка 1 — unit-тесты Instrument Feed Команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/feeds/test_instrument_feed.py \ -q ``` Результат: ```text .......... [100%] 10 passed in 0.01s ``` Статус: ```text PASSED ``` --- ### Проверка 2 — Python compilation Команда: ```bash python -m py_compile \ src/market_data/acquisition/feeds/instrument_feed.py \ tests/unit/market_data/acquisition/feeds/test_instrument_feed.py ``` Результат: ```text Команда завершилась без ошибок и без вывода. ``` Статус: ```text PASSED ``` --- ### Проверка 3 — полный набор тестов проекта Команда: ```bash python -m pytest -q ``` Результат: ```text .............................................................................................................................................. [100%] 142 passed in 0.08s ``` Статус: ```text PASSED ``` --- ### Проверка 4 — отсутствие преждевременной production-интеграции Команда: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "InstrumentFeed|InstrumentFeedProtocol" \ src tests ``` Полученные production-использования: ```text src/market_data/acquisition/protocol.py: InstrumentFeedProtocol src/market_data/acquisition/feeds/instrument_feed.py: InstrumentFeed ``` Остальные использования находятся исключительно в unit-тестах: ```text tests/unit/market_data/acquisition/test_protocol.py tests/unit/market_data/acquisition/feeds/test_instrument_feed.py ``` Не обнаружено подключения к: ```text ExchangeService Registry Acquisition Service Telegram UI AutoTrade Trading runtime другим production-потребителям ``` Статус: ```text PASSED ``` --- ## 28. Архитектура после Build 010 После завершения Build 010 новая часть подсистемы имеет следующую структуру: ```text market_data/ └── acquisition/ ├── exceptions.py │ ├── MarketDataAcquisitionError │ ├── InstrumentReferenceTransportError │ ├── InstrumentReferenceSchemaError │ ├── InstrumentReferenceParseError │ ├── InstrumentReferenceValueError │ └── InstrumentReferenceMappingError │ ├── protocol.py │ ├── InstrumentDocumentSource │ ├── InstrumentDocumentHandler │ └── InstrumentFeedProtocol │ ├── 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 registry.py service.py ``` --- ## 29. Полная архитектурная цепочка после Build 010 После Build 010 реализована единая техническая цепочка: ```text Dzengi REST API ↓ ExchangeRestClient.get_payload() ↓ DzengiInstrumentDocumentSource ↓ object ↓ InstrumentFeed ↓ DzengiInstrumentDocumentHandler ↓ validate_exchange_info_schema() ↓ ValidatedExchangeInfoDocument ↓ parse_exchange_info() ↓ DzengiExchangeInfoResponse ↓ validate_exchange_info_values() ↓ map_dzengi_exchange_info_to_instruments() ↓ tuple[Instrument, ...] ``` При этом сам `InstrumentFeed` не знает, что используются: ```text DzengiInstrumentDocumentSource DzengiInstrumentDocumentHandler ``` Для него зависимости представлены только общими контрактами: ```text InstrumentDocumentSource InstrumentDocumentHandler ``` --- ## 30. Что ещё не реализовано После Build 010 новая acquisition pipeline уже способна технически выполнить: ```text REST API ↓ document ↓ validation ↓ parsing ↓ value validation ↓ mapping ↓ tuple[Instrument, ...] ``` Однако пока отсутствуют: ```text Registry; 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(); перенос кэша. ``` Поэтому новая цепочка остаётся изолированной от существующего production runtime. --- ## 31. Влияние на legacy-систему Build 010 не подключён к существующим компонентам: ```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-путь продолжает работать без изменений. --- ## 32. Обратная совместимость Подтверждено сохранение: ```text сигнатур существующих методов; старых импортов; существующего формата legacy-ошибок; Telegram UI; автоторговли; runtime-поведения; legacy ExchangeSymbol; legacy-кэша. ``` Build 010 имеет полную обратную совместимость. --- ## 33. Классификация изменений | Изменение | Классификация | |---|---| | `InstrumentFeed` | Обязательное архитектурное изменение | | Соединение Source и Handler | Обязательное архитектурное изменение | | Dependency injection Source и Handler | Обязательное разделение ответственности | | Keyword-only зависимости | Улучшение надёжности API | | Сохранение специализированных ошибок | Улучшение диагностируемости | | Новый Feed exception | Не создаётся | | Retry | Не добавляется | | Кэширование | Не добавляется | | Фильтрация и сортировка | Не выполняются | | Изменение legacy-кода | Отсутствует | | Изменение production-поведения | Отсутствует | --- ## 34. Условие завершения Build 010 Все условия выполнены: ```text InstrumentFeed реализован; InstrumentFeedProtocol соблюдён; InstrumentDocumentSource передаётся как явная зависимость; InstrumentDocumentHandler передаётся как явная зависимость; Source вызывается ровно один раз; Handler вызывается ровно один раз после успешного Source; документ передаётся Handler без изменения; результат возвращается без копирования и преобразования; порядок инструментов сохраняется; пустой tuple поддерживается; специализированные ошибки сохраняются без wrapping; retry отсутствует; Dzengi-зависимости в Feed отсутствуют; unit-тесты проходят; полный pytest проходит; Feed не подключён к production runtime. ``` --- ## 35. Итог Build 010 Build 010 завершён успешно. Реализовано: ```text InstrumentFeed InstrumentDocumentSource ↓ fetch_instrument_document() ↓ object ↓ InstrumentDocumentHandler ↓ handle_instrument_document() ↓ tuple[Instrument, ...] ``` Подтверждено: ```text 10 Instrument Feed tests passed 142 total project tests passed Python compilation passed No premature production integration detected Legacy bot behavior unchanged ``` Итоговый статус: ```text BUILD 010 — COMPLETE ``` --- ## 36. Следующий этап Следующий этап утверждённого плана: ```text Build 011 — Registry ``` Registry хранит доступные Instrument Feed и предоставляет нужный Feed по идентификатору источника. Хранение актуального справочника относится к будущему переносу кэша: ```text Build 019 — подготовка переноса кэша Build 020 — перенос кэша в Storage ``` Предполагаемая архитектурная граница: ```text tuple[Instrument, ...] ↓ Instrument Registry ↓ доступ к актуальному справочнику инструментов ``` Build 011 не должен: ```text создавать Acquisition Service; изменять ExchangeService; переключать legacy get_exchange_symbols(); создавать compatibility mapper; переносить legacy-кэш; подключаться к Telegram UI; изменять AutoTrade; изменять trading runtime. ```