Build 060.12: add WebSocket Trade Value Validation
This commit is contained in:
@@ -18,6 +18,7 @@ from src.market_data.acquisition.adapters.dzengi.models import (
|
|||||||
DzengiTicker24hrResponse,
|
DzengiTicker24hrResponse,
|
||||||
DzengiWebSocketOhlcEvent,
|
DzengiWebSocketOhlcEvent,
|
||||||
DzengiWebSocketQuoteResponse,
|
DzengiWebSocketQuoteResponse,
|
||||||
|
DzengiWebSocketTradeEvent,
|
||||||
)
|
)
|
||||||
from src.market_data.acquisition.exceptions import (
|
from src.market_data.acquisition.exceptions import (
|
||||||
CandleValueError,
|
CandleValueError,
|
||||||
@@ -820,6 +821,44 @@ def _candle_decimal(
|
|||||||
return decimal_value
|
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(
|
def validate_rest_agg_trade_values(
|
||||||
trades: tuple[DzengiRestAggTrade, ...],
|
trades: tuple[DzengiRestAggTrade, ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
1102
docs/migrations/build_060_12.md
Normal file
1102
docs/migrations/build_060_12.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user