395 lines
13 KiB
Markdown
395 lines
13 KiB
Markdown
# 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**.
|