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:
bnair123
2025-12-27 15:28:28 +04:00
parent 7d63e43b7b
commit eca17b42fe
15 changed files with 2579 additions and 1 deletions

183
tests/test_strategy_base.py Normal file
View 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)