# Build 005 — Value Validation для Instrument Reference Data **Статус:** Завершён **Подсистема:** `market_data/acquisition` **Область:** Instrument Reference Data **Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime **Результат полного набора тестов:** `84 passed` --- ## 1. Цель Build 005 Цель Build 005 — реализовать отдельный слой проверки допустимости значений, полученных после: 1. проверки структуры исходного документа `exchangeInfo`; 2. преобразования проверенного документа в типизированные raw-модели адаптера Dzengi. Build 005 добавляет третий этап обработки Instrument Reference Data: ```text raw JSON ↓ schema validation ↓ parser ↓ value validation ``` Value validation проверяет не структуру JSON и не типы полей исходного документа, а предметную допустимость уже распарсенных значений. Например: ```text tickSize = 0 ``` может быть корректным числовым значением с точки зрения JSON и parser layer, но недопустимым значением для шага цены торгового инструмента. --- ## 2. Почему Build 005 реализован именно на этом этапе До Build 005 были завершены следующие этапы миграции: ```text Build 001 ↓ Каноническая source-independent модель Instrument Build 002 ↓ Raw transport models адаптера Dzengi Build 003 ↓ Schema validation исходного exchangeInfo Build 004 ↓ Parser: validated document → Dzengi raw models Build 005 ↓ Value validation Dzengi raw models ``` Такой порядок позволяет строго разделить ответственность слоёв. ### Schema validation отвечает за: ```text Есть ли ожидаемые JSON-объекты? Есть ли массив symbols? Являются ли элементы symbols объектами? Является ли filters массивом? ``` ### Parser отвечает за: ```text Как преобразовать проверенный JSON-документ в типизированные raw-модели Dzengi? ``` ### Value validation отвечает за: ```text Допустимы ли конкретные значения полей с точки зрения контракта Instrument Reference Data? ``` Это исключает смешивание: - проверки JSON-структуры; - транспортного parsing; - проверки предметных значений; - source-independent mapping. --- ## 3. Изменённые файлы В рамках Build 005 изменены: ```text app/src/market_data/acquisition/exceptions.py app/src/market_data/acquisition/validation/values.py ``` Добавлен тестовый файл: ```text app/tests/unit/market_data/acquisition/validation/test_values.py ``` --- ## 4. Новая типизированная ошибка В файл: ```text app/src/market_data/acquisition/exceptions.py ``` добавлена ошибка: ```python class InstrumentReferenceValueError(MarketDataAcquisitionError): pass ``` Иерархия ошибок Instrument Reference Data теперь выглядит так: ```text MarketDataAcquisitionError ├── InstrumentReferenceSchemaError ├── InstrumentReferenceParseError └── InstrumentReferenceValueError ``` Каждый этап обработки имеет собственную категорию ошибки: | Этап | Ошибка | |---|---| | Schema validation | `InstrumentReferenceSchemaError` | | Parsing | `InstrumentReferenceParseError` | | Value validation | `InstrumentReferenceValueError` | Это позволяет будущему orchestration/service layer точно определять, на каком этапе обработки произошла ошибка. --- ## 5. Главная функция Build 005 В файл: ```text app/src/market_data/acquisition/validation/values.py ``` добавлена функция: ```python def validate_exchange_info_values( response: DzengiExchangeInfoResponse, ) -> None: ``` Функция принимает: ```python DzengiExchangeInfoResponse ``` то есть результат работы parser layer из Build 004. При корректных значениях функция возвращает: ```python None ``` При обнаружении недопустимого значения выбрасывается: ```python InstrumentReferenceValueError ``` --- ## 6. Проверяемые обязательные строки инструмента Для каждого `DzengiExchangeInfoSymbol` проверяются следующие обязательные строковые поля: ```text symbol name status baseAsset quoteAsset marketType ``` Они не должны быть пустыми или состоять только из пробелов. Пример недопустимого значения: ```python symbol=" " ``` Результат: ```text InstrumentReferenceValueError: $.payload.symbols[0].symbol не должен быть пустым. ``` --- ## 7. Проверка `orderTypes` и `marketModes` Каждый элемент следующих последовательностей проверяется на непустое строковое значение: ```text orderTypes marketModes ``` Например: ```python order_types=("LIMIT", " ") ``` отклоняется с ошибкой: ```text $.payload.symbols[0].orderTypes[1] не должен быть пустым. ``` Аналогично: ```python market_modes=("REGULAR", "") ``` отклоняется как недопустимое значение. --- ## 8. Проверка integer-полей Следующие optional integer-поля не могут быть отрицательными: ```text baseAssetPrecision quotePrecision swapChargeInterval ``` Допустимо: ```text None 0 1 2 ... ``` Недопустимо: ```text -1 ``` При нарушении выбрасывается: ```text InstrumentReferenceValueError ``` --- ## 9. Проверка `tickSize` Для: ```text tickSize ``` применяется правило: ```text tickSize > 0 ``` Допустимы только положительные конечные числа. Отклоняются: ```text 0 отрицательные значения NaN +Infinity -Infinity ``` Это правило подтверждено анализом реального sample Dzengi: ```text tickSize: present=51 min=1E-8 max=1 zero=0 negative=0 invalid=[] ``` --- ## 10. Проверка конечности числовых значений Для optional numeric-полей проверяется, что значение является конечным числом. Проверяются: ```text tickValue tradingFee exchangeFee longRate shortRate minSLGap maxSLGap minTPGap maxTPGap ``` Отклоняются: ```text NaN +Infinity -Infinity ``` При этом нулевые значения не запрещаются автоматически. --- ## 11. Проверка LOT_SIZE Для: ```text DzengiLotSizeFilter ``` проверяются: ```text minQty maxQty stepSize ``` Если значение присутствует, оно должно: 1. корректно преобразовываться в `Decimal`; 2. быть конечным; 3. быть строго больше нуля. Дополнительно проверяется отношение: ```text minQty <= maxQty ``` Если: ```text minQty > maxQty ``` выбрасывается: ```text InstrumentReferenceValueError ``` --- ## 12. Проверка MIN_NOTIONAL Для: ```text DzengiMinNotionalFilter ``` проверяется: ```text minNotional >= 0 ``` Нулевое значение разрешено. Отрицательное значение отклоняется. --- ## 13. Использование Decimal Строковые и числовые значения ограничений торгового инструмента преобразуются для проверки через: ```python Decimal(str(value)) ``` Это позволяет избежать ненужной потери точности при обработке таких значений, как: ```text 0.0001 0.00000001 6.9E-7 ``` Именно такой подход соответствует уже принятому контракту канонической модели `Instrument`, где торговые числовые ограничения представлены через `Decimal`. --- ## 14. Особенность полей country, sector и industry В ходе первой проверки Build 005 была обнаружена ошибка первоначальной реализации. Изначально к полям: ```text country sector industry ``` применялось правило: ```text если значение присутствует, строка не должна быть пустой ``` Однако анализ реального ответа Dzengi и уже созданных raw-моделей показал, что Dzengi штатно возвращает: ```python country="" sector="" industry="" ``` Поэтому пустая строка для этих полей является допустимым транспортным состоянием. После исправления Build 005: ```text country="" sector="" industry="" ``` не считаются ошибкой. Это соответствует реальному контракту Dzengi и не выполняет преждевременную нормализацию transport-specific данных. --- ## 15. Проверка отрицательных longRate и shortRate Отрицательные значения: ```text longRate shortRate ``` являются допустимыми. Это подтверждено анализом реального sample Dzengi: ```text longRate: present=51 min=-0.0684932 max=0.1389493 negative=43 shortRate: present=51 min=-0.1608693 max=0.01 negative=38 ``` Поэтому value validation проверяет только: ```text значение является корректным конечным числом ``` но не требует: ```text value >= 0 ``` --- ## 16. Проверка допустимости нулевых optional numeric values Нулевые значения разрешены для полей, для которых реальный контракт Dzengi допускает `0`. В частности: ```text tickValue tradingFee exchangeFee minSLGap maxSLGap minTPGap maxTPGap ``` Это соответствует анализу реального sample: ```text tickValue: zero=1 tradingFee: zero=40 minSLGap: zero=51 minTPGap: zero=51 ``` Value validation не вводит ограничений, которые не подтверждены фактическими данными или контрактом источника. --- ## 17. Проверка неизвестных instrument filters Для неизвестного фильтра инструмента: ```python DzengiUnknownFilter ``` поле: ```text filterType ``` должно быть непустым. Это позволяет сохранить forward compatibility с новыми типами фильтров Dzengi, одновременно предотвращая попадание полностью неопределённых instrument filters в дальнейшую обработку. --- ## 18. Особенность global exchange filters Для: ```text exchangeFilters ``` сохранено более мягкое поведение. Реальный transport contract может содержать global filter без meaningful `filterType`, поэтому пустое значение не отклоняется автоматически. Таким образом, Build 005 различает: ```text instrument-level filters ``` и: ```text global exchange filters ``` и не навязывает им одинаковые ограничения без подтверждения реального контракта. --- ## 19. Результаты анализа реального sample Dzengi Перед реализацией Build 005 был выполнен анализ реального `exchangeInfo` sample. Получены следующие результаты. ### Обязательные строки Пустые значения отсутствуют для: ```text symbol name status baseAsset quoteAsset marketType ``` ### Precision ```text baseAssetPrecision: count=51 min=2 max=8 invalid=[] quotePrecision: count=51 min=2 max=8 invalid=[] ``` ### Числовые диапазоны ```text tickSize: present=51 min=1E-8 max=1 zero=0 negative=0 invalid=[] tickValue: present=39 min=0 max=50665.5 zero=1 negative=0 invalid=[] minQty: present=51 min=0.0001 max=1 zero=0 negative=0 invalid=[] maxQty: present=51 min=100 max=10000000 zero=0 negative=0 invalid=[] stepSize: present=51 min=0.0001 max=1 zero=0 negative=0 invalid=[] minNotional: present=39 min=6.9E-7 max=507 zero=0 negative=0 invalid=[] tradingFee: present=51 min=0 max=0.075 zero=40 negative=0 invalid=[] exchangeFee: present=0 min=None max=None zero=0 negative=0 invalid=[] longRate: present=51 min=-0.0684932 max=0.1389493 zero=0 negative=43 invalid=[] shortRate: present=51 min=-0.1608693 max=0.01 zero=0 negative=38 invalid=[] minSLGap: present=51 min=0 max=0 zero=51 negative=0 invalid=[] maxSLGap: present=51 min=20.0 max=50.0 zero=0 negative=0 invalid=[] minTPGap: present=51 min=0 max=0 zero=51 negative=0 invalid=[] maxTPGap: present=51 min=20.0 max=50.0 zero=0 negative=0 invalid=[] ``` ### Проверка отношения minQty и maxQty Не обнаружено ни одного случая: ```text minQty > maxQty ``` Результат: ```text [] ``` --- ## 20. Реализованные тесты Добавлен файл: ```text app/tests/unit/market_data/acquisition/validation/test_values.py ``` В нём реализовано: ```text 34 теста ``` Тестами покрыты: - корректный полный `exchangeInfo`; - пустые обязательные строки; - пустой `orderType`; - пустой `marketMode`; - отрицательные precision values; - отрицательный `swapChargeInterval`; - нулевой `tickSize`; - отрицательный `tickSize`; - `NaN`; - `+Infinity`; - `-Infinity`; - нечисловой `minQty`; - неположительный `minQty`; - неположительный `maxQty`; - неположительный `stepSize`; - `minQty > maxQty`; - отрицательный `minNotional`; - нулевой `minNotional`; - отрицательные `longRate`; - отрицательные `shortRate`; - допустимые нулевые optional numeric values; - rate limits; - неизвестные instrument filters; - global exchange filters. --- ## 21. Выполненные проверки ### Проверка 1 — unit-тесты Build 005 Команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/validation/test_values.py \ -q ``` Результат: ```text 34 passed in 0.02s ``` --- ### Проверка 2 — Python compilation Команда: ```bash python -m py_compile \ src/market_data/acquisition/exceptions.py \ src/market_data/acquisition/validation/values.py \ tests/unit/market_data/acquisition/validation/test_values.py ``` Результат: ```text успешно, без ошибок ``` --- ### Проверка 3 — полная ручная цепочка обработки Проверена последовательность: ```text raw JSON ↓ validate_exchange_info_schema() ↓ parse_exchange_info() ↓ validate_exchange_info_values() ``` Результат: ```text Validation result: None Symbol: BTC/USD_LEVERAGE Filters: ( DzengiLotSizeFilter( filter_type='LOT_SIZE', min_qty='0.0001', max_qty='1000', step_size='0.0001' ), DzengiMinNotionalFilter( filter_type='MIN_NOTIONAL', min_notional='1' ) ) ``` Это подтверждает совместимость Build 003, Build 004 и Build 005. --- ### Проверка 4 — полный набор тестов проекта Команда: ```bash python -m pytest -q ``` Результат: ```text 84 passed in 0.06s ``` Регрессий не обнаружено. --- ### Проверка 5 — контроль области использования Выполнен поиск: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "validate_exchange_info_values|InstrumentReferenceValueError" \ src tests ``` Подтверждено: - `validate_exchange_info_values()` используется только в собственном модуле и unit-тестах; - `InstrumentReferenceValueError` используется только в `exceptions.py`, `values.py` и unit-тестах; - новый validator ещё не подключён к legacy `ExchangeService`; - UI не изменён; - runtime не изменён; - автоторговля не изменена; - production-поведение работающего бота не изменено. --- ## 22. Архитектурная граница Build 005 Build 005 намеренно не реализует: ```text REST client новой подсистемы mapping Dzengi raw models → Instrument instrument feed instrument handler registry service orchestration cache legacy compatibility adapter переключение production consumers удаление старого ExchangeSymbol ``` Эти задачи относятся к следующим этапам миграции. Build 005 отвечает только за: ```text проверку допустимости значений в уже распарсенных raw-моделях Dzengi exchangeInfo ``` --- ## 23. Влияние на работающего legacy-бота Build 005 не изменяет production-поведение существующего бота. Старый runtime продолжает использовать существующие компоненты: ```text src/integrations/exchange/models.py src/integrations/exchange/service.py src/integrations/exchange/status.py src/integrations/exchange/symbol_utils.py ``` Новая реализация находится отдельно: ```text src/market_data/acquisition/ ``` На момент завершения Build 005 она ещё не подключена к production consumers. Это соответствует принятой стратегии миграции: ```text сначала построить и протестировать новый путь параллельно, затем переключать consumers поэтапно, не ломая работающего бота ``` --- ## 24. Итог Build 005 Build 005 успешно завершён. Реализовано: ```text InstrumentReferenceValueError validate_exchange_info_values() валидация обязательных строк валидация orderTypes валидация marketModes валидация precision валидация tickSize валидация finite numeric values валидация LOT_SIZE валидация MIN_NOTIONAL валидация rate limits поддержка отрицательных longRate/shortRate поддержка допустимых нулевых значений поддержка пустых country/sector/industry проверка unknown filters 34 unit-теста ``` Финальный результат: ```text 84 passed in 0.06s ``` Регрессий не обнаружено. Legacy production runtime не изменён. --- ## 25. Текущее состояние pipeline после Build 005 На момент завершения Build 005 построена следующая часть новой подсистемы: ```text Dzengi exchangeInfo raw JSON ↓ validate_exchange_info_schema() ↓ ValidatedExchangeInfoDocument ↓ parse_exchange_info() ↓ DzengiExchangeInfoResponse ↓ validate_exchange_info_values() ↓ validated Dzengi raw models ``` Следующий архитектурный шаг должен продолжить pipeline от проверенных source-specific raw-моделей к канонической source-independent модели: ```text validated Dzengi raw models ↓ mapper ↓ Instrument ``` Таким образом, естественным следующим этапом является реализация mapping layer: ```text DzengiExchangeInfoSymbol → Instrument ``` при сохранении изоляции от legacy production runtime до завершения и проверки нового пути.