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