"""Unit tests for Strategy base class.""" from decimal import Decimal from unittest.mock import Mock import pytest from tradefinder.adapters.types import Candle, Side from tradefinder.core.regime import Regime from tradefinder.strategies.base import Strategy from tradefinder.strategies.signals import Signal, SignalType class MockStrategy(Strategy): """Concrete implementation of Strategy for testing.""" name = "mock_strategy" def __init__(self) -> None: self._parameters = {"period": 14, "multiplier": 2.0} self._suitable_regimes = [Regime.TRENDING_UP, Regime.TRENDING_DOWN] def generate_signal(self, candles: list[Candle]) -> Signal | None: # Mock implementation - return a signal if we have enough candles if len(candles) >= 5: return Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=Decimal("52000.00"), confidence=0.8, timestamp=candles[-1].timestamp, strategy_name=self.name, ) return None def get_stop_loss(self, entry_price: Decimal, side: Side) -> Decimal: if side == Side.BUY: return entry_price * Decimal("0.98") # 2% below else: return entry_price * Decimal("1.02") # 2% above @property def parameters(self) -> dict[str, int | float]: return self._parameters @property def suitable_regimes(self) -> list[Regime]: return self._suitable_regimes class TestStrategyAbstract: """Tests for Strategy abstract base class.""" def test_strategy_is_abstract(self) -> None: """Strategy cannot be instantiated directly.""" with pytest.raises(TypeError): Strategy() def test_mock_strategy_implements_interface(self) -> None: """MockStrategy properly implements the Strategy interface.""" strategy = MockStrategy() assert strategy.name == "mock_strategy" assert isinstance(strategy.parameters, dict) assert isinstance(strategy.suitable_regimes, list) class TestStrategyGenerateSignal: """Tests for generate_signal method.""" def test_generate_signal_insufficient_candles(self) -> None: """None returned when insufficient candles.""" strategy = MockStrategy() candles = [Mock(spec=Candle) for _ in range(3)] # Less than 5 signal = strategy.generate_signal(candles) assert signal is None def test_generate_signal_sufficient_candles(self) -> None: """Signal returned when sufficient candles.""" strategy = MockStrategy() candles = [] for _i in range(5): candle = Mock(spec=Candle) candle.timestamp = Mock() candles.append(candle) signal = strategy.generate_signal(candles) assert signal is not None assert isinstance(signal, Signal) assert signal.signal_type == SignalType.ENTRY_LONG assert signal.strategy_name == "mock_strategy" class TestStrategyGetStopLoss: """Tests for get_stop_loss method.""" def test_get_stop_loss_long_position(self) -> None: """Stop loss calculated for long position.""" strategy = MockStrategy() entry_price = Decimal("50000.00") stop_loss = strategy.get_stop_loss(entry_price, Side.BUY) expected = entry_price * Decimal("0.98") # 2% below assert stop_loss == expected def test_get_stop_loss_short_position(self) -> None: """Stop loss calculated for short position.""" strategy = MockStrategy() entry_price = Decimal("50000.00") stop_loss = strategy.get_stop_loss(entry_price, Side.SELL) expected = entry_price * Decimal("1.02") # 2% above assert stop_loss == expected class TestStrategyParameters: """Tests for parameters property.""" def test_parameters_property(self) -> None: """Parameters returned correctly.""" strategy = MockStrategy() params = strategy.parameters assert params == {"period": 14, "multiplier": 2.0} class TestStrategySuitableRegimes: """Tests for suitable_regimes property.""" def test_suitable_regimes_property(self) -> None: """Suitable regimes returned correctly.""" strategy = MockStrategy() regimes = strategy.suitable_regimes assert regimes == [Regime.TRENDING_UP, Regime.TRENDING_DOWN] class TestStrategyValidateCandles: """Tests for validate_candles helper method.""" def test_validate_candles_sufficient(self) -> None: """True returned when enough candles.""" strategy = MockStrategy() candles = [Mock(spec=Candle) for _ in range(10)] result = strategy.validate_candles(candles, 5) assert result is True def test_validate_candles_insufficient(self) -> None: """False returned when insufficient candles.""" strategy = MockStrategy() candles = [Mock(spec=Candle) for _ in range(3)] result = strategy.validate_candles(candles, 5) assert result is False def test_validate_candles_logs_debug(self) -> None: """Debug message logged when insufficient candles.""" strategy = MockStrategy() candles = [Mock(spec=Candle) for _ in range(3)] # Test that the method works - logging is tested elsewhere result = strategy.validate_candles(candles, 5) assert result is False class TestStrategyExample: """Test the example implementation from docstring.""" def test_example_implementation_structure(self) -> None: """Example strategy structure is valid.""" # This tests that the example in the docstring would work # We can't instantiate it since it's just documentation, but we can verify the structure # Verify that the required attributes exist on our mock strategy = MockStrategy() assert hasattr(strategy, "name") assert hasattr(strategy, "generate_signal") assert hasattr(strategy, "get_stop_loss") assert hasattr(strategy, "parameters") assert hasattr(strategy, "suitable_regimes") # Verify parameters is a property assert isinstance(strategy.parameters, dict) # Verify suitable_regimes is a property assert isinstance(strategy.suitable_regimes, list) assert all(isinstance(regime, Regime) for regime in strategy.suitable_regimes)