build 059.4: validate websocket OHLC values

This commit is contained in:
2026-07-16 23:40:46 +03:00
parent c8c0bb6951
commit 925b447d45
4 changed files with 1235 additions and 0 deletions

View File

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

View File

@@ -15,15 +15,34 @@ from src.market_data.acquisition.adapters.dzengi.models import (
DzengiUnknownFilter,
DzengiKlinesResponse,
DzengiTicker24hrResponse,
DzengiWebSocketOhlcEvent,
DzengiWebSocketQuoteResponse,
)
from src.market_data.acquisition.exceptions import (
CandleValueError,
CandleWebSocketValueError,
InstrumentReferenceValueError,
QuoteValueError,
)
_SUPPORTED_WEBSOCKET_OHLC_INTERVALS = frozenset(
{
"1m",
"5m",
"15m",
"1h",
}
)
_SUPPORTED_WEBSOCKET_OHLC_TYPES = frozenset(
{
"classic",
"heikin-ashi",
}
)
def validate_exchange_info_values(
response: DzengiExchangeInfoResponse,
) -> None:
@@ -553,6 +572,126 @@ def validate_dzengi_websocket_quote_values(
"$.payload.timestamp должно быть больше нуля."
)
def validate_dzengi_websocket_ohlc_values(
event: DzengiWebSocketOhlcEvent,
) -> None:
"""
Проверить предметную допустимость значений Dzengi WebSocket OHLC.
Функция не изменяет transport-модель, не преобразует timestamp
в datetime и не выполняет mapping во внутреннюю модель Dzentra.
"""
if not event.symbol.strip():
raise CandleWebSocketValueError(
"$.payload.symbol не должен быть пустым."
)
if event.interval not in _SUPPORTED_WEBSOCKET_OHLC_INTERVALS:
supported = ", ".join(
sorted(_SUPPORTED_WEBSOCKET_OHLC_INTERVALS)
)
raise CandleWebSocketValueError(
"$.payload.interval содержит неподдерживаемый интервал "
f"'{event.interval}'. Поддерживаются: {supported}."
)
if event.candle_type not in _SUPPORTED_WEBSOCKET_OHLC_TYPES:
supported = ", ".join(
sorted(_SUPPORTED_WEBSOCKET_OHLC_TYPES)
)
raise CandleWebSocketValueError(
"$.payload.type содержит неподдерживаемый тип свечи "
f"'{event.candle_type}'. Поддерживаются: {supported}."
)
if event.open_time <= 0:
raise CandleWebSocketValueError(
"$.payload.t должно быть целым числом больше нуля."
)
open_price = _websocket_ohlc_positive_decimal(
event.open_price,
path="$.payload.o",
)
high_price = _websocket_ohlc_positive_decimal(
event.high_price,
path="$.payload.h",
)
low_price = _websocket_ohlc_positive_decimal(
event.low_price,
path="$.payload.l",
)
close_price = _websocket_ohlc_positive_decimal(
event.close_price,
path="$.payload.c",
)
if high_price < low_price:
raise CandleWebSocketValueError(
"$.payload.h не должно быть меньше $.payload.l."
)
if high_price < open_price:
raise CandleWebSocketValueError(
"$.payload.h не должно быть меньше $.payload.o."
)
if high_price < close_price:
raise CandleWebSocketValueError(
"$.payload.h не должно быть меньше $.payload.c."
)
if low_price > open_price:
raise CandleWebSocketValueError(
"$.payload.l не должно превышать $.payload.o."
)
if low_price > close_price:
raise CandleWebSocketValueError(
"$.payload.l не должно превышать $.payload.c."
)
def _websocket_ohlc_positive_decimal(
value: DzengiRawNumeric,
*,
path: str,
) -> Decimal:
decimal_value = _websocket_ohlc_decimal(
value,
path=path,
)
if decimal_value <= 0:
raise CandleWebSocketValueError(
f"{path} должно быть больше нуля."
)
return decimal_value
def _websocket_ohlc_decimal(
value: DzengiRawNumeric,
*,
path: str,
) -> Decimal:
try:
decimal_value = Decimal(str(value))
except (InvalidOperation, ValueError) as exc:
raise CandleWebSocketValueError(
f"{path} должно быть корректным числом."
) from exc
if not decimal_value.is_finite():
raise CandleWebSocketValueError(
f"{path} должно быть конечным числом."
)
return decimal_value
def validate_candles_values(
response: DzengiKlinesResponse,
) -> None: