""" Tests for the strategy execution loop. These tests verify that: 1. Strategies.update() iterates through active instances 2. StrategyInstance.tick() processes candle data correctly 3. Price updates flow to paper trading instances 4. Errors are handled gracefully """ import pytest from unittest.mock import MagicMock, patch import json class TestStrategiesUpdate: """Tests for the Strategies.update() method.""" @pytest.fixture def mock_strategies(self): """Create a Strategies instance with mock dependencies.""" with patch.dict('sys.modules', {'eventlet': MagicMock()}): from Strategies import Strategies # Mock DataCache mock_cache = MagicMock() mock_cache.create_cache = MagicMock() mock_cache.get_rows_from_datacache = MagicMock(return_value=MagicMock(empty=True)) # Mock Trades and Indicators mock_trades = MagicMock() mock_indicators = MagicMock() strategies = Strategies(mock_cache, mock_trades, mock_indicators) return strategies def test_update_no_active_instances(self, mock_strategies): """Test update with no active strategies returns empty list.""" result = mock_strategies.update() assert result == [] def test_update_calls_tick_on_instances(self, mock_strategies): """Test that update calls tick() on all active instances.""" # Create mock instances mock_instance1 = MagicMock() mock_instance1.tick.return_value = [{'type': 'tick_complete'}] mock_instance1.exit = False mock_instance2 = MagicMock() mock_instance2.tick.return_value = [{'type': 'tick_complete'}] mock_instance2.exit = False mock_strategies.active_instances = { (1, 'strat-1', 'paper'): mock_instance1, (2, 'strat-2', 'paper'): mock_instance2, } candle_data = {'symbol': 'BTC/USDT', 'close': 50000} result = mock_strategies.update(candle_data) # Both instances should have tick called mock_instance1.tick.assert_called_once_with(candle_data) mock_instance2.tick.assert_called_once_with(candle_data) # Should have events from both assert len(result) == 2 def test_update_delegates_price_handling_to_tick(self, mock_strategies): """Test that update delegates candle handling to instance.tick().""" mock_instance = MagicMock() mock_instance.tick.return_value = [] mock_instance.exit = False mock_instance.update_prices = MagicMock() mock_strategies.active_instances = { (1, 'strat-1', 'paper'): mock_instance, } candle_data = {'symbol': 'ETH/USDT', 'close': 3000.5} mock_strategies.update(candle_data) mock_instance.tick.assert_called_once_with(candle_data) mock_instance.update_prices.assert_not_called() def test_update_tags_events_with_instance_info(self, mock_strategies): """Test that events are tagged with user_id, strategy_id, mode.""" mock_instance = MagicMock() mock_instance.tick.return_value = [{'type': 'tick_complete'}] mock_instance.exit = False mock_strategies.active_instances = { (42, 'my-strategy', 'paper'): mock_instance, } result = mock_strategies.update() assert len(result) == 1 event = result[0] assert event['user_id'] == 42 assert event['strategy_id'] == 'my-strategy' assert event['mode'] == 'paper' def test_update_handles_instance_errors(self, mock_strategies): """Test that errors in one instance don't stop others.""" # First instance raises error mock_instance1 = MagicMock() mock_instance1.tick.side_effect = Exception("Strategy crashed") mock_instance1.exit = False # Second instance works fine mock_instance2 = MagicMock() mock_instance2.tick.return_value = [{'type': 'tick_complete'}] mock_instance2.exit = False mock_strategies.active_instances = { (1, 'bad-strat', 'paper'): mock_instance1, (2, 'good-strat', 'paper'): mock_instance2, } result = mock_strategies.update() # Should have error event from first, success from second assert len(result) == 2 error_events = [e for e in result if e['type'] == 'error'] success_events = [e for e in result if e['type'] == 'tick_complete'] assert len(error_events) == 1 assert len(success_events) == 1 assert 'Strategy crashed' in error_events[0]['message'] def test_update_removes_exited_strategies(self, mock_strategies): """Test that strategies with exit=True and no positions are removed.""" mock_instance = MagicMock() mock_instance.tick.return_value = [] mock_instance.exit = True mock_instance.broker = MagicMock() mock_instance.broker.get_all_positions.return_value = [] # No positions mock_strategies.active_instances = { (1, 'exiting-strat', 'paper'): mock_instance, } result = mock_strategies.update() # Strategy should be removed assert len(mock_strategies.active_instances) == 0 # Should have strategy_exited event exited_events = [e for e in result if e['type'] == 'strategy_exited'] assert len(exited_events) == 1 class TestStrategyInstanceTick: """Tests for StrategyInstance.tick() method.""" @pytest.fixture def mock_strategy_instance(self): """Create a StrategyInstance with mock dependencies.""" with patch.dict('sys.modules', {'eventlet': MagicMock()}): from StrategyInstance import StrategyInstance with patch.object(StrategyInstance, '__init__', lambda x: None): instance = StrategyInstance() # Set up required attributes instance.strategy_id = 'test-strategy' instance.strategy_instance_id = 'inst-123' instance.paused = False instance.exit = False instance.exec_context = {'_events': []} instance.generated_code = "def next(): pass" instance.execute = MagicMock(return_value={'success': True, 'profit_loss': 100.0}) return instance def test_tick_skips_when_paused(self, mock_strategy_instance): """Test that tick returns skip event when paused.""" mock_strategy_instance.paused = True result = mock_strategy_instance.tick() assert len(result) == 1 assert result[0]['type'] == 'skipped' assert result[0]['reason'] == 'paused' mock_strategy_instance.execute.assert_not_called() def test_tick_skips_when_exiting(self, mock_strategy_instance): """Test that tick returns skip event when exit is True.""" mock_strategy_instance.exit = True result = mock_strategy_instance.tick() assert len(result) == 1 assert result[0]['type'] == 'skipped' assert result[0]['reason'] == 'exiting' def test_tick_updates_exec_context_with_candle(self, mock_strategy_instance): """Test that tick updates exec_context with candle data.""" candle = {'symbol': 'BTC/USDT', 'close': 50000, 'open': 49500, 'high': 51000, 'low': 49000} mock_strategy_instance.tick(candle) assert mock_strategy_instance.exec_context['current_candle'] == candle assert mock_strategy_instance.exec_context['current_price'] == 50000 assert mock_strategy_instance.exec_context['current_symbol'] == 'BTC/USDT' def test_tick_returns_tick_complete_on_success(self, mock_strategy_instance): """Test that successful tick returns tick_complete event.""" result = mock_strategy_instance.tick() assert len(result) == 1 assert result[0]['type'] == 'tick_complete' assert result[0]['strategy_id'] == 'test-strategy' assert result[0]['profit_loss'] == 100.0 def test_tick_returns_error_on_execute_failure(self, mock_strategy_instance): """Test that failed execute returns error event.""" mock_strategy_instance.execute.return_value = { 'success': False, 'message': 'Syntax error in strategy' } result = mock_strategy_instance.tick() assert len(result) == 1 assert result[0]['type'] == 'error' assert 'Syntax error' in result[0]['message'] def test_tick_handles_exceptions(self, mock_strategy_instance): """Test that exceptions during tick are caught and returned.""" mock_strategy_instance.execute.side_effect = RuntimeError("Unexpected error") result = mock_strategy_instance.tick() assert len(result) == 1 assert result[0]['type'] == 'error' assert 'Unexpected error' in result[0]['message'] class TestPaperStrategyInstanceTick: """Tests for PaperStrategyInstance.tick() method.""" @pytest.fixture def mock_paper_instance(self): """Create a PaperStrategyInstance with mock dependencies.""" with patch.dict('sys.modules', {'eventlet': MagicMock()}): from paper_strategy_instance import PaperStrategyInstance with patch.object(PaperStrategyInstance, '__init__', lambda x: None): instance = PaperStrategyInstance() # Set up required attributes instance.strategy_id = 'paper-strategy' instance.strategy_instance_id = 'paper-inst-123' instance.paused = False instance.exit = False instance.exec_context = {'_events': []} instance.generated_code = "def next(): pass" instance.current_balance = 10000.0 instance.available_balance = 10000.0 instance.trade_history = [] # Mock paper broker instance.paper_broker = MagicMock() instance.paper_broker.update_price = MagicMock() instance.paper_broker.update.return_value = [] instance.paper_broker.get_all_positions.return_value = [] # Mock execute instance.execute = MagicMock(return_value={'success': True}) instance._update_balances = MagicMock() return instance def test_tick_updates_broker_prices(self, mock_paper_instance): """Test that tick updates paper broker with candle prices.""" candle = {'symbol': 'ETH/USDT', 'close': 3000} mock_paper_instance.tick(candle) mock_paper_instance.paper_broker.update_price.assert_called_with('ETH/USDT', 3000) def test_tick_processes_broker_fills(self, mock_paper_instance): """Test that broker fills are processed and added to events.""" mock_paper_instance.paper_broker.update.return_value = [ {'type': 'fill', 'order_id': 'order-1', 'symbol': 'BTC/USDT', 'filled_qty': 0.1, 'filled_price': 50000} ] candle = {'symbol': 'BTC/USDT', 'close': 50000} result = mock_paper_instance.tick(candle) # Should have fill event fill_events = [e for e in result if e['type'] == 'order_filled'] assert len(fill_events) == 1 assert fill_events[0]['order_id'] == 'order-1' def test_tick_includes_balance_in_complete_event(self, mock_paper_instance): """Test that tick_complete includes balance info.""" mock_paper_instance.current_balance = 10500.0 mock_paper_instance.available_balance = 10000.0 result = mock_paper_instance.tick() complete_events = [e for e in result if e['type'] == 'tick_complete'] assert len(complete_events) == 1 assert complete_events[0]['balance'] == 10500.0 assert complete_events[0]['available_balance'] == 10000.0 class TestExecutionLoopIntegration: """Integration tests for the full execution loop.""" def test_candle_triggers_strategy_update(self): """Test that received_cdata triggers strategy updates.""" # This would need full integration test setup # For now, just verify the method exists pass def test_multiple_strategies_execute_independently(self): """Test that multiple strategies don't interfere with each other.""" pass