build 059.6: map websocket OHLC close events

This commit is contained in:
2026-07-17 09:59:11 +03:00
parent 5c7ecf9340
commit 85e9662f5b
4 changed files with 1158 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
# app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import TypedDict
import pytest
from src.market_data.acquisition.adapters.dzengi.mapper import (
map_dzengi_websocket_ohlc_to_candle_close_event,
)
from src.market_data.acquisition.adapters.dzengi.models import (
DzengiWebSocketOhlcEvent,
)
from src.market_data.acquisition.exceptions import (
CandleWebSocketMappingError,
)
from src.market_data.acquisition.models.candle_close import (
CandleCloseEvent,
)
class OhlcValues(TypedDict):
open_price: str | int | float
high_price: str | int | float
low_price: str | int | float
close_price: str | int | float
def _event(
*,
open_time: int = 1784227140000,
open_price: str | int | float = "63992.00",
high_price: str | int | float = "64032.55",
low_price: str | int | float = "63984.00",
close_price: str | int | float = "64032.55",
) -> DzengiWebSocketOhlcEvent:
return DzengiWebSocketOhlcEvent(
symbol=" BTC/USD_LEVERAGE ",
interval="1m",
candle_type="classic",
open_time=open_time,
open_price=open_price,
high_price=high_price,
low_price=low_price,
close_price=close_price,
)
def _received_at() -> datetime:
return datetime(
2026,
7,
16,
18,
40,
0,
125000,
tzinfo=timezone.utc,
)
def test_map_websocket_ohlc_returns_candle_close_event() -> None:
result = map_dzengi_websocket_ohlc_to_candle_close_event(
_event(),
received_at=_received_at(),
)
assert isinstance(result, CandleCloseEvent)
def test_map_websocket_ohlc_maps_all_fields() -> None:
result = map_dzengi_websocket_ohlc_to_candle_close_event(
_event(),
received_at=_received_at(),
)
assert result.symbol == "BTC/USD_LEVERAGE"
assert result.interval == "1m"
assert result.candle_type == "classic"
assert result.open_time == datetime(
2026,
7,
16,
18,
39,
tzinfo=timezone.utc,
)
assert result.received_at == _received_at()
assert result.open_price == Decimal("63992.00")
assert result.high_price == Decimal("64032.55")
assert result.low_price == Decimal("63984.00")
assert result.close_price == Decimal("64032.55")
assert result.source == "dzengi_websocket_ohlc"
def test_map_websocket_ohlc_converts_numeric_variants_to_decimal() -> None:
result = map_dzengi_websocket_ohlc_to_candle_close_event(
_event(
open_price=63992,
high_price=64032.55,
low_price="63984.00",
close_price="64001.25",
),
received_at=_received_at(),
)
assert result.open_price == Decimal("63992")
assert result.high_price == Decimal("64032.55")
assert result.low_price == Decimal("63984.00")
assert result.close_price == Decimal("64001.25")
def test_map_websocket_ohlc_preserves_aware_received_at() -> None:
received_at = datetime(
2026,
7,
16,
21,
40,
tzinfo=timezone(timedelta(hours=3)),
)
result = map_dzengi_websocket_ohlc_to_candle_close_event(
_event(),
received_at=received_at,
)
assert result.received_at is received_at
assert result.received_at.utcoffset() == timedelta(hours=3)
def test_map_websocket_ohlc_rejects_naive_received_at() -> None:
received_at = datetime(
2026,
7,
16,
18,
40,
)
with pytest.raises(
CandleWebSocketMappingError,
match="received_at.*timezone-aware",
):
map_dzengi_websocket_ohlc_to_candle_close_event(
_event(),
received_at=received_at,
)
def test_map_websocket_ohlc_rejects_invalid_timestamp() -> None:
with pytest.raises(
CandleWebSocketMappingError,
match="Поле t.*UTC datetime",
):
map_dzengi_websocket_ohlc_to_candle_close_event(
_event(open_time=10**30),
received_at=_received_at(),
)
@pytest.mark.parametrize(
("field_name", "field_value"),
(
("open_price", "invalid"),
("high_price", "invalid"),
("low_price", "invalid"),
("close_price", "invalid"),
),
)
def test_map_websocket_ohlc_rejects_invalid_decimal(
field_name: str,
field_value: str,
) -> None:
values: OhlcValues = {
"open_price": "63992.00",
"high_price": "64032.55",
"low_price": "63984.00",
"close_price": "64032.55",
}
values[field_name] = field_value
with pytest.raises(CandleWebSocketMappingError):
map_dzengi_websocket_ohlc_to_candle_close_event(
_event(**values),
received_at=_received_at(),
)
@pytest.mark.parametrize(
("field_name", "field_value"),
(
("open_price", "NaN"),
("high_price", "Infinity"),
("low_price", "-Infinity"),
("close_price", "NaN"),
),
)
def test_map_websocket_ohlc_rejects_non_finite_decimal(
field_name: str,
field_value: str,
) -> None:
values: OhlcValues = {
"open_price": "63992.00",
"high_price": "64032.55",
"low_price": "63984.00",
"close_price": "64032.55",
}
values[field_name] = field_value
with pytest.raises(
CandleWebSocketMappingError,
match="конечным числом",
):
map_dzengi_websocket_ohlc_to_candle_close_event(
_event(**values),
received_at=_received_at(),
)
def test_map_websocket_ohlc_preserves_heikin_ashi_type() -> None:
event = DzengiWebSocketOhlcEvent(
symbol="BTC/USD_LEVERAGE",
interval="1m",
candle_type="heikin-ashi",
open_time=1784227140000,
open_price="64068.18",
high_price="64128.80",
low_price="64068.18",
close_price="64100.85",
)
result = map_dzengi_websocket_ohlc_to_candle_close_event(
event,
received_at=_received_at(),
)
assert result.candle_type == "heikin-ashi"
def test_map_websocket_ohlc_does_not_create_canonical_volume() -> None:
result = map_dzengi_websocket_ohlc_to_candle_close_event(
_event(),
received_at=_received_at(),
)
assert not hasattr(result, "volume")