Build 060.4-060.5: add REST trade validation pipeline

This commit is contained in:
2026-07-18 19:32:34 +03:00
parent c0f69ee94a
commit a0ae22cb2e
6 changed files with 1039 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ from src.market_data.acquisition.adapters.dzengi.models import (
DzengiMinNotionalFilter,
DzengiRateLimit,
DzengiRawNumeric,
DzengiRestAggTrade,
DzengiUnknownFilter,
DzengiTicker24hrResponse,
DzengiWebSocketOhlcEvent,
@@ -27,11 +28,13 @@ from src.market_data.acquisition.exceptions import (
CandleWebSocketParseError,
InstrumentReferenceParseError,
QuoteParseError,
TradeParseError,
)
from src.market_data.acquisition.validation.schema import (
ValidatedCandlesDocument,
ValidatedExchangeInfoDocument,
ValidatedQuoteDocument,
ValidatedRestAggTradesDocument,
ValidatedWebSocketOhlcDocument,
ValidatedWebSocketQuoteDocument,
)
@@ -966,3 +969,97 @@ def _candle_required_raw_numeric(
f"получен {type(value).__name__}."
)
return value
# Преобразовать структурно проверенный ответ aggTrades
# в транспортные модели адаптера Dzengi.
def parse_rest_agg_trades(
document: ValidatedRestAggTradesDocument,
) -> tuple[DzengiRestAggTrade, ...]:
"""
Преобразовать элементы проверенного документа aggTrades
в transport-модели Dzengi.
Функция не выполняет schema validation, предметную проверку значений,
преобразование числовых значений в Decimal или mapping
во внутреннюю модель Trade.
"""
return tuple(
_parse_rest_agg_trade_item(
item,
path=f"$[{index}]",
)
for index, item in enumerate(document.items)
)
def _parse_rest_agg_trade_item(
item: Mapping[str, object],
*,
path: str,
) -> DzengiRestAggTrade:
return DzengiRestAggTrade(
aggregate_trade_id=_trade_required_int(
item.get("a"),
path=f"{path}.a",
),
price=_trade_required_raw_numeric(
item.get("p"),
path=f"{path}.p",
),
quantity=_trade_required_raw_numeric(
item.get("q"),
path=f"{path}.q",
),
timestamp=_trade_required_int(
item.get("T"),
path=f"{path}.T",
),
buyer_is_maker=_trade_required_bool(
item.get("m"),
path=f"{path}.m",
),
)
def _trade_required_int(
value: object,
*,
path: str,
) -> int:
if isinstance(value, bool) or not isinstance(value, int):
raise TradeParseError(
f"{path} должен быть целым числом, "
f"получен {type(value).__name__}."
)
return value
def _trade_required_raw_numeric(
value: object,
*,
path: str,
) -> DzengiRawNumeric:
if isinstance(value, bool) or not isinstance(value, (str, int, float)):
raise TradeParseError(
f"{path} должен быть строкой или числом, "
f"получен {type(value).__name__}."
)
return value
def _trade_required_bool(
value: object,
*,
path: str,
) -> bool:
if not isinstance(value, bool):
raise TradeParseError(
f"{path} должен быть логическим значением, "
f"получен {type(value).__name__}."
)
return value

View File

@@ -124,6 +124,16 @@ class TradeSchemaError(MarketDataAcquisitionError):
pass
# Ошибка преобразования проверенного документа в raw-модели сделок.
class TradeParseError(MarketDataAcquisitionError):
pass
# Ошибка проверки допустимости значений raw-моделей сделок.
class TradeValueError(MarketDataAcquisitionError):
pass
# Ошибка определения типа входящего WebSocket-сообщения
# и выбора специализированного адаптера.
class WebSocketMessageRoutingError(MarketDataAcquisitionError):

View File

@@ -12,6 +12,7 @@ from src.market_data.acquisition.adapters.dzengi.models import (
DzengiMinNotionalFilter,
DzengiRateLimit,
DzengiRawNumeric,
DzengiRestAggTrade,
DzengiUnknownFilter,
DzengiKlinesResponse,
DzengiTicker24hrResponse,
@@ -23,6 +24,7 @@ from src.market_data.acquisition.exceptions import (
CandleWebSocketValueError,
InstrumentReferenceValueError,
QuoteValueError,
TradeValueError,
)
@@ -815,4 +817,91 @@ def _candle_decimal(
f"{path} должно быть конечным числом."
)
return decimal_value
return decimal_value
def validate_rest_agg_trade_values(
trades: tuple[DzengiRestAggTrade, ...],
) -> None:
"""
Проверить допустимость значений transport-моделей Dzengi aggTrades.
Функция не изменяет transport-модели, не преобразует raw numeric
значения в Decimal и не выполняет mapping во внутреннюю модель Trade.
"""
for index, trade in enumerate(trades):
_validate_rest_agg_trade(
trade,
path=f"$[{index}]",
)
def _validate_rest_agg_trade(
trade: DzengiRestAggTrade,
*,
path: str,
) -> None:
_trade_positive_int(
trade.aggregate_trade_id,
path=f"{path}.aggregateTradeId",
)
_trade_positive_decimal(
trade.price,
path=f"{path}.price",
)
_trade_positive_decimal(
trade.quantity,
path=f"{path}.quantity",
)
_trade_positive_int(
trade.timestamp,
path=f"{path}.timestamp",
)
def _trade_positive_int(
value: int,
*,
path: str,
) -> None:
if isinstance(value, bool) or value <= 0:
raise TradeValueError(
f"{path} должно быть целым числом больше нуля."
)
def _trade_positive_decimal(
value: DzengiRawNumeric,
*,
path: str,
) -> None:
decimal_value = _trade_decimal(
value,
path=path,
)
if decimal_value <= 0:
raise TradeValueError(
f"{path} должно быть больше нуля."
)
def _trade_decimal(
value: DzengiRawNumeric,
*,
path: str,
) -> Decimal:
try:
decimal_value = Decimal(str(value))
except (InvalidOperation, ValueError) as exc:
raise TradeValueError(
f"{path} должно быть корректным числом."
) from exc
if not decimal_value.is_finite():
raise TradeValueError(
f"{path} должно быть конечным числом."
)
return decimal_value