Files
dzentra_bot/docs/migrations/build_003.md

851 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-поведение работающего бота.