Files
CryptoTrading/tests/test_signals.py
bnair123 eca17b42fe Add Phase 2 foundation: regime classifier, strategy framework, WebSocket streamer
Phase 1 completion:
- Add DataStreamer for real-time Binance Futures WebSocket data (klines, mark price)
- Add DataValidator for candle validation and gap detection
- Add timeframes module with interval mappings

Phase 2 foundation:
- Add RegimeClassifier with ADX/ATR/Bollinger Band analysis
- Add Regime enum (TRENDING_UP/DOWN, RANGING, HIGH_VOLATILITY, UNCERTAIN)
- Add Strategy ABC defining generate_signal, get_stop_loss, parameters, suitable_regimes
- Add Signal dataclass and SignalType enum for strategy outputs

Testing:
- Add comprehensive test suites for all new modules
- 159 tests passing, 24 skipped (async WebSocket timing)
- 82% code coverage

Dependencies:
- Add pandas-stubs to dev dependencies for mypy compatibility
2025-12-27 15:28:28 +04:00

420 lines
15 KiB
Python

"""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