Build 060.7: implement REST Trade Adapter

This commit is contained in:
2026-07-19 10:56:06 +03:00
parent 6b0d5badce
commit 7eee8ce36c
5 changed files with 2318 additions and 27 deletions

View File

@@ -0,0 +1,41 @@
# src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py
from __future__ import annotations
from src.market_data.acquisition.adapters.dzengi.mapper import (
map_dzengi_rest_agg_trades_to_trades,
)
from src.market_data.acquisition.adapters.dzengi.parser import (
parse_rest_agg_trades,
)
from src.market_data.acquisition.models.trade import Trade
from src.market_data.acquisition.validation.schema import (
ValidatedRestAggTradesDocument,
)
from src.market_data.acquisition.validation.values import (
validate_rest_agg_trade_values,
)
def adapt_rest_agg_trades_document(
document: ValidatedRestAggTradesDocument,
*,
symbol: str,
) -> tuple[Trade, ...]:
"""
Преобразовать структурно проверенный документ Dzengi aggTrades
в канонический immutable-набор Trade.
Функция последовательно выполняет parsing, value validation
и mapping. Schema validation должна быть выполнена вызывающим слоем.
Исключения отдельных этапов не перехватываются и не оборачиваются.
"""
trades = parse_rest_agg_trades(document)
validate_rest_agg_trade_values(trades)
return map_dzengi_rest_agg_trades_to_trades(
trades,
symbol=symbol,
)

View File

@@ -0,0 +1,188 @@
# tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
from types import MappingProxyType
import pytest
from src.market_data.acquisition.adapters.dzengi import rest_trade_adapter
from src.market_data.acquisition.adapters.dzengi.rest_trade_adapter import (
adapt_rest_agg_trades_document,
)
from src.market_data.acquisition.exceptions import (
TradeMappingError,
TradeParseError,
TradeValueError,
)
from src.market_data.acquisition.models.trade import TradeAggressorSide
from src.market_data.acquisition.validation.schema import (
ValidatedRestAggTradesDocument,
)
def test_adapt_rest_agg_trades_document_returns_canonical_trades() -> None:
document = ValidatedRestAggTradesDocument(
items=(
MappingProxyType(
{
"a": 101,
"p": "43210.50",
"q": "0.125",
"T": 1_700_000_000_000,
"m": False,
}
),
MappingProxyType(
{
"a": 102,
"p": "43211.75",
"q": "0.250",
"T": 1_700_000_001_000,
"m": True,
}
),
)
)
result = adapt_rest_agg_trades_document(
document,
symbol=" BTCUSDT ",
)
assert isinstance(result, tuple)
assert len(result) == 2
first_trade = result[0]
assert first_trade.symbol == "BTCUSDT"
assert first_trade.trade_id == 101
assert first_trade.price == Decimal("43210.50")
assert first_trade.quantity == Decimal("0.125")
assert first_trade.executed_at == datetime(
2023,
11,
14,
22,
13,
20,
tzinfo=timezone.utc,
)
assert first_trade.aggressor_side is TradeAggressorSide.BUY
assert first_trade.source == "dzengi"
second_trade = result[1]
assert second_trade.symbol == "BTCUSDT"
assert second_trade.trade_id == 102
assert second_trade.price == Decimal("43211.75")
assert second_trade.quantity == Decimal("0.250")
assert second_trade.executed_at == datetime(
2023,
11,
14,
22,
13,
21,
tzinfo=timezone.utc,
)
assert second_trade.aggressor_side is TradeAggressorSide.SELL
assert second_trade.source == "dzengi"
def test_adapt_rest_agg_trades_document_propagates_parse_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
document = ValidatedRestAggTradesDocument(items=())
expected_error = TradeParseError("test parse error")
def raise_parse_error(
_: ValidatedRestAggTradesDocument,
) -> tuple[()]:
raise expected_error
monkeypatch.setattr(
rest_trade_adapter,
"parse_rest_agg_trades",
raise_parse_error,
)
with pytest.raises(TradeParseError) as exc_info:
adapt_rest_agg_trades_document(
document,
symbol="BTCUSDT",
)
assert exc_info.value is expected_error
def test_adapt_rest_agg_trades_document_propagates_value_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
document = ValidatedRestAggTradesDocument(items=())
expected_error = TradeValueError("test value error")
monkeypatch.setattr(
rest_trade_adapter,
"parse_rest_agg_trades",
lambda _: (),
)
def raise_value_error(_: tuple[object, ...]) -> None:
raise expected_error
monkeypatch.setattr(
rest_trade_adapter,
"validate_rest_agg_trade_values",
raise_value_error,
)
with pytest.raises(TradeValueError) as exc_info:
adapt_rest_agg_trades_document(
document,
symbol="BTCUSDT",
)
assert exc_info.value is expected_error
def test_adapt_rest_agg_trades_document_propagates_mapping_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
document = ValidatedRestAggTradesDocument(items=())
expected_error = TradeMappingError("test mapping error")
monkeypatch.setattr(
rest_trade_adapter,
"parse_rest_agg_trades",
lambda _: (),
)
monkeypatch.setattr(
rest_trade_adapter,
"validate_rest_agg_trade_values",
lambda _: None,
)
def raise_mapping_error(
_: tuple[object, ...],
*,
symbol: str,
) -> tuple[()]:
del symbol
raise expected_error
monkeypatch.setattr(
rest_trade_adapter,
"map_dzengi_rest_agg_trades_to_trades",
raise_mapping_error,
)
with pytest.raises(TradeMappingError) as exc_info:
adapt_rest_agg_trades_document(
document,
symbol="BTCUSDT",
)
assert exc_info.value is expected_error

