"""Unit tests for trading signals (Signal, SignalType).""" from datetime import datetime from decimal import Decimal import pytest from tradefinder.strategies.signals import Signal, SignalType class TestSignalType: """Tests for SignalType enum.""" def test_signal_type_values(self) -> None: """SignalType has correct string values.""" assert SignalType.ENTRY_LONG.value == "entry_long" assert SignalType.ENTRY_SHORT.value == "entry_short" assert SignalType.EXIT_LONG.value == "exit_long" assert SignalType.EXIT_SHORT.value == "exit_short" class TestSignal: """Tests for Signal dataclass.""" def test_signal_creation_valid(self) -> None: """Valid signal can be created.""" signal = 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=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.signal_type == SignalType.ENTRY_LONG assert signal.symbol == "BTCUSDT" assert signal.price == Decimal("50000.00") assert signal.stop_loss == Decimal("49000.00") assert signal.take_profit == Decimal("52000.00") assert signal.confidence == 0.8 assert signal.strategy_name == "test_strategy" def test_signal_creation_without_take_profit(self) -> None: """Signal can be created without take profit.""" signal = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.take_profit is None def test_signal_validation_confidence_too_low(self) -> None: """Confidence below 0.0 raises ValueError.""" with pytest.raises(ValueError, match="Confidence must be between 0.0 and 1.0"): Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=-0.1, # Invalid timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) def test_signal_validation_confidence_too_high(self) -> None: """Confidence above 1.0 raises ValueError.""" with pytest.raises(ValueError, match="Confidence must be between 0.0 and 1.0"): Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=1.1, # Invalid timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) def test_signal_validation_zero_price(self) -> None: """Zero price raises ValueError.""" with pytest.raises(ValueError, match="Price must be positive"): Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("0"), # Invalid stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) def test_signal_validation_negative_price(self) -> None: """Negative price raises ValueError.""" with pytest.raises(ValueError, match="Price must be positive"): Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("-50000.00"), # Invalid stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) def test_signal_validation_zero_stop_loss(self) -> None: """Zero stop loss raises ValueError.""" with pytest.raises(ValueError, match="Stop loss must be positive"): Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("0"), # Invalid take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) def test_signal_validation_negative_stop_loss(self) -> None: """Negative stop loss raises ValueError.""" with pytest.raises(ValueError, match="Stop loss must be positive"): Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("-49000.00"), # Invalid take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) def test_signal_validation_boundary_confidence(self) -> None: """Boundary confidence values are accepted.""" # Test 0.0 signal_low = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.0, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal_low.confidence == 0.0 # Test 1.0 signal_high = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=1.0, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal_high.confidence == 1.0 class TestSignalProperties: """Tests for Signal computed properties.""" def test_is_entry_property(self) -> None: """is_entry property works correctly.""" entry_long = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_long.is_entry is True entry_short = Signal( signal_type=SignalType.ENTRY_SHORT, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("51000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_short.is_entry is True exit_long = Signal( signal_type=SignalType.EXIT_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert exit_long.is_entry is False def test_is_exit_property(self) -> None: """is_exit property works correctly.""" exit_long = Signal( signal_type=SignalType.EXIT_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert exit_long.is_exit is True exit_short = Signal( signal_type=SignalType.EXIT_SHORT, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("51000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert exit_short.is_exit is True entry_long = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_long.is_exit is False def test_is_long_property(self) -> None: """is_long property works correctly.""" entry_long = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_long.is_long is True exit_long = Signal( signal_type=SignalType.EXIT_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert exit_long.is_long is True entry_short = Signal( signal_type=SignalType.ENTRY_SHORT, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("51000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_short.is_long is False def test_is_short_property(self) -> None: """is_short property works correctly.""" entry_short = Signal( signal_type=SignalType.ENTRY_SHORT, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("51000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_short.is_short is True exit_short = Signal( signal_type=SignalType.EXIT_SHORT, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("51000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert exit_short.is_short is True entry_long = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert entry_long.is_short is False class TestSignalRiskReward: """Tests for risk/reward ratio calculation.""" def test_risk_reward_ratio_with_take_profit(self) -> None: """Risk/reward ratio is calculated correctly.""" signal = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), # 1000 loss take_profit=Decimal("52000.00"), # 2000 reward confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.risk_reward_ratio == Decimal("2.0") # 2000/1000 def test_risk_reward_ratio_short_position(self) -> None: """Risk/reward ratio works for short positions.""" signal = Signal( signal_type=SignalType.ENTRY_SHORT, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("51000.00"), # 1000 loss take_profit=Decimal("48000.00"), # 2000 reward confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.risk_reward_ratio == Decimal("2.0") # 2000/1000 def test_risk_reward_ratio_without_take_profit(self) -> None: """None returned when no take profit set.""" signal = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.risk_reward_ratio is None def test_risk_reward_ratio_zero_risk(self) -> None: """None returned when stop loss equals entry price.""" signal = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("50000.00"), # Zero risk take_profit=Decimal("52000.00"), confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.risk_reward_ratio is None class TestSignalMetadata: """Tests for signal metadata handling.""" def test_signal_metadata_default(self) -> None: """Metadata defaults to empty dict.""" signal = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", ) assert signal.metadata == {} def test_signal_metadata_custom(self) -> None: """Custom metadata is stored.""" metadata = {"indicator_value": 0.75, "period": 14} signal = Signal( signal_type=SignalType.ENTRY_LONG, symbol="BTCUSDT", price=Decimal("50000.00"), stop_loss=Decimal("49000.00"), take_profit=None, confidence=0.8, timestamp=datetime(2024, 1, 1, 12, 0, 0), strategy_name="test_strategy", metadata=metadata, ) assert signal.metadata == metadata