build 059.1-059.2: add websocket OHLC transport and schema validation
This commit is contained in:
@@ -147,4 +147,23 @@ class DzengiKline:
|
||||
# Транспортное представление ответа Dzengi GET /api/v1/klines.
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DzengiKlinesResponse:
|
||||
items: tuple[DzengiKline, ...]
|
||||
items: tuple[DzengiKline, ...]
|
||||
|
||||
|
||||
# Транспортное представление одного события Dzengi WebSocket ohlc.event.
|
||||
#
|
||||
# Событие содержит завершённую OHLC-свечу без объёма и поэтому не является
|
||||
# канонической моделью Candle. Поле candle_type принимает значения,
|
||||
# подтверждённые runtime-контрактом Dzengi: classic или heikin-ashi.
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DzengiWebSocketOhlcEvent:
|
||||
symbol: str
|
||||
interval: str
|
||||
candle_type: str
|
||||
|
||||
open_time: int
|
||||
|
||||
open_price: DzengiRawNumeric
|
||||
high_price: DzengiRawNumeric
|
||||
low_price: DzengiRawNumeric
|
||||
close_price: DzengiRawNumeric
|
||||
|
||||
@@ -78,6 +78,11 @@ class CandleSchemaError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
|
||||
# Ошибка структуры события Candles Feed из WebSocket.
|
||||
class CandleWebSocketSchemaError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
|
||||
# Ошибка преобразования проверенного документа в raw-модели свечей.
|
||||
class CandleParseError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Mapping
|
||||
|
||||
from src.market_data.acquisition.exceptions import (
|
||||
CandleSchemaError,
|
||||
CandleWebSocketSchemaError,
|
||||
InstrumentReferenceSchemaError,
|
||||
QuoteSchemaError,
|
||||
)
|
||||
@@ -591,4 +592,134 @@ def _require_candle_mapping(
|
||||
f"типа {type(key).__name__}."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# Структурно проверенное представление события Dzengi WebSocket ohlc.event.
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ValidatedWebSocketOhlcDocument:
|
||||
payload: Mapping[str, object]
|
||||
status: object
|
||||
destination: object
|
||||
correlation_id: object | None
|
||||
|
||||
|
||||
def validate_dzengi_websocket_ohlc_schema(
|
||||
document: object,
|
||||
) -> ValidatedWebSocketOhlcDocument:
|
||||
"""
|
||||
Проверить структуру события Dzengi WebSocket OHLC без проверки значений.
|
||||
|
||||
Ожидаемый runtime-контракт:
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"destination": "ohlc.event",
|
||||
"payload": {
|
||||
"symbol": "BTC/USD_LEVERAGE",
|
||||
"interval": "1m",
|
||||
"type": "classic",
|
||||
"t": 1784224740000,
|
||||
"o": 63992.0,
|
||||
"h": 64032.55,
|
||||
"l": 63984.0,
|
||||
"c": 64032.55
|
||||
}
|
||||
}
|
||||
|
||||
Функция проверяет только структуру сообщения:
|
||||
|
||||
- корневой JSON-объект;
|
||||
- наличие status;
|
||||
- наличие destination;
|
||||
- наличие payload;
|
||||
- обязательный набор полей OHLC;
|
||||
- строковые ключи объектов.
|
||||
|
||||
Функция не проверяет предметные значения, не преобразует timestamp,
|
||||
не преобразует цены и не создаёт transport-модель адаптера.
|
||||
"""
|
||||
|
||||
root = _require_websocket_ohlc_mapping(
|
||||
document,
|
||||
path="$",
|
||||
)
|
||||
|
||||
if "status" not in root:
|
||||
raise CandleWebSocketSchemaError(
|
||||
"$.status отсутствует в событии WebSocket OHLC."
|
||||
)
|
||||
|
||||
if "destination" not in root:
|
||||
raise CandleWebSocketSchemaError(
|
||||
"$.destination отсутствует в событии WebSocket OHLC."
|
||||
)
|
||||
|
||||
if "payload" not in root:
|
||||
raise CandleWebSocketSchemaError(
|
||||
"$.payload отсутствует в событии WebSocket OHLC."
|
||||
)
|
||||
|
||||
payload = _require_websocket_ohlc_mapping(
|
||||
root.get("payload"),
|
||||
path="$.payload",
|
||||
)
|
||||
|
||||
_validate_websocket_ohlc_payload(payload)
|
||||
|
||||
return ValidatedWebSocketOhlcDocument(
|
||||
payload=MappingProxyType(dict(payload)),
|
||||
status=root.get("status"),
|
||||
destination=root.get("destination"),
|
||||
correlation_id=root.get("correlationId"),
|
||||
)
|
||||
|
||||
|
||||
def _validate_websocket_ohlc_payload(
|
||||
payload: Mapping[str, object],
|
||||
) -> None:
|
||||
required_fields = (
|
||||
"symbol",
|
||||
"interval",
|
||||
"type",
|
||||
"t",
|
||||
"o",
|
||||
"h",
|
||||
"l",
|
||||
"c",
|
||||
)
|
||||
|
||||
missing_fields = tuple(
|
||||
field_name
|
||||
for field_name in required_fields
|
||||
if field_name not in payload
|
||||
)
|
||||
|
||||
if missing_fields:
|
||||
formatted_fields = ", ".join(missing_fields)
|
||||
|
||||
raise CandleWebSocketSchemaError(
|
||||
"$.payload не содержит обязательные поля WebSocket OHLC: "
|
||||
f"{formatted_fields}."
|
||||
)
|
||||
|
||||
|
||||
def _require_websocket_ohlc_mapping(
|
||||
value: object,
|
||||
*,
|
||||
path: str,
|
||||
) -> Mapping[str, object]:
|
||||
if not isinstance(value, dict):
|
||||
raise CandleWebSocketSchemaError(
|
||||
f"{path} должен быть JSON-объектом, "
|
||||
f"получен {type(value).__name__}."
|
||||
)
|
||||
|
||||
for key in value:
|
||||
if not isinstance(key, str):
|
||||
raise CandleWebSocketSchemaError(
|
||||
f"{path} содержит нестроковый ключ "
|
||||
f"типа {type(key).__name__}."
|
||||
)
|
||||
|
||||
return value
|
||||
Reference in New Issue
Block a user