Build 060.3: add REST trade schema validation
This commit is contained in:
@@ -119,6 +119,11 @@ class CandleFeedRegistryError(MarketDataAcquisitionError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Ошибка структуры документа Trades Feed.
|
||||||
|
class TradeSchemaError(MarketDataAcquisitionError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Ошибка определения типа входящего WebSocket-сообщения
|
# Ошибка определения типа входящего WebSocket-сообщения
|
||||||
# и выбора специализированного адаптера.
|
# и выбора специализированного адаптера.
|
||||||
class WebSocketMessageRoutingError(MarketDataAcquisitionError):
|
class WebSocketMessageRoutingError(MarketDataAcquisitionError):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from src.market_data.acquisition.exceptions import (
|
|||||||
CandleWebSocketSchemaError,
|
CandleWebSocketSchemaError,
|
||||||
InstrumentReferenceSchemaError,
|
InstrumentReferenceSchemaError,
|
||||||
QuoteSchemaError,
|
QuoteSchemaError,
|
||||||
|
TradeSchemaError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -595,6 +596,94 @@ def _require_candle_mapping(
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# Структурно проверенное представление ответа Dzengi /api/v2/aggTrades.
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ValidatedRestAggTradesDocument:
|
||||||
|
items: tuple[Mapping[str, object], ...]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rest_agg_trades_schema(
|
||||||
|
document: object,
|
||||||
|
) -> ValidatedRestAggTradesDocument:
|
||||||
|
"""
|
||||||
|
Проверить структуру ответа Dzengi aggTrades без проверки значений.
|
||||||
|
|
||||||
|
Ожидается корневой JSON-массив, каждый элемент которого является
|
||||||
|
JSON-объектом с обязательными полями:
|
||||||
|
|
||||||
|
- a — идентификатор агрегированной сделки;
|
||||||
|
- p — цена;
|
||||||
|
- q — количество;
|
||||||
|
- T — временная метка;
|
||||||
|
- m — признак buyer is maker.
|
||||||
|
|
||||||
|
Валидатор проверяет только структуру документа. Типы и допустимость
|
||||||
|
значений должны проверяться последующими слоями acquisition pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(document, list):
|
||||||
|
raise TradeSchemaError(
|
||||||
|
"$ должен быть JSON-массивом, "
|
||||||
|
f"получен {type(document).__name__}."
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_items: list[Mapping[str, object]] = []
|
||||||
|
|
||||||
|
for index, item in enumerate(document):
|
||||||
|
path = f"$[{index}]"
|
||||||
|
|
||||||
|
mapping = _require_trade_mapping(
|
||||||
|
item,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
_validate_rest_agg_trade_item(
|
||||||
|
mapping,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_items.append(
|
||||||
|
MappingProxyType(dict(mapping))
|
||||||
|
)
|
||||||
|
|
||||||
|
return ValidatedRestAggTradesDocument(
|
||||||
|
items=tuple(validated_items),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_rest_agg_trade_item(
|
||||||
|
item: Mapping[str, object],
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
for key in ("a", "p", "q", "T", "m"):
|
||||||
|
if key not in item:
|
||||||
|
raise TradeSchemaError(
|
||||||
|
f"{path}.{key} отсутствует в документе aggTrades."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_trade_mapping(
|
||||||
|
value: object,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
) -> Mapping[str, object]:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise TradeSchemaError(
|
||||||
|
f"{path} должен быть JSON-объектом, "
|
||||||
|
f"получен {type(value).__name__}."
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in value:
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise TradeSchemaError(
|
||||||
|
f"{path} содержит нестроковый ключ "
|
||||||
|
f"типа {type(key).__name__}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Структурно проверенное представление события Dzengi WebSocket ohlc.event.
|
# Структурно проверенное представление события Dzengi WebSocket ohlc.event.
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class ValidatedWebSocketOhlcDocument:
|
class ValidatedWebSocketOhlcDocument:
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import pytest
|
|||||||
|
|
||||||
from src.market_data.acquisition.exceptions import (
|
from src.market_data.acquisition.exceptions import (
|
||||||
InstrumentReferenceSchemaError,
|
InstrumentReferenceSchemaError,
|
||||||
|
TradeSchemaError,
|
||||||
)
|
)
|
||||||
from src.market_data.acquisition.validation.schema import (
|
from src.market_data.acquisition.validation.schema import (
|
||||||
validate_exchange_info_schema,
|
validate_exchange_info_schema,
|
||||||
|
validate_rest_agg_trades_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -265,4 +267,163 @@ def test_reject_non_object_payload_collection_item(key: str) -> None:
|
|||||||
InstrumentReferenceSchemaError,
|
InstrumentReferenceSchemaError,
|
||||||
match=rf"\.{key}\[0\] должен быть JSON-объектом",
|
match=rf"\.{key}\[0\] должен быть JSON-объектом",
|
||||||
):
|
):
|
||||||
validate_exchange_info_schema(document)
|
validate_exchange_info_schema(document)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rest_agg_trades_document() -> None:
|
||||||
|
document = [
|
||||||
|
{
|
||||||
|
"a": 2134857062,
|
||||||
|
"p": "64497.25",
|
||||||
|
"q": "0.005",
|
||||||
|
"T": 1784218066823,
|
||||||
|
"m": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": 2134857063,
|
||||||
|
"p": "64498.10",
|
||||||
|
"q": "0.002",
|
||||||
|
"T": 1784218067000,
|
||||||
|
"m": True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
validated = validate_rest_agg_trades_schema(document)
|
||||||
|
|
||||||
|
assert len(validated.items) == 2
|
||||||
|
assert validated.items[0]["a"] == 2134857062
|
||||||
|
assert validated.items[0]["p"] == "64497.25"
|
||||||
|
assert validated.items[0]["q"] == "0.005"
|
||||||
|
assert validated.items[0]["T"] == 1784218066823
|
||||||
|
assert validated.items[0]["m"] is False
|
||||||
|
assert isinstance(validated.items, tuple)
|
||||||
|
assert isinstance(validated.items[0], MappingProxyType)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_empty_rest_agg_trades_document() -> None:
|
||||||
|
validated = validate_rest_agg_trades_schema([])
|
||||||
|
|
||||||
|
assert validated.items == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_agg_trades_schema_preserves_unchecked_field_values() -> None:
|
||||||
|
document = [
|
||||||
|
{
|
||||||
|
"a": "invalid-id",
|
||||||
|
"p": None,
|
||||||
|
"q": [],
|
||||||
|
"T": False,
|
||||||
|
"m": 123,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
validated = validate_rest_agg_trades_schema(document)
|
||||||
|
|
||||||
|
assert validated.items[0]["a"] == "invalid-id"
|
||||||
|
assert validated.items[0]["p"] is None
|
||||||
|
assert validated.items[0]["q"] == []
|
||||||
|
assert validated.items[0]["T"] is False
|
||||||
|
assert validated.items[0]["m"] == 123
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_agg_trades_schema_preserves_additional_fields() -> None:
|
||||||
|
document = [
|
||||||
|
{
|
||||||
|
"a": 2134857062,
|
||||||
|
"p": "64497.25",
|
||||||
|
"q": "0.005",
|
||||||
|
"T": 1784218066823,
|
||||||
|
"m": False,
|
||||||
|
"extra": "preserved",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
validated = validate_rest_agg_trades_schema(document)
|
||||||
|
|
||||||
|
assert validated.items[0]["extra"] == "preserved"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"document",
|
||||||
|
[
|
||||||
|
None,
|
||||||
|
{},
|
||||||
|
"invalid",
|
||||||
|
123,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_reject_non_list_rest_agg_trades_root(
|
||||||
|
document: object,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
TradeSchemaError,
|
||||||
|
match=r"\$ должен быть JSON-массивом",
|
||||||
|
):
|
||||||
|
validate_rest_agg_trades_schema(document)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"item",
|
||||||
|
[
|
||||||
|
None,
|
||||||
|
[],
|
||||||
|
"invalid",
|
||||||
|
123,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_reject_non_object_rest_agg_trade_item(
|
||||||
|
item: object,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
TradeSchemaError,
|
||||||
|
match=r"\$\[0\] должен быть JSON-объектом",
|
||||||
|
):
|
||||||
|
validate_rest_agg_trades_schema([item])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"missing_key",
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"p",
|
||||||
|
"q",
|
||||||
|
"T",
|
||||||
|
"m",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_reject_rest_agg_trade_with_missing_required_field(
|
||||||
|
missing_key: str,
|
||||||
|
) -> None:
|
||||||
|
item: dict[str, object] = {
|
||||||
|
"a": 2134857062,
|
||||||
|
"p": "64497.25",
|
||||||
|
"q": "0.005",
|
||||||
|
"T": 1784218066823,
|
||||||
|
"m": False,
|
||||||
|
}
|
||||||
|
del item[missing_key]
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
TradeSchemaError,
|
||||||
|
match=rf"\$\[0\]\.{missing_key} отсутствует",
|
||||||
|
):
|
||||||
|
validate_rest_agg_trades_schema([item])
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_rest_agg_trade_with_non_string_key() -> None:
|
||||||
|
document = [
|
||||||
|
{
|
||||||
|
"a": 2134857062,
|
||||||
|
"p": "64497.25",
|
||||||
|
"q": "0.005",
|
||||||
|
"T": 1784218066823,
|
||||||
|
"m": False,
|
||||||
|
1: "invalid",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
TradeSchemaError,
|
||||||
|
match=r"\$\[0\] содержит нестроковый ключ типа int",
|
||||||
|
):
|
||||||
|
validate_rest_agg_trades_schema(document)
|
||||||
|
|||||||
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