90 lines
2.9 KiB
Python
90 lines
2.9 KiB
Python
|
|
|
||
|
|
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
|