Build 060.8: add Dzengi REST Trades Document Source
This commit is contained in:
@@ -9,12 +9,14 @@ from src.market_data.acquisition.exceptions import (
|
|||||||
CandleTransportError,
|
CandleTransportError,
|
||||||
InstrumentReferenceTransportError,
|
InstrumentReferenceTransportError,
|
||||||
QuoteTransportError,
|
QuoteTransportError,
|
||||||
|
TradeTransportError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
|
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
|
||||||
_TICKER_24HR_PATH = "/api/v1/ticker/24hr"
|
_TICKER_24HR_PATH = "/api/v1/ticker/24hr"
|
||||||
_KLINES_PATH = "/api/v1/klines"
|
_KLINES_PATH = "/api/v1/klines"
|
||||||
|
_AGG_TRADES_PATH = "/api/v1/aggTrades"
|
||||||
|
|
||||||
|
|
||||||
# Минимальный транспортный контракт, необходимый Dzengi REST adapter.
|
# Минимальный транспортный контракт, необходимый Dzengi REST adapter.
|
||||||
@@ -155,3 +157,59 @@ class DzengiCandlesDocumentSource:
|
|||||||
f"от Dzengi для символа '{symbol}', "
|
f"от Dzengi для символа '{symbol}', "
|
||||||
f"интервала '{interval}' и типа цены '{price_type}': {exc}"
|
f"интервала '{interval}' и типа цены '{price_type}': {exc}"
|
||||||
) from 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
|
||||||
@@ -119,6 +119,11 @@ class CandleFeedRegistryError(MarketDataAcquisitionError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Ошибка получения Trades Feed от внешнего источника.
|
||||||
|
class TradeTransportError(MarketDataAcquisitionError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Ошибка структуры документа Trades Feed.
|
# Ошибка структуры документа Trades Feed.
|
||||||
class TradeSchemaError(MarketDataAcquisitionError):
|
class TradeSchemaError(MarketDataAcquisitionError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
1902
docs/migrations/build_060_8.md
Normal file
1902
docs/migrations/build_060_8.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user