build 059.8: add unified websocket adapter

This commit is contained in:
2026-07-17 13:30:32 +03:00
parent 20a70df1f0
commit e5a4efa5d1
4 changed files with 690 additions and 0 deletions

View File

@@ -12,6 +12,9 @@ from src.market_data.acquisition.adapters.dzengi.parser import (
parse_dzengi_websocket_ohlc,
parse_dzengi_websocket_quote,
)
from src.market_data.acquisition.exceptions import (
WebSocketMessageRoutingError,
)
from src.market_data.acquisition.models.candle_close import CandleCloseEvent
from src.market_data.acquisition.models.quote import Quote
from src.market_data.acquisition.validation.schema import (
@@ -58,4 +61,44 @@ class DzengiWebSocketOhlcAdapter:
return map_dzengi_websocket_ohlc_to_candle_close_event(
event,
received_at=received_at or datetime.now(timezone.utc),
)
# Определяет тип декодированного сообщения Dzengi WebSocket
# и передаёт его соответствующему специализированному адаптеру.
class DzengiUnifiedWebSocketAdapter:
def __init__(
self,
*,
quote_adapter: DzengiWebSocketQuoteAdapter | None = None,
ohlc_adapter: DzengiWebSocketOhlcAdapter | None = None,
) -> None:
self._quote_adapter = quote_adapter or DzengiWebSocketQuoteAdapter()
self._ohlc_adapter = ohlc_adapter or DzengiWebSocketOhlcAdapter()
def map_message(
self,
document: object,
*,
received_at: datetime | None = None,
) -> Quote | CandleCloseEvent:
if not isinstance(document, dict):
raise WebSocketMessageRoutingError(
"Сообщение Dzengi WebSocket должно быть объектом."
)
if document.get("destination") == "ohlc.event":
return self._ohlc_adapter.map_message(
document,
received_at=received_at,
)
if "Payload" in document:
return self._quote_adapter.map_message(
document,
received_at=received_at,
)
raise WebSocketMessageRoutingError(
"Не удалось определить тип сообщения Dzengi WebSocket."
)

View File

@@ -117,3 +117,9 @@ class CandleMappingError(MarketDataAcquisitionError):
# Ошибка регистрации или получения Candles Feed.
class CandleFeedRegistryError(MarketDataAcquisitionError):
pass
# Ошибка определения типа входящего WebSocket-сообщения
# и выбора специализированного адаптера.
class WebSocketMessageRoutingError(MarketDataAcquisitionError):
pass

View File

@@ -0,0 +1,116 @@
# app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_unified_adapter.py
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from src.market_data.acquisition.adapters.dzengi.websocket import (
DzengiUnifiedWebSocketAdapter,
)
from src.market_data.acquisition.exceptions import (
WebSocketMessageRoutingError,
)
from src.market_data.acquisition.models.candle_close import CandleCloseEvent
from src.market_data.acquisition.models.quote import Quote
def test_adapter_routes_quote_message() -> None:
result = DzengiUnifiedWebSocketAdapter().map_message(
{
"Payload": {
"symbolName": "BTC/USD",
"bids": [["10", "1"]],
"asks": [["12", "1"]],
"timestamp": 1000,
}
}
)
assert isinstance(result, Quote)
assert result.symbol == "BTC/USD"
assert str(result.last_price) == "11"
def test_adapter_routes_ohlc_message() -> None:
result = DzengiUnifiedWebSocketAdapter().map_message(
{
"status": "OK",
"destination": "ohlc.event",
"payload": {
"symbol": "BTC/USD",
"interval": "1m",
"type": "classic",
"t": 1752364800000,
"o": "100",
"h": "105",
"l": "99",
"c": "103",
},
}
)
assert isinstance(result, CandleCloseEvent)
assert result.symbol == "BTC/USD"
assert result.interval == "1m"
assert str(result.close_price) == "103"
def test_adapter_passes_received_at_to_quote_adapter() -> None:
received_at = datetime(2026, 7, 17, tzinfo=timezone.utc)
result = DzengiUnifiedWebSocketAdapter().map_message(
{
"Payload": {
"symbolName": "BTC/USD",
"bids": [["10", "1"]],
"asks": [["12", "1"]],
"timestamp": 1000,
}
},
received_at=received_at,
)
assert isinstance(result, Quote)
assert result.received_at is received_at
def test_adapter_passes_received_at_to_ohlc_adapter() -> None:
received_at = datetime(2026, 7, 17, tzinfo=timezone.utc)
result = DzengiUnifiedWebSocketAdapter().map_message(
{
"status": "OK",
"destination": "ohlc.event",
"payload": {
"symbol": "BTC/USD",
"interval": "1m",
"type": "classic",
"t": 1752364800000,
"o": "100",
"h": "105",
"l": "99",
"c": "103",
},
},
received_at=received_at,
)
assert isinstance(result, CandleCloseEvent)
assert result.received_at is received_at
@pytest.mark.parametrize(
"document",
[
None,
[],
{},
{"status": "OK"},
{"destination": "unknown.event", "payload": {}},
],
)
def test_adapter_rejects_unknown_message(document: object) -> None:
with pytest.raises(WebSocketMessageRoutingError):
DzengiUnifiedWebSocketAdapter().map_message(document)