build 050: switch MarketAnalysisService to canonical candles
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
# app/tests/unit/trading/market_analysis/test_service_candles.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
import src.trading.market_analysis.service as module
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
from src.trading.market_analysis.service import MarketAnalysisService
|
||||
|
||||
|
||||
def _set_test_attribute(
|
||||
target: object,
|
||||
name: str,
|
||||
value: object,
|
||||
) -> None:
|
||||
setattr(cast(Any, target), name, value)
|
||||
|
||||
|
||||
def _candle(
|
||||
*,
|
||||
index: int = 0,
|
||||
symbol: str = "BTC/USD_LEVERAGE",
|
||||
close_price: str = "100",
|
||||
) -> Candle:
|
||||
return Candle(
|
||||
symbol=symbol,
|
||||
interval="5m",
|
||||
open_time=datetime.fromtimestamp(
|
||||
1_750_000_000 + index * 300,
|
||||
tz=timezone.utc,
|
||||
),
|
||||
open_price=Decimal("99"),
|
||||
high_price=Decimal("101"),
|
||||
low_price=Decimal("98"),
|
||||
close_price=Decimal(close_price),
|
||||
volume=Decimal("10"),
|
||||
source="test",
|
||||
)
|
||||
|
||||
|
||||
def test_analyze_uses_get_candles_with_exact_arguments(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
candles = (_candle(symbol="BTC/USD_LEVERAGE"),)
|
||||
expected = object()
|
||||
|
||||
class FakeExchangeService:
|
||||
def get_candles(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str,
|
||||
limit: int,
|
||||
) -> tuple[Candle, ...]:
|
||||
calls.append(
|
||||
{
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"limit": limit,
|
||||
}
|
||||
)
|
||||
return candles
|
||||
|
||||
def fake_unknown(**kwargs: object) -> object:
|
||||
assert kwargs["symbol"] == "BTC/USD_LEVERAGE"
|
||||
assert kwargs["candles_count"] == 1
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(module, "ExchangeService", FakeExchangeService)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"build_unknown_market_analysis_result",
|
||||
fake_unknown,
|
||||
)
|
||||
|
||||
result = MarketAnalysisService().analyze(
|
||||
" btc/usd ",
|
||||
interval="5m",
|
||||
limit=123,
|
||||
)
|
||||
|
||||
assert result is expected
|
||||
assert calls == [
|
||||
{
|
||||
"symbol": " btc/usd ",
|
||||
"interval": "5m",
|
||||
"limit": 123,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_analyze_uses_requested_symbol_when_candles_are_empty(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
expected = object()
|
||||
|
||||
class FakeExchangeService:
|
||||
def get_candles(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str,
|
||||
limit: int,
|
||||
) -> tuple[Candle, ...]:
|
||||
return ()
|
||||
|
||||
def fake_unknown(**kwargs: object) -> object:
|
||||
assert kwargs["symbol"] == "ETH/USD_LEVERAGE"
|
||||
assert kwargs["candles_count"] == 0
|
||||
assert kwargs["reason"] == "Недостаточно свечей для анализа рынка."
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(module, "ExchangeService", FakeExchangeService)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"build_unknown_market_analysis_result",
|
||||
fake_unknown,
|
||||
)
|
||||
|
||||
result = MarketAnalysisService().analyze("ETH/USD_LEVERAGE")
|
||||
|
||||
assert result is expected
|
||||
|
||||
|
||||
def test_analyze_returns_unknown_when_get_candles_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
expected = object()
|
||||
original_error = RuntimeError("candles unavailable")
|
||||
|
||||
class FakeExchangeService:
|
||||
def get_candles(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str,
|
||||
limit: int,
|
||||
) -> tuple[Candle, ...]:
|
||||
raise original_error
|
||||
|
||||
def fake_unknown(**kwargs: object) -> object:
|
||||
assert kwargs["symbol"] == "BTC/USD_LEVERAGE"
|
||||
assert kwargs["interval"] == "15m"
|
||||
assert kwargs["reason"] == (
|
||||
"Не удалось получить свечи: candles unavailable"
|
||||
)
|
||||
assert "candles_count" not in kwargs
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(module, "ExchangeService", FakeExchangeService)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"build_unknown_market_analysis_result",
|
||||
fake_unknown,
|
||||
)
|
||||
|
||||
result = MarketAnalysisService().analyze(
|
||||
"BTC/USD_LEVERAGE",
|
||||
interval="15m",
|
||||
limit=80,
|
||||
)
|
||||
|
||||
assert result is expected
|
||||
|
||||
|
||||
def test_analyze_rejects_non_finite_canonical_close_price(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
candles = tuple(
|
||||
_candle(
|
||||
index=index,
|
||||
close_price="NaN" if index == 10 else str(100 + index),
|
||||
)
|
||||
for index in range(60)
|
||||
)
|
||||
expected = object()
|
||||
|
||||
class FakeExchangeService:
|
||||
def get_candles(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str,
|
||||
limit: int,
|
||||
) -> tuple[Candle, ...]:
|
||||
return candles
|
||||
|
||||
def fake_unknown(**kwargs: object) -> object:
|
||||
assert kwargs["symbol"] == "BTC/USD_LEVERAGE"
|
||||
assert kwargs["candles_count"] == 60
|
||||
assert kwargs["reason"] == (
|
||||
"Получены некорректные цены закрытия свечей."
|
||||
)
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(module, "ExchangeService", FakeExchangeService)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"build_unknown_market_analysis_result",
|
||||
fake_unknown,
|
||||
)
|
||||
|
||||
result = MarketAnalysisService().analyze("BTC/USD_LEVERAGE")
|
||||
|
||||
assert result is expected
|
||||
|
||||
|
||||
def test_analyze_converts_decimal_closes_to_float_before_indicators(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
candles = tuple(
|
||||
_candle(
|
||||
index=index,
|
||||
close_price=str(100 + index),
|
||||
)
|
||||
for index in range(60)
|
||||
)
|
||||
ema_calls: list[list[float]] = []
|
||||
expected = object()
|
||||
|
||||
class FakeExchangeService:
|
||||
def get_candles(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str,
|
||||
limit: int,
|
||||
) -> tuple[Candle, ...]:
|
||||
return candles
|
||||
|
||||
def fake_ema(
|
||||
values: list[float],
|
||||
period: int,
|
||||
) -> None:
|
||||
ema_calls.append(values)
|
||||
return None
|
||||
|
||||
def fake_unknown(**kwargs: object) -> object:
|
||||
assert kwargs["reason"] == (
|
||||
"Недостаточно данных для расчёта EMA / ATR."
|
||||
)
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(module, "ExchangeService", FakeExchangeService)
|
||||
monkeypatch.setattr(module, "ema", fake_ema)
|
||||
monkeypatch.setattr(module, "atr", lambda candles, period: 1.0)
|
||||
monkeypatch.setattr(module, "rsi", lambda values, period: 50.0)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"build_unknown_market_analysis_result",
|
||||
fake_unknown,
|
||||
)
|
||||
|
||||
result = MarketAnalysisService().analyze("BTC/USD_LEVERAGE")
|
||||
|
||||
assert result is expected
|
||||
assert len(ema_calls) == 2
|
||||
assert all(
|
||||
isinstance(value, float)
|
||||
for values in ema_calls
|
||||
for value in values
|
||||
)
|
||||
assert ema_calls[0][0] == 100.0
|
||||
assert ema_calls[0][-1] == 159.0
|
||||
Reference in New Issue
Block a user