View File

@@ -0,0 +1,328 @@
# Build 060.5 — REST Trade Value Validation
## Цель
Реализовать слой проверки допустимости значений (`Value Validation`) для REST aggTrades после завершения этапов структурной проверки (`Schema Validation`) и преобразования в transport-модели (`REST Parser`).
Build завершает третий этап конвейера обработки REST Trades и обеспечивает, что все transport-модели содержат только корректные значения перед передачей в Mapper.
---
# Место Build в общей архитектуре
До Build 060.5 конвейер выглядел следующим образом:
```text
REST JSON
Schema Validation
ValidatedRestAggTradesDocument
REST Parser
tuple[DzengiRestAggTrade]
```
После Build 060.5:
```text
REST JSON
Schema Validation
ValidatedRestAggTradesDocument
REST Parser
tuple[DzengiRestAggTrade]
REST Trade Value Validation
tuple[DzengiRestAggTrade]
```
Следующим этапом станет:
```text
Mapper
Canonical Trade
```
---
# Архитектурная ответственность
Value Validation отвечает исключительно за проверку допустимости значений transport-моделей.
Validator:
- не изменяет transport-модель;
- не выполняет mapping;
- не преобразует данные в canonical-модель;
- не нормализует значения;
- не возвращает преобразованные объекты.
При успешной проверке функция завершается без результата.
При обнаружении ошибки выбрасывается специализированное исключение.
---
# Добавлено новое исключение
Файл:
```text
src/market_data/acquisition/exceptions.py
```
Добавлено:
```python
TradeValueError
```
Назначение:
- ошибки проверки допустимости значений REST Trade transport-моделей.
---
# Новый публичный API
Файл:
```text
src/market_data/acquisition/validation/values.py
```
Добавлена функция:
```python
validate_rest_agg_trade_values(
trades: tuple[DzengiRestAggTrade, ...],
) -> None
```
Ответственность функции:
- проверить все transport-модели;
- при первой ошибке выбросить `TradeValueError`;
- при отсутствии ошибок успешно завершиться.
---
# Внутренняя архитектура
Реализация построена по тому же принципу, что уже используется для Instrument, Quote и Candle.
```text
validate_rest_agg_trade_values()
_validate_rest_agg_trade()
├────────────► _trade_positive_int()
├────────────► _trade_positive_decimal()
└────────────► _trade_decimal()
```
Каждая helper-функция отвечает только за одну проверку.
---
# Проверяемые поля
## aggregate_trade_id
Проверяется:
- тип int (bool исключается);
- значение больше нуля.
---
## price
Проверяется:
- успешное преобразование в Decimal;
- конечность числа;
- значение больше нуля.
Поддерживаются значения:
- str
- int
- float
---
## quantity
Проверяется аналогично полю price.
---
## timestamp
Проверяется:
- тип int (bool исключается);
- значение больше нуля.
---
## buyer_is_maker
Дополнительная проверка отсутствует.
Корректность типа bool уже гарантируется предыдущим этапом — REST Parser.
---
# Что НЕ входит в Value Validation
Build сознательно не выполняет:
- преобразование transport-моделей;
- mapping;
- создание Canonical Trade;
- преобразование чисел в Decimal для дальнейшей обработки;
- проверку возраста сделки;
- проверку порядка timestamp;
- проверку последовательности aggregateTradeId;
- проверку дубликатов.
Все перечисленные задачи относятся к последующим слоям Acquisition Pipeline.
---
# Диагностика ошибок
Все сообщения содержат полный путь до ошибочного поля.
Примеры:
```text
$[0].price должно быть больше нуля.
$[1].quantity должно быть корректным числом.
$[2].timestamp должно быть целым числом больше нуля.
```
Такой формат полностью соответствует существующей архитектуре Validation Layer.
---
# Unit Tests
Добавлены тесты для:
## Позитивных сценариев
- одна корректная сделка;
- несколько корректных сделок;
- пустой tuple;
- строковые числовые значения;
- int;
- float;
- оба значения buyer_is_maker.
---
## Негативных сценариев
Проверяются:
- aggregate_trade_id ≤ 0;
- timestamp ≤ 0;
- price ≤ 0;
- quantity ≤ 0;
- NaN;
- Infinity;
- -Infinity;
- некорректные числовые строки;
- корректное формирование пути ошибки.
---
# Результаты проверки
Target tests:
```text
68 passed
```
Полная регрессия проекта:
```text
1072 passed
```
Дополнительно выполнено:
```text
python -m compileall src
```
Результат:
```text
OK
```
Также выполнено:
```text
git diff --check
```
Ошибок форматирования не обнаружено.
---
# Архитектурный результат Build
После завершения Build 060.5 REST Trades получили полностью независимый трехуровневый pipeline проверки данных.
```text
REST JSON
Schema Validation
ValidatedRestAggTradesDocument
REST Parser
tuple[DzengiRestAggTrade]
REST Trade Value Validation
tuple[DzengiRestAggTrade]
```
Каждый этап отвечает исключительно за собственную область ответственности.
Следующий Build (060.6) впервые переведет transport-модели в внутреннюю каноническую модель `Trade`, независимую от конкретной биржи.

