From 034f5984ca2e10a5bd63798b822ef4f238e32c02 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sun, 19 Jul 2026 17:25:51 +0300 Subject: [PATCH] Build 060.10: add WebSocket Trade Schema Validation --- .../acquisition/validation/schema.py | 128 ++ .../validation/test_websocket_trade_schema.py | 226 ++++ docs/migrations/build_060_10.md | 1073 +++++++++++++++++ 3 files changed, 1427 insertions(+) create mode 100644 app/tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py create mode 100644 docs/migrations/build_060_10.md diff --git a/app/src/market_data/acquisition/validation/schema.py b/app/src/market_data/acquisition/validation/schema.py index 7a1ca1c..307518b 100644 --- a/app/src/market_data/acquisition/validation/schema.py +++ b/app/src/market_data/acquisition/validation/schema.py @@ -811,4 +811,132 @@ def _require_websocket_ohlc_mapping( f"типа {type(key).__name__}." ) + return value + + +# Структурно проверенное представление события Dzengi WebSocket internal.trade. +@dataclass(frozen=True, slots=True) +class ValidatedWebSocketTradeDocument: + payload: Mapping[str, object] + status: object + destination: object + correlation_id: object | None + + +def validate_dzengi_websocket_trade_schema( + document: object, +) -> ValidatedWebSocketTradeDocument: + """ + Проверить структуру события Dzengi WebSocket Trade без проверки значений. + + Ожидаемый runtime-контракт: + + { + "status": "OK", + "destination": "internal.trade", + "payload": { + "buyer": false, + "id": 2134846831, + "orderId": "00a02503-0079-54c4-0000-000081e62b58", + "price": 64555.55, + "size": 0.002, + "symbol": "BTC/USD_LEVERAGE", + "ts": 1784218012030 + } + } + + Функция проверяет только структуру сообщения: + + - корневой JSON-объект; + - наличие status; + - наличие destination; + - наличие payload; + - обязательный набор полей Trade; + - строковые ключи объектов. + + Функция не проверяет предметные значения, не преобразует timestamp, + не преобразует цену и объём и не создаёт transport-модель адаптера. + """ + + root = _require_websocket_trade_mapping( + document, + path="$", + ) + + if "status" not in root: + raise TradeSchemaError( + "$.status отсутствует в событии WebSocket Trade." + ) + + if "destination" not in root: + raise TradeSchemaError( + "$.destination отсутствует в событии WebSocket Trade." + ) + + if "payload" not in root: + raise TradeSchemaError( + "$.payload отсутствует в событии WebSocket Trade." + ) + + payload = _require_websocket_trade_mapping( + root.get("payload"), + path="$.payload", + ) + + _validate_websocket_trade_payload(payload) + + return ValidatedWebSocketTradeDocument( + payload=MappingProxyType(dict(payload)), + status=root.get("status"), + destination=root.get("destination"), + correlation_id=root.get("correlationId"), + ) + + +def _validate_websocket_trade_payload( + payload: Mapping[str, object], +) -> None: + required_fields = ( + "id", + "price", + "size", + "ts", + "symbol", + "buyer", + "orderId", + ) + + missing_fields = tuple( + field_name + for field_name in required_fields + if field_name not in payload + ) + + if missing_fields: + formatted_fields = ", ".join(missing_fields) + + raise TradeSchemaError( + "$.payload не содержит обязательные поля WebSocket Trade: " + f"{formatted_fields}." + ) + + +def _require_websocket_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 \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py b/app/tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py new file mode 100644 index 0000000..9e2fe42 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py @@ -0,0 +1,226 @@ +# app/tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py + +from __future__ import annotations + +from types import MappingProxyType + +import pytest + +from src.market_data.acquisition.exceptions import TradeSchemaError +from src.market_data.acquisition.validation.schema import ( + ValidatedWebSocketTradeDocument, + validate_dzengi_websocket_trade_schema, +) + + +def _valid_document() -> dict[str, object]: + return { + "status": "OK", + "destination": "internal.trade", + "payload": { + "buyer": False, + "id": 2134846831, + "orderId": "00a02503-0079-54c4-0000-000081e62b58", + "price": 64555.55, + "size": 0.002, + "symbol": "BTC/USD_LEVERAGE", + "ts": 1784218012030, + }, + } + + +def test_validate_websocket_trade_schema_returns_immutable_document() -> None: + result = validate_dzengi_websocket_trade_schema( + _valid_document() + ) + + assert isinstance(result, ValidatedWebSocketTradeDocument) + assert isinstance(result.payload, MappingProxyType) + + assert result.status == "OK" + assert result.destination == "internal.trade" + assert result.correlation_id is None + + assert result.payload == { + "buyer": False, + "id": 2134846831, + "orderId": "00a02503-0079-54c4-0000-000081e62b58", + "price": 64555.55, + "size": 0.002, + "symbol": "BTC/USD_LEVERAGE", + "ts": 1784218012030, + } + + +def test_validate_websocket_trade_schema_preserves_correlation_id() -> None: + document = _valid_document() + document["correlationId"] = "correlation-1" + + result = validate_dzengi_websocket_trade_schema(document) + + assert result.correlation_id == "correlation-1" + + +def test_validate_websocket_trade_schema_copies_payload() -> None: + document = _valid_document() + payload = document["payload"] + + assert isinstance(payload, dict) + + result = validate_dzengi_websocket_trade_schema(document) + + payload["price"] = 1.0 + + assert result.payload["price"] == 64555.55 + + +def test_validate_websocket_trade_schema_rejects_non_mapping_root() -> None: + with pytest.raises( + TradeSchemaError, + match=r"\$ должен быть JSON-объектом", + ): + validate_dzengi_websocket_trade_schema([]) + + +@pytest.mark.parametrize( + "field_name", + [ + "status", + "destination", + "payload", + ], +) +def test_validate_websocket_trade_schema_rejects_missing_root_field( + field_name: str, +) -> None: + document = _valid_document() + del document[field_name] + + with pytest.raises( + TradeSchemaError, + match=rf"\$\.{field_name} отсутствует", + ): + validate_dzengi_websocket_trade_schema(document) + + +def test_validate_websocket_trade_schema_rejects_non_mapping_payload() -> None: + document = _valid_document() + document["payload"] = [] + + with pytest.raises( + TradeSchemaError, + match=r"\$\.payload должен быть JSON-объектом", + ): + validate_dzengi_websocket_trade_schema(document) + + +@pytest.mark.parametrize( + "field_name", + [ + "id", + "price", + "size", + "ts", + "symbol", + "buyer", + "orderId", + ], +) +def test_validate_websocket_trade_schema_rejects_missing_payload_field( + field_name: str, +) -> None: + document = _valid_document() + payload = document["payload"] + + assert isinstance(payload, dict) + + del payload[field_name] + + with pytest.raises( + TradeSchemaError, + match=rf"обязательные поля WebSocket Trade: {field_name}", + ): + validate_dzengi_websocket_trade_schema(document) + + +def test_validate_websocket_trade_schema_reports_all_missing_fields() -> None: + document = _valid_document() + payload = document["payload"] + + assert isinstance(payload, dict) + + del payload["id"] + del payload["size"] + del payload["orderId"] + + with pytest.raises( + TradeSchemaError, + match=r"id, size, orderId", + ): + validate_dzengi_websocket_trade_schema(document) + + +def test_validate_websocket_trade_schema_does_not_validate_values() -> None: + document = _valid_document() + payload = document["payload"] + + assert isinstance(payload, dict) + + document["status"] = 123 + document["destination"] = None + + payload["id"] = "invalid-id" + payload["price"] = None + payload["size"] = [] + payload["ts"] = -1 + payload["symbol"] = False + payload["buyer"] = "unknown" + payload["orderId"] = object() + + result = validate_dzengi_websocket_trade_schema(document) + + assert result.status == 123 + assert result.destination is None + assert result.payload["id"] == "invalid-id" + assert result.payload["ts"] == -1 + + +def test_validate_websocket_trade_schema_allows_additional_fields() -> None: + document = _valid_document() + payload = document["payload"] + + assert isinstance(payload, dict) + + payload["clientOrderId"] = "optional-client-order-id" + payload["unknownField"] = "preserved" + + result = validate_dzengi_websocket_trade_schema(document) + + assert result.payload["clientOrderId"] == "optional-client-order-id" + assert result.payload["unknownField"] == "preserved" + + +def test_validate_websocket_trade_schema_rejects_non_string_root_key() -> None: + document = _valid_document() + document[1] = "invalid" # type: ignore[index] + + with pytest.raises( + TradeSchemaError, + match=r"\$ содержит нестроковый ключ", + ): + validate_dzengi_websocket_trade_schema(document) + + +def test_validate_websocket_trade_schema_rejects_non_string_payload_key() -> None: + document = _valid_document() + payload = document["payload"] + + assert isinstance(payload, dict) + + payload[1] = "invalid" # type: ignore[index] + + with pytest.raises( + TradeSchemaError, + match=r"\$\.payload содержит нестроковый ключ", + ): + validate_dzengi_websocket_trade_schema(document) \ No newline at end of file diff --git a/docs/migrations/build_060_10.md b/docs/migrations/build_060_10.md new file mode 100644 index 0000000..3d18d77 --- /dev/null +++ b/docs/migrations/build_060_10.md @@ -0,0 +1,1073 @@ +# Build 060.10 — WebSocket Trade Schema Validation + +**Engineering Migration Report** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|----------| +| Build | 060.10 | +| Название | WebSocket Trade Schema Validation | +| Статус | Completed | +| Проект | Dzentra | +| Подсистема | Market Data Acquisition | +| Компонент | Trades Feed | +| Версия | 1.0 | + +--- + +# Цель Build + +После завершения Build 060.9 система получила транспортную модель WebSocket Trade (`DzengiWebSocketTradeEvent`), описывающую одно событие биржи на транспортном уровне. + +Следующим обязательным этапом развития WebSocket-конвейера является реализация слоя структурной проверки входящих сообщений. + +Build 060.10 вводит механизм **Schema Validation** для сообщений Dzengi WebSocket канала `internal.trade`. + +Основная задача Build — гарантировать, что последующие этапы обработки получают структурно корректный документ, содержащий все обязательные элементы транспортного контракта. + +Данный Build ограничивается исключительно проверкой структуры сообщения и не затрагивает: + +- Parser; +- Value Validation; +- Mapper; +- Adapter; +- Runtime; +- Routing; +- Trades Feed. + +--- + +# Предпосылки + +К моменту начала Build архитектура Dzentra уже содержала завершённые уровни Schema Validation для остальных типов WebSocket-событий. + +## WebSocket Quote + +```text +Raw WebSocket Quote + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketQuoteDocument + │ + ▼ +Quote Parser +``` + +## WebSocket OHLC + +```text +Raw WebSocket OHLC + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketOhlcDocument + │ + ▼ +OHLC Parser +``` + +После завершения Build 060.9 появилась транспортная модель: + +```text +DzengiWebSocketTradeEvent +``` + +Однако между необработанным JSON-документом и Parser отсутствовал слой, отвечающий за структурную проверку сообщения. + +В результате WebSocket-конвейер обработки сделок оставался незавершённым и отличался от уже реализованных конвейеров Quote и OHLC. + +--- + +# Архитектурное основание + +В архитектуре Dzentra каждый уровень Pipeline имеет строго определённую область ответственности. + +Schema Validation располагается между транспортным JSON-документом и Parser и отвечает исключительно за проверку структуры сообщения. + +На данном уровне выполняется: + +- проверка структуры корневого объекта; +- проверка обязательных элементов транспортной оболочки; +- проверка структуры объекта `payload`; +- проверка наличия обязательных полей транспортного события. + +Schema Validation принципиально **не выполняет**: + +- преобразование типов; +- преобразование числовых значений; +- проверку диапазонов; +- проверку бизнес-семантики; +- создание транспортной модели; +- преобразование в каноническую модель `Trade`. + +Такое разделение позволяет каждому уровню Pipeline выполнять одну строго определённую задачу и исключает смешивание ответственности между компонентами. + +--- + +# Результаты архитектурного аудита + +Перед началом реализации был выполнен аудит существующей подсистемы Validation. + +Подтверждено наличие полностью реализованных компонентов: + +- `ValidatedWebSocketQuoteDocument`; +- `ValidatedWebSocketOhlcDocument`; +- `validate_dzengi_websocket_quote_schema()`; +- `validate_dzengi_websocket_ohlc_schema()`. + +Также подтверждено существование полной иерархии ошибок обработки Trade: + +- `TradeTransportError`; +- `TradeSchemaError`; +- `TradeParseError`; +- `TradeValueError`; +- `TradeMappingError`. + +Одновременно подтверждено отсутствие собственного уровня Schema Validation для WebSocket Trade. + +Таким образом Build 060.10 полностью соответствует утверждённой дорожной карте серии 060 и закрывает второй этап WebSocket-ветки обработки сделок. + +--- + +# Архитектурное решение + +По итогам аудита было принято решение не проектировать отдельную архитектуру для Trade. + +Вместо этого реализован третий экземпляр уже существующего архитектурного шаблона. + +В систему добавлены: + +```text +ValidatedWebSocketTradeDocument + +validate_dzengi_websocket_trade_schema(...) +``` + +Архитектура всех WebSocket-конвейеров стала полностью симметричной. + +```text +Quote + +Raw WebSocket + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketQuoteDocument + +OHLC + +Raw WebSocket + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketOhlcDocument + +Trade + +Raw WebSocket + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketTradeDocument +``` + +Build 060.10 не изменяет существующее поведение Quote и OHLC, а лишь расширяет существующую архитектуру новым типом рыночных данных. + +--- + +# Реализованный документ Schema Validation + +В файл + +```text +src/market_data/acquisition/validation/schema.py +``` + +добавлен новый документ структурной проверки: + +```python +@dataclass(frozen=True, slots=True) +class ValidatedWebSocketTradeDocument: + payload: Mapping[str, object] + status: object + destination: object + correlation_id: object | None +``` + +Документ представляет собой неизменяемый результат успешной проверки структуры WebSocket-сообщения. + +Экземпляр содержит: + +- транспортную оболочку сообщения; +- неизменяемый `payload`; +- статус сообщения; +- назначение сообщения; +- необязательный `correlationId`. + +После создания объект используется исключительно последующими стадиями Pipeline и не предполагает модификации. + +--- + +# Почему используется отдельный документ Validation + +`ValidatedWebSocketTradeDocument` не является транспортной моделью биржи. + +Он представляет собой результат успешной структурной проверки входящего JSON-документа. + +Документ: + +- не содержит бизнес-логики; +- не преобразует данные; +- не выполняет Parsing; +- не выполняет Value Validation; +- не выполняет Mapping; +- не является канонической моделью `Trade`. + +Его единственная задача — гарантировать Parser, что структура сообщения соответствует ожидаемому контракту. + +Такое разделение полностью повторяет архитектурный подход, уже применяемый для Quote и OHLC. + +--- + +# Проверяемая структура WebSocket-документа + +В рамках Build 060.10 реализована проверка исключительно структуры сообщения. + +Ожидаемый контракт WebSocket-события имеет следующий вид: + +```json +{ + "status": "OK", + "destination": "internal.trade", + "payload": { + "buyer": false, + "id": 2134846831, + "orderId": "00a02503-0079-54c4-0000-000081e62b58", + "price": 64555.55, + "size": 0.002, + "symbol": "BTC/USD_LEVERAGE", + "ts": 1784218012030 + } +} +``` + +Build 060.10 гарантирует наличие обязательных элементов данного контракта, но принципиально не анализирует их содержимое. + +--- + +# Проверяемые элементы транспортной оболочки + +На уровне корневого документа выполняется проверка наличия обязательных полей: + +```text +status +destination +payload +``` + +Кроме того, проверяется, что сам корневой объект является JSON Mapping и содержит только строковые ключи. + +Поле + +```text +correlationId +``` + +не является обязательным. + +При наличии оно сохраняется в результирующем документе без какой-либо дополнительной обработки. + +Следует отметить, что Build 060.10 проверяет **наличие** поля + +```text +destination +``` + +но **не проверяет его значение**. + +В частности, валидатор не сравнивает содержимое поля со строкой + +```text +internal.trade +``` + +Подобная проверка относится не к структурной корректности сообщения, а к его семантике и поэтому не входит в область ответственности Schema Validation. + +Такое разделение полностью соответствует архитектурному принципу Dzentra, согласно которому Schema Validation отвечает исключительно за структуру входящего документа, а не за интерпретацию его содержимого. + +--- + +# Проверяемые элементы payload + +После успешной проверки транспортной оболочки выполняется проверка структуры объекта + +```text +payload +``` + +Payload обязан быть JSON Mapping. + +Для объекта выполняется проверка строковых ключей и наличие обязательных элементов транспортного контракта. + +Обязательными являются: + +```text +id +price +size +symbol +ts +buyer +orderId +``` + +Отсутствие любого из перечисленных полей приводит к генерации исключения + +```text +TradeSchemaError +``` + +с указанием отсутствующих элементов. + +--- + +# Проверка Mapping + +Build 060.10 следует архитектурному правилу Dzentra: + +любая структура JSON после прохождения Schema Validation должна представлять собой Mapping со строковыми ключами. + +Поэтому реализованы две независимые проверки. + +Первая проверяет корневой документ: + +```text +$ +``` + +Вторая проверяет вложенный объект: + +```text +$.payload +``` + +В обоих случаях: + +- объект обязан быть Mapping; +- каждый ключ обязан иметь тип `str`. + +Подобный подход полностью совпадает с реализацией Quote и OHLC. + +--- + +# Использование MappingProxyType + +После успешной проверки содержимое + +```text +payload +``` + +копируется в + +```python +MappingProxyType(dict(payload)) +``` + +Таким образом создаётся неизменяемое представление транспортного документа. + +Использование `MappingProxyType` решает сразу несколько задач. + +Во-первых, предотвращается случайное изменение данных после завершения Schema Validation. + +Во-вторых, исключается влияние внешнего кода на результаты проверки структуры. + +В-третьих, последующие уровни Pipeline получают гарантированно неизменяемый документ. + +Parser, Value Validation и Mapper могут безопасно использовать полученные данные, не опасаясь их изменения между этапами обработки. + +--- + +# Почему не используется транспортная модель + +Build 060.10 принципиально не создаёт экземпляр + +```text +DzengiWebSocketTradeEvent +``` + +Это является обязанностью Parser. + +Schema Validation лишь подтверждает корректность структуры сообщения. + +Создание транспортной модели переносится на следующий Build. + +Подобное разделение ответственности уже используется в существующих WebSocket-конвейерах Quote и OHLC. + +--- + +# Граница ответственности Build + +Build 060.10 отвечает исключительно за структурную корректность документа. + +В его обязанности входит: + +- проверка структуры корневого объекта; +- проверка транспортной оболочки; +- проверка структуры payload; +- проверка обязательных полей; +- формирование неизменяемого документа Validation. + +Build **не выполняет**: + +- Parsing; +- преобразование JSON в транспортную модель; +- преобразование типов; +- Decimal-конвертацию; +- проверку диапазонов; +- проверку timestamp; +- проверку символа; +- проверку стороны сделки; +- Mapping; +- создание канонической модели `Trade`. + +Каждая из перечисленных задач относится к отдельному архитектурному уровню и будет реализована в следующих Build. + +--- + +# Использование TradeSchemaError + +Для всех ошибок структуры используется уже существующее исключение + +```text +TradeSchemaError +``` + +Build не вводит новых типов исключений. + +Это сохраняет единый подход ко всей подсистеме Validation и полностью соответствует существующей архитектуре обработки ошибок. + +--- + +# Целевой конвейер обработки WebSocket Trade + +После завершения Build 060.10 конвейер обработки принимает следующий вид. + +```text +Raw WebSocket Object + │ + ▼ +WebSocket Trade Schema Validation + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +Build 060.11 — WebSocket Trade Parser + │ + ▼ +DzengiWebSocketTradeEvent + │ + ▼ +Build 060.12 — WebSocket Trade Value Validation + │ + ▼ +Validated Trade Transport Event + │ + ▼ +Build 060.13 — WebSocket Trade Mapper + │ + ▼ +Trade +``` + +Таким образом Build 060.10 завершает второй архитектурный уровень WebSocket-конвейера обработки сделок. + +--- + +# Соотношение с транспортной моделью + +Build 060.9 и Build 060.10 реализуют два различных архитектурных уровня. + +```text +Build 060.9 +``` + +вводит транспортную модель + +```text +DzengiWebSocketTradeEvent +``` + +которая описывает уже разобранное WebSocket-событие. + +```text +Build 060.10 +``` + +вводит документ + +```text +ValidatedWebSocketTradeDocument +``` + +который представляет собой результат проверки структуры исходного JSON-документа. + +Таким образом последовательность обработки становится следующей. + +```text +Raw JSON + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +DzengiWebSocketTradeEvent + │ + ▼ +Trade +``` + +Каждый объект относится к собственному архитектурному уровню и не дублирует ответственность другого. + +--- + +# Изменённые файлы + +В рамках Build были изменены только два файла. + +## Schema Validation + +```text +src/market_data/acquisition/validation/schema.py +``` + +Добавлены: + +```text +ValidatedWebSocketTradeDocument + +validate_dzengi_websocket_trade_schema(...) + +_validate_websocket_trade_payload(...) + +_require_websocket_trade_mapping(...) +``` + +При этом существующие валидаторы Quote и OHLC, импорты и поведение файла не изменялись. + +--- + +## Unit-тесты + +```text +tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py +``` + +Добавлен полный набор unit-тестов нового уровня Schema Validation. + +--- + +# Добавленные тесты + +В рамках Build реализовано двадцать unit-тестов, полностью покрывающих функциональность нового валидатора. + +## Проверка успешной валидации + +Тест + +```text +test_validate_websocket_trade_schema_returns_immutable_document +``` + +проверяет: + +- успешную проверку корректного документа; +- создание `ValidatedWebSocketTradeDocument`; +- использование `MappingProxyType`; +- сохранение обязательных полей. + +--- + +## Проверка correlationId + +Тест + +```text +test_validate_websocket_trade_schema_preserves_correlation_id +``` + +подтверждает корректное сохранение необязательного поля +`correlationId`. + +--- + +## Проверка копирования payload + +Тест + +```text +test_validate_websocket_trade_schema_copies_payload +``` + +подтверждает, что изменения исходного словаря после Validation +не влияют на содержимое результирующего документа. + +--- + +## Проверка структуры корневого объекта + +Тест + +```text +test_validate_websocket_trade_schema_rejects_non_mapping_root +``` + +подтверждает генерацию `TradeSchemaError`, +если корневой объект не является JSON Mapping. + +--- + +## Проверка обязательных полей транспортной оболочки + +Параметризованный тест + +```text +test_validate_websocket_trade_schema_rejects_missing_root_field +``` + +проверяет отсутствие: + +- status; +- destination; +- payload. + +--- + +## Проверка структуры payload + +Тест + +```text +test_validate_websocket_trade_schema_rejects_non_mapping_payload +``` + +проверяет, что поле `payload` +обязано быть JSON Mapping. + +--- + +## Проверка обязательных полей Trade + +Параметризованный тест + +```text +test_validate_websocket_trade_schema_rejects_missing_payload_field +``` + +проверяет отсутствие каждого обязательного элемента: + +- id; +- price; +- size; +- ts; +- symbol; +- buyer; +- orderId. + +--- + +## Проверка сообщения об ошибке + +Тест + +```text +test_validate_websocket_trade_schema_reports_all_missing_fields +``` + +подтверждает, +что исключение содержит полный список отсутствующих полей. + +--- + +## Отсутствие проверки значений + +Тест + +```text +test_validate_websocket_trade_schema_does_not_validate_values +``` + +подтверждает архитектурный принцип Build. + +Schema Validation проверяет исключительно структуру +и принципиально не анализирует содержимое полей. + +--- + +## Дополнительные поля + +Тест + +```text +test_validate_websocket_trade_schema_allows_additional_fields +``` + +подтверждает, +что неподтверждённые Production поля +не вызывают ошибку Validation. + +В частности проверяется возможность присутствия + +```text +clientOrderId +``` + +и других дополнительных элементов. + +--- + +## Проверка строковых ключей + +Реализованы отдельные тесты проверки строковых ключей +для: + +- корневого объекта; +- объекта payload. + +Это полностью соответствует архитектуре существующих +WebSocket Schema Validation. + +--- + +# Результаты тестирования + +Выполнен целевой запуск нового набора unit-тестов. + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py \ + -q +``` + +Результат: + +```text +20 passed in 0.02s +``` + +Все проверки новой функциональности успешно завершены. + +--- + +# Регрессионное тестирование + +После завершения реализации выполнен полный запуск +подсистемы Validation. + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/validation \ + -q +``` + +Результат: + +```text +302 passed in 0.10s +``` + +Регрессий существующей функциональности не обнаружено. + +--- + +# Проверка компиляции + +Выполнена проверка компиляции изменённых файлов. + +```bash +python -m compileall \ + src/market_data/acquisition/validation/schema.py \ + tests/unit/market_data/acquisition/validation/test_websocket_trade_schema.py +``` + +Компиляция завершилась успешно. + +Синтаксические ошибки отсутствуют. + +--- + +# Проверка Git diff + +Выполнена финальная проверка изменений. + +```bash +git diff --check +``` + +Ошибок не обнаружено. + +Это подтверждает отсутствие: + +- trailing whitespace; +- конфликтов окончания строк; +- ошибок форматирования diff. + +--- + +# Scope Build 060.10 + +В рамках данного Build реализовано только: + +```text +WebSocket Trade Schema Validation +``` + +Build **не включает**: + +- WebSocket Trade Parser; +- WebSocket Trade Value Validation; +- WebSocket Trade Mapper; +- WebSocket Trade Adapter; +- Runtime Integration; +- Unified Routing; +- Trades Feed. + +Это полностью соответствует принципу атомарной реализации Build. + +--- + +# Архитектурный результат + +После завершения Build система содержит завершённый +уровень Schema Validation +для всех поддерживаемых WebSocket-событий. + +```text +Quote + +Raw WebSocket + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketQuoteDocument + +OHLC + +Raw WebSocket + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketOhlcDocument + +Trade + +Raw WebSocket + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketTradeDocument +``` + +Архитектура стала полностью симметричной. + +--- + +# Состояние WebSocket Trade Pipeline + +После завершения Build 060.10 конвейер имеет следующий вид. + +```text +Raw WebSocket Trade Document + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +Parser + │ + ▼ +DzengiWebSocketTradeEvent + │ + ▼ +Value Validation + │ + ▼ +Mapper + │ + ▼ +Trade +``` + +Статус реализации компонентов: + +| Компонент | Build | Статус | +|-----------|-------|--------| +| Canonical Trade Model | 060.1 | ✔ Completed | +| WebSocket Trade Transport Model | 060.9 | ✔ Completed | +| WebSocket Trade Schema Validation | 060.10 | ✔ Completed | +| WebSocket Trade Parser | 060.11 | Pending | +| WebSocket Trade Value Validation | 060.12 | Pending | +| WebSocket Trade Mapper | 060.13 | Pending | +| WebSocket Trade Adapter | 060.14 | Pending | +| Unified WebSocket Routing | 060.15 | Pending | + +--- + +# Соблюдение архитектурных принципов + +В рамках Build полностью сохранены архитектурные инварианты Dzentra. + +## Локальность изменений + +Изменены только: + +- Schema Validation; +- unit-тесты нового валидатора. + +--- + +## Повторное использование архитектуры + +Новая реализация полностью повторяет существующий шаблон Quote и OHLC. + +Новая архитектура не создавалась. + +--- + +## Разделение ответственности + +Schema Validation отвечает исключительно +за проверку структуры сообщения. + +Parser, +Value Validation, +Mapper +и Runtime остаются полностью независимыми уровнями. + +--- + +## Отсутствие бизнес-логики + +Build не выполняет: + +- Parsing; +- преобразование типов; +- Value Validation; +- Mapping; +- Runtime Integration. + +Это полностью соответствует архитектуре Dzentra. + +--- + +## Обратная совместимость + +Существующая обработка Quote, +OHLC +и REST Trade +не изменилась. + +Новая функциональность добавлена изолированно +и не влияет на ранее реализованные Build. + +--- + +# Критерии завершения Build + +Build 060.10 считается завершённым, +поскольку выполнены все поставленные задачи. + +- ✔ реализован `ValidatedWebSocketTradeDocument`; +- ✔ реализована функция `validate_dzengi_websocket_trade_schema()`; +- ✔ проверяется структура транспортной оболочки; +- ✔ проверяется структура payload; +- ✔ проверяются обязательные поля Trade; +- ✔ используется `TradeSchemaError`; +- ✔ payload становится неизменяемым; +- ✔ реализовано двадцать unit-тестов; +- ✔ все целевые тесты успешно проходят; +- ✔ регрессионное тестирование успешно завершено; +- ✔ компиляция выполнена без ошибок; +- ✔ `git diff --check` не выявил замечаний; +- ✔ изменения не выходят за пределы согласованного scope. + +--- + +# Следующий этап + +Следующим этапом дорожной карты является + +```text +Build 060.11 — WebSocket Trade Parser +``` + +Цель Build: + +- преобразование `ValidatedWebSocketTradeDocument`; +- создание транспортной модели `DzengiWebSocketTradeEvent`; +- нормализация имён транспортных полей; +- преобразование JSON-ключей: + - `id → trade_id`; + - `ts → timestamp`; + - `orderId → order_id`; +- сохранение исходных числовых значений без преобразования типов; +- отсутствие Value Validation. + +После завершения Build 060.11 конвейер примет следующий вид: + +```text +Raw WebSocket Object + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +WebSocket Trade Parser + │ + ▼ +DzengiWebSocketTradeEvent +``` + +Build 060.11 по-прежнему не будет выполнять: + +- проверку диапазонов значений; +- проверку корректности timestamp; +- проверку корректности цены и объёма; +- Mapping в каноническую модель `Trade`; +- Runtime Integration. + +Все перечисленные задачи будут реализованы на последующих этапах дорожной карты серии 060. + +--- + +# Итог + +Build 060.10 завершил формирование уровня **Schema Validation** для WebSocket Trade и сделал архитектуру обработки всех WebSocket-событий Dzentra полностью симметричной. + +Новая реализация основана на существующем шаблоне Quote и OHLC, использует единый подход к структурной проверке сообщений, повторно применяет существующую иерархию исключений и не изменяет ранее реализованное поведение системы. + +Build ограничен согласованным scope, успешно прошёл целевое и регрессионное тестирование и создаёт необходимый фундамент для следующего этапа — **Build 060.11 — WebSocket Trade Parser**. \ No newline at end of file