"""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