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
This commit is contained in:
183
tests/test_strategy_base.py
Normal file
183
tests/test_strategy_base.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user