641 lines
15 KiB
Markdown
641 lines
15 KiB
Markdown
# Build 060.3 — REST Trade Schema Validation
|
||
|
||
## Статус
|
||
|
||
✅ Завершён
|
||
|
||
---
|
||
|
||
# Цель Build
|
||
|
||
После завершения Build 060.2 в системе уже существовал transport-контракт отдельной сделки:
|
||
|
||
```
|
||
REST JSON
|
||
↓
|
||
DzengiRestAggTrade
|
||
```
|
||
|
||
Однако отсутствовал обязательный архитектурный слой Schema Validation.
|
||
|
||
В соответствии с архитектурой Acquisition Pipeline каждый внешний документ обязан проходить структурную проверку до начала парсинга.
|
||
|
||
Для REST Trades это означало необходимость добавить отдельный слой:
|
||
|
||
```
|
||
REST JSON
|
||
↓
|
||
Schema Validation
|
||
↓
|
||
ValidatedRestAggTradesDocument
|
||
↓
|
||
REST Trade Parser
|
||
↓
|
||
DzengiRestAggTrade
|
||
```
|
||
|
||
Именно этот слой реализован в Build 060.3.
|
||
|
||
---
|
||
|
||
# Причины появления Build
|
||
|
||
REST endpoint `/api/v2/aggTrades` возвращает JSON-массив объектов.
|
||
|
||
Пример ответа:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"a": 2134857062,
|
||
"p": "64497.25",
|
||
"q": "0.005",
|
||
"T": 1784218066823,
|
||
"m": false
|
||
},
|
||
{
|
||
"a": 2134857063,
|
||
"p": "64498.10",
|
||
"q": "0.002",
|
||
"T": 1784218067000,
|
||
"m": true
|
||
}
|
||
]
|
||
```
|
||
|
||
До Build 060.3 никакой проверки структуры документа не существовало.
|
||
|
||
Parser был вынужден бы принимать произвольный объект.
|
||
|
||
Это нарушало принятую архитектуру Dzentra.
|
||
|
||
---
|
||
|
||
# Архитектурное решение
|
||
|
||
Во время проектирования рассматривались два варианта.
|
||
|
||
## Вариант A
|
||
|
||
Schema Validation получает весь REST-документ.
|
||
|
||
```
|
||
REST response
|
||
↓
|
||
ValidatedRestAggTradesDocument
|
||
↓
|
||
parse_rest_agg_trades()
|
||
↓
|
||
tuple[DzengiRestAggTrade]
|
||
```
|
||
|
||
---
|
||
|
||
## Вариант B
|
||
|
||
Schema Validation работает с одной сделкой.
|
||
|
||
```
|
||
REST response
|
||
↓
|
||
trade
|
||
↓
|
||
ValidatedRestAggTradeDocument
|
||
```
|
||
|
||
После анализа существующей архитектуры проекта был выбран вариант A.
|
||
|
||
Причины:
|
||
|
||
- ExchangeInfo валидируется целиком;
|
||
- Candles валидируются целиком;
|
||
- WebSocket сообщения валидируются целиком;
|
||
- Parser всегда получает уже структурно проверенный документ.
|
||
|
||
Trades Feed должен следовать тем же правилам.
|
||
|
||
---
|
||
|
||
# Итоговая архитектура
|
||
|
||
После Build 060.3 pipeline выглядит следующим образом.
|
||
|
||
```
|
||
REST JSON
|
||
│
|
||
▼
|
||
validate_rest_agg_trades_schema()
|
||
│
|
||
▼
|
||
ValidatedRestAggTradesDocument
|
||
│
|
||
▼
|
||
parse_rest_agg_trades()
|
||
│
|
||
▼
|
||
tuple[DzengiRestAggTrade]
|
||
│
|
||
▼
|
||
Value Validation
|
||
│
|
||
▼
|
||
Canonical Trade
|
||
```
|
||
|
||
Каждый слой отвечает только за собственную область ответственности.
|
||
|
||
---
|
||
|
||
# Ответственность Schema Validation
|
||
|
||
Schema Validation проверяет исключительно структуру документа.
|
||
|
||
Проверяется:
|
||
|
||
- корневой JSON-массив;
|
||
- каждый элемент массива является JSON-объектом;
|
||
- все ключи являются строками;
|
||
- обязательные поля присутствуют;
|
||
- документ переводится в immutable-представление.
|
||
|
||
Schema Validation не занимается:
|
||
|
||
- проверкой типов значений;
|
||
- преобразованием данных;
|
||
- проверкой диапазонов;
|
||
- проверкой бизнес-ограничений;
|
||
- созданием transport-моделей.
|
||
|
||
Все перечисленные обязанности принадлежат последующим слоям Acquisition Pipeline.
|
||
|
||
---
|
||
|
||
# Новое исключение
|
||
|
||
В Build добавлено новое исключение.
|
||
|
||
```python
|
||
class TradeSchemaError(MarketDataAcquisitionError):
|
||
pass
|
||
```
|
||
|
||
Исключение используется исключительно для ошибок структуры REST Trade документа.
|
||
|
||
Build сознательно не добавляет:
|
||
|
||
- TradeParseError;
|
||
- TradeValueError;
|
||
- TradeMappingError.
|
||
|
||
Они будут появляться только в соответствующих Build.
|
||
|
||
Это позволяет сохранить строгую границу ответственности каждого этапа разработки.
|
||
|
||
---
|
||
|
||
# Новый контракт Schema Layer
|
||
|
||
Добавлен immutable-контракт.
|
||
|
||
```python
|
||
@dataclass(frozen=True, slots=True)
|
||
class ValidatedRestAggTradesDocument:
|
||
items: tuple[Mapping[str, object], ...]
|
||
```
|
||
|
||
Контракт хранит уже структурно проверенный REST-документ.
|
||
|
||
Каждый элемент массива:
|
||
|
||
- является Mapping;
|
||
- является immutable;
|
||
- сохраняет все поля документа;
|
||
- ещё не преобразован в transport model.
|
||
|
||
Parser получает именно этот контракт.
|
||
|
||
# Реализация Schema Validation
|
||
|
||
В Build реализована новая функция.
|
||
|
||
```python
|
||
validate_rest_agg_trades_schema(
|
||
document: object,
|
||
) -> ValidatedRestAggTradesDocument
|
||
```
|
||
|
||
Функция является единственной точкой входа для проверки структуры REST Trade документа.
|
||
|
||
Parser больше не должен принимать произвольный JSON.
|
||
|
||
Он обязан получать только результат работы Schema Validation.
|
||
|
||
---
|
||
|
||
# Проверка корневого объекта
|
||
|
||
Первым этапом валидируется корень документа.
|
||
|
||
Ожидается исключительно JSON-массив.
|
||
|
||
Допустимый пример:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"a": 1,
|
||
"p": "1.25",
|
||
"q": "5",
|
||
"T": 123,
|
||
"m": false
|
||
}
|
||
]
|
||
```
|
||
|
||
Недопустимые варианты:
|
||
|
||
```json
|
||
{}
|
||
```
|
||
|
||
```json
|
||
123
|
||
```
|
||
|
||
```json
|
||
"text"
|
||
```
|
||
|
||
```json
|
||
null
|
||
```
|
||
|
||
Во всех подобных случаях возбуждается
|
||
|
||
```python
|
||
TradeSchemaError
|
||
```
|
||
|
||
Parser никогда не увидит подобный документ.
|
||
|
||
---
|
||
|
||
# Проверка элементов массива
|
||
|
||
После проверки корня валидируется каждый элемент массива.
|
||
|
||
Каждый элемент обязан быть JSON-объектом.
|
||
|
||
Допустимо:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"a": 1
|
||
}
|
||
]
|
||
```
|
||
|
||
Недопустимо:
|
||
|
||
```json
|
||
[
|
||
1
|
||
]
|
||
```
|
||
|
||
```json
|
||
[
|
||
[]
|
||
]
|
||
```
|
||
|
||
```json
|
||
[
|
||
null
|
||
]
|
||
```
|
||
|
||
```json
|
||
[
|
||
"trade"
|
||
]
|
||
```
|
||
|
||
Подобные документы отклоняются ещё до Parser.
|
||
|
||
---
|
||
|
||
# Проверка ключей
|
||
|
||
После проверки структуры объекта производится проверка ключей.
|
||
|
||
Каждый ключ обязан быть строкой.
|
||
|
||
Например,
|
||
|
||
допустимо:
|
||
|
||
```json
|
||
{
|
||
"a": 1
|
||
}
|
||
```
|
||
|
||
недопустимо:
|
||
|
||
```python
|
||
{
|
||
1: "value"
|
||
}
|
||
```
|
||
|
||
Подобная ситуация невозможна для корректного JSON, однако тест присутствует специально.
|
||
|
||
Причины:
|
||
|
||
- защита от программного формирования словаря;
|
||
- единообразие со всеми остальными Schema Validator;
|
||
- гарантированный контракт Parser.
|
||
|
||
---
|
||
|
||
# Проверка обязательных полей
|
||
|
||
Для каждого Trade объекта проверяется наличие обязательных полей.
|
||
|
||
Обязательными являются:
|
||
|
||
```
|
||
a
|
||
p
|
||
q
|
||
T
|
||
m
|
||
```
|
||
|
||
Отсутствие любого поля считается ошибкой структуры документа.
|
||
|
||
Например,
|
||
|
||
```json
|
||
{
|
||
"a": 1,
|
||
"p": "10"
|
||
}
|
||
```
|
||
|
||
будет отклонён ещё на этапе Schema Validation.
|
||
|
||
---
|
||
|
||
# Дополнительные поля
|
||
|
||
Schema Validation не запрещает дополнительные поля.
|
||
|
||
Например,
|
||
|
||
```json
|
||
{
|
||
"a": 1,
|
||
"p": "100",
|
||
"q": "5",
|
||
"T": 123,
|
||
"m": false,
|
||
"exchange": "spot",
|
||
"custom": "value"
|
||
}
|
||
```
|
||
|
||
считается корректным документом.
|
||
|
||
Дополнительные поля полностью сохраняются.
|
||
|
||
Это решение принято намеренно.
|
||
|
||
Schema Layer отвечает только за минимальный контракт документа.
|
||
|
||
Любые дополнительные поля могут использоваться последующими Build без изменения существующей Schema Validation.
|
||
|
||
---
|
||
|
||
# Отсутствие проверки типов
|
||
|
||
Build принципиально не проверяет типы значений.
|
||
|
||
Следующий документ считается структурно корректным.
|
||
|
||
```python
|
||
[
|
||
{
|
||
"a": "invalid",
|
||
"p": None,
|
||
"q": [],
|
||
"T": False,
|
||
"m": 123,
|
||
}
|
||
]
|
||
```
|
||
|
||
Причина заключается в разделении ответственности Acquisition Pipeline.
|
||
|
||
Schema Validation отвечает только за структуру документа.
|
||
|
||
Проверка:
|
||
|
||
- int;
|
||
- bool;
|
||
- Decimal;
|
||
- строки;
|
||
- диапазоны значений;
|
||
- корректность timestamp;
|
||
|
||
будет выполняться исключительно Parser и Value Validation.
|
||
|
||
Это полностью соответствует архитектуре проекта.
|
||
|
||
---
|
||
|
||
# Immutable представление
|
||
|
||
После успешной проверки структура документа преобразуется в immutable-контракт.
|
||
|
||
Каждый элемент копируется в новый словарь.
|
||
|
||
После этого словарь оборачивается в
|
||
|
||
```python
|
||
MappingProxyType
|
||
```
|
||
|
||
Коллекция объектов преобразуется в
|
||
|
||
```python
|
||
tuple
|
||
```
|
||
|
||
Итоговый контракт становится полностью неизменяемым.
|
||
|
||
Это гарантирует:
|
||
|
||
- невозможность случайного изменения Parser;
|
||
- защиту между слоями Pipeline;
|
||
- детерминированность обработки;
|
||
- отсутствие побочных эффектов.
|
||
|
||
Подобная схема уже используется ExchangeInfo и Candles Schema Validation.
|
||
|
||
Trades Feed теперь полностью ей соответствует.
|
||
|
||
---
|
||
|
||
# Поведение при пустом ответе
|
||
|
||
REST endpoint может вернуть пустой массив.
|
||
|
||
Например,
|
||
|
||
```json
|
||
[]
|
||
```
|
||
|
||
Такой ответ считается полностью корректным.
|
||
|
||
После проверки возвращается
|
||
|
||
```python
|
||
ValidatedRestAggTradesDocument(
|
||
items=(),
|
||
)
|
||
```
|
||
|
||
Пустой документ не считается ошибкой.
|
||
|
||
Это позволяет корректно обрабатывать участки истории, где сделки отсутствуют.
|
||
|
||
---
|
||
|
||
# Unit-тесты
|
||
|
||
Для новой функциональности добавлен отдельный набор тестов.
|
||
|
||
Проверяются:
|
||
|
||
- корректный REST-документ;
|
||
- пустой массив;
|
||
- сохранение дополнительных полей;
|
||
- отсутствие проверки типов значений;
|
||
- отклонение некорректного корня документа;
|
||
- отклонение элементов, не являющихся JSON-объектами;
|
||
- отсутствие обязательных полей;
|
||
- нестроковые ключи.
|
||
|
||
Таким образом фиксируется полный публичный контракт нового Schema Validator.
|
||
|
||
Любое изменение поведения в будущем будет обнаружено автоматически.
|
||
|
||
---
|
||
|
||
# Результаты Build
|
||
|
||
После завершения реализации были выполнены проверки проекта.
|
||
|
||
Schema Tests
|
||
|
||
```
|
||
38 passed
|
||
```
|
||
|
||
Полный набор unit-тестов
|
||
|
||
```
|
||
1011 passed
|
||
```
|
||
|
||
Дополнительно успешно выполнены
|
||
|
||
```
|
||
python -m compileall src
|
||
```
|
||
|
||
и
|
||
|
||
```
|
||
git diff --check
|
||
```
|
||
|
||
Ошибок форматирования обнаружено не было.
|
||
|
||
---
|
||
|
||
# Изменённые файлы
|
||
|
||
В рамках Build изменены только три файла.
|
||
|
||
```
|
||
src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
Добавлено исключение
|
||
|
||
```
|
||
TradeSchemaError
|
||
```
|
||
|
||
---
|
||
|
||
```
|
||
src/market_data/acquisition/validation/schema.py
|
||
```
|
||
|
||
Добавлены:
|
||
|
||
- ValidatedRestAggTradesDocument;
|
||
- validate_rest_agg_trades_schema();
|
||
- вспомогательные функции проверки структуры.
|
||
|
||
---
|
||
|
||
```
|
||
tests/unit/market_data/acquisition/validation/test_schema.py
|
||
```
|
||
|
||
Добавлен полный набор unit-тестов нового Schema Validator.
|
||
|
||
Никакие другие части проекта не изменялись.
|
||
|
||
---
|
||
|
||
# Итог
|
||
|
||
Build 060.3 завершает формирование первого уровня REST Trades Acquisition Pipeline.
|
||
|
||
После его завершения система получила полноценный слой структурной проверки документов.
|
||
|
||
Pipeline теперь имеет следующий вид.
|
||
|
||
```
|
||
REST JSON
|
||
│
|
||
▼
|
||
Schema Validation
|
||
│
|
||
▼
|
||
ValidatedRestAggTradesDocument
|
||
│
|
||
▼
|
||
REST Trade Parser
|
||
│
|
||
▼
|
||
DzengiRestAggTrade
|
||
│
|
||
▼
|
||
Value Validation
|
||
│
|
||
▼
|
||
Canonical Trade
|
||
```
|
||
|
||
Следующим этапом серии станет **Build 060.4 — REST Trade Parser**, который будет отвечать за преобразование структурно проверенного документа в transport-модель `DzengiRestAggTrade`, не нарушая границ ответственности, установленных данным Build.
|
||
|