Build 060.4-060.5: add REST trade validation pipeline
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -124,6 +124,16 @@ class TradeSchemaError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
|
||||
# Ошибка преобразования проверенного документа в raw-модели сделок.
|
||||
class TradeParseError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
|
||||
# Ошибка проверки допустимости значений raw-моделей сделок.
|
||||
class TradeValueError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
|
||||
# Ошибка определения типа входящего WebSocket-сообщения
|
||||
# и выбора специализированного адаптера.
|
||||
class WebSocketMessageRoutingError(MarketDataAcquisitionError):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user