Build 060.12: add WebSocket Trade Value Validation

This commit is contained in:
2026-07-19 20:12:34 +03:00
parent d3f8680921
commit 6d121abe96
3 changed files with 1416 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ from src.market_data.acquisition.adapters.dzengi.models import (
DzengiTicker24hrResponse,
DzengiWebSocketOhlcEvent,
DzengiWebSocketQuoteResponse,
DzengiWebSocketTradeEvent,
)
from src.market_data.acquisition.exceptions import (
CandleValueError,
@@ -820,6 +821,44 @@ def _candle_decimal(
return decimal_value
def validate_dzengi_websocket_trade_values(
event: DzengiWebSocketTradeEvent,
) -> None:
"""
Проверить допустимость значений transport-модели Dzengi WebSocket Trade.
Функция не изменяет transport-модель, не преобразует raw numeric
значения в Decimal и не выполняет mapping во внутреннюю модель Trade.
"""
_trade_positive_int(
event.trade_id,
path="$.payload.id",
)
_trade_positive_decimal(
event.price,
path="$.payload.price",
)
_trade_positive_decimal(
event.size,
path="$.payload.size",
)
_trade_positive_int(
event.timestamp,
path="$.payload.ts",
)
if not event.symbol.strip():
raise TradeValueError(
"$.payload.symbol не должен быть пустым."
)
if not event.order_id.strip():
raise TradeValueError(
"$.payload.orderId не должен быть пустым."
)
def validate_rest_agg_trade_values(
trades: tuple[DzengiRestAggTrade, ...],
) -> None:

View File

@@ -0,0 +1,275 @@
# app/tests/unit/market_data/acquisition/validation/test_websocket_trade_values.py
from __future__ import annotations
import pytest
from src.market_data.acquisition.adapters.dzengi.models import (
DzengiWebSocketTradeEvent,
)
from src.market_data.acquisition.exceptions import TradeValueError
from src.market_data.acquisition.validation.values import (
validate_dzengi_websocket_trade_values,
)
def _event(
**overrides: object,
) -> DzengiWebSocketTradeEvent:
values: dict[str, object] = {
"trade_id": 123456,
"price": "63992.50",
"size": "0.125",
"timestamp": 1784224740000,
"symbol": "BTC/USD_LEVERAGE",
"buyer": True,
"order_id": "order-123456",
}
values.update(overrides)
return DzengiWebSocketTradeEvent(
trade_id=values["trade_id"], # type: ignore[arg-type]
price=values["price"], # type: ignore[arg-type]
size=values["size"], # type: ignore[arg-type]
timestamp=values["timestamp"], # type: ignore[arg-type]
symbol=values["symbol"], # type: ignore[arg-type]
buyer=values["buyer"], # type: ignore[arg-type]
order_id=values["order_id"], # type: ignore[arg-type]
)
@pytest.mark.parametrize(
"buyer",
[
True,
False,
],
)
def test_validate_websocket_trade_values_accepts_buyer_values(
buyer: bool,
) -> None:
validate_dzengi_websocket_trade_values(
_event(buyer=buyer)
)
@pytest.mark.parametrize(
("field_name", "value"),
[
("price", "63992.50"),
("price", 63992),
("price", 63992.5),
("size", "0.125"),
("size", 1),
("size", 0.125),
],
)
def test_validate_websocket_trade_values_accepts_numeric_representations(
field_name: str,
value: object,
) -> None:
validate_dzengi_websocket_trade_values(
_event(**{field_name: value})
)
def test_validate_websocket_trade_values_does_not_modify_event() -> None:
event = _event()
original = event
validate_dzengi_websocket_trade_values(event)
assert event == original
@pytest.mark.parametrize(
"trade_id",
[
0,
-1,
-123456,
],
)
def test_validate_websocket_trade_values_rejects_non_positive_trade_id(
trade_id: int,
) -> None:
with pytest.raises(
TradeValueError,
match=r"\$\.payload\.id должно быть целым числом больше нуля",
):
validate_dzengi_websocket_trade_values(
_event(trade_id=trade_id)
)
@pytest.mark.parametrize(
"timestamp",
[
0,
-1,
-1784224740000,
],
)
def test_validate_websocket_trade_values_rejects_non_positive_timestamp(
timestamp: int,
) -> None:
with pytest.raises(
TradeValueError,
match=r"\$\.payload\.ts должно быть целым числом больше нуля",
):
validate_dzengi_websocket_trade_values(
_event(timestamp=timestamp)
)
@pytest.mark.parametrize(
"field_name",
[
"price",
"size",
],
)
@pytest.mark.parametrize(
"value",
[
0,
-1,
"-0.01",
],
)
def test_validate_websocket_trade_values_rejects_non_positive_numeric_values(
field_name: str,
value: object,
) -> None:
path_by_field = {
"price": "price",
"size": "size",
}
with pytest.raises(
TradeValueError,
match=(
rf"\$\.payload\.{path_by_field[field_name]} "
r"должно быть больше нуля"
),
):
validate_dzengi_websocket_trade_values(
_event(**{field_name: value})
)
@pytest.mark.parametrize(
"field_name",
[
"price",
"size",
],
)
@pytest.mark.parametrize(
"value",
[
"NaN",
"Infinity",
"-Infinity",
float("nan"),
float("inf"),
float("-inf"),
],
)
def test_validate_websocket_trade_values_rejects_non_finite_numeric_values(
field_name: str,
value: object,
) -> None:
path_by_field = {
"price": "price",
"size": "size",
}
with pytest.raises(
TradeValueError,
match=(
rf"\$\.payload\.{path_by_field[field_name]} "
r"должно быть конечным числом"
),
):
validate_dzengi_websocket_trade_values(
_event(**{field_name: value})
)
@pytest.mark.parametrize(
"field_name",
[
"price",
"size",
],
)
@pytest.mark.parametrize(
"value",
[
"",
"invalid",
"--1",
],
)
def test_validate_websocket_trade_values_rejects_invalid_numeric_strings(
field_name: str,
value: str,
) -> None:
path_by_field = {
"price": "price",
"size": "size",
}
with pytest.raises(
TradeValueError,
match=(
rf"\$\.payload\.{path_by_field[field_name]} "
r"должно быть корректным числом"
),
):
validate_dzengi_websocket_trade_values(
_event(**{field_name: value})
)
@pytest.mark.parametrize(
"symbol",
[
"",
" ",
"\t",
"\n",
],
)
def test_validate_websocket_trade_values_rejects_empty_symbol(
symbol: str,
) -> None:
with pytest.raises(
TradeValueError,
match=r"\$\.payload\.symbol не должен быть пустым",
):
validate_dzengi_websocket_trade_values(
_event(symbol=symbol)
)
@pytest.mark.parametrize(
"order_id",
[
"",
" ",
"\t",
"\n",
],
)
def test_validate_websocket_trade_values_rejects_empty_order_id(
order_id: str,
) -> None:
with pytest.raises(
TradeValueError,
match=r"\$\.payload\.orderId не должен быть пустым",
):
validate_dzengi_websocket_trade_values(
_event(order_id=order_id)
)