# 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-поведение работающего бота.