View File

@@ -2185,42 +2185,42 @@ Production Integration — намеренно не выполнялась
# Следующий этап
Следующим этапом развития серии Build 060 становится интеграция построенного Pipeline в рабочую подсистему получения данных.
Наиболее логичным продолжением является:
Следующим этапом развития серии Build 060 становится:
```text
Build 060.7 — REST Trades Client
Build 060.7 — REST Trade Adapter
```
На этом этапе будет реализован специализированный клиент получения агрегированных сделок через REST API Dzengi, использующий полностью сформированный Pipeline:
На этом этапе будет реализован адаптер REST-сделок, который объединит уже созданные компоненты обработки:
```text
REST Request
REST Response
Schema Validation
Validated REST document
Parser
Value Validation
Mapper
tuple[Trade]
```
Build 060.7 станет первым этапом, на котором сформированный Pipeline начнёт использоваться в реальном процессе получения исторических сделок.
Build 060.7 не должен самостоятельно выполнять HTTP-запросы и получать документ из внешнего источника.
Получение исходного REST-документа будет выделено в следующий отдельный этап:
```text
Build 060.8 — REST Trades Document Source
```
Такое разделение сохраняет независимость:
* источника документа;
* адаптера обработки документа;
* transport-моделей;
* validation;
* parser;
* mapper;
* Canonical Trade.
После завершения Build 060.7 система получит единый REST Trade Adapter, способный преобразовывать уже полученный REST-документ в канонические сделки, но ещё не связанный с конкретным механизмом HTTP-доступа.

File diff suppressed because it is too large Load Diff