brighter-trading/tests/test_execution_loop.py

323 lines
12 KiB
Python

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