# Build 060.4 — REST Trade Parser ## Статус ✅ Завершён --- # Цель Build После завершения Build 060.3 система уже умела принимать REST-ответ endpoint `/api/v2/aggTrades`, проверять его структуру и преобразовывать в immutable-контракт: ```text REST JSON ↓ validate_rest_agg_trades_schema() ↓ ValidatedRestAggTradesDocument ``` Однако структурно проверенный документ ещё не преобразовывался в transport-модели адаптера Dzengi. Цель Build 060.4 — добавить отдельный REST Trade Parser, который: - принимает только `ValidatedRestAggTradesDocument`; - проверяет типы полей каждой сделки; - создаёт `DzengiRestAggTrade`; - возвращает immutable-последовательность transport-моделей; - не выполняет value validation и canonical mapping. Итоговый участок pipeline после Build 060.4: ```text REST JSON ↓ Schema Validation ↓ ValidatedRestAggTradesDocument ↓ REST Trade Parser ↓ tuple[DzengiRestAggTrade, ...] ``` --- # Причины появления Build Schema Validation отвечает только за структуру внешнего документа. После Build 060.3 она гарантирует, что корень документа является JSON-массивом, каждый элемент является JSON-объектом, обязательные поля `a`, `p`, `q`, `T`, `m` присутствуют, ключи объектов являются строками, а документ переведён в immutable-представление. Но Schema Validation намеренно не определяет, являются ли значения полей допустимыми транспортными типами. Именно Parser обязан отклонить такой документ и не допустить создание некорректной transport-модели. --- # Архитектурное решение В Build реализован отдельный parser-функционал для REST Trades: ```python parse_rest_agg_trades( document: ValidatedRestAggTradesDocument, ) -> tuple[DzengiRestAggTrade, ...] ``` Parser работает со всем REST-документом целиком и не принимает произвольный `dict` или `list`. Его входной контракт уже подтверждает, что документ прошёл Schema Validation. ```text ValidatedRestAggTradesDocument ↓ parse_rest_agg_trades() ↓ tuple[DzengiRestAggTrade, ...] ``` --- # Граница ответственности Parser REST Trade Parser отвечает за: - проверку типов полей; - исключение недопустимого использования `bool` как числа; - создание `DzengiRestAggTrade`; - сохранение исходных raw numeric значений; - формирование immutable `tuple`; - диагностический путь с индексом элемента. Parser не отвечает за: - проверку положительности цены и количества; - проверку диапазона timestamp; - проверку неотрицательности aggregate trade id; - преобразование `str | int | float` в `Decimal`; - построение canonical `Trade`; - определение стороны сделки; - REST transport, feed registry и runtime orchestration. --- # Новое исключение В Build добавлено исключение: ```python class TradeParseError(MarketDataAcquisitionError): pass ``` Оно используется только для ошибок преобразования структурно проверенного REST Trade документа в transport-модели Dzengi. Build сознательно не добавляет `TradeValueError`, `TradeMappingError`, `TradeTransportError` и `TradeFeedRegistryError`. Каждое исключение должно появляться только в Build, реализующем соответствующий слой pipeline. --- # Входной контракт Parser принимает: ```python ValidatedRestAggTradesDocument ``` Контракт был добавлен в Build 060.3: ```python @dataclass(frozen=True, slots=True) class ValidatedRestAggTradesDocument: items: tuple[Mapping[str, object], ...] ``` Каждый элемент `items` уже является mapping, содержит обязательные поля, имеет строковые ключи и защищён от изменения через `MappingProxyType`. Parser не повторяет Schema Validation. --- # Выходной контракт Parser возвращает: ```python tuple[DzengiRestAggTrade, ...] ``` Каждый элемент является immutable transport-моделью: ```python @dataclass(frozen=True, slots=True) class DzengiRestAggTrade: aggregate_trade_id: int price: DzengiRawNumeric quantity: DzengiRawNumeric timestamp: int buyer_is_maker: bool ``` После Build 060.4 REST-ответ уже представлен типизированными transport-объектами, но ещё не преобразован во внутреннюю canonical-модель Dzentra. --- # Реализация Parser Основная функция: ```python def parse_rest_agg_trades( document: ValidatedRestAggTradesDocument, ) -> tuple[DzengiRestAggTrade, ...]: return tuple( _parse_rest_agg_trade_item( item, path=f"$[{index}]", ) for index, item in enumerate(document.items) ) ``` Функция проходит по всем элементам документа, сохраняет исходный порядок сделок, передаёт индекс элемента в diagnostic path и создаёт immutable `tuple`. Пустой документ преобразуется в `()` и не считается ошибкой. --- # Преобразование отдельной сделки Каждый элемент преобразуется функцией `_parse_rest_agg_trade_item()`. Соответствие внешних полей transport-модели: | REST поле | Transport поле | |---|---| | `a` | `aggregate_trade_id` | | `p` | `price` | | `q` | `quantity` | | `T` | `timestamp` | | `m` | `buyer_is_maker` | Parser не изменяет семантику значений. --- # Контракт типов Build фиксирует следующий transport type contract: - `a` — строго `int`, но не `bool`; - `p` — `str | int | float`, но не `bool`; - `q` — `str | int | float`, но не `bool`; - `T` — строго `int`, но не `bool`; - `m` — строго `bool`. Проверка диапазонов и смысловой допустимости значений в этот Build не входит. --- # Почему `bool` исключается из числовых полей В Python выражение `isinstance(True, int)` возвращает `True`. Без специальной проверки значения `True` и `False` могли бы быть ошибочно приняты как идентификатор сделки, timestamp, цена или количество. Поэтому числовые helper-функции сначала проверяют `isinstance(value, bool)` и отклоняют такое значение до общей проверки числа. Это является частью transport contract, а не value validation. --- # Raw numeric preservation Поля `price` и `quantity` используют тип `DzengiRawNumeric`. Parser не преобразует значения в `Decimal` и не нормализует их: - строка `"1.2300"` остаётся строкой `"1.2300"`; - целое число `5` остаётся `5`; - число `1.25` остаётся `1.25`. Это сохраняет исходное представление данных биржи до отдельного этапа Value Validation и Normalization. --- # Diagnostic path Parser формирует путь к каждому полю с учётом индекса элемента. Например, ошибка цены во второй сделке будет представлена как: ```text $[1].p должен быть строкой или числом ``` Это позволяет точно определить индекс некорректной сделки и имя поля. --- # Отдельные Trade helper-функции В Build добавлены специализированные helper-функции: ```python _trade_required_int() _trade_required_raw_numeric() _trade_required_bool() ``` Они следуют общей parser-инфраструктуре проекта, но возбуждают именно `TradeParseError`. Переиспользование helper-функций другого feed было бы некорректным, поскольку они используют другие error contracts. --- # Unit-тесты В Build добавлен отдельный набор unit-тестов REST Trade Parser. Проверяются: - корректное преобразование нескольких сделок; - сохранение порядка элементов; - пустой документ; - immutable `tuple`; - создание `DzengiRestAggTrade`; - сохранение raw numeric значений; - отклонение неверных типов `a`, `T`, `p`, `q`, `m`; - отдельное отклонение `bool` в числовых полях; - корректный индекс элемента в сообщении об ошибке. --- # Результаты проверки Target tests: ```text 46 passed ``` Полная регрессия проекта: ```text 1038 passed ``` Дополнительно успешно выполнены: ```bash python -m compileall src git diff --check ``` Файл unit-тестов содержит корректный завершающий перевод строки. --- # Изменённые файлы В рамках Build изменены только три файла. ## `src/market_data/acquisition/exceptions.py` Добавлено `TradeParseError`. ## `src/market_data/acquisition/adapters/dzengi/parser.py` Добавлены: - `parse_rest_agg_trades()`; - `_parse_rest_agg_trade_item()`; - `_trade_required_int()`; - `_trade_required_raw_numeric()`; - `_trade_required_bool()`; - необходимые импорты. ## `tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py` Добавлены helper создания validated-документа и полный набор позитивных и негативных parser-тестов. Другие части проекта не изменялись. --- # Что не изменялось Build не затрагивает: ```text src/market_data/acquisition/adapters/dzengi/models.py src/market_data/acquisition/adapters/dzengi/mapper.py src/market_data/acquisition/validation/schema.py src/market_data/acquisition/validation/values.py src/market_data/acquisition/feeds/ src/market_data/acquisition/runtime/ ``` Также не изменялись существующие parser-функции для ExchangeInfo, Quotes, Candles, WebSocket Quote и WebSocket OHLC. --- # Архитектурный результат После Build 060.4 REST Trades pipeline выглядит так: ```text REST JSON │ ▼ validate_rest_agg_trades_schema() │ ▼ ValidatedRestAggTradesDocument │ ▼ parse_rest_agg_trades() │ ▼ tuple[DzengiRestAggTrade, ...] ``` Теперь внешний REST-документ структурно проверен, типизирован, преобразован в transport-модели, защищён от изменения и готов к Value Validation. --- # Итог Build 060.4 завершает parser-этап REST Trades Acquisition Pipeline. Система получила строго типизированное преобразование: ```text ValidatedRestAggTradesDocument ↓ DzengiRestAggTrade ``` При этом сохранены архитектурные границы: - Schema Validation проверяет структуру; - Parser проверяет transport-типы; - Value Validation будет проверять допустимость значений; - Mapper будет строить canonical `Trade`. Следующим этапом серии должен стать **Build 060.5 — REST Trade Value Validation**.