Build 060.7: implement REST Trade Adapter
This commit is contained in:
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
328
docs/migrations/build_060_5.md
Normal file
328
docs/migrations/build_060_5.md
Normal 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`, независимую от конкретной биржи.
|
||||
@@ -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
|
||||
|
||||
Validated REST document
|
||||
↓
|
||||
|
||||
REST Response
|
||||
|
||||
↓
|
||||
|
||||
Schema Validation
|
||||
|
||||
↓
|
||||
|
||||
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-доступа.
|
||||
1734
docs/migrations/build_060_7.md
Normal file
1734
docs/migrations/build_060_7.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user