Build 060.6: add REST trade mapper

This commit is contained in:
2026-07-19 09:49:01 +03:00
parent a0ae22cb2e
commit 6b0d5badce
4 changed files with 2599 additions and 2 deletions

View File

@@ -3,12 +3,14 @@
from __future__ import annotations
from dataclasses import FrozenInstanceError, replace
from datetime import datetime, timezone
from decimal import Decimal
import pytest
from src.market_data.acquisition.adapters.dzengi.mapper import (
map_dzengi_exchange_info_to_instruments,
map_dzengi_rest_agg_trades_to_trades,
map_dzengi_symbol_to_instrument,
)
from src.market_data.acquisition.adapters.dzengi.models import (
@@ -17,10 +19,16 @@ from src.market_data.acquisition.adapters.dzengi.models import (
DzengiExchangeInfoSymbol,
DzengiLotSizeFilter,
DzengiMinNotionalFilter,
DzengiRestAggTrade,
DzengiUnknownFilter,
)
from src.market_data.acquisition.exceptions import (
InstrumentReferenceMappingError,
TradeMappingError,
)
from src.market_data.acquisition.models.trade import (
Trade,
TradeAggressorSide,
)
@@ -82,6 +90,23 @@ def _response(
)
def _rest_agg_trade(
*,
aggregate_trade_id: int = 101,
price: str | int | float = "123.45",
quantity: str | int | float = "0.25",
timestamp: int = 1783537921471,
buyer_is_maker: bool = False,
) -> DzengiRestAggTrade:
return DzengiRestAggTrade(
aggregate_trade_id=aggregate_trade_id,
price=price,
quantity=quantity,
timestamp=timestamp,
buyer_is_maker=buyer_is_maker,
)
def test_map_complete_dzengi_symbol_to_instrument() -> None:
instrument = map_dzengi_symbol_to_instrument(
_complete_symbol()
@@ -404,4 +429,244 @@ def test_mapped_instrument_is_immutable() -> None:
)
with pytest.raises(FrozenInstanceError):
instrument.status = "BREAK" # type: ignore[misc]
instrument.status = "BREAK" # type: ignore[misc]
def test_map_dzengi_rest_agg_trade_to_trade() -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(_rest_agg_trade(),),
symbol="BTC/USDT",
)
assert isinstance(trades, tuple)
assert len(trades) == 1
trade = trades[0]
assert isinstance(trade, Trade)
assert trade.symbol == "BTC/USDT"
assert trade.trade_id == 101
assert trade.price == Decimal("123.45")
assert trade.quantity == Decimal("0.25")
assert trade.executed_at == datetime.fromtimestamp(
1783537921471 / 1000,
tz=timezone.utc,
)
assert trade.aggressor_side is TradeAggressorSide.BUY
assert trade.source == "dzengi"
def test_trade_mapper_maps_buyer_taker_to_buy_aggressor() -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(
_rest_agg_trade(
buyer_is_maker=False,
),
),
symbol="BTC/USDT",
)
assert trades[0].aggressor_side is TradeAggressorSide.BUY
def test_trade_mapper_maps_seller_taker_to_sell_aggressor() -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(
_rest_agg_trade(
buyer_is_maker=True,
),
),
symbol="BTC/USDT",
)
assert trades[0].aggressor_side is TradeAggressorSide.SELL
def test_trade_mapper_preserves_input_order() -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(
_rest_agg_trade(
aggregate_trade_id=103,
),
_rest_agg_trade(
aggregate_trade_id=101,
),
_rest_agg_trade(
aggregate_trade_id=102,
),
),
symbol="BTC/USDT",
)
assert tuple(
trade.trade_id
for trade in trades
) == (
103,
101,
102,
)
def test_trade_mapper_maps_empty_tuple() -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(),
symbol="BTC/USDT",
)
assert trades == ()
def test_trade_mapper_strips_symbol() -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(_rest_agg_trade(),),
symbol=" BTC/USDT ",
)
assert trades[0].symbol == "BTC/USDT"
@pytest.mark.parametrize(
("price", "quantity", "expected_price", "expected_quantity"),
[
(
"123.4500",
"0.2500",
Decimal("123.4500"),
Decimal("0.2500"),
),
(
123,
2,
Decimal("123"),
Decimal("2"),
),
(
123.5,
0.25,
Decimal("123.5"),
Decimal("0.25"),
),
],
)
def test_trade_mapper_converts_numeric_values_to_decimal(
price: str | int | float,
quantity: str | int | float,
expected_price: Decimal,
expected_quantity: Decimal,
) -> None:
trades = map_dzengi_rest_agg_trades_to_trades(
(
_rest_agg_trade(
price=price,
quantity=quantity,
),
),
symbol="BTC/USDT",
)
assert trades[0].price == expected_price
assert trades[0].quantity == expected_quantity
def test_trade_mapper_converts_timestamp_to_utc_datetime() -> None:
timestamp = 1783537921471
trades = map_dzengi_rest_agg_trades_to_trades(
(
_rest_agg_trade(
timestamp=timestamp,
),
),
symbol="BTC/USDT",
)
assert trades[0].executed_at == datetime.fromtimestamp(
timestamp / 1000,
tz=timezone.utc,
)
assert trades[0].executed_at.tzinfo is timezone.utc
def test_mapped_trade_is_immutable() -> None:
trade = map_dzengi_rest_agg_trades_to_trades(
(_rest_agg_trade(),),
symbol="BTC/USDT",
)[0]
with pytest.raises(FrozenInstanceError):
trade.price = Decimal("1") # type: ignore[misc]
@pytest.mark.parametrize(
"field_name",
[
"price",
"quantity",
],
)
def test_trade_mapper_rejects_invalid_decimal_value(
field_name: str,
) -> None:
trade = _rest_agg_trade()
invalid_trade = replace(
trade,
**{field_name: "not-a-number"},
)
with pytest.raises(
TradeMappingError,
match=rf"{field_name}.*невозможно преобразовать в Decimal",
):
map_dzengi_rest_agg_trades_to_trades(
(invalid_trade,),
symbol="BTC/USDT",
)
@pytest.mark.parametrize(
("field_name", "invalid_value"),
[
("price", float("nan")),
("price", float("inf")),
("price", float("-inf")),
("quantity", float("nan")),
("quantity", float("inf")),
("quantity", float("-inf")),
],
)
def test_trade_mapper_rejects_non_finite_decimal_value(
field_name: str,
invalid_value: float,
) -> None:
trade = _rest_agg_trade()
invalid_trade = replace(
trade,
**{field_name: invalid_value},
)
with pytest.raises(
TradeMappingError,
match=rf"{field_name}.*должно быть конечным числом",
):
map_dzengi_rest_agg_trades_to_trades(
(invalid_trade,),
symbol="BTC/USDT",
)
def test_trade_mapper_rejects_unrepresentable_timestamp() -> None:
trade = _rest_agg_trade(
timestamp=10**30,
)
with pytest.raises(
TradeMappingError,
match=r"timestamp.*невозможно преобразовать в UTC datetime",
):
map_dzengi_rest_agg_trades_to_trades(
(trade,),
symbol="BTC/USDT",
)