Local changes: Updated model training, removed debug instrumentation, and configuration improvements
This commit is contained in:
2
tests/unit/strategies/__init__.py
Normal file
2
tests/unit/strategies/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Unit tests for strategy framework."""
|
||||
|
||||
89
tests/unit/strategies/test_base.py
Normal file
89
tests/unit/strategies/test_base.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Tests for base strategy class."""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from src.strategies.base import BaseStrategy, StrategyRegistry
|
||||
|
||||
|
||||
class ConcreteStrategy(BaseStrategy):
|
||||
"""Concrete strategy for testing."""
|
||||
|
||||
async def on_data(self, new_data: pd.DataFrame):
|
||||
"""Handle new data."""
|
||||
self.current_data = pd.concat([self.current_data, new_data]).tail(100)
|
||||
|
||||
async def generate_signal(self):
|
||||
"""Generate signal."""
|
||||
if len(self.current_data) > 0:
|
||||
return {"signal": "hold", "price": self.current_data['close'].iloc[-1]}
|
||||
return {"signal": "hold", "price": None}
|
||||
|
||||
async def calculate_position_size(self, capital: float, risk_percentage: float) -> float:
|
||||
"""Calculate position size."""
|
||||
return capital * risk_percentage
|
||||
|
||||
|
||||
class TestBaseStrategy:
|
||||
"""Tests for BaseStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create strategy instance."""
|
||||
return ConcreteStrategy(
|
||||
strategy_id=1,
|
||||
name="test_strategy",
|
||||
symbol="BTC/USD",
|
||||
timeframe="1h",
|
||||
parameters={}
|
||||
)
|
||||
|
||||
def test_strategy_initialization(self, strategy):
|
||||
"""Test strategy initialization."""
|
||||
assert strategy.strategy_id == 1
|
||||
assert strategy.name == "test_strategy"
|
||||
assert strategy.symbol == "BTC/USD"
|
||||
assert strategy.timeframe == "1h"
|
||||
assert not strategy.is_active
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strategy_start_stop(self, strategy):
|
||||
"""Test strategy start and stop."""
|
||||
await strategy.start()
|
||||
assert strategy.is_active
|
||||
|
||||
await strategy.stop()
|
||||
assert not strategy.is_active
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_signal(self, strategy):
|
||||
"""Test signal generation."""
|
||||
signal = await strategy.generate_signal()
|
||||
assert "signal" in signal
|
||||
assert signal["signal"] in ["buy", "sell", "hold"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calculate_position_size(self, strategy):
|
||||
"""Test position size calculation."""
|
||||
size = await strategy.calculate_position_size(1000.0, 0.01)
|
||||
assert size == 10.0
|
||||
|
||||
|
||||
class TestStrategyRegistry:
|
||||
"""Tests for StrategyRegistry."""
|
||||
|
||||
def test_register_strategy(self):
|
||||
"""Test strategy registration."""
|
||||
StrategyRegistry.register_strategy("test_strategy", ConcreteStrategy)
|
||||
assert "test_strategy" in StrategyRegistry.list_available()
|
||||
|
||||
def test_get_strategy_class(self):
|
||||
"""Test getting strategy class."""
|
||||
StrategyRegistry.register_strategy("test_strategy", ConcreteStrategy)
|
||||
strategy_class = StrategyRegistry.get_strategy_class("test_strategy")
|
||||
assert strategy_class == ConcreteStrategy
|
||||
|
||||
def test_get_nonexistent_strategy(self):
|
||||
"""Test getting non-existent strategy."""
|
||||
with pytest.raises(ValueError, match="not registered"):
|
||||
StrategyRegistry.get_strategy_class("nonexistent")
|
||||
|
||||
55
tests/unit/strategies/test_bollinger_mean_reversion.py
Normal file
55
tests/unit/strategies/test_bollinger_mean_reversion.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Tests for Bollinger Bands mean reversion strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from src.strategies.technical.bollinger_mean_reversion import BollingerMeanReversionStrategy
|
||||
from src.strategies.base import SignalType
|
||||
|
||||
|
||||
class TestBollingerMeanReversionStrategy:
|
||||
"""Tests for BollingerMeanReversionStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create Bollinger mean reversion strategy instance."""
|
||||
return BollingerMeanReversionStrategy(
|
||||
name="test_bollinger_mr",
|
||||
parameters={
|
||||
'period': 20,
|
||||
'std_dev': 2.0,
|
||||
'trend_filter': True,
|
||||
'trend_ma_period': 50,
|
||||
'entry_threshold': 0.95,
|
||||
'exit_threshold': 0.5
|
||||
}
|
||||
)
|
||||
|
||||
def test_initialization(self, strategy):
|
||||
"""Test strategy initialization."""
|
||||
assert strategy.period == 20
|
||||
assert strategy.std_dev == 2.0
|
||||
assert strategy.trend_filter is True
|
||||
assert strategy.trend_ma_period == 50
|
||||
assert strategy.entry_threshold == 0.95
|
||||
assert strategy.exit_threshold == 0.5
|
||||
|
||||
def test_on_tick_insufficient_data(self, strategy):
|
||||
"""Test that strategy returns None with insufficient data."""
|
||||
signal = strategy.on_tick(
|
||||
symbol="BTC/USD",
|
||||
price=Decimal("50000"),
|
||||
timeframe="1h",
|
||||
data={'volume': 1000}
|
||||
)
|
||||
assert signal is None
|
||||
|
||||
def test_position_tracking(self, strategy):
|
||||
"""Test position tracking."""
|
||||
assert strategy._in_position is False
|
||||
assert strategy._entry_price is None
|
||||
|
||||
def test_strategy_metadata(self, strategy):
|
||||
"""Test strategy metadata."""
|
||||
assert strategy.name == "test_bollinger_mr"
|
||||
assert strategy.enabled is False
|
||||
|
||||
61
tests/unit/strategies/test_confirmed_strategy.py
Normal file
61
tests/unit/strategies/test_confirmed_strategy.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Tests for Confirmed strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from src.strategies.technical.confirmed_strategy import ConfirmedStrategy
|
||||
from src.strategies.base import SignalType
|
||||
|
||||
|
||||
class TestConfirmedStrategy:
|
||||
"""Tests for ConfirmedStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create Confirmed strategy instance."""
|
||||
return ConfirmedStrategy(
|
||||
name="test_confirmed",
|
||||
parameters={
|
||||
'rsi_period': 14,
|
||||
'macd_fast': 12,
|
||||
'macd_slow': 26,
|
||||
'macd_signal': 9,
|
||||
'ma_fast': 10,
|
||||
'ma_slow': 30,
|
||||
'min_confirmations': 2,
|
||||
'require_rsi': True,
|
||||
'require_macd': True,
|
||||
'require_ma': True
|
||||
}
|
||||
)
|
||||
|
||||
def test_initialization(self, strategy):
|
||||
"""Test strategy initialization."""
|
||||
assert strategy.rsi_period == 14
|
||||
assert strategy.macd_fast == 12
|
||||
assert strategy.ma_fast == 10
|
||||
assert strategy.min_confirmations == 2
|
||||
|
||||
def test_on_tick_insufficient_data(self, strategy):
|
||||
"""Test that strategy returns None with insufficient data."""
|
||||
signal = strategy.on_tick(
|
||||
symbol="BTC/USD",
|
||||
price=Decimal("50000"),
|
||||
timeframe="1h",
|
||||
data={'volume': 1000}
|
||||
)
|
||||
assert signal is None
|
||||
|
||||
def test_min_confirmations_requirement(self, strategy):
|
||||
"""Test that signal requires minimum confirmations."""
|
||||
# This would require actual price history to generate real signals
|
||||
# For now, we test the structure
|
||||
assert strategy.min_confirmations == 2
|
||||
assert strategy.require_rsi is True
|
||||
assert strategy.require_macd is True
|
||||
assert strategy.require_ma is True
|
||||
|
||||
def test_strategy_metadata(self, strategy):
|
||||
"""Test strategy metadata."""
|
||||
assert strategy.name == "test_confirmed"
|
||||
assert strategy.enabled is False
|
||||
|
||||
53
tests/unit/strategies/test_consensus_strategy.py
Normal file
53
tests/unit/strategies/test_consensus_strategy.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Tests for Consensus (ensemble) strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from src.strategies.ensemble.consensus_strategy import ConsensusStrategy
|
||||
from src.strategies.base import SignalType
|
||||
|
||||
|
||||
class TestConsensusStrategy:
|
||||
"""Tests for ConsensusStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create Consensus strategy instance."""
|
||||
return ConsensusStrategy(
|
||||
name="test_consensus",
|
||||
parameters={
|
||||
'strategy_names': ['rsi', 'macd'],
|
||||
'min_consensus': 2,
|
||||
'use_weights': True,
|
||||
'min_weight': 0.3,
|
||||
'exclude_strategies': []
|
||||
}
|
||||
)
|
||||
|
||||
def test_initialization(self, strategy):
|
||||
"""Test strategy initialization."""
|
||||
assert strategy.min_consensus == 2
|
||||
assert strategy.use_weights is True
|
||||
assert strategy.min_weight == 0.3
|
||||
|
||||
def test_on_tick_no_strategies(self, strategy):
|
||||
"""Test that strategy handles empty strategy list."""
|
||||
# Strategy should handle cases where no strategies are available
|
||||
signal = strategy.on_tick(
|
||||
symbol="BTC/USD",
|
||||
price=Decimal("50000"),
|
||||
timeframe="1h",
|
||||
data={'volume': 1000}
|
||||
)
|
||||
# May return None if no strategies available or no consensus
|
||||
assert signal is None or isinstance(signal, (type(None), object))
|
||||
|
||||
def test_strategy_metadata(self, strategy):
|
||||
"""Test strategy metadata."""
|
||||
assert strategy.name == "test_consensus"
|
||||
assert strategy.enabled is False
|
||||
|
||||
def test_consensus_calculation(self, strategy):
|
||||
"""Test consensus calculation parameters."""
|
||||
assert strategy.min_consensus == 2
|
||||
assert strategy.use_weights is True
|
||||
|
||||
52
tests/unit/strategies/test_dca_strategy.py
Normal file
52
tests/unit/strategies/test_dca_strategy.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Tests for DCA strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, timedelta
|
||||
from src.strategies.dca.dca_strategy import DCAStrategy
|
||||
|
||||
|
||||
def test_dca_strategy_initialization():
|
||||
"""Test DCA strategy initializes correctly."""
|
||||
strategy = DCAStrategy("Test DCA", {"amount": 10, "interval": "daily"})
|
||||
assert strategy.name == "Test DCA"
|
||||
assert strategy.amount == Decimal("10")
|
||||
assert strategy.interval == "daily"
|
||||
|
||||
|
||||
def test_dca_daily_interval():
|
||||
"""Test DCA with daily interval."""
|
||||
strategy = DCAStrategy("Daily DCA", {"amount": 10, "interval": "daily"})
|
||||
assert strategy.interval_delta == timedelta(days=1)
|
||||
|
||||
|
||||
def test_dca_weekly_interval():
|
||||
"""Test DCA with weekly interval."""
|
||||
strategy = DCAStrategy("Weekly DCA", {"amount": 10, "interval": "weekly"})
|
||||
assert strategy.interval_delta == timedelta(weeks=1)
|
||||
|
||||
|
||||
def test_dca_monthly_interval():
|
||||
"""Test DCA with monthly interval."""
|
||||
strategy = DCAStrategy("Monthly DCA", {"amount": 10, "interval": "monthly"})
|
||||
assert strategy.interval_delta == timedelta(days=30)
|
||||
|
||||
|
||||
def test_dca_signal_generation():
|
||||
"""Test DCA generates buy signals."""
|
||||
strategy = DCAStrategy("Test DCA", {"amount": 10, "interval": "daily"})
|
||||
strategy.last_purchase_time = None
|
||||
|
||||
signal = strategy.on_tick("BTC/USD", Decimal("100"), "1h", {})
|
||||
assert signal is not None
|
||||
assert signal.signal_type.value == "buy"
|
||||
assert signal.quantity == Decimal("0.1") # 10 / 100
|
||||
|
||||
|
||||
def test_dca_interval_respect():
|
||||
"""Test DCA respects interval timing."""
|
||||
strategy = DCAStrategy("Test DCA", {"amount": 10, "interval": "daily"})
|
||||
strategy.last_purchase_time = datetime.utcnow() - timedelta(hours=12)
|
||||
|
||||
signal = strategy.on_tick("BTC/USD", Decimal("100"), "1h", {})
|
||||
assert signal is None # Should not generate signal yet
|
||||
59
tests/unit/strategies/test_divergence_strategy.py
Normal file
59
tests/unit/strategies/test_divergence_strategy.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Tests for Divergence strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from src.strategies.technical.divergence_strategy import DivergenceStrategy
|
||||
from src.strategies.base import SignalType
|
||||
|
||||
|
||||
class TestDivergenceStrategy:
|
||||
"""Tests for DivergenceStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create Divergence strategy instance."""
|
||||
return DivergenceStrategy(
|
||||
name="test_divergence",
|
||||
parameters={
|
||||
'indicator_type': 'rsi',
|
||||
'rsi_period': 14,
|
||||
'lookback': 20,
|
||||
'min_swings': 2,
|
||||
'min_confidence': 0.5
|
||||
}
|
||||
)
|
||||
|
||||
def test_initialization(self, strategy):
|
||||
"""Test strategy initialization."""
|
||||
assert strategy.indicator_type == 'rsi'
|
||||
assert strategy.rsi_period == 14
|
||||
assert strategy.lookback == 20
|
||||
assert strategy.min_swings == 2
|
||||
assert strategy.min_confidence == 0.5
|
||||
|
||||
def test_on_tick_insufficient_data(self, strategy):
|
||||
"""Test that strategy returns None with insufficient data."""
|
||||
signal = strategy.on_tick(
|
||||
symbol="BTC/USD",
|
||||
price=Decimal("50000"),
|
||||
timeframe="1h",
|
||||
data={'volume': 1000}
|
||||
)
|
||||
assert signal is None
|
||||
|
||||
def test_indicator_type_selection(self, strategy):
|
||||
"""Test indicator type selection."""
|
||||
assert strategy.indicator_type == 'rsi'
|
||||
|
||||
# Test MACD indicator type
|
||||
macd_strategy = DivergenceStrategy(
|
||||
name="test_divergence_macd",
|
||||
parameters={'indicator_type': 'macd'}
|
||||
)
|
||||
assert macd_strategy.indicator_type == 'macd'
|
||||
|
||||
def test_strategy_metadata(self, strategy):
|
||||
"""Test strategy metadata."""
|
||||
assert strategy.name == "test_divergence"
|
||||
assert strategy.enabled is False
|
||||
|
||||
69
tests/unit/strategies/test_grid_strategy.py
Normal file
69
tests/unit/strategies/test_grid_strategy.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Tests for Grid strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from src.strategies.grid.grid_strategy import GridStrategy
|
||||
|
||||
|
||||
def test_grid_strategy_initialization():
|
||||
"""Test Grid strategy initializes correctly."""
|
||||
strategy = GridStrategy("Test Grid", {
|
||||
"grid_spacing": 1,
|
||||
"num_levels": 10,
|
||||
"profit_target": 2
|
||||
})
|
||||
assert strategy.name == "Test Grid"
|
||||
assert strategy.grid_spacing == Decimal("0.01")
|
||||
assert strategy.num_levels == 10
|
||||
|
||||
|
||||
def test_grid_levels_calculation():
|
||||
"""Test grid levels are calculated correctly."""
|
||||
strategy = GridStrategy("Test Grid", {
|
||||
"grid_spacing": 1,
|
||||
"num_levels": 5,
|
||||
"center_price": 100
|
||||
})
|
||||
|
||||
strategy._update_grid_levels(Decimal("100"))
|
||||
assert len(strategy.buy_levels) == 5
|
||||
assert len(strategy.sell_levels) == 5
|
||||
|
||||
# Buy levels should be below center
|
||||
assert all(level < Decimal("100") for level in strategy.buy_levels)
|
||||
# Sell levels should be above center
|
||||
assert all(level > Decimal("100") for level in strategy.sell_levels)
|
||||
|
||||
|
||||
def test_grid_buy_signal():
|
||||
"""Test grid generates buy signal at lower level."""
|
||||
strategy = GridStrategy("Test Grid", {
|
||||
"grid_spacing": 1,
|
||||
"num_levels": 5,
|
||||
"center_price": 100,
|
||||
"position_size": Decimal("0.1")
|
||||
})
|
||||
|
||||
# Price at buy level
|
||||
signal = strategy.on_tick("BTC/USD", Decimal("99"), "1h", {})
|
||||
assert signal is not None
|
||||
assert signal.signal_type.value == "buy"
|
||||
|
||||
|
||||
def test_grid_profit_taking():
|
||||
"""Test grid takes profit at target."""
|
||||
strategy = GridStrategy("Test Grid", {
|
||||
"grid_spacing": 1,
|
||||
"num_levels": 5,
|
||||
"profit_target": 2
|
||||
})
|
||||
|
||||
# Simulate position
|
||||
entry_price = Decimal("100")
|
||||
strategy.positions[entry_price] = Decimal("0.1")
|
||||
|
||||
# Price with profit
|
||||
signal = strategy.on_tick("BTC/USD", Decimal("102"), "1h", {})
|
||||
assert signal is not None
|
||||
assert signal.signal_type.value == "sell"
|
||||
assert entry_price not in strategy.positions # Position removed
|
||||
45
tests/unit/strategies/test_macd_strategy.py
Normal file
45
tests/unit/strategies/test_macd_strategy.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Tests for MACD strategy."""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from src.strategies.technical.macd_strategy import MACDStrategy
|
||||
|
||||
|
||||
class TestMACDStrategy:
|
||||
"""Tests for MACDStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create MACD strategy instance."""
|
||||
return MACDStrategy(
|
||||
strategy_id=1,
|
||||
name="test_macd",
|
||||
symbol="BTC/USD",
|
||||
timeframe="1h",
|
||||
parameters={
|
||||
"fast_period": 12,
|
||||
"slow_period": 26,
|
||||
"signal_period": 9
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_macd_strategy_initialization(self, strategy):
|
||||
"""Test MACD strategy initialization."""
|
||||
assert strategy.fast_period == 12
|
||||
assert strategy.slow_period == 26
|
||||
assert strategy.signal_period == 9
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_signal(self, strategy):
|
||||
"""Test signal generation."""
|
||||
# Create minimal data
|
||||
data = pd.DataFrame({
|
||||
'close': [100 + i * 0.1 for i in range(50)]
|
||||
})
|
||||
strategy.current_data = data
|
||||
|
||||
signal = await strategy.generate_signal()
|
||||
assert "signal" in signal
|
||||
assert signal["signal"] in ["buy", "sell", "hold"]
|
||||
|
||||
72
tests/unit/strategies/test_momentum_strategy.py
Normal file
72
tests/unit/strategies/test_momentum_strategy.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Tests for Momentum strategy."""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
import pandas as pd
|
||||
from src.strategies.momentum.momentum_strategy import MomentumStrategy
|
||||
|
||||
|
||||
def test_momentum_strategy_initialization():
|
||||
"""Test Momentum strategy initializes correctly."""
|
||||
strategy = MomentumStrategy("Test Momentum", {
|
||||
"lookback_period": 20,
|
||||
"momentum_threshold": 0.05
|
||||
})
|
||||
assert strategy.name == "Test Momentum"
|
||||
assert strategy.lookback_period == 20
|
||||
assert strategy.momentum_threshold == Decimal("0.05")
|
||||
|
||||
|
||||
def test_momentum_calculation():
|
||||
"""Test momentum calculation."""
|
||||
strategy = MomentumStrategy("Test", {"lookback_period": 5})
|
||||
|
||||
# Create price history
|
||||
prices = pd.Series([100, 101, 102, 103, 104, 105])
|
||||
momentum = strategy._calculate_momentum(prices)
|
||||
|
||||
# Should be positive (price increased)
|
||||
assert momentum > 0
|
||||
assert momentum == 0.05 # (105 - 100) / 100
|
||||
|
||||
|
||||
def test_momentum_entry_signal():
|
||||
"""Test momentum generates entry signal."""
|
||||
strategy = MomentumStrategy("Test", {
|
||||
"lookback_period": 5,
|
||||
"momentum_threshold": 0.05,
|
||||
"volume_threshold": 1.0
|
||||
})
|
||||
|
||||
# Build price history with momentum
|
||||
for i in range(10):
|
||||
price = 100 + i * 2 # Strong upward momentum
|
||||
volume = 1000 * (1.5 if i >= 5 else 1.0) # Volume increase
|
||||
strategy.on_tick("BTC/USD", Decimal(str(price)), "1h", {"volume": volume})
|
||||
|
||||
# Should generate buy signal
|
||||
signal = strategy.on_tick("BTC/USD", Decimal("120"), "1h", {"volume": 2000})
|
||||
assert signal is not None
|
||||
assert signal.signal_type.value == "buy"
|
||||
assert strategy._in_position == True
|
||||
|
||||
|
||||
def test_momentum_exit_signal():
|
||||
"""Test momentum generates exit signal on reversal."""
|
||||
strategy = MomentumStrategy("Test", {
|
||||
"lookback_period": 5,
|
||||
"exit_threshold": -0.02
|
||||
})
|
||||
|
||||
strategy._in_position = True
|
||||
strategy._entry_price = Decimal("100")
|
||||
|
||||
# Build history with reversal
|
||||
for i in range(10):
|
||||
price = 100 - i # Downward momentum
|
||||
strategy.on_tick("BTC/USD", Decimal(str(price)), "1h", {"volume": 1000})
|
||||
|
||||
signal = strategy.on_tick("BTC/USD", Decimal("90"), "1h", {"volume": 1000})
|
||||
assert signal is not None
|
||||
assert signal.signal_type.value == "sell"
|
||||
assert strategy._in_position == False
|
||||
45
tests/unit/strategies/test_moving_avg_strategy.py
Normal file
45
tests/unit/strategies/test_moving_avg_strategy.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Tests for Moving Average strategy."""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from src.strategies.technical.moving_avg_strategy import MovingAverageStrategy
|
||||
|
||||
|
||||
class TestMovingAverageStrategy:
|
||||
"""Tests for MovingAverageStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create Moving Average strategy instance."""
|
||||
return MovingAverageStrategy(
|
||||
strategy_id=1,
|
||||
name="test_ma",
|
||||
symbol="BTC/USD",
|
||||
timeframe="1h",
|
||||
parameters={
|
||||
"short_period": 10,
|
||||
"long_period": 30,
|
||||
"ma_type": "SMA"
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ma_strategy_initialization(self, strategy):
|
||||
"""Test Moving Average strategy initialization."""
|
||||
assert strategy.short_period == 10
|
||||
assert strategy.long_period == 30
|
||||
assert strategy.ma_type == "SMA"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_signal(self, strategy):
|
||||
"""Test signal generation."""
|
||||
# Create data with trend
|
||||
data = pd.DataFrame({
|
||||
'close': [100 + i * 0.5 for i in range(50)]
|
||||
})
|
||||
strategy.current_data = data
|
||||
|
||||
signal = await strategy.generate_signal()
|
||||
assert "signal" in signal
|
||||
assert signal["signal"] in ["buy", "sell", "hold"]
|
||||
|
||||
89
tests/unit/strategies/test_pairs_trading.py
Normal file
89
tests/unit/strategies/test_pairs_trading.py
Normal file
@@ -0,0 +1,89 @@
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
from decimal import Decimal
|
||||
from src.strategies.technical.pairs_trading import PairsTradingStrategy
|
||||
from src.strategies.base import SignalType
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pricing_service():
|
||||
service = MagicMock()
|
||||
service.get_ohlcv = MagicMock()
|
||||
return service
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(mock_pricing_service):
|
||||
with patch('src.strategies.technical.pairs_trading.get_pricing_service', return_value=mock_pricing_service):
|
||||
params = {
|
||||
'second_symbol': 'AVAX/USD',
|
||||
'lookback_period': 5,
|
||||
'z_score_threshold': 1.5,
|
||||
'symbol': 'SOL/USD'
|
||||
}
|
||||
strat = PairsTradingStrategy("test_pairs", params)
|
||||
strat.enabled = True
|
||||
return strat
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pairs_trading_short_spread_signal(strategy, mock_pricing_service):
|
||||
# Setup Data
|
||||
# Scenario: SOL (A) pumps relative to AVAX (B) -> Spread widens -> Z-Score High -> Sell A / Buy B
|
||||
|
||||
# Prices for A (SOL): 100, 100, 100, 100, 120 (Pump)
|
||||
ohlcv_a = [
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 120, 120, 120, 120, 1000],
|
||||
]
|
||||
|
||||
# Prices for B (AVAX): 25, 25, 25, 25, 25 (Flat)
|
||||
ohlcv_b = [
|
||||
[0, 25, 25, 25, 25, 1000],
|
||||
[0, 25, 25, 25, 25, 1000],
|
||||
[0, 25, 25, 25, 25, 1000],
|
||||
[0, 25, 25, 25, 25, 1000],
|
||||
[0, 25, 25, 25, 25, 1000],
|
||||
]
|
||||
|
||||
# Spread: 4, 4, 4, 4, 4.8
|
||||
# Mean: 4.16, StdDev: approx small but let's see.
|
||||
# Actually StdDev will be non-zero because of the last value.
|
||||
|
||||
mock_pricing_service.get_ohlcv.side_effect = [ohlcv_a, ohlcv_b]
|
||||
|
||||
# Execute
|
||||
signal = await strategy.on_tick("SOL/USD", Decimal(120), "1h", {})
|
||||
|
||||
# Verify
|
||||
assert signal is not None
|
||||
assert signal.signal_type == SignalType.SELL # Sell Primary (SOL)
|
||||
assert signal.metadata['secondary_action'] == 'buy' # Buy Secondary (AVAX)
|
||||
assert signal.metadata['z_score'] > 1.5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pairs_trading_long_spread_signal(strategy, mock_pricing_service):
|
||||
# Scenario: SOL (A) dumps -> Spread drops -> Z-Score Low -> Buy A / Sell B
|
||||
|
||||
ohlcv_a = [
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 100, 100, 100, 100, 1000],
|
||||
[0, 80, 80, 80, 80, 1000], # Dump
|
||||
]
|
||||
ohlcv_b = [
|
||||
[0, 25, 25, 25, 25, 1000] for _ in range(5)
|
||||
]
|
||||
|
||||
mock_pricing_service.get_ohlcv.side_effect = [ohlcv_a, ohlcv_b]
|
||||
|
||||
signal = await strategy.on_tick("SOL/USD", Decimal(80), "1h", {})
|
||||
|
||||
assert signal is not None
|
||||
assert signal.signal_type == SignalType.BUY # Buy Primary
|
||||
assert signal.metadata['secondary_action'] == 'sell' # Sell Secondary
|
||||
assert signal.metadata['z_score'] < -1.5
|
||||
67
tests/unit/strategies/test_rsi_strategy.py
Normal file
67
tests/unit/strategies/test_rsi_strategy.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Tests for RSI strategy."""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from src.strategies.technical.rsi_strategy import RSIStrategy
|
||||
|
||||
|
||||
class TestRSIStrategy:
|
||||
"""Tests for RSIStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create RSI strategy instance."""
|
||||
return RSIStrategy(
|
||||
strategy_id=1,
|
||||
name="test_rsi",
|
||||
symbol="BTC/USD",
|
||||
timeframe="1h",
|
||||
parameters={
|
||||
"rsi_period": 14,
|
||||
"overbought": 70,
|
||||
"oversold": 30
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_data(self):
|
||||
"""Create sample price data."""
|
||||
dates = pd.date_range(start='2025-01-01', periods=50, freq='1H')
|
||||
# Create data with clear trend for RSI calculation
|
||||
prices = [100 - i * 0.5 for i in range(50)] # Downward trend
|
||||
return pd.DataFrame({
|
||||
'timestamp': dates,
|
||||
'open': prices,
|
||||
'high': [p + 1 for p in prices],
|
||||
'low': [p - 1 for p in prices],
|
||||
'close': prices,
|
||||
'volume': [1000.0] * 50
|
||||
})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rsi_strategy_initialization(self, strategy):
|
||||
"""Test RSI strategy initialization."""
|
||||
assert strategy.rsi_period == 14
|
||||
assert strategy.overbought == 70
|
||||
assert strategy.oversold == 30
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_data(self, strategy, sample_data):
|
||||
"""Test on_data method."""
|
||||
await strategy.on_data(sample_data)
|
||||
assert len(strategy.current_data) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_signal_oversold(self, strategy, sample_data):
|
||||
"""Test signal generation for oversold condition."""
|
||||
await strategy.on_data(sample_data)
|
||||
# Calculate RSI - should be low for downward trend
|
||||
from src.data.indicators import get_indicators
|
||||
indicators = get_indicators()
|
||||
rsi = indicators.rsi(strategy.current_data['close'], period=14)
|
||||
|
||||
# If RSI is low, should generate buy signal
|
||||
signal = await strategy.generate_signal()
|
||||
assert "signal" in signal
|
||||
assert signal["signal"] in ["buy", "sell", "hold"]
|
||||
|
||||
88
tests/unit/strategies/test_trend_filter.py
Normal file
88
tests/unit/strategies/test_trend_filter.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Tests for trend filter functionality."""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from decimal import Decimal
|
||||
from src.strategies.base import BaseStrategy, StrategySignal, SignalType
|
||||
from src.strategies.technical.rsi_strategy import RSIStrategy
|
||||
|
||||
|
||||
class TestTrendFilter:
|
||||
"""Tests for trend filter in BaseStrategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def strategy(self):
|
||||
"""Create strategy instance with trend filter enabled."""
|
||||
strategy = RSIStrategy(
|
||||
name="test_rsi_with_filter",
|
||||
parameters={'use_trend_filter': True}
|
||||
)
|
||||
return strategy
|
||||
|
||||
@pytest.fixture
|
||||
def ohlcv_data(self):
|
||||
"""Create sample OHLCV data."""
|
||||
dates = pd.date_range(start='2025-01-01', periods=50, freq='1H')
|
||||
base_price = 50000
|
||||
return pd.DataFrame({
|
||||
'high': [base_price + 100 + i * 10 for i in range(50)],
|
||||
'low': [base_price - 100 + i * 10 for i in range(50)],
|
||||
'close': [base_price + i * 10 for i in range(50)],
|
||||
'open': [base_price - 50 + i * 10 for i in range(50)],
|
||||
'volume': [1000.0] * 50
|
||||
}, index=dates)
|
||||
|
||||
def test_trend_filter_method_exists(self, strategy):
|
||||
"""Test that apply_trend_filter method exists."""
|
||||
assert hasattr(strategy, 'apply_trend_filter')
|
||||
assert callable(getattr(strategy, 'apply_trend_filter'))
|
||||
|
||||
def test_trend_filter_insufficient_data(self, strategy):
|
||||
"""Test trend filter with insufficient data."""
|
||||
signal = StrategySignal(
|
||||
signal_type=SignalType.BUY,
|
||||
symbol="BTC/USD",
|
||||
strength=0.8,
|
||||
price=Decimal("50000")
|
||||
)
|
||||
|
||||
insufficient_data = pd.DataFrame({
|
||||
'high': [51000],
|
||||
'low': [49000],
|
||||
'close': [50000]
|
||||
})
|
||||
|
||||
# Should allow signal when insufficient data
|
||||
result = strategy.apply_trend_filter(signal, insufficient_data)
|
||||
assert result is not None
|
||||
|
||||
def test_trend_filter_none_data(self, strategy):
|
||||
"""Test trend filter with None data."""
|
||||
signal = StrategySignal(
|
||||
signal_type=SignalType.BUY,
|
||||
symbol="BTC/USD",
|
||||
strength=0.8,
|
||||
price=Decimal("50000")
|
||||
)
|
||||
|
||||
# Should allow signal when no data provided
|
||||
result = strategy.apply_trend_filter(signal, None)
|
||||
assert result is not None
|
||||
|
||||
def test_trend_filter_when_disabled(self, strategy):
|
||||
"""Test that trend filter doesn't filter when disabled."""
|
||||
strategy_no_filter = RSIStrategy(
|
||||
name="test_rsi_no_filter",
|
||||
parameters={'use_trend_filter': False}
|
||||
)
|
||||
|
||||
signal = StrategySignal(
|
||||
signal_type=SignalType.BUY,
|
||||
symbol="BTC/USD",
|
||||
strength=0.8,
|
||||
price=Decimal("50000")
|
||||
)
|
||||
|
||||
result = strategy_no_filter.apply_trend_filter(signal, None)
|
||||
assert result == signal
|
||||
|
||||
Reference in New Issue
Block a user