Build 060.4-060.5: add REST trade validation pipeline

This commit is contained in:
2026-07-18 19:32:34 +03:00
parent c0f69ee94a
commit a0ae22cb2e
6 changed files with 1039 additions and 3 deletions

View File

@@ -0,0 +1,394 @@
# 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**.