build 044: establish canonical candles feed foundation
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# app/tests/unit/market_data/acquisition/adapters/dzengi/test_candle_mapper.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from src.market_data.acquisition.adapters.dzengi.mapper import (
|
||||
map_dzengi_klines_to_candles,
|
||||
)
|
||||
from src.market_data.acquisition.adapters.dzengi.models import (
|
||||
DzengiKline,
|
||||
DzengiKlinesResponse,
|
||||
)
|
||||
|
||||
|
||||
def _item(open_time: int) -> DzengiKline:
|
||||
return DzengiKline(
|
||||
open_time=open_time,
|
||||
open_price="100.10",
|
||||
high_price="110.20",
|
||||
low_price="90.30",
|
||||
close_price="105.40",
|
||||
volume="12.50",
|
||||
)
|
||||
|
||||
|
||||
def test_map_dzengi_klines_to_candles_maps_canonical_values() -> None:
|
||||
response = DzengiKlinesResponse(items=(_item(1000),))
|
||||
|
||||
result = map_dzengi_klines_to_candles(
|
||||
response,
|
||||
symbol="BTC/USD",
|
||||
interval="1m",
|
||||
source="rest_klines:bid",
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
candle = result[0]
|
||||
assert candle.symbol == "BTC/USD"
|
||||
assert candle.interval == "1m"
|
||||
assert candle.open_time.tzinfo is timezone.utc
|
||||
assert candle.open_time.timestamp() == 1
|
||||
assert candle.open_price == Decimal("100.10")
|
||||
assert candle.high_price == Decimal("110.20")
|
||||
assert candle.low_price == Decimal("90.30")
|
||||
assert candle.close_price == Decimal("105.40")
|
||||
assert candle.volume == Decimal("12.50")
|
||||
assert candle.source == "rest_klines:bid"
|
||||
|
||||
|
||||
def test_map_dzengi_klines_to_candles_sorts_by_open_time() -> None:
|
||||
response = DzengiKlinesResponse(
|
||||
items=(
|
||||
_item(3000),
|
||||
_item(1000),
|
||||
_item(2000),
|
||||
)
|
||||
)
|
||||
|
||||
result = map_dzengi_klines_to_candles(
|
||||
response,
|
||||
symbol="BTC/USD",
|
||||
interval="1m",
|
||||
source="rest_klines:bid",
|
||||
)
|
||||
|
||||
assert [item.open_time.timestamp() for item in result] == [1, 2, 3]
|
||||
|
||||
|
||||
def test_map_dzengi_klines_to_candles_returns_tuple() -> None:
|
||||
result = map_dzengi_klines_to_candles(
|
||||
DzengiKlinesResponse(items=()),
|
||||
symbol="BTC/USD",
|
||||
interval="1m",
|
||||
source="rest_klines:bid",
|
||||
)
|
||||
|
||||
assert result == ()
|
||||
assert isinstance(result, tuple)
|
||||
@@ -0,0 +1,135 @@
|
||||
# app/tests/unit/market_data/acquisition/adapters/dzengi/test_candle_parser.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.adapters.dzengi.parser import parse_candles
|
||||
from src.market_data.acquisition.exceptions import CandleParseError
|
||||
from src.market_data.acquisition.validation.schema import (
|
||||
validate_candles_schema,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_candles_parses_object_item() -> None:
|
||||
document = validate_candles_schema(
|
||||
[
|
||||
{
|
||||
"openTime": 1000,
|
||||
"open": "100",
|
||||
"high": "110",
|
||||
"low": "90",
|
||||
"close": "105",
|
||||
"volume": "12.5",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = parse_candles(document)
|
||||
|
||||
assert len(result.items) == 1
|
||||
item = result.items[0]
|
||||
assert item.open_time == 1000
|
||||
assert item.open_price == "100"
|
||||
assert item.high_price == "110"
|
||||
assert item.low_price == "90"
|
||||
assert item.close_price == "105"
|
||||
assert item.volume == "12.5"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"time_key",
|
||||
["openTime", "open_time", "time", "timestamp"],
|
||||
)
|
||||
def test_parse_candles_supports_time_aliases(time_key: str) -> None:
|
||||
document = validate_candles_schema(
|
||||
[
|
||||
{
|
||||
time_key: 1000,
|
||||
"open": "100",
|
||||
"high": "110",
|
||||
"low": "90",
|
||||
"close": "105",
|
||||
"volume": "12.5",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = parse_candles(document)
|
||||
|
||||
assert result.items[0].open_time == 1000
|
||||
|
||||
|
||||
def test_parse_candles_parses_array_item() -> None:
|
||||
document = validate_candles_schema(
|
||||
[[1000, "100", "110", "90", "105", "12.5", "ignored"]]
|
||||
)
|
||||
|
||||
result = parse_candles(document)
|
||||
|
||||
item = result.items[0]
|
||||
assert item.open_time == 1000
|
||||
assert item.open_price == "100"
|
||||
assert item.high_price == "110"
|
||||
assert item.low_price == "90"
|
||||
assert item.close_price == "105"
|
||||
assert item.volume == "12.5"
|
||||
|
||||
|
||||
def test_parse_candles_accepts_empty_document() -> None:
|
||||
result = parse_candles(validate_candles_schema([]))
|
||||
|
||||
assert result.items == ()
|
||||
|
||||
|
||||
def test_parse_candles_rejects_missing_required_field() -> None:
|
||||
document = validate_candles_schema(
|
||||
[
|
||||
{
|
||||
"openTime": 1000,
|
||||
"open": "100",
|
||||
"high": "110",
|
||||
"low": "90",
|
||||
"volume": "12.5",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(CandleParseError, match="close"):
|
||||
parse_candles(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_time",
|
||||
[True, 1.5, "1000", None],
|
||||
)
|
||||
def test_parse_candles_rejects_invalid_open_time(
|
||||
invalid_time: object,
|
||||
) -> None:
|
||||
document = validate_candles_schema(
|
||||
[[invalid_time, "100", "110", "90", "105", "12.5"]]
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
CandleParseError,
|
||||
match=r"\$\.candles\[0\]\[0\]",
|
||||
):
|
||||
parse_candles(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_value",
|
||||
[True, None, {}, []],
|
||||
)
|
||||
def test_parse_candles_rejects_invalid_numeric_transport_value(
|
||||
invalid_value: object,
|
||||
) -> None:
|
||||
document = validate_candles_schema(
|
||||
[[1000, invalid_value, "110", "90", "105", "12.5"]]
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
CandleParseError,
|
||||
match=r"\$\.candles\[0\]\[1\]",
|
||||
):
|
||||
parse_candles(document)
|
||||
@@ -0,0 +1,100 @@
|
||||
# app/tests/unit/market_data/acquisition/adapters/dzengi/test_candle_rest.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.adapters.dzengi.rest import (
|
||||
DzengiCandlesDocumentSource,
|
||||
)
|
||||
from src.market_data.acquisition.exceptions import CandleTransportError
|
||||
|
||||
|
||||
class StubPayloadRestClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
result: object = None,
|
||||
error: Exception | None = None,
|
||||
) -> None:
|
||||
self.result = result
|
||||
self.error = error
|
||||
self.calls: list[
|
||||
tuple[str, dict[str, str] | None, dict[str, str] | None]
|
||||
] = []
|
||||
|
||||
def get_payload(
|
||||
self,
|
||||
path: str,
|
||||
params: dict[str, str] | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> object:
|
||||
self.calls.append((path, params, headers))
|
||||
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
|
||||
return self.result
|
||||
|
||||
|
||||
def test_fetch_candles_document_calls_klines_endpoint() -> None:
|
||||
document = {"klines": []}
|
||||
client = StubPayloadRestClient(result=document)
|
||||
source = DzengiCandlesDocumentSource(client=client)
|
||||
|
||||
result = source.fetch_candles_document(
|
||||
"BTC/USD",
|
||||
interval="5m",
|
||||
limit=200,
|
||||
price_type="ask",
|
||||
)
|
||||
|
||||
assert result is document
|
||||
assert client.calls == [
|
||||
(
|
||||
"/api/v1/klines",
|
||||
{
|
||||
"symbol": "BTC/USD",
|
||||
"interval": "5m",
|
||||
"limit": "200",
|
||||
"priceType": "ask",
|
||||
},
|
||||
None,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_fetch_candles_document_does_not_transform_document() -> None:
|
||||
document = [
|
||||
[1000, "100", "110", "90", "105", "12.5"],
|
||||
]
|
||||
client = StubPayloadRestClient(result=document)
|
||||
source = DzengiCandlesDocumentSource(client=client)
|
||||
|
||||
result = source.fetch_candles_document(
|
||||
"BTC/USD",
|
||||
interval="1m",
|
||||
limit=1,
|
||||
price_type="bid",
|
||||
)
|
||||
|
||||
assert result is document
|
||||
|
||||
|
||||
def test_fetch_candles_document_wraps_transport_error() -> None:
|
||||
original_error = RuntimeError("transport unavailable")
|
||||
client = StubPayloadRestClient(error=original_error)
|
||||
source = DzengiCandlesDocumentSource(client=client)
|
||||
|
||||
with pytest.raises(
|
||||
CandleTransportError,
|
||||
match="Не удалось получить свечи",
|
||||
) as error_info:
|
||||
source.fetch_candles_document(
|
||||
"BTC/USD",
|
||||
interval="1m",
|
||||
limit=100,
|
||||
price_type="bid",
|
||||
)
|
||||
|
||||
assert error_info.value.__cause__ is original_error
|
||||
@@ -0,0 +1,164 @@
|
||||
# app/tests/unit/market_data/acquisition/feeds/test_candles_feed.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.feeds.candles_feed import CandlesFeed
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
|
||||
|
||||
def _candle() -> Candle:
|
||||
return Candle(
|
||||
symbol="BTC/USD",
|
||||
interval="1m",
|
||||
open_time=datetime(2026, 7, 15, 6, 0, tzinfo=timezone.utc),
|
||||
open_price=Decimal("100"),
|
||||
high_price=Decimal("110"),
|
||||
low_price=Decimal("90"),
|
||||
close_price=Decimal("105"),
|
||||
volume=Decimal("12"),
|
||||
source="rest_klines:bid",
|
||||
)
|
||||
|
||||
|
||||
class StubCandlesDocumentSource:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
document: object,
|
||||
error: Exception | None = None,
|
||||
) -> None:
|
||||
self.document = document
|
||||
self.error = error
|
||||
self.calls: list[tuple[str, str, int, str]] = []
|
||||
|
||||
def fetch_candles_document(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str,
|
||||
limit: int,
|
||||
price_type: str,
|
||||
) -> object:
|
||||
self.calls.append((symbol, interval, limit, price_type))
|
||||
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
|
||||
return self.document
|
||||
|
||||
|
||||
class StubCandlesDocumentHandler:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
candles: tuple[Candle, ...],
|
||||
error: Exception | None = None,
|
||||
) -> None:
|
||||
self.candles = candles
|
||||
self.error = error
|
||||
self.calls: list[tuple[object, str, str, str]] = []
|
||||
|
||||
def handle_candles_document(
|
||||
self,
|
||||
document: object,
|
||||
*,
|
||||
symbol: str,
|
||||
interval: str,
|
||||
source: str,
|
||||
) -> tuple[Candle, ...]:
|
||||
self.calls.append((document, symbol, interval, source))
|
||||
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
|
||||
return self.candles
|
||||
|
||||
|
||||
def test_load_candles_coordinates_source_and_handler() -> None:
|
||||
document = {"klines": []}
|
||||
candles = (_candle(),)
|
||||
source = StubCandlesDocumentSource(document=document)
|
||||
handler = StubCandlesDocumentHandler(candles=candles)
|
||||
feed = CandlesFeed(source=source, handler=handler)
|
||||
|
||||
result = feed.load_candles(
|
||||
"BTC/USD",
|
||||
interval="1m",
|
||||
limit=100,
|
||||
price_type="bid",
|
||||
)
|
||||
|
||||
assert result is candles
|
||||
assert source.calls == [("BTC/USD", "1m", 100, "bid")]
|
||||
assert handler.calls == [
|
||||
(
|
||||
document,
|
||||
"BTC/USD",
|
||||
"1m",
|
||||
"rest_klines:bid",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_load_candles_does_not_trim_handler_result() -> None:
|
||||
candles = (_candle(), _candle())
|
||||
source = StubCandlesDocumentSource(document=[])
|
||||
handler = StubCandlesDocumentHandler(candles=candles)
|
||||
feed = CandlesFeed(source=source, handler=handler)
|
||||
|
||||
result = feed.load_candles(
|
||||
"BTC/USD",
|
||||
interval="1m",
|
||||
limit=1,
|
||||
price_type="ask",
|
||||
)
|
||||
|
||||
assert result is candles
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_load_candles_propagates_source_error() -> None:
|
||||
original_error = RuntimeError("source failed")
|
||||
source = StubCandlesDocumentSource(
|
||||
document=None,
|
||||
error=original_error,
|
||||
)
|
||||
handler = StubCandlesDocumentHandler(candles=())
|
||||
feed = CandlesFeed(source=source, handler=handler)
|
||||
|
||||
with pytest.raises(RuntimeError, match="source failed") as error_info:
|
||||
feed.load_candles(
|
||||
"BTC/USD",
|
||||
interval="1m",
|
||||
limit=100,
|
||||
price_type="bid",
|
||||
)
|
||||
|
||||
assert error_info.value is original_error
|
||||
assert handler.calls == []
|
||||
|
||||
|
||||
def test_load_candles_propagates_handler_error() -> None:
|
||||
original_error = ValueError("handler failed")
|
||||
document = {"klines": []}
|
||||
source = StubCandlesDocumentSource(document=document)
|
||||
handler = StubCandlesDocumentHandler(
|
||||
candles=(),
|
||||
error=original_error,
|
||||
)
|
||||
feed = CandlesFeed(source=source, handler=handler)
|
||||
|
||||
with pytest.raises(ValueError, match="handler failed") as error_info:
|
||||
feed.load_candles(
|
||||
"BTC/USD",
|
||||
interval="5m",
|
||||
limit=50,
|
||||
price_type="ask",
|
||||
)
|
||||
|
||||
assert error_info.value is original_error
|
||||
@@ -0,0 +1,31 @@
|
||||
# app/tests/unit/market_data/acquisition/handlers/test_candles_handler.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from src.market_data.acquisition.handlers.candles_handler import (
|
||||
DzengiCandlesDocumentHandler,
|
||||
)
|
||||
|
||||
|
||||
def test_handle_candles_document_runs_complete_pipeline() -> None:
|
||||
handler = DzengiCandlesDocumentHandler()
|
||||
|
||||
result = handler.handle_candles_document(
|
||||
{
|
||||
"payload": {
|
||||
"klines": [
|
||||
[2000, "101", "111", "91", "106", "13"],
|
||||
[1000, "100", "110", "90", "105", "12"],
|
||||
]
|
||||
}
|
||||
},
|
||||
symbol="BTC/USD",
|
||||
interval="1m",
|
||||
source="rest_klines:bid",
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
assert [item.open_time.timestamp() for item in result] == [1, 2]
|
||||
assert all(item.symbol == "BTC/USD" for item in result)
|
||||
assert all(item.interval == "1m" for item in result)
|
||||
assert all(item.source == "rest_klines:bid" for item in result)
|
||||
52
app/tests/unit/market_data/acquisition/models/test_candle.py
Normal file
52
app/tests/unit/market_data/acquisition/models/test_candle.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# app/tests/unit/market_data/acquisition/models/test_candle.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import FrozenInstanceError
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
|
||||
|
||||
def _candle() -> Candle:
|
||||
return Candle(
|
||||
symbol="BTC/USD",
|
||||
interval="1m",
|
||||
open_time=datetime(2026, 7, 15, 6, 0, tzinfo=timezone.utc),
|
||||
open_price=Decimal("100"),
|
||||
high_price=Decimal("110"),
|
||||
low_price=Decimal("90"),
|
||||
close_price=Decimal("105"),
|
||||
volume=Decimal("12.5"),
|
||||
source="rest_klines:bid",
|
||||
)
|
||||
|
||||
|
||||
def test_candle_stores_canonical_values() -> None:
|
||||
candle = _candle()
|
||||
|
||||
assert candle.symbol == "BTC/USD"
|
||||
assert candle.interval == "1m"
|
||||
assert candle.open_time.tzinfo is timezone.utc
|
||||
assert candle.open_price == Decimal("100")
|
||||
assert candle.high_price == Decimal("110")
|
||||
assert candle.low_price == Decimal("90")
|
||||
assert candle.close_price == Decimal("105")
|
||||
assert candle.volume == Decimal("12.5")
|
||||
assert candle.source == "rest_klines:bid"
|
||||
|
||||
|
||||
def test_candle_is_frozen() -> None:
|
||||
candle = _candle()
|
||||
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
candle.close_price = Decimal("106") # type: ignore[misc]
|
||||
|
||||
|
||||
def test_candle_uses_slots() -> None:
|
||||
candle = _candle()
|
||||
|
||||
assert not hasattr(candle, "__dict__")
|
||||
@@ -0,0 +1,120 @@
|
||||
# app/tests/unit/market_data/acquisition/validation/test_candle_schema.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import MappingProxyType
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.exceptions import CandleSchemaError
|
||||
from src.market_data.acquisition.validation.schema import (
|
||||
validate_candles_schema,
|
||||
)
|
||||
|
||||
|
||||
_VALID_ARRAY_ITEM = [1000, "100", "110", "90", "105", "12.5"]
|
||||
_VALID_OBJECT_ITEM = {
|
||||
"openTime": 1000,
|
||||
"open": "100",
|
||||
"high": "110",
|
||||
"low": "90",
|
||||
"close": "105",
|
||||
"volume": "12.5",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"document",
|
||||
[
|
||||
[_VALID_ARRAY_ITEM],
|
||||
{"klines": [_VALID_ARRAY_ITEM]},
|
||||
{"candles": [_VALID_ARRAY_ITEM]},
|
||||
{"data": [_VALID_ARRAY_ITEM]},
|
||||
{"result": [_VALID_ARRAY_ITEM]},
|
||||
{"payload": [_VALID_ARRAY_ITEM]},
|
||||
{"payload": {"klines": [_VALID_ARRAY_ITEM]}},
|
||||
{"payload": {"candles": [_VALID_ARRAY_ITEM]}},
|
||||
{"payload": {"data": [_VALID_ARRAY_ITEM]}},
|
||||
],
|
||||
)
|
||||
def test_validate_candles_schema_accepts_supported_envelopes(
|
||||
document: object,
|
||||
) -> None:
|
||||
result = validate_candles_schema(document)
|
||||
|
||||
assert len(result.items) == 1
|
||||
assert result.items[0] == tuple(_VALID_ARRAY_ITEM)
|
||||
|
||||
|
||||
def test_validate_candles_schema_accepts_object_item() -> None:
|
||||
result = validate_candles_schema([_VALID_OBJECT_ITEM])
|
||||
|
||||
assert len(result.items) == 1
|
||||
assert isinstance(result.items[0], MappingProxyType)
|
||||
assert dict(result.items[0]) == _VALID_OBJECT_ITEM
|
||||
|
||||
|
||||
def test_validate_candles_schema_accepts_empty_list() -> None:
|
||||
result = validate_candles_schema([])
|
||||
|
||||
assert result.items == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"document",
|
||||
[
|
||||
None,
|
||||
"invalid",
|
||||
123,
|
||||
{"unknown": []},
|
||||
{"payload": {"unknown": []}},
|
||||
],
|
||||
)
|
||||
def test_validate_candles_schema_rejects_unknown_document(
|
||||
document: object,
|
||||
) -> None:
|
||||
with pytest.raises(CandleSchemaError):
|
||||
validate_candles_schema(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"document",
|
||||
[
|
||||
{"klines": {}},
|
||||
{"payload": {"klines": {}}},
|
||||
],
|
||||
)
|
||||
def test_validate_candles_schema_rejects_non_list_container(
|
||||
document: object,
|
||||
) -> None:
|
||||
with pytest.raises(CandleSchemaError, match="JSON-массивом"):
|
||||
validate_candles_schema(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"item",
|
||||
[
|
||||
"invalid",
|
||||
123,
|
||||
None,
|
||||
True,
|
||||
],
|
||||
)
|
||||
def test_validate_candles_schema_rejects_invalid_item(
|
||||
item: object,
|
||||
) -> None:
|
||||
with pytest.raises(
|
||||
CandleSchemaError,
|
||||
match="JSON-объектом или JSON-массивом",
|
||||
):
|
||||
validate_candles_schema([item])
|
||||
|
||||
|
||||
def test_validate_candles_schema_rejects_short_array() -> None:
|
||||
with pytest.raises(CandleSchemaError, match="минимум 6"):
|
||||
validate_candles_schema([[1000, "100", "110"]])
|
||||
|
||||
|
||||
def test_validate_candles_schema_rejects_non_string_object_key() -> None:
|
||||
with pytest.raises(CandleSchemaError, match="нестроковый ключ"):
|
||||
validate_candles_schema([{1: "invalid"}])
|
||||
@@ -0,0 +1,109 @@
|
||||
# app/tests/unit/market_data/acquisition/validation/test_candle_values.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.adapters.dzengi.models import (
|
||||
DzengiKline,
|
||||
DzengiKlinesResponse,
|
||||
)
|
||||
from src.market_data.acquisition.exceptions import CandleValueError
|
||||
from src.market_data.acquisition.validation.values import (
|
||||
validate_candles_values,
|
||||
)
|
||||
|
||||
|
||||
def _response(
|
||||
*,
|
||||
open_time: int = 1000,
|
||||
open_price: str | int | float = "100",
|
||||
high_price: str | int | float = "110",
|
||||
low_price: str | int | float = "90",
|
||||
close_price: str | int | float = "105",
|
||||
volume: str | int | float = "12.5",
|
||||
) -> DzengiKlinesResponse:
|
||||
return DzengiKlinesResponse(
|
||||
items=(
|
||||
DzengiKline(
|
||||
open_time=open_time,
|
||||
open_price=open_price,
|
||||
high_price=high_price,
|
||||
low_price=low_price,
|
||||
close_price=close_price,
|
||||
volume=volume,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_validate_candles_values_accepts_valid_response() -> None:
|
||||
validate_candles_values(_response())
|
||||
|
||||
|
||||
def test_validate_candles_values_accepts_zero_volume() -> None:
|
||||
validate_candles_values(_response(volume=0))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field_name", "value"),
|
||||
[
|
||||
("open_price", 0),
|
||||
("high_price", 0),
|
||||
("low_price", 0),
|
||||
("close_price", 0),
|
||||
("volume", -1),
|
||||
],
|
||||
)
|
||||
def test_validate_candles_values_rejects_invalid_bounds(
|
||||
field_name: str,
|
||||
value: int,
|
||||
) -> None:
|
||||
kwargs = {field_name: value}
|
||||
|
||||
with pytest.raises(CandleValueError):
|
||||
validate_candles_values(_response(**kwargs))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
["NaN", "Infinity", "-Infinity"],
|
||||
)
|
||||
def test_validate_candles_values_rejects_non_finite_values(
|
||||
value: str,
|
||||
) -> None:
|
||||
with pytest.raises(CandleValueError):
|
||||
validate_candles_values(_response(open_price=value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"open_time",
|
||||
[0, -1],
|
||||
)
|
||||
def test_validate_candles_values_rejects_invalid_open_time(
|
||||
open_time: int,
|
||||
) -> None:
|
||||
with pytest.raises(
|
||||
CandleValueError,
|
||||
match=r"\$\.candles\[0\]\.openTime",
|
||||
):
|
||||
validate_candles_values(_response(open_time=open_time))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"response",
|
||||
[
|
||||
_response(high_price="99"),
|
||||
_response(low_price="101"),
|
||||
_response(high_price="80", low_price="90"),
|
||||
],
|
||||
)
|
||||
def test_validate_candles_values_rejects_invalid_ohlc(
|
||||
response: DzengiKlinesResponse,
|
||||
) -> None:
|
||||
with pytest.raises(CandleValueError):
|
||||
validate_candles_values(response)
|
||||
|
||||
|
||||
def test_validate_candles_values_accepts_empty_response() -> None:
|
||||
validate_candles_values(DzengiKlinesResponse(items=()))
|
||||
Reference in New Issue
Block a user