# Build 009 — Instrument Handler **Статус:** Завершён **Подсистема:** `market_data/acquisition` **Область:** Instrument Reference Data **Тип изменения:** Изолированное добавление processing handler без подключения к production runtime **Результат полного набора тестов:** `132 passed` --- ## 1. Цель Build 009 Цель Build 009 — реализовать конкретный обработчик документа Instrument Reference Data для формата Dzengi `exchangeInfo`, соответствующий созданному в Build 007 протоколу: ```python class InstrumentDocumentHandler(Protocol): def handle_instrument_document( self, document: object, ) -> tuple[Instrument, ...]: ... ``` Реализован класс: ```text DzengiInstrumentDocumentHandler ``` Архитектурная граница Build 009: ```text object ↓ DzengiInstrumentDocumentHandler ↓ tuple[Instrument, ...] ``` Handler объединяет уже реализованные стадии обработки: ```text object ↓ validate_exchange_info_schema() ↓ ValidatedExchangeInfoDocument ↓ parse_exchange_info() ↓ DzengiExchangeInfoResponse ↓ validate_exchange_info_values() ↓ map_dzengi_exchange_info_to_instruments() ↓ tuple[Instrument, ...] ``` Build 009 не выполняет получение документа по сети, не создаёт Feed, не работает с Registry и не подключается к production runtime. --- ## 2. Почему Build 009 выполняется именно сейчас До начала Build 009 были завершены: ```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 008 существовали две отдельные части будущей acquisition-цепочки. Получение документа: ```text Dzengi REST API ↓ ExchangeRestClient.get_payload() ↓ DzengiInstrumentDocumentSource ↓ object ``` Обработка документа: ```text object ↓ validate_exchange_info_schema() ↓ parse_exchange_info() ↓ validate_exchange_info_values() ↓ map_dzengi_exchange_info_to_instruments() ↓ tuple[Instrument, ...] ``` До Build 009 processing pipeline представлял собой набор отдельных функций. Следующим необходимым шагом стало объединение этих функций в конкретную реализацию контракта: ```text InstrumentDocumentHandler ``` --- ## 3. Изменённые файлы В рамках Build 009 реализован production-файл: ```text app/src/market_data/acquisition/handlers/instrument_handler.py ``` Создан тестовый файл: ```text app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py ``` Другие production-файлы не изменялись. --- ## 4. Реализованный Instrument Handler В файле: ```text app/src/market_data/acquisition/handlers/instrument_handler.py ``` реализован класс: ```python class DzengiInstrumentDocumentHandler: ... ``` Его публичный метод: ```python def handle_instrument_document( self, document: object, ) -> tuple[Instrument, ...]: ... ``` соответствует контракту: ```text InstrumentDocumentHandler ``` --- ## 5. Реализованный processing pipeline Внутри `DzengiInstrumentDocumentHandler` последовательно вызываются четыре ранее реализованные стадии: ```python validated_document = validate_exchange_info_schema(document) response = parse_exchange_info(validated_document) validate_exchange_info_values(response) return map_dzengi_exchange_info_to_instruments(response) ``` Полная последовательность: ```text сырой декодированный JSON-документ ↓ schema validation ↓ проверенный структурный документ ↓ parser ↓ Dzengi raw-модели ↓ value validation ↓ проверенные Dzengi raw-модели ↓ mapper ↓ внутренние source-independent модели Instrument ``` Таким образом, Handler является orchestration boundary для обработки одного документа, но не реализует самостоятельно логику отдельных стадий. --- ## 6. Разделение ответственности Каждая стадия сохраняет собственную ответственность. ### Schema Validation ```text validate_exchange_info_schema() ``` Отвечает за структуру JSON-документа: ```text корневой тип; wrapped/unwrapped формат; payload; symbols; filters; типы JSON-полей. ``` ### Parser ```text parse_exchange_info() ``` Отвечает за преобразование проверенного документа в: ```text DzengiExchangeInfoResponse DzengiExchangeInfoPayload DzengiExchangeInfoSymbol DzengiInstrumentFilter ``` ### Value Validation ```text validate_exchange_info_values() ``` Отвечает за допустимость значений: ```text обязательные непустые строки; числовые ограничения; finite numbers; положительные значения; отношение minQty ≤ maxQty; семантику filters. ``` ### Mapper ```text map_dzengi_exchange_info_to_instruments() ``` Отвечает за преобразование source-specific raw-моделей Dzengi во внутренние source-independent модели: ```text Instrument ``` ### Instrument Handler ```text DzengiInstrumentDocumentHandler ``` Отвечает только за правильную последовательность вызова этих стадий. --- ## 7. Почему Handler является source-specific Конкретный Handler использует: ```text Dzengi parser Dzengi raw models Dzengi mapper ``` Поэтому класс явно называется: ```text DzengiInstrumentDocumentHandler ``` а не: ```text InstrumentHandler ``` Это сохраняет важную архитектурную границу: ```text общий Protocol ↓ InstrumentDocumentHandler конкретная source-specific реализация ↓ DzengiInstrumentDocumentHandler ``` В будущем другой источник может предоставить собственную реализацию: ```text AnotherExchangeInstrumentDocumentHandler ``` при сохранении общего контракта: ```text InstrumentDocumentHandler ``` --- ## 8. Отсутствие нового Handler exception В Build 009 намеренно не создавалась ошибка: ```text InstrumentReferenceHandlerError ``` Handler сохраняет без дополнительного wrapping специализированные ошибки отдельных стадий: ```text InstrumentReferenceSchemaError InstrumentReferenceParseError InstrumentReferenceValueError InstrumentReferenceMappingError ``` Это позволяет точно определить этап отказа: ```text Schema Error → документ имеет неверную структуру Parse Error → проверенный документ невозможно корректно преобразовать в Dzengi raw-модели Value Error → структура корректна, но значения недопустимы Mapping Error → raw-модель корректна, но её невозможно однозначно преобразовать во внутреннюю модель Instrument ``` Добавление общей Handler-ошибки поверх этих исключений ухудшило бы диагностируемость. --- ## 9. Отсутствие try/except в Handler В `DzengiInstrumentDocumentHandler` намеренно отсутствует: ```python try: ... except Exception: ... ``` Handler не скрывает и не переклассифицирует ошибки нижележащих стадий. Например: ```text validate_exchange_info_schema() ↓ InstrumentReferenceSchemaError ↓ выходит из Handler без изменения ``` Аналогично: ```text parse_exchange_info() ↓ InstrumentReferenceParseError ``` ```text validate_exchange_info_values() ↓ InstrumentReferenceValueError ``` ```text map_dzengi_exchange_info_to_instruments() ↓ InstrumentReferenceMappingError ``` --- ## 10. Отсутствие Dependency Injection функций pipeline В Build 009 не добавлялась передача через конструктор: ```text schema validator; parser; value validator; mapper. ``` Причины: ```text все четыре компонента уже реализованы как чистые функции; существует одна утверждённая последовательность обработки; unit-тесты могут проверять реальный pipeline; нет нескольких production-реализаций этих стадий; нет текущей необходимости конфигурировать pipeline. ``` Добавление dependency injection четырёх функций на текущем этапе создало бы лишнюю абстракцию без практической потребности. --- ## 11. Поддержка wrapped и unwrapped документов Handler поддерживает оба формата, уже утверждённые в Build 003. ### Unwrapped ```json { "timezone": "UTC", "serverTime": 1783537921471, "rateLimits": [], "exchangeFilters": [], "symbols": [] } ``` ### Wrapped ```json { "status": "OK", "correlationId": "2", "payload": { "timezone": "UTC", "serverTime": 1783537921471, "rateLimits": [], "exchangeFilters": [], "symbols": [] } } ``` Handler не реализует отдельную логику определения формата. Эта ответственность остаётся у: ```text validate_exchange_info_schema() ``` --- ## 12. Возвращаемый тип Handler возвращает: ```python tuple[Instrument, ...] ``` а не: ```python list[Instrument] ``` Это соответствует: ```text InstrumentDocumentHandler Protocol ``` и принятой модели immutable sequences в новой подсистеме. При отсутствии инструментов: ```json { "symbols": [] } ``` возвращается: ```python () ``` --- ## 13. Точное преобразование числовых значений Тестами подтверждено, что после прохождения полного Handler pipeline числовые значения внутренней модели представлены через: ```python Decimal ``` Проверены: ```text tick_size tick_value step_size min_qty max_qty min_notional ``` Пример: ```python assert instrument.tick_size == Decimal("0.05") assert instrument.tick_value == Decimal("3878.86") assert instrument.step_size == Decimal("0.0001") assert instrument.min_qty == Decimal("0.0001") assert instrument.max_qty == Decimal("1000") assert instrument.min_notional == Decimal("1") ``` Это подтверждает, что Handler корректно проводит документ через уже реализованный mapper без потери точности внутренней модели. --- ## 14. Тестирование специализированных ошибок Тестами проверено прохождение четырёх специализированных типов ошибок. ### Schema Error Использован невалидный корневой документ: ```python [] ``` Ожидаемая ошибка: ```text InstrumentReferenceSchemaError ``` Ошибка возникает на стадии: ```text validate_exchange_info_schema() ``` --- ### Parse Error Использовано: ```python "baseAssetPrecision": True ``` Значение проходит структурную границу JSON, но не может быть принято parser как корректное целочисленное значение precision. Ожидаемая ошибка: ```text InstrumentReferenceParseError ``` Ошибка возникает на стадии: ```text parse_exchange_info() ``` --- ### Value Error Использовано: ```python "tickSize": 0 ``` Документ успешно проходит: ```text schema validation parser ``` но отклоняется: ```text value validation ``` Ожидаемая ошибка: ```text InstrumentReferenceValueError ``` --- ### Mapping Error Для проверки mapper использованы два корректных фильтра: ```text LOT_SIZE LOT_SIZE ``` Каждый фильтр отдельно является корректным: ```json { "filterType": "LOT_SIZE", "minQty": "0.0001", "maxQty": "1000", "stepSize": "0.0001" } ``` Документ успешно проходит: ```text schema validation parser value validation ``` но mapper обнаруживает неоднозначность: ```text несколько фильтров LOT_SIZE ``` и выбрасывает: ```text InstrumentReferenceMappingError ``` --- ## 15. Исправление первоначального теста Mapping Error Первоначально тест использовал: ```python "minQty": None ``` и ожидал: ```text InstrumentReferenceMappingError ``` Однако тест завершился ошибкой: ```text Failed: DID NOT RAISE InstrumentReferenceMappingError ``` Анализ показал, что по текущему контракту: ```text minQty=None ``` является допустимым значением. Последовательность была корректной: ```text parser допускает None value validation допускает отсутствующий minQty mapper преобразует None → None ``` Следовательно, production-код работал правильно, а неверным был тестовый сценарий. Тест был исправлен без изменения production-кода. Вместо `minQty=None` использованы два корректных `LOT_SIZE`, что гарантированно вызывает ошибку именно на mapper. --- ## 16. Что Handler не делает Build 009 сознательно не выполняет: ```text HTTP-запросы; создание DzengiInstrumentDocumentSource; создание Instrument Feed; создание Registry; создание Acquisition Service; кэширование; retry; логирование; формирование Telegram-сообщений; проверку exchange_enabled; создание legacy ExchangeSymbol; изменение ExchangeService; production-подключение. ``` Handler не знает: ```text откуда был получен документ; когда был получен документ; нужно ли повторять запрос; куда сохранять результат; кто является потребителем результата. ``` Его единственная ответственность: ```text object ↓ полный processing pipeline ↓ tuple[Instrument, ...] ``` --- ## 17. Отсутствие transport-зависимости `DzengiInstrumentDocumentHandler` не импортирует: ```text DzengiInstrumentDocumentSource ExchangeRestClient urllib httpx requests ``` Он не выполняет сетевые операции. Архитектурная граница остаётся: ```text Build 008 ↓ DzengiInstrumentDocumentSource ↓ object Build 009 ↓ DzengiInstrumentDocumentHandler ↓ tuple[Instrument, ...] ``` Соединение Source и Handler относится к следующему этапу: ```text Build 010 — Instrument Feed ``` --- ## 18. Реализованные тестовые сценарии Создан файл: ```text app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py ``` Реализовано 9 тестов. Проверены следующие сценарии: 1. `DzengiInstrumentDocumentHandler` соответствует `InstrumentDocumentHandler`; 2. корректно обрабатывается valid unwrapped document; 3. корректно обрабатывается valid wrapped document; 4. возвращаются точные `Decimal`-значения; 5. для пустого `symbols` возвращается пустой `tuple`; 6. сохраняется `InstrumentReferenceSchemaError`; 7. сохраняется `InstrumentReferenceParseError`; 8. сохраняется `InstrumentReferenceValueError`; 9. сохраняется `InstrumentReferenceMappingError`. --- ## 19. Выполненные проверки ### Проверка 1 — unit-тесты Instrument Handler Команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/handlers/test_instrument_handler.py \ -q ``` Первоначальный результат: ```text ........F [100%] FAILED test_handler_preserves_mapping_error 1 failed, 8 passed in 0.04s ``` Причина: ```text тест ожидал InstrumentReferenceMappingError для minQty=None, но такое значение допустимо текущим контрактом. ``` Production-код не изменялся. После исправления тестового сценария: ```bash python -m pytest \ tests/unit/market_data/acquisition/handlers/test_instrument_handler.py \ -q ``` получен результат: ```text ......... [100%] 9 passed in 0.02s ``` Статус: ```text PASSED ``` --- ### Проверка 2 — Python compilation Команда: ```bash python -m py_compile \ src/market_data/acquisition/handlers/instrument_handler.py \ tests/unit/market_data/acquisition/handlers/test_instrument_handler.py ``` Результат: ```text Команда завершилась без ошибок и без вывода. ``` Статус: ```text PASSED ``` --- ### Проверка 3 — полный набор тестов проекта Команда: ```bash python -m pytest -q ``` Результат: ```text .................................................................................................................................... [100%] 132 passed in 0.08s ``` Статус: ```text PASSED ``` --- ### Проверка 4 — отсутствие преждевременной production-интеграции Команда: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "DzengiInstrumentDocumentHandler|InstrumentDocumentHandler" \ src tests ``` Полученные production-использования: ```text src/market_data/acquisition/protocol.py: InstrumentDocumentHandler src/market_data/acquisition/handlers/instrument_handler.py: DzengiInstrumentDocumentHandler ``` Остальные использования находятся исключительно в unit-тестах: ```text tests/unit/market_data/acquisition/test_protocol.py tests/unit/market_data/acquisition/handlers/test_instrument_handler.py ``` Не обнаружено подключения к: ```text ExchangeService Telegram UI AutoTrade Trading runtime другим production-потребителям ``` Статус: ```text PASSED ``` --- ## 20. Архитектура после Build 009 После завершения Build 009 новая часть подсистемы имеет следующую структуру: ```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 │ └── adapters/ └── dzengi/ ├── models.py ├── parser.py ├── mapper.py └── rest.py ├── _PayloadRestClient └── DzengiInstrumentDocumentSource ``` На текущем этапе ещё не реализованы: ```text feeds/instrument_feed.py registry.py service.py ``` --- ## 21. Полная архитектурная цепочка после Build 009 После Build 009 реализованы две изолированные части. ### Transport ```text Dzengi REST API ↓ ExchangeRestClient.get_payload() ↓ DzengiInstrumentDocumentSource ↓ object ``` ### Processing ```text object ↓ DzengiInstrumentDocumentHandler │ ├── validate_exchange_info_schema() │ ↓ ├── parse_exchange_info() │ ↓ ├── validate_exchange_info_values() │ ↓ └── map_dzengi_exchange_info_to_instruments() ↓ tuple[Instrument, ...] ``` Они пока намеренно не соединены в production-коде. Следующая архитектурная связь: ```text DzengiInstrumentDocumentSource ↓ DzengiInstrumentDocumentHandler ``` будет реализована через общий Protocol в: ```text Build 010 — Instrument Feed ``` --- ## 22. Влияние на legacy-систему Build 009 не подключён к существующим компонентам: ```text ExchangeService ExchangeSymbol SymbolValidationResult Telegram UI AutoTrade Market Stream Market Data Runner Execution Quality ``` Не изменены: ```text ExchangeService.get_exchange_symbols() ExchangeService.validate_symbol() ExchangeService.get_symbol_runtime_status() normalize_symbol() symbol_candidates() ``` Старый production-путь продолжает работать без изменений. Новый Handler существует изолированно и пока вызывается только unit-тестами. --- ## 23. Обратная совместимость Подтверждено сохранение: ```text сигнатур существующих методов; старых импортов; существующего формата legacy-ошибок; Telegram UI; автоторговли; runtime-поведения; legacy ExchangeSymbol; legacy-кэша. ``` Build 009 имеет полную обратную совместимость. --- ## 24. Классификация изменений | Изменение | Классификация | |---|---| | `DzengiInstrumentDocumentHandler` | Обязательное архитектурное изменение | | Объединение schema → parser → values → mapper | Обязательное архитектурное изменение | | Сохранение специализированных ошибок | Улучшение диагностируемости | | Новый Handler exception | Не создаётся | | Dependency injection функций pipeline | Не добавляется | | HTTP-зависимость | Отсутствует | | Изменение legacy-кода | Отсутствует | | Изменение production-поведения | Отсутствует | --- ## 25. Условие завершения Build 009 Все условия выполнены: ```text DzengiInstrumentDocumentHandler реализован; InstrumentDocumentHandler соблюдён; schema validation выполняется первой; parser получает только структурно проверенный документ; value validation выполняется после parser; mapper выполняется только после успешной value validation; tuple[Instrument, ...] возвращается как итоговый результат; специализированные ошибки сохраняются без дополнительного wrapping; HTTP-зависимость отсутствует; unit-тесты проходят; полный pytest проходит; Handler не подключён к production runtime. ``` --- ## 26. Итог Build 009 Build 009 завершён успешно. Реализовано: ```text DzengiInstrumentDocumentHandler object ↓ schema validation ↓ parser ↓ value validation ↓ mapper ↓ tuple[Instrument, ...] ``` Подтверждено: ```text 9 Instrument Handler tests passed 132 total project tests passed Python compilation passed No premature production integration detected Legacy bot behavior unchanged ``` Итоговый статус: ```text BUILD 009 — COMPLETE ``` --- ## 27. Следующий этап Следующий этап утверждённого плана: ```text Build 010 — Instrument Feed ``` Его задача — соединить уже реализованные общие контракты: ```text InstrumentDocumentSource ↓ InstrumentDocumentHandler ↓ InstrumentFeedProtocol ``` Предполагаемая архитектурная граница Build 010: ```text InstrumentDocumentSource ↓ fetch_instrument_document() ↓ object ↓ InstrumentDocumentHandler ↓ handle_instrument_document() ↓ tuple[Instrument, ...] ``` Build 010 не должен: ```text создавать Registry; создавать Acquisition Service; изменять ExchangeService; переключать legacy get_exchange_symbols(); создавать compatibility mapper; переносить кэш; подключаться к Telegram UI; изменять AutoTrade или trading runtime. ```