# 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 до завершения и проверки нового пути.