Build 060.3: add REST trade schema validation
This commit is contained in:
640
docs/migrations/build_060_3.md
Normal file
640
docs/migrations/build_060_3.md
Normal file
@@ -0,0 +1,640 @@
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user