# Dzentra — Instrument Reference Data Migration ## Build 003 — структурная валидация `exchangeInfo` **Статус:** Завершён **Подсистема:** Market Data Acquisition **Область:** Instrument Reference Data **Проект:** Dzentra **Язык реализации:** Python 3.12 --- ## 1. Цель Build 003 Цель Build 003 — создать отдельный слой структурной валидации сырого JSON-документа `exchangeInfo` до его преобразования в raw-модели Dzengi. После завершения Build 003 формируется следующий архитектурный поток: ```text JSON response ↓ Schema validation ↓ Parser ↓ Dzengi Raw Models ``` В рамках этого Build реализована только проверка формы и структуры входных данных. Schema validation не выполняет: - преобразование JSON в raw-модели Dzengi; - преобразование camelCase в snake_case; - преобразование строковых чисел в `Decimal`, `float` или `int`; - нормализацию символов; - проверку допустимости числовых значений; - проверку торговой семантики статусов; - создание внутренней модели `Instrument`; - фильтрацию или отбрасывание инструментов. --- ## 2. Почему Build 003 выполняется перед parser После Build 002 уже существуют типизированные raw-модели ответа Dzengi. Следующий этап — parser, однако parser не должен одновременно: - определять wrapped или unwrapped формат ответа; - проверять тип корневого объекта; - проверять наличие `symbols`; - проверять тип `symbols`; - проверять структуру элементов `symbols`; - проверять структуру вложенных коллекций; - создавать raw-модели. Поэтому перед parser был выделен отдельный слой: ```text validation/schema.py ``` Разделение ответственности теперь выглядит следующим образом: ```text schema.py Проверяет форму и структуру данных. parser.py Преобразует структурно корректные данные в raw-модели Dzengi. values.py Проверяет допустимость предметных значений. mapper.py Преобразует raw-модели Dzengi во внутреннюю модель Instrument. ``` Это является обязательным архитектурным изменением для утверждённой структуры Dzentra. --- ## 3. Изменённые и созданные файлы В рамках Build 003 были изменены или созданы только следующие файлы: ```text app/src/market_data/acquisition/exceptions.py app/src/market_data/acquisition/validation/schema.py app/tests/unit/market_data/acquisition/validation/test_schema.py ``` Другие файлы проекта не изменялись. --- ## 4. Реализованные исключения В файле: ```text app/src/market_data/acquisition/exceptions.py ``` добавлены: ```python class MarketDataAcquisitionError(Exception): pass ``` и: ```python class InstrumentReferenceSchemaError(MarketDataAcquisitionError): pass ``` Иерархия ошибок: ```text Exception ↓ MarketDataAcquisitionError ↓ InstrumentReferenceSchemaError ``` `InstrumentReferenceSchemaError` используется для явного обозначения ошибки структуры документа Instrument Reference Data. Это позволяет в последующих Build отличать структурную ошибку от: - сетевой ошибки; - ошибки HTTP; - ошибки JSON-декодирования; - ошибки значений; - ошибки parser; - ошибки mapper. Исключение создано не «на будущее»: оно непосредственно используется schema validation в Build 003. --- ## 5. Реализованный контракт schema validation В файле: ```text app/src/market_data/acquisition/validation/schema.py ``` создана модель: ```python @dataclass(frozen=True, slots=True) class ValidatedExchangeInfoDocument: payload: Mapping[str, object] is_wrapped: bool status: object | None correlation_id: object | None ``` Она представляет структурно проверенный документ `exchangeInfo`. Модель содержит: | Поле | Назначение | |---|---| | `payload` | Проверенный payload с данными `exchangeInfo` | | `is_wrapped` | Признак wrapped/unwrapped формата | | `status` | Верхнеуровневый статус wrapped-ответа | | `correlation_id` | Верхнеуровневый `correlationId` wrapped-ответа | Основная функция: ```python validate_exchange_info_schema(document: object) -> ValidatedExchangeInfoDocument ``` принимает произвольный объект и либо: - возвращает `ValidatedExchangeInfoDocument`; - либо выбрасывает `InstrumentReferenceSchemaError`. --- ## 6. Поддерживаемые форматы ответа ### 6.1. Unwrapped-формат Поддерживается непосредственный payload: ```json { "timezone": "UTC", "serverTime": 1783537921471, "symbols": [] } ``` Результат: ```text is_wrapped = False status = None correlation_id = None ``` --- ### 6.2. Wrapped-формат Поддерживается ответ с вложенным `payload`: ```json { "status": "OK", "correlationId": "2", "payload": { "timezone": "UTC", "serverTime": 1783537921471, "symbols": [] } } ``` Результат: ```text is_wrapped = True status = "OK" correlation_id = "2" ``` --- ## 7. Что проверяет schema validation Build 003 проверяет следующие структурные свойства документа. ### 7.1. Корень документа Корень должен быть JSON-объектом. Отклоняются: ```json [] ``` ```json null ``` ```json "invalid" ``` ```json 123 ``` --- ### 7.2. Wrapped payload Если присутствует ключ: ```text payload ``` его значение должно быть JSON-объектом. Например, отклоняется: ```json { "status": "OK", "payload": [] } ``` --- ### 7.3. Наличие `symbols` Payload должен содержать: ```text symbols ``` Отсутствие `symbols` считается ошибкой структуры. --- ### 7.4. Тип `symbols` `symbols` должен быть JSON-массивом. Отклоняется: ```json { "symbols": {} } ``` --- ### 7.5. Элементы `symbols` Каждый элемент `symbols` должен быть JSON-объектом. Отклоняется: ```json { "symbols": [ "BTC/USD" ] } ``` --- ### 7.6. `filters` Если у инструмента присутствует: ```text filters ``` то: - `filters` должен быть JSON-массивом; - каждый элемент `filters` должен быть JSON-объектом. Отклоняется: ```json { "symbols": [ { "symbol": "BTC/USD", "filters": {} } ] } ``` Также отклоняется: ```json { "symbols": [ { "symbol": "BTC/USD", "filters": [ "LOT_SIZE" ] } ] } ``` --- ### 7.7. `marketModes` Если присутствует: ```text marketModes ``` то: - значение должно быть JSON-массивом; - каждый элемент должен быть строкой. --- ### 7.8. `orderTypes` Если присутствует: ```text orderTypes ``` то: - значение должно быть JSON-массивом; - каждый элемент должен быть строкой. --- ### 7.9. `rateLimits` Если присутствует: ```text rateLimits ``` то: - значение должно быть JSON-массивом; - каждый элемент должен быть JSON-объектом. --- ### 7.10. `exchangeFilters` Если присутствует: ```text exchangeFilters ``` то: - значение должно быть JSON-массивом; - каждый элемент должен быть JSON-объектом. --- ## 8. Что намеренно не проверяется в Build 003 Build 003 не проверяет допустимость значений. Следующие примеры не относятся к schema validation: ```json { "tickSize": -1 } ``` ```json { "baseAssetPrecision": -5 } ``` ```json { "status": "" } ``` ```json { "minQty": "not-a-number" } ``` Такие проверки относятся к: ```text app/src/market_data/acquisition/validation/values.py ``` и должны реализовываться отдельно, без смешения структурной и предметной валидации. --- ## 9. Защита проверенного payload Поле: ```python payload: Mapping[str, object] ``` возвращается через: ```python MappingProxyType ``` Это предотвращает случайное изменение корневого payload последующим parser. Таким образом, schema validation передаёт следующему слою проверенное read-only представление данных. --- ## 10. Диагностические пути ошибок Schema validation формирует ошибки с указанием пути до проблемного значения. Примеры: ```text $ должен быть JSON-объектом, получен list. ``` ```text $.payload.symbols должен быть JSON-массивом, получен dict. ``` ```text $.payload.symbols[0] должен быть JSON-объектом, получен str. ``` ```text $.payload.symbols[0].filters должен быть JSON-массивом, получен dict. ``` На текущем этапе для `symbols` используется унифицированный диагностический путь: ```text $.payload.symbols ``` как для wrapped-, так и для unwrapped-формата. Это сознательное упрощение текущего контракта и не влияет на production-поведение. --- ## 11. Реализованные тесты Создан файл: ```text app/tests/unit/market_data/acquisition/validation/test_schema.py ``` Он проверяет: - корректный unwrapped-документ; - корректный wrapped-документ; - отклонение не-объекта в корне; - отклонение некорректного wrapped payload; - отсутствие `symbols`; - некорректный тип `symbols`; - некорректный элемент `symbols`; - некорректный тип `filters`; - некорректный элемент `filters`; - некорректный тип `marketModes`; - некорректный тип `orderTypes`; - нестроковый элемент `marketModes`; - нестроковый элемент `orderTypes`; - некорректный тип `rateLimits`; - некорректный элемент `rateLimits`; - некорректный тип `exchangeFilters`; - некорректный элемент `exchangeFilters`. Итог: ```text 20 passed ``` --- ## 12. Выполненные проверки ### Проверка 1 — unit-тесты Build 003 Команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/validation/test_schema.py \ -q ``` Результат: ```text .................... [100%] 20 passed in 0.01s ``` Статус: ```text PASSED ``` --- ### Проверка 2 — компиляция файлов Build 003 Команда: ```bash python -m py_compile \ src/market_data/acquisition/exceptions.py \ src/market_data/acquisition/validation/schema.py \ tests/unit/market_data/acquisition/validation/test_schema.py ``` Результат: ```text Ошибок нет. ``` Статус: ```text PASSED ``` --- ### Проверка 3 — ручная проверка wrapped/unwrapped контрактов Получен результат: ```text Unwrapped: is_wrapped: False status: None correlation_id: None symbols: [] Wrapped: is_wrapped: True status: OK correlation_id: 2 symbols: [] ``` Подтверждено: - unwrapped-формат определяется корректно; - wrapped-формат определяется корректно; - `status` сохраняется; - `correlationId` сохраняется как `correlation_id`; - `symbols` доступны через проверенный payload. Статус: ```text PASSED ``` --- ### Проверка 4 — типизированные ошибки Проверены четыре некорректных документа. Получен результат: ```text 1: InstrumentReferenceSchemaError: $ должен быть JSON-объектом, получен list. 2: InstrumentReferenceSchemaError: $.payload.symbols должен быть JSON-массивом, получен dict. 3: InstrumentReferenceSchemaError: $.payload.symbols[0] должен быть JSON-объектом, получен str. 4: InstrumentReferenceSchemaError: $.payload.symbols[0].filters должен быть JSON-массивом, получен dict. ``` Ни один ошибочный документ не был принят. Статус: ```text PASSED ``` --- ### Проверка 5 — полный набор тестов проекта Команда: ```bash python -m pytest -q ``` Результат: ```text ............................... [100%] 31 passed in 0.04s ``` Статус: ```text PASSED ``` --- ### Проверка 6 — отсутствие подключения к production-коду Выполнен поиск: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "validate_exchange_info_schema|ValidatedExchangeInfoDocument|InstrumentReferenceSchemaError" \ src tests ``` Подтверждено, что новые сущности Build 003 используются только в: ```text src/market_data/acquisition/exceptions.py src/market_data/acquisition/validation/schema.py tests/unit/market_data/acquisition/validation/test_schema.py ``` Они пока не подключены к: ```text ExchangeService legacy exchangeInfo Telegram UI runtime автоторговле market stream production parser production mapper ``` Статус: ```text PASSED ``` --- ## 13. Обратная совместимость Build 003 не изменяет: ```text ExchangeService.get_exchange_symbols() ExchangeService.validate_symbol() ExchangeService.get_symbol_runtime_status() ExchangeService.get_symbol_market_status() ``` Не изменены: ```text ExchangeSymbol SymbolValidationResult normalize_symbol() symbol_candidates() ``` Не изменено поведение: - Telegram UI; - автоторговли; - runtime-проверок символа; - legacy-кэша инструментов; - получения цен; - market stream; - существующего `exchangeInfo`. Новый schema validation пока изолирован от production-кода. --- ## 14. Точки обратной совместимости После Build 003 продолжают действовать прежние точки совместимости: ```text ExchangeService.get_exchange_symbols() → list[ExchangeSymbol] ExchangeService.validate_symbol() → SymbolValidationResult ExchangeService.get_symbol_runtime_status() → ExchangeRuntimeStatus ExchangeService.get_symbol_market_status() → dict[str, object] ``` Ни одна из этих сигнатур не изменена. --- ## 15. Классификация изменений | Изменение | Классификация | |---|---| | Создание schema validation | Обязательное архитектурное изменение | | Создание `MarketDataAcquisitionError` | Обязательное архитектурное изменение | | Создание `InstrumentReferenceSchemaError` | Обязательное архитектурное изменение | | Поддержка wrapped/unwrapped форматов | Улучшение надёжности | | Явные структурные ошибки | Улучшение надёжности | | Read-only представление проверенного payload | Улучшение надёжности | | Изменение production-поведения | Отсутствует | | Изменение публичных legacy-интерфейсов | Отсутствует | --- ## 16. Условие завершения Build 003 Build 003 считается завершённым, потому что выполнены все условия: - создан отдельный schema validation layer; - поддержан wrapped-формат; - поддержан unwrapped-формат; - проверяется обязательная структура `symbols`; - проверяются вложенные коллекции; - ошибки типизированы; - создан read-only контракт для передачи данных parser; - написаны unit-тесты; - все тесты Build проходят; - полный набор тестов проекта проходит; - production-код не изменён; - обратная совместимость сохранена. --- ## 17. Итоговый статус ```text Build 003 — COMPLETED ``` Итоговый набор тестов проекта: ```text 31 passed in 0.04s ``` Следующий этап: ```text Build 004 — Parser exchangeInfo ``` На Build 004 новый parser должен: 1. принимать структурно проверенный `ValidatedExchangeInfoDocument`; 2. преобразовывать payload в raw-модели Dzengi, созданные в Build 002; 3. не выполнять повторную schema validation; 4. не создавать внутреннюю модель `Instrument`; 5. не подключаться к `ExchangeService`; 6. не менять production-поведение работающего бота.