Build 060.8: add Dzengi REST Trades Document Source

This commit is contained in:
2026-07-19 11:54:58 +03:00
parent 7eee8ce36c
commit 2523d715e7
4 changed files with 2199 additions and 0 deletions

View File

@@ -9,12 +9,14 @@ from src.market_data.acquisition.exceptions import (
CandleTransportError,
InstrumentReferenceTransportError,
QuoteTransportError,
TradeTransportError,
)
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
_TICKER_24HR_PATH = "/api/v1/ticker/24hr"
_KLINES_PATH = "/api/v1/klines"
_AGG_TRADES_PATH = "/api/v1/aggTrades"
# Минимальный транспортный контракт, необходимый Dzengi REST adapter.
@@ -155,3 +157,59 @@ class DzengiCandlesDocumentSource:
f"от Dzengi для символа '{symbol}', "
f"интервала '{interval}' и типа цены '{price_type}': {exc}"
) from exc
class DzengiTradesDocumentSource:
"""Источник сырого документа агрегированных сделок через Dzengi REST API."""
def __init__(
self,
client: _PayloadRestClient | None = None,
) -> None:
self._client = client
def fetch_trades_document(
self,
symbol: str,
*,
start_time: int | None = None,
end_time: int | None = None,
limit: int | None = None,
) -> object:
"""
Получить декодированный ответ Dzengi aggTrades без его обработки.
Метод не выполняет schema validation, parsing, value validation,
mapping, сортировку, дедупликацию или кэширование.
"""
params: dict[str, str] = {
"symbol": symbol,
}
if start_time is not None:
params["startTime"] = str(start_time)
if end_time is not None:
params["endTime"] = str(end_time)
if limit is not None:
params["limit"] = str(limit)
try:
client: _PayloadRestClient = (
self._client
if self._client is not None
else ExchangeRestClient()
)
return client.get_payload(
_AGG_TRADES_PATH,
params=params,
)
except Exception as exc:
raise TradeTransportError(
"Не удалось получить агрегированные сделки "
f"от Dzengi для символа '{symbol}': {exc}"
) from exc

View File

@@ -119,6 +119,11 @@ class CandleFeedRegistryError(MarketDataAcquisitionError):
pass
# Ошибка получения Trades Feed от внешнего источника.
class TradeTransportError(MarketDataAcquisitionError):
pass
# Ошибка структуры документа Trades Feed.
class TradeSchemaError(MarketDataAcquisitionError):
pass

View File

@@ -0,0 +1,234 @@
# tests/unit/market_data/acquisition/adapters/dzengi/test_trade_rest.py
from __future__ import annotations
import pytest
from src.market_data.acquisition.adapters.dzengi.rest import (
DzengiTradesDocumentSource,
)
from src.market_data.acquisition.exceptions import TradeTransportError
class _StubRestClient:
def __init__(self) -> None:
self.path: str | None = None
self.params: dict[str, str] | None = None
self.headers: dict[str, str] | None = None
self.payload: object = object()
def get_payload(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> object:
self.path = path
self.params = params
self.headers = headers
return self.payload
class _FailingRestClient:
def get_payload(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> object:
raise RuntimeError("network failure")
def test_source_exposes_fetch_trades_document() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
assert hasattr(source, "fetch_trades_document")
def test_fetch_trades_document_uses_injected_client() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
)
assert client.path == "/api/v1/aggTrades"
def test_fetch_trades_document_returns_raw_object() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
document = source.fetch_trades_document(
"BTCUSDT",
)
assert document is client.payload
def test_fetch_trades_document_uses_expected_endpoint() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
)
assert client.path == "/api/v1/aggTrades"
def test_fetch_trades_document_passes_symbol() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
)
assert client.params == {
"symbol": "BTCUSDT",
}
def test_fetch_trades_document_passes_all_optional_parameters() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
start_time=1000,
end_time=2000,
limit=500,
)
assert client.params == {
"symbol": "BTCUSDT",
"startTime": "1000",
"endTime": "2000",
"limit": "500",
}
def test_fetch_trades_document_omits_optional_parameters_when_none() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
start_time=None,
end_time=None,
limit=None,
)
assert client.params == {
"symbol": "BTCUSDT",
}
def test_fetch_trades_document_converts_start_time_to_string() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
start_time=123456789,
)
assert client.params == {
"symbol": "BTCUSDT",
"startTime": "123456789",
}
def test_fetch_trades_document_converts_end_time_to_string() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
end_time=987654321,
)
assert client.params == {
"symbol": "BTCUSDT",
"endTime": "987654321",
}
def test_fetch_trades_document_converts_limit_to_string() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
limit=100,
)
assert client.params == {
"symbol": "BTCUSDT",
"limit": "100",
}
def test_fetch_trades_document_does_not_pass_headers() -> None:
client = _StubRestClient()
source = DzengiTradesDocumentSource(client)
source.fetch_trades_document(
"BTCUSDT",
)
assert client.headers is None
def test_fetch_trades_document_wraps_transport_exception() -> None:
source = DzengiTradesDocumentSource(
_FailingRestClient(),
)
with pytest.raises(
TradeTransportError,
match="Не удалось получить агрегированные сделки",
) as exc_info:
source.fetch_trades_document(
"BTCUSDT",
)
assert isinstance(exc_info.value.__cause__, RuntimeError)
def test_fetch_trades_document_lazily_creates_exchange_rest_client(
monkeypatch: pytest.MonkeyPatch,
) -> None:
created_clients: list[_StubRestClient] = []
def create_client() -> _StubRestClient:
client = _StubRestClient()
created_clients.append(client)
return client
monkeypatch.setattr(
"src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient",
create_client,
)
source = DzengiTradesDocumentSource()
assert created_clients == []
document = source.fetch_trades_document(
"BTCUSDT",
limit=10,
)
assert len(created_clients) == 1
assert document is created_clients[0].payload
assert created_clients[0].path == "/api/v1/aggTrades"
assert created_clients[0].params == {
"symbol": "BTCUSDT",
"limit": "10",
}

File diff suppressed because it is too large Load Diff