325 lines
12 KiB
Python
325 lines
12 KiB
Python
|
|
"""Redis Integration Tests for SignalBridgeActor.
|
||
|
|
|
||
|
|
Uses fakeredis to provide a mock Redis server for testing.
|
||
|
|
This tests the full signal flow from Redis to Nautilus.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import Mock, patch, PropertyMock
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
|
||
|
|
class TestRedisConnection:
|
||
|
|
"""Test Redis connection and basic operations."""
|
||
|
|
|
||
|
|
def test_fakeredis_available(self):
|
||
|
|
"""Test that fakeredis is available and working."""
|
||
|
|
import fakeredis
|
||
|
|
|
||
|
|
# Create a fake Redis server
|
||
|
|
server = fakeredis.FakeServer()
|
||
|
|
r = fakeredis.FakeStrictRedis(server=server)
|
||
|
|
|
||
|
|
# Test basic operations
|
||
|
|
r.set('test_key', 'test_value')
|
||
|
|
value = r.get('test_key')
|
||
|
|
|
||
|
|
assert value.decode() == 'test_value'
|
||
|
|
|
||
|
|
def test_fakeredis_streams(self):
|
||
|
|
"""Test that fakeredis supports Redis Streams."""
|
||
|
|
import fakeredis
|
||
|
|
|
||
|
|
server = fakeredis.FakeServer()
|
||
|
|
r = fakeredis.FakeStrictRedis(server=server)
|
||
|
|
|
||
|
|
# Add entry to stream
|
||
|
|
r.xadd('test_stream', {'data': 'value1'})
|
||
|
|
r.xadd('test_stream', {'data': 'value2'})
|
||
|
|
|
||
|
|
# Read from stream
|
||
|
|
messages = r.xread({'test_stream': '0'}, count=10)
|
||
|
|
|
||
|
|
assert len(messages) == 1
|
||
|
|
stream_name, entries = messages[0]
|
||
|
|
assert len(entries) == 2
|
||
|
|
|
||
|
|
|
||
|
|
class TestSignalBridgeWithRedis:
|
||
|
|
"""Test SignalBridgeActor with Redis integration."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def redis_setup(self):
|
||
|
|
"""Set up fake Redis server and SignalBridgeActor."""
|
||
|
|
import fakeredis
|
||
|
|
from nautilus_dolphin.nautilus.signal_bridge import SignalBridgeActor, SignalBridgeConfig
|
||
|
|
from nautilus_trader.common.component import TestClock
|
||
|
|
|
||
|
|
# Create fake Redis server
|
||
|
|
server = fakeredis.FakeServer()
|
||
|
|
fake_redis = fakeredis.FakeStrictRedis(server=server)
|
||
|
|
|
||
|
|
# Create SignalBridgeActor with mocked Redis
|
||
|
|
config = SignalBridgeConfig(
|
||
|
|
redis_url='redis://localhost:6379',
|
||
|
|
stream_key='dolphin:signals:stream',
|
||
|
|
max_signal_age_sec=60
|
||
|
|
)
|
||
|
|
actor = SignalBridgeActor(config)
|
||
|
|
|
||
|
|
# Mock the clock
|
||
|
|
clock = TestClock()
|
||
|
|
clock.set_time(int(datetime.now().timestamp() * 1e9))
|
||
|
|
|
||
|
|
with patch.object(type(actor), 'clock', new_callable=PropertyMock) as mock_clock:
|
||
|
|
mock_clock.return_value = clock
|
||
|
|
|
||
|
|
# Replace Redis connection with fake
|
||
|
|
actor._redis = fake_redis
|
||
|
|
|
||
|
|
yield actor, fake_redis, clock
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_signal_bridge_consumes_signal(self, redis_setup):
|
||
|
|
"""Test that SignalBridgeActor consumes signals from Redis."""
|
||
|
|
actor, fake_redis, clock = redis_setup
|
||
|
|
|
||
|
|
# Create a valid signal
|
||
|
|
signal = {
|
||
|
|
'timestamp': int(clock.timestamp_ns() / 1e9), # seconds
|
||
|
|
'asset': 'BTCUSDT',
|
||
|
|
'direction': 'SHORT',
|
||
|
|
'vel_div': -0.025,
|
||
|
|
'strength': 0.75,
|
||
|
|
'irp_alignment': 0.5,
|
||
|
|
'direction_confirm': True,
|
||
|
|
'lookback_momentum': 0.0001
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add signal to Redis stream
|
||
|
|
fake_redis.xadd(
|
||
|
|
'dolphin:signals:stream',
|
||
|
|
{'signal': json.dumps(signal)}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Manually call _consume_stream (simulated)
|
||
|
|
# Read from stream
|
||
|
|
messages = fake_redis.xread({'dolphin:signals:stream': '0'}, count=10)
|
||
|
|
|
||
|
|
assert len(messages) == 1
|
||
|
|
stream_name, entries = messages[0]
|
||
|
|
assert len(entries) == 1
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_signal_bridge_validates_signal(self, redis_setup):
|
||
|
|
"""Test signal validation in SignalBridgeActor."""
|
||
|
|
actor, fake_redis, clock = redis_setup
|
||
|
|
|
||
|
|
# Create a valid signal
|
||
|
|
valid_signal = {
|
||
|
|
'timestamp': int(clock.timestamp_ns() / 1e9),
|
||
|
|
'asset': 'BTCUSDT',
|
||
|
|
'direction': 'SHORT',
|
||
|
|
'vel_div': -0.025,
|
||
|
|
'strength': 0.75,
|
||
|
|
'irp_alignment': 0.5,
|
||
|
|
'direction_confirm': True,
|
||
|
|
'lookback_momentum': 0.0001
|
||
|
|
}
|
||
|
|
|
||
|
|
# Test validation
|
||
|
|
is_valid = actor._validate_signal(valid_signal)
|
||
|
|
assert is_valid is True
|
||
|
|
|
||
|
|
# Test invalid signal (missing field)
|
||
|
|
invalid_signal = {
|
||
|
|
'timestamp': int(clock.timestamp_ns() / 1e9),
|
||
|
|
'asset': 'BTCUSDT'
|
||
|
|
}
|
||
|
|
|
||
|
|
is_valid = actor._validate_signal(invalid_signal)
|
||
|
|
assert is_valid is False
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_signal_bridge_rejects_stale_signal(self, redis_setup):
|
||
|
|
"""Test that stale signals are rejected."""
|
||
|
|
actor, fake_redis, clock = redis_setup
|
||
|
|
|
||
|
|
# Create a stale signal (70 seconds old, older than max_signal_age_sec=60)
|
||
|
|
stale_signal = {
|
||
|
|
'timestamp': int((clock.timestamp_ns() / 1e9) - 70),
|
||
|
|
'asset': 'BTCUSDT',
|
||
|
|
'direction': 'SHORT',
|
||
|
|
'vel_div': -0.025,
|
||
|
|
'strength': 0.75,
|
||
|
|
'irp_alignment': 0.5,
|
||
|
|
'direction_confirm': True,
|
||
|
|
'lookback_momentum': 0.0001
|
||
|
|
}
|
||
|
|
|
||
|
|
is_valid = actor._validate_signal(stale_signal)
|
||
|
|
assert is_valid is False
|
||
|
|
|
||
|
|
|
||
|
|
class TestSignalFlow:
|
||
|
|
"""Test complete signal flow from Redis to strategy."""
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_full_signal_flow(self):
|
||
|
|
"""Test complete signal flow: Redis -> SignalBridge -> Strategy."""
|
||
|
|
import fakeredis
|
||
|
|
from nautilus_dolphin.nautilus.signal_bridge import SignalBridgeActor, SignalBridgeConfig
|
||
|
|
from nautilus_dolphin.nautilus.strategy import DolphinExecutionStrategyForTesting
|
||
|
|
from nautilus_trader.common.component import TestClock
|
||
|
|
|
||
|
|
# Set up fake Redis
|
||
|
|
server = fakeredis.FakeServer()
|
||
|
|
fake_redis = fakeredis.FakeStrictRedis(server=server)
|
||
|
|
|
||
|
|
# Create SignalBridgeActor
|
||
|
|
bridge_config = SignalBridgeConfig(
|
||
|
|
redis_url='redis://localhost:6379',
|
||
|
|
stream_key='dolphin:signals:stream',
|
||
|
|
max_signal_age_sec=60
|
||
|
|
)
|
||
|
|
bridge_actor = SignalBridgeActor(bridge_config)
|
||
|
|
bridge_actor._redis = fake_redis
|
||
|
|
|
||
|
|
# Create strategy
|
||
|
|
strategy_config = {
|
||
|
|
'venue': 'BINANCE_FUTURES',
|
||
|
|
'acb_enabled': True,
|
||
|
|
'max_leverage': 5.0
|
||
|
|
}
|
||
|
|
strategy = DolphinExecutionStrategyForTesting(strategy_config)
|
||
|
|
|
||
|
|
# Set up clock
|
||
|
|
clock = TestClock()
|
||
|
|
clock.set_time(int(datetime.now().timestamp() * 1e9))
|
||
|
|
|
||
|
|
with patch.object(type(bridge_actor), 'clock', new_callable=PropertyMock) as mock_clock:
|
||
|
|
mock_clock.return_value = clock
|
||
|
|
|
||
|
|
# Create and publish signal to Redis
|
||
|
|
signal = {
|
||
|
|
'timestamp': int(clock.timestamp_ns() / 1e9),
|
||
|
|
'asset': 'BTCUSDT',
|
||
|
|
'direction': 'SHORT',
|
||
|
|
'vel_div': -0.025,
|
||
|
|
'strength': 0.75,
|
||
|
|
'irp_alignment': 0.5,
|
||
|
|
'direction_confirm': True,
|
||
|
|
'lookback_momentum': 0.0001,
|
||
|
|
'price': 50000.0
|
||
|
|
}
|
||
|
|
|
||
|
|
fake_redis.xadd(
|
||
|
|
'dolphin:signals:stream',
|
||
|
|
{'signal': json.dumps(signal)}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Verify signal is in Redis
|
||
|
|
messages = fake_redis.xread({'dolphin:signals:stream': '0'}, count=10)
|
||
|
|
assert len(messages) == 1
|
||
|
|
|
||
|
|
# Verify strategy would process it (filters)
|
||
|
|
can_trade = strategy._should_trade(signal)
|
||
|
|
assert can_trade == "" # Empty string means can trade
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_multiple_signals_processing(self):
|
||
|
|
"""Test processing multiple signals from Redis."""
|
||
|
|
import fakeredis
|
||
|
|
from nautilus_dolphin.nautilus.signal_bridge import SignalBridgeActor, SignalBridgeConfig
|
||
|
|
from nautilus_trader.common.component import TestClock
|
||
|
|
|
||
|
|
# Set up fake Redis
|
||
|
|
server = fakeredis.FakeServer()
|
||
|
|
fake_redis = fakeredis.FakeStrictRedis(server=server)
|
||
|
|
|
||
|
|
# Create SignalBridgeActor
|
||
|
|
bridge_config = SignalBridgeConfig(
|
||
|
|
redis_url='redis://localhost:6379',
|
||
|
|
stream_key='dolphin:signals:stream',
|
||
|
|
max_signal_age_sec=60
|
||
|
|
)
|
||
|
|
bridge_actor = SignalBridgeActor(bridge_config)
|
||
|
|
bridge_actor._redis = fake_redis
|
||
|
|
|
||
|
|
clock = TestClock()
|
||
|
|
clock.set_time(int(datetime.now().timestamp() * 1e9))
|
||
|
|
|
||
|
|
with patch.object(type(bridge_actor), 'clock', new_callable=PropertyMock) as mock_clock:
|
||
|
|
mock_clock.return_value = clock
|
||
|
|
|
||
|
|
# Publish multiple signals
|
||
|
|
assets = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']
|
||
|
|
for i, asset in enumerate(assets):
|
||
|
|
signal = {
|
||
|
|
'timestamp': int(clock.timestamp_ns() / 1e9),
|
||
|
|
'asset': asset,
|
||
|
|
'direction': 'SHORT',
|
||
|
|
'vel_div': -0.025 - (i * 0.01),
|
||
|
|
'strength': 0.75,
|
||
|
|
'irp_alignment': 0.5,
|
||
|
|
'direction_confirm': True,
|
||
|
|
'lookback_momentum': 0.0001,
|
||
|
|
'price': 50000.0 - (i * 1000)
|
||
|
|
}
|
||
|
|
fake_redis.xadd(
|
||
|
|
'dolphin:signals:stream',
|
||
|
|
{'signal': json.dumps(signal)}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Verify all signals are in Redis
|
||
|
|
messages = fake_redis.xread({'dolphin:signals:stream': '0'}, count=10)
|
||
|
|
assert len(messages) == 1
|
||
|
|
stream_name, entries = messages[0]
|
||
|
|
assert len(entries) == 3
|
||
|
|
|
||
|
|
|
||
|
|
class TestRedisConnectionConfig:
|
||
|
|
"""Test Redis connection configuration."""
|
||
|
|
|
||
|
|
def test_signal_bridge_config_defaults(self):
|
||
|
|
"""Test SignalBridgeConfig default values."""
|
||
|
|
from nautilus_dolphin.nautilus.signal_bridge import SignalBridgeConfig
|
||
|
|
|
||
|
|
config = SignalBridgeConfig()
|
||
|
|
|
||
|
|
assert config.redis_url == "redis://localhost:6379"
|
||
|
|
assert config.stream_key == "dolphin:signals:stream"
|
||
|
|
assert config.max_signal_age_sec == 10
|
||
|
|
|
||
|
|
def test_signal_bridge_config_custom(self):
|
||
|
|
"""Test SignalBridgeConfig custom values."""
|
||
|
|
from nautilus_dolphin.nautilus.signal_bridge import SignalBridgeConfig
|
||
|
|
|
||
|
|
config = SignalBridgeConfig(
|
||
|
|
redis_url='redis://custom:6380',
|
||
|
|
stream_key='custom:stream',
|
||
|
|
max_signal_age_sec=30
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.redis_url == "redis://custom:6380"
|
||
|
|
assert config.stream_key == "custom:stream"
|
||
|
|
assert config.max_signal_age_sec == 30
|
||
|
|
|
||
|
|
def test_config_yaml_matches(self):
|
||
|
|
"""Test that config.yaml matches SignalBridgeConfig."""
|
||
|
|
import yaml
|
||
|
|
from nautilus_dolphin.nautilus.signal_bridge import SignalBridgeConfig
|
||
|
|
|
||
|
|
# Load config.yaml
|
||
|
|
with open('config/config.yaml', 'r') as f:
|
||
|
|
config = yaml.safe_load(f)
|
||
|
|
|
||
|
|
signal_bridge_config = config.get('signal_bridge', {})
|
||
|
|
|
||
|
|
# Verify keys match
|
||
|
|
assert 'redis_url' in signal_bridge_config
|
||
|
|
assert 'stream_key' in signal_bridge_config
|
||
|
|
assert 'max_signal_age_sec' in signal_bridge_config
|