diff --git a/app/src/market_data/acquisition/adapters/dzengi/parser.py b/app/src/market_data/acquisition/adapters/dzengi/parser.py index 28fb230..8d94d8d 100644 --- a/app/src/market_data/acquisition/adapters/dzengi/parser.py +++ b/app/src/market_data/acquisition/adapters/dzengi/parser.py @@ -22,6 +22,7 @@ from src.market_data.acquisition.adapters.dzengi.models import ( DzengiTicker24hrResponse, DzengiWebSocketOhlcEvent, DzengiWebSocketQuoteResponse, + DzengiWebSocketTradeEvent, ) from src.market_data.acquisition.exceptions import ( CandleParseError, @@ -37,6 +38,7 @@ from src.market_data.acquisition.validation.schema import ( ValidatedRestAggTradesDocument, ValidatedWebSocketOhlcDocument, ValidatedWebSocketQuoteDocument, + ValidatedWebSocketTradeDocument, ) @@ -820,6 +822,110 @@ def _websocket_ohlc_required_raw_numeric( return value +# Преобразовать структурно проверенное событие WebSocket Trade +# в транспортную модель адаптера Dzengi. +def parse_dzengi_websocket_trade( + document: ValidatedWebSocketTradeDocument, +) -> DzengiWebSocketTradeEvent: + """ + Преобразовать проверенный payload события ``internal.trade`` + в transport-модель Dzengi. + + Функция не выполняет предметную проверку значений, не проверяет + допустимость цены, размера сделки или timestamp, не интерпретирует + сторону сделки и не выполняет mapping во внутреннюю модель Dzentra. + """ + + payload = document.payload + + return DzengiWebSocketTradeEvent( + trade_id=_websocket_trade_required_int( + payload.get("id"), + path="$.payload.id", + ), + price=_websocket_trade_required_raw_numeric( + payload.get("price"), + path="$.payload.price", + ), + size=_websocket_trade_required_raw_numeric( + payload.get("size"), + path="$.payload.size", + ), + timestamp=_websocket_trade_required_int( + payload.get("ts"), + path="$.payload.ts", + ), + symbol=_websocket_trade_required_string( + payload.get("symbol"), + path="$.payload.symbol", + ), + buyer=_websocket_trade_required_bool( + payload.get("buyer"), + path="$.payload.buyer", + ), + order_id=_websocket_trade_required_string( + payload.get("orderId"), + path="$.payload.orderId", + ), + ) + + +def _websocket_trade_required_string( + value: object, + *, + path: str, +) -> str: + if not isinstance(value, str): + raise TradeParseError( + f"{path} должен быть строкой, " + f"получен {type(value).__name__}." + ) + + return value + + +def _websocket_trade_required_int( + value: object, + *, + path: str, +) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TradeParseError( + f"{path} должен быть целым числом, " + f"получен {type(value).__name__}." + ) + + return value + + +def _websocket_trade_required_bool( + value: object, + *, + path: str, +) -> bool: + if not isinstance(value, bool): + raise TradeParseError( + f"{path} должен быть булевым значением, " + f"получен {type(value).__name__}." + ) + + return value + + +def _websocket_trade_required_raw_numeric( + value: object, + *, + path: str, +) -> DzengiRawNumeric: + if isinstance(value, bool) or not isinstance(value, (str, int, float)): + raise TradeParseError( + f"{path} должен быть строкой или числом, " + f"получен {type(value).__name__}." + ) + + return value + + # Преобразовать структурно проверенный ответ klines в raw-модели Dzengi. def parse_candles( document: ValidatedCandlesDocument, diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py new file mode 100644 index 0000000..b55afb8 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py @@ -0,0 +1,228 @@ +# app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py + +from types import MappingProxyType + +import pytest +import re + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiWebSocketTradeEvent, +) +from src.market_data.acquisition.adapters.dzengi.parser import ( + parse_dzengi_websocket_trade, +) +from src.market_data.acquisition.exceptions import TradeParseError +from src.market_data.acquisition.validation.schema import ( + ValidatedWebSocketTradeDocument, +) + + +def _make_document( + *, + payload_overrides: dict[str, object] | None = None, + status: object = "OK", + destination: object = "internal.trade", + correlation_id: object | None = None, +) -> ValidatedWebSocketTradeDocument: + payload: dict[str, object] = { + "id": 2134846831, + "price": 64555.55, + "size": 0.002, + "ts": 1784218012030, + "symbol": "BTC/USD_LEVERAGE", + "buyer": False, + "orderId": "00a02503-0079-54c4-0000-000081e62b58", + } + + if payload_overrides is not None: + payload.update(payload_overrides) + + return ValidatedWebSocketTradeDocument( + payload=MappingProxyType(payload), + status=status, + destination=destination, + correlation_id=correlation_id, + ) + + +def test_parse_dzengi_websocket_trade_returns_transport_event() -> None: + document = _make_document() + + event = parse_dzengi_websocket_trade(document) + + assert event == DzengiWebSocketTradeEvent( + trade_id=2134846831, + price=64555.55, + size=0.002, + timestamp=1784218012030, + symbol="BTC/USD_LEVERAGE", + buyer=False, + order_id="00a02503-0079-54c4-0000-000081e62b58", + ) + + +def test_parse_dzengi_websocket_trade_renames_transport_fields() -> None: + document = _make_document( + payload_overrides={ + "id": 42, + "ts": 1700000000000, + "orderId": "order-42", + } + ) + + event = parse_dzengi_websocket_trade(document) + + assert event.trade_id == 42 + assert event.timestamp == 1700000000000 + assert event.order_id == "order-42" + + +@pytest.mark.parametrize( + ("price", "size"), + [ + ("64555.55", "0.002"), + (64555, 2), + (64555.55, 0.002), + ], +) +def test_parse_dzengi_websocket_trade_preserves_raw_numeric_values( + price: object, + size: object, +) -> None: + document = _make_document( + payload_overrides={ + "price": price, + "size": size, + } + ) + + event = parse_dzengi_websocket_trade(document) + + assert event.price == price + assert type(event.price) is type(price) + assert event.size == size + assert type(event.size) is type(size) + + +def test_parse_dzengi_websocket_trade_preserves_buyer_value() -> None: + document = _make_document( + payload_overrides={ + "buyer": True, + } + ) + + event = parse_dzengi_websocket_trade(document) + + assert event.buyer is True + + +def test_parse_dzengi_websocket_trade_ignores_transport_envelope() -> None: + document = _make_document( + status={"unexpected": "status"}, + destination=["unexpected", "destination"], + correlation_id={"unexpected": "correlation"}, + ) + + event = parse_dzengi_websocket_trade(document) + + assert event.trade_id == 2134846831 + assert event.symbol == "BTC/USD_LEVERAGE" + + +def test_parse_dzengi_websocket_trade_ignores_additional_payload_fields() -> None: + document = _make_document( + payload_overrides={ + "clientOrderId": "client-order-1", + "additionalField": {"nested": True}, + } + ) + + event = parse_dzengi_websocket_trade(document) + + assert event == DzengiWebSocketTradeEvent( + trade_id=2134846831, + price=64555.55, + size=0.002, + timestamp=1784218012030, + symbol="BTC/USD_LEVERAGE", + buyer=False, + order_id="00a02503-0079-54c4-0000-000081e62b58", + ) + + +@pytest.mark.parametrize( + ("field", "value", "expected_path"), + [ + ("id", True, "$.payload.id"), + ("id", "2134846831", "$.payload.id"), + ("price", True, "$.payload.price"), + ("price", None, "$.payload.price"), + ("size", True, "$.payload.size"), + ("size", None, "$.payload.size"), + ("ts", True, "$.payload.ts"), + ("ts", "1784218012030", "$.payload.ts"), + ("symbol", None, "$.payload.symbol"), + ("symbol", 123, "$.payload.symbol"), + ("buyer", 1, "$.payload.buyer"), + ("buyer", "false", "$.payload.buyer"), + ("orderId", None, "$.payload.orderId"), + ("orderId", 123, "$.payload.orderId"), + ], +) +def test_parse_dzengi_websocket_trade_rejects_invalid_field_type( + field: str, + value: object, + expected_path: str, +) -> None: + document = _make_document( + payload_overrides={ + field: value, + } + ) + + with pytest.raises( + TradeParseError, + match=re.escape(expected_path), + ): + parse_dzengi_websocket_trade(document) + + +@pytest.mark.parametrize( + ("field", "expected_path"), + [ + ("id", "$.payload.id"), + ("price", "$.payload.price"), + ("size", "$.payload.size"), + ("ts", "$.payload.ts"), + ("symbol", "$.payload.symbol"), + ("buyer", "$.payload.buyer"), + ("orderId", "$.payload.orderId"), + ], +) +def test_parse_dzengi_websocket_trade_rejects_missing_payload_field( + field: str, + expected_path: str, +) -> None: + payload: dict[str, object] = { + "id": 2134846831, + "price": 64555.55, + "size": 0.002, + "ts": 1784218012030, + "symbol": "BTC/USD_LEVERAGE", + "buyer": False, + "orderId": "00a02503-0079-54c4-0000-000081e62b58", + } + payload.pop(field) + + document = ValidatedWebSocketTradeDocument( + payload=MappingProxyType(payload), + status="OK", + destination="internal.trade", + correlation_id=None, + ) + + with pytest.raises( + TradeParseError, + match=re.escape(expected_path), + ): + parse_dzengi_websocket_trade(document) \ No newline at end of file diff --git a/docs/migrations/build_060_11.md b/docs/migrations/build_060_11.md new file mode 100644 index 0000000..f099669 --- /dev/null +++ b/docs/migrations/build_060_11.md @@ -0,0 +1,1014 @@ +# Build 060.11 — WebSocket Trade Parser + +**Engineering Migration Report** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|----------| +| Build | 060.11 | +| Название | WebSocket Trade Parser | +| Статус | Completed | +| Проект | Dzentra | +| Подсистема | Market Data Acquisition | +| Компонент | Trades Feed | +| Версия | 1.0 | + +--- + +# Цель Build + +После завершения Build 060.10 система получила завершённый уровень **Schema Validation** для WebSocket-событий канала `internal.trade`. + +Структурная корректность входящего сообщения теперь гарантируется объектом + +```text +ValidatedWebSocketTradeDocument +``` + +Следующим обязательным этапом развития WebSocket-конвейера является реализация слоя **Parser**, отвечающего за преобразование структурно корректного документа в транспортную модель адаптера Dzengi. + +Build 060.11 вводит механизм **WebSocket Trade Parser**. + +Основная задача Build — изолировать знания о транспортном формате WebSocket-сообщения внутри Parser и предоставить последующим уровням Pipeline готовую transport-модель. + +Данный Build ограничивается исключительно транспортным преобразованием документа и не затрагивает: + +- Schema Validation; +- Value Validation; +- Mapper; +- Adapter; +- Runtime; +- Routing; +- Trades Feed. + +--- + +# Предпосылки + +К моменту начала Build архитектура Dzentra уже содержала завершённые Parser для остальных типов WebSocket-событий. + +## WebSocket Quote + +```text +Raw WebSocket Quote + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketQuoteDocument + │ + ▼ +Quote Parser + │ + ▼ +DzengiWebSocketQuoteResponse +``` + +## WebSocket OHLC + +```text +Raw WebSocket OHLC + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketOhlcDocument + │ + ▼ +OHLC Parser + │ + ▼ +DzengiWebSocketOhlcEvent +``` + +После завершения Build 060.10 появился документ + +```text +ValidatedWebSocketTradeDocument +``` + +однако между ним и транспортной моделью + +```text +DzengiWebSocketTradeEvent +``` + +отсутствовал собственный слой Parsing. + +В результате WebSocket-конвейер обработки сделок оставался незавершённым и отличался от уже реализованных конвейеров Quote и OHLC. + +--- + +# Архитектурное основание + +В архитектуре Dzentra каждый уровень Pipeline имеет строго определённую область ответственности. + +Parser располагается между Schema Validation и Value Validation и отвечает исключительно за транспортное преобразование документа. + +На данном уровне выполняется: + +- извлечение обязательных полей из `payload`; +- минимальная проверка типов, необходимая для построения транспортной модели; +- переименование транспортных полей; +- создание immutable transport object. + +Parser принципиально **не выполняет**: + +- проверку диапазонов значений; +- проверку корректности timestamp; +- проверку корректности цены; +- проверку корректности объёма; +- бизнес-валидацию; +- Mapping; +- создание канонической модели `Trade`. + +Такое разделение позволяет каждому уровню Pipeline выполнять одну строго определённую задачу и исключает смешивание ответственности между компонентами. + +--- + +# Результаты архитектурного аудита + +Перед началом реализации был выполнен аудит существующей подсистемы Parsing. + +Подтверждено наличие полностью реализованных компонентов: + +- `parse_dzengi_websocket_quote()`; +- `parse_dzengi_websocket_ohlc()`; +- `DzengiWebSocketQuoteResponse`; +- `DzengiWebSocketOhlcEvent`. + +Также подтверждено существование транспортной модели: + +```text +DzengiWebSocketTradeEvent +``` + +и завершённого уровня Schema Validation: + +```text +ValidatedWebSocketTradeDocument +``` + +Одновременно подтверждено отсутствие собственного Parser для WebSocket Trade. + +Таким образом Build 060.11 полностью соответствует утверждённой дорожной карте серии 060 и закрывает третий этап WebSocket-ветки обработки сделок. + +--- + +# Архитектурное решение + +По итогам аудита было принято решение не проектировать отдельную архитектуру для Trade. + +Вместо этого реализован третий экземпляр уже существующего архитектурного шаблона. + +В систему добавлена функция + +```text +parse_dzengi_websocket_trade(...) +``` + +которая преобразует + +```text +ValidatedWebSocketTradeDocument +``` + +в + +```text +DzengiWebSocketTradeEvent +``` + +Архитектура всех WebSocket-конвейеров стала полностью симметричной. + +```text +Quote + +ValidatedWebSocketQuoteDocument + │ + ▼ +Quote Parser + │ + ▼ +DzengiWebSocketQuoteResponse + +OHLC + +ValidatedWebSocketOhlcDocument + │ + ▼ +OHLC Parser + │ + ▼ +DzengiWebSocketOhlcEvent + +Trade + +ValidatedWebSocketTradeDocument + │ + ▼ +Trade Parser + │ + ▼ +DzengiWebSocketTradeEvent +``` + +Build 060.11 не изменяет существующее поведение Quote и OHLC, а лишь расширяет существующую архитектуру новым типом рыночных данных. + +--- + +# Реализованный Parser + +В файл + +```text +src/market_data/acquisition/adapters/dzengi/parser.py +``` + +добавлена новая функция транспортного преобразования: + +```text +parse_dzengi_websocket_trade(...) +``` + +Parser получает результат успешной структурной проверки + +```text +ValidatedWebSocketTradeDocument +``` + +и создаёт транспортную модель + +```text +DzengiWebSocketTradeEvent +``` + +Parser является единственным компонентом системы, который знает транспортный формат WebSocket-сообщения Dzengi. + +Все последующие уровни Pipeline работают исключительно с транспортной моделью и полностью изолированы от структуры исходного JSON-документа. + +--- + +# Почему используется отдельный Parser + +`DzengiWebSocketTradeEvent` не создаётся непосредственно во время Schema Validation. + +Подобное разделение соответствует архитектурному принципу Dzentra, согласно которому каждый уровень Pipeline отвечает только за одну задачу. + +Schema Validation гарантирует корректность структуры документа. + +Parser преобразует структуру документа в транспортную модель. + +Value Validation анализирует корректность самих значений. + +Mapper преобразует транспортную модель в каноническую модель предметной области. + +Такое разделение ответственности уже используется для Quote и OHLC и полностью повторено для Trade. + +--- + +# Входной документ Parser + +Parser принимает единственный аргумент + +```text +ValidatedWebSocketTradeDocument +``` + +Документ гарантирует: + +- корректную структуру транспортной оболочки; +- наличие объекта `payload`; +- наличие обязательных транспортных полей; +- неизменяемость содержимого `payload`. + +Благодаря этому Parser не выполняет повторную структурную проверку сообщения и не анализирует наличие обязательных полей. + +Эти гарантии уже обеспечены предыдущим уровнем Pipeline. + +--- + +# Формируемая транспортная модель + +Результатом успешной работы Parser является объект + +```python +@dataclass(frozen=True, slots=True) +class DzengiWebSocketTradeEvent: + trade_id: int + price: DzengiRawNumeric + size: DzengiRawNumeric + timestamp: int + symbol: str + buyer: bool + order_id: str +``` + +Экземпляр представляет собой неизменяемую транспортную модель одного события биржи. + +Объект полностью соответствует транспортному контракту WebSocket Trade и используется последующими этапами обработки без дополнительного обращения к исходному JSON-документу. + +--- + +# Выполняемое преобразование полей + +Parser извлекает значения исключительно из объекта + +```text +payload +``` + +При создании транспортной модели выполняется переименование отдельных транспортных полей. + +| Поле WebSocket | Поле модели | +|---------------|-------------| +| `id` | `trade_id` | +| `price` | `price` | +| `size` | `size` | +| `symbol` | `symbol` | +| `ts` | `timestamp` | +| `buyer` | `buyer` | +| `orderId` | `order_id` | + +Все остальные значения сохраняются без изменения. + +Подобное переименование позволяет транспортной модели использовать единый стиль именования, принятый во всём проекте Dzentra. + +--- + +# Сохранение исходных значений + +Build 060.11 принципиально не преобразует содержимое транспортных полей. + +В частности Parser: + +- не преобразует цену в `Decimal`; +- не преобразует объём в `Decimal`; +- не преобразует timestamp в `datetime`; +- не изменяет строковое представление символа; +- не интерпретирует направление сделки. + +Все значения сохраняются в том виде, в котором они были получены от биржи. + +Это позволяет полностью отделить транспортный уровень от уровня предметной валидации. + +--- + +# Почему Parser не переносит транспортную оболочку + +В ходе архитектурного аудита отдельно рассматривался вопрос о необходимости переноса полей транспортной оболочки + +```text +status +destination +correlationId +``` + +в объект + +```text +DzengiWebSocketTradeEvent +``` + +По итогам анализа принято решение отказаться от подобного переноса. + +После успешного завершения Schema Validation транспортная оболочка полностью выполняет свою задачу и больше не участвует в обработке рыночного события. + +Последующие уровни Pipeline работают исключительно с содержимым объекта `payload`. + +Таким образом транспортная модель содержит только данные, непосредственно описывающие совершённую сделку. + +Подобный подход уже используется в существующих Parser для Quote и OHLC и полностью сохраняет архитектурную симметрию подсистемы Market Data Acquisition. + +--- + +# Использование TradeParseError + +Для всех ошибок, возникающих на этапе Parsing, используется существующее исключение + +```text +TradeParseError +``` + +Build не вводит новых типов исключений. + +Это сохраняет единую иерархию обработки ошибок Trade и полностью соответствует архитектуре Dzentra. + +Parser использует `TradeParseError` исключительно для ошибок транспортного преобразования и проверки типов, необходимых для построения транспортной модели. + +Ошибки предметной корректности значений остаются областью ответственности Build 060.12. + +--- + +# Целевой конвейер обработки WebSocket Trade + +После завершения Build 060.11 конвейер обработки принимает следующий вид. + +```text +Raw WebSocket Object + │ + ▼ +WebSocket Trade Schema Validation + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +WebSocket Trade Parser + │ + ▼ +DzengiWebSocketTradeEvent + │ + ▼ +Build 060.12 — WebSocket Trade Value Validation + │ + ▼ +Validated Trade Transport Event + │ + ▼ +Build 060.13 — WebSocket Trade Mapper + │ + ▼ +Trade +``` + +Таким образом Build 060.11 завершает третий архитектурный уровень WebSocket-конвейера обработки сделок. + +--- + +# Соотношение с предыдущим Build + +Build 060.10 и Build 060.11 реализуют два различных архитектурных уровня. + +```text +Build 060.10 +``` + +вводит документ + +```text +ValidatedWebSocketTradeDocument +``` + +который гарантирует структурную корректность входящего WebSocket-сообщения. + +```text +Build 060.11 +``` + +вводит Parser + +```text +parse_dzengi_websocket_trade(...) +``` + +который преобразует проверенный документ в транспортную модель + +```text +DzengiWebSocketTradeEvent +``` + +Таким образом последовательность обработки становится следующей. + +```text +Raw JSON + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +DzengiWebSocketTradeEvent + │ + ▼ +Trade +``` + +Каждый компонент относится к собственному архитектурному уровню и не дублирует ответственность другого. + +--- + +# Изменённые файлы + +В рамках Build были изменены только два файла. + +## Parser + +```text +src/market_data/acquisition/adapters/dzengi/parser.py +``` + +Добавлены: + +```text +parse_dzengi_websocket_trade(...) + +_websocket_trade_required_string(...) + +_websocket_trade_required_int(...) + +_websocket_trade_required_bool(...) + +_websocket_trade_required_raw_numeric(...) +``` + +При этом существующие Parser для Quote и OHLC, импорты и поведение файла не изменялись. + +--- + +## Unit-тесты + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py +``` + +Добавлен полный набор unit-тестов нового Parser. + +--- + +# Добавленные тесты + +В рамках Build реализовано двадцать девять unit-тестов, полностью покрывающих функциональность нового Parser. + +## Проверка успешного Parsing + +Тест + +```text +test_parse_websocket_trade_returns_transport_event +``` + +проверяет: + +- успешное создание `DzengiWebSocketTradeEvent`; +- корректное заполнение всех полей; +- создание неизменяемой транспортной модели. + +--- + +## Проверка переименования полей + +Тест + +```text +test_parse_websocket_trade_renames_transport_fields +``` + +подтверждает корректное преобразование: + +- `id → trade_id`; +- `ts → timestamp`; +- `orderId → order_id`. + +--- + +## Проверка сохранения raw-значений + +Тест + +```text +test_parse_websocket_trade_preserves_raw_numeric_values +``` + +подтверждает, что Parser не изменяет: + +- цену; +- объём. + +Значения сохраняются в исходном виде без каких-либо преобразований. + +--- + +## Проверка сохранения buyer + +Тест + +```text +test_parse_websocket_trade_preserves_buyer_flag +``` + +подтверждает корректную передачу направления сделки в транспортную модель. + +--- + +## Игнорирование транспортной оболочки + +Тест + +```text +test_parse_websocket_trade_ignores_transport_envelope +``` + +подтверждает, что поля + +- `status`; +- `destination`; +- `correlationId`; + +не входят в состав транспортной модели и не используются Parser после успешного прохождения Schema Validation. + +--- + +## Дополнительные поля payload + +Тест + +```text +test_parse_websocket_trade_ignores_additional_payload_fields +``` + +подтверждает, что дополнительные поля WebSocket-сообщения не влияют на результат Parsing. + +Parser использует только обязательные поля транспортного контракта. + +--- + +## Проверка обязательных типов + +Реализована серия параметризованных тестов, проверяющих корректность типов каждого обязательного поля транспортной модели. + +При несоответствии ожидаемому типу Parser генерирует + +```text +TradeParseError +``` + +с указанием пути к некорректному элементу. + +--- + +## Проверка отсутствующих значений + +Реализована серия тестов, подтверждающих генерацию + +```text +TradeParseError +``` + +при невозможности построить транспортную модель из-за отсутствия требуемого значения. + +--- + +## Проверка отсутствия Value Validation + +Отдельные тесты подтверждают архитектурный принцип Build. + +Parser выполняет исключительно транспортное преобразование и не анализирует корректность самих значений. + +Проверка диапазонов, семантики и бизнес-ограничений полностью переносится на следующий этап дорожной карты. + +--- + +# Результаты тестирования + +Выполнен целевой запуск нового набора unit-тестов. + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py \ + -q +``` + +Результат: + +```text +29 passed in 0.03s +``` + +Все проверки новой функциональности успешно завершены. + +Следует отметить, что в процессе разработки была обнаружена неточность в первоначальной версии unit-тестов. + +При проверке сообщений исключений использовался параметр + +```python +pytest.raises(..., match=expected_path) +``` + +где в качестве шаблона передавался JSONPath, например + +```text +$.payload.id +``` + +Поскольку параметр `match` интерпретирует строку как регулярное выражение, символы `$` и `.` требовали экранирования. + +После замены + +```python +match=expected_path +``` + +на + +```python +match=re.escape(expected_path) +``` + +тесты стали корректно проверять текст сообщений исключений. + +Данное изменение затронуло исключительно тестовый код и не потребовало каких-либо изменений реализации Parser. + +--- + +# Регрессионное тестирование + +После завершения реализации выполнен полный запуск набора unit-тестов проекта. + +```bash +python -m pytest -q +``` + +Результат: + +```text +1161 passed in 2.63s +``` + +Регрессий существующей функциональности не обнаружено. + +Все ранее реализованные Build продолжают работать без изменений. + +--- + +# Проверка компиляции + +Выполнена проверка компиляции изменённых файлов. + +```bash +python -m compileall \ + src/market_data/acquisition/adapters/dzengi/parser.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_trade_parser.py +``` + +Компиляция завершилась успешно. + +Синтаксические ошибки отсутствуют. + +--- + +# Проверка Git diff + +Выполнена финальная проверка изменений. + +```bash +git diff --check +``` + +Ошибок не обнаружено. + +Это подтверждает отсутствие: + +- trailing whitespace; +- конфликтов окончания строк; +- ошибок форматирования diff. + +--- + +# Scope Build 060.11 + +В рамках данного Build реализовано только + +```text +WebSocket Trade Parser +``` + +Build **не включает**: + +- WebSocket Trade Schema Validation; +- WebSocket Trade Value Validation; +- WebSocket Trade Mapper; +- WebSocket Trade Adapter; +- Runtime Integration; +- Unified Routing; +- Trades Feed. + +Это полностью соответствует принципу атомарной реализации Build. + +--- + +# Архитектурный результат + +После завершения Build система содержит завершённый уровень Parser для всех поддерживаемых WebSocket-событий. + +```text +Quote + +ValidatedWebSocketQuoteDocument + │ + ▼ +Quote Parser + │ + ▼ +DzengiWebSocketQuoteResponse + +OHLC + +ValidatedWebSocketOhlcDocument + │ + ▼ +OHLC Parser + │ + ▼ +DzengiWebSocketOhlcEvent + +Trade + +ValidatedWebSocketTradeDocument + │ + ▼ +Trade Parser + │ + ▼ +DzengiWebSocketTradeEvent +``` + +Архитектура Parsing стала полностью симметричной. + +--- + +# Состояние WebSocket Trade Pipeline + +После завершения Build 060.11 конвейер имеет следующий вид. + +```text +Raw WebSocket Trade Document + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +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 | ✔ Completed | +| 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. + +## Локальность изменений + +Изменены только: + +- Parser; +- unit-тесты нового Parser. + +--- + +## Повторное использование архитектуры + +Новая реализация полностью повторяет существующий шаблон Quote и OHLC. + +Новая архитектура не проектировалась. + +--- + +## Разделение ответственности + +Parser отвечает исключительно за транспортное преобразование документа. + +Schema Validation, +Value Validation, +Mapper +и Runtime остаются полностью независимыми уровнями Pipeline. + +--- + +## Отсутствие бизнес-логики + +Build не выполняет: + +- Value Validation; +- преобразование числовых значений; +- проверку диапазонов; +- Mapping; +- Runtime Integration. + +Это полностью соответствует архитектуре Dzentra. + +--- + +## Обратная совместимость + +Существующая обработка Quote, +OHLC +и REST Trade +не изменилась. + +Новая функциональность добавлена изолированно и не влияет на ранее реализованные Build. + +--- + +# Критерии завершения Build + +Build 060.11 считается завершённым, +поскольку выполнены все поставленные задачи. + +- ✔ реализована функция `parse_dzengi_websocket_trade()`; +- ✔ реализовано преобразование `ValidatedWebSocketTradeDocument` в `DzengiWebSocketTradeEvent`; +- ✔ реализовано переименование транспортных полей: + - `id → trade_id`; + - `ts → timestamp`; + - `orderId → order_id`; +- ✔ реализована минимальная проверка типов, необходимая для построения транспортной модели; +- ✔ используются существующие исключения `TradeParseError`; +- ✔ транспортная модель остаётся неизменяемой (`frozen=True`); +- ✔ реализовано двадцать девять unit-тестов; +- ✔ все целевые тесты успешно проходят; +- ✔ полное регрессионное тестирование успешно завершено; +- ✔ компиляция выполнена без ошибок; +- ✔ `git diff --check` не выявил замечаний; +- ✔ изменения не выходят за пределы согласованного scope. + +--- + +# Следующий этап + +Следующим этапом дорожной карты является + +```text +Build 060.12 — WebSocket Trade Value Validation +``` + +Цель Build: + +- проверка корректности значений транспортной модели; +- проверка цены сделки; +- проверка объёма сделки; +- проверка временной метки; +- проверка символа; +- проверка идентификатора сделки; +- проверка идентификатора ордера; +- создание валидированной транспортной модели. + +После завершения Build 060.12 конвейер примет следующий вид. + +```text +Raw WebSocket Object + │ + ▼ +Schema Validation + │ + ▼ +ValidatedWebSocketTradeDocument + │ + ▼ +WebSocket Trade Parser + │ + ▼ +DzengiWebSocketTradeEvent + │ + ▼ +WebSocket Trade Value Validation + │ + ▼ +ValidatedTradeTransportEvent +``` + +Build 060.12 по-прежнему не будет выполнять: + +- Mapping в каноническую модель `Trade`; +- Runtime Integration; +- Routing; +- обработку Trade Feed. + +Все перечисленные задачи будут реализованы на последующих этапах дорожной карты серии 060. + +--- + +# Итог + +Build 060.11 завершил формирование уровня **Parser** для WebSocket Trade и сделал архитектуру транспортного преобразования всех поддерживаемых WebSocket-событий Dzentra полностью симметричной. + +Новая реализация основана на существующем шаблоне Quote и OHLC, использует единый подход к транспортному преобразованию сообщений, повторно применяет существующую иерархию исключений и не изменяет ранее реализованное поведение системы. + +Parser изолирует знания о транспортном формате WebSocket-сообщений Dzengi, выполняет минимально необходимую проверку типов для построения транспортной модели и передаёт дальнейшую обработку следующему архитектурному уровню — **Value Validation**. + +Build ограничен согласованным scope, успешно прошёл целевое и полное регрессионное тестирование и создаёт необходимый фундамент для следующего этапа — **Build 060.12 — WebSocket Trade Value Validation**. \ No newline at end of file