build 059.1-059.2: add websocket OHLC transport and schema validation

This commit is contained in:
2026-07-16 22:14:25 +03:00
parent ebb578db35
commit e0aa04b32a
6 changed files with 1016 additions and 1 deletions

View File

@@ -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

View File

@@ -78,6 +78,11 @@ class CandleSchemaError(MarketDataAcquisitionError):
pass
# Ошибка структуры события Candles Feed из WebSocket.
class CandleWebSocketSchemaError(MarketDataAcquisitionError):
pass
# Ошибка преобразования проверенного документа в raw-модели свечей.
class CandleParseError(MarketDataAcquisitionError):
pass

View File

@@ -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