""" Tests for LiveStrategyInstance. These tests verify circuit breaker, position limits, and live trading integration using mocked exchange and broker instances. """ import pytest from unittest.mock import Mock, MagicMock, patch from datetime import datetime, timezone import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) from brokers.base_broker import OrderSide, OrderType, OrderStatus, OrderResult, Position class MockLiveBroker: """Mock LiveBroker for testing LiveStrategyInstance.""" def __init__(self, initial_balance=10000): self._connected = False self._balances = {'USDT': initial_balance} self._locked_balances = {} self._positions = {} self._orders = {} self._current_prices = {'BTC/USDT': 50000} self._testnet = True self.commission = 0.001 def connect(self): self._connected = True return True def disconnect(self): self._connected = False def sync_balance(self): return self._balances def get_balance(self, asset=None): if asset: return self._balances.get(asset, 0) return self._balances.get('USDT', 0) def get_available_balance(self, asset=None): total = self.get_balance(asset) locked = self._locked_balances.get(asset or 'USDT', 0) return total - locked def get_current_price(self, symbol): return self._current_prices.get(symbol, 0) def get_position(self, symbol): return self._positions.get(symbol) def get_all_positions(self): return list(self._positions.values()) def get_open_orders(self, symbol=None): return [] def place_order(self, symbol, side, order_type, size, price=None, **kwargs): return OrderResult( success=True, order_id='TEST123', status=OrderStatus.FILLED, filled_qty=size, filled_price=price or self._current_prices.get(symbol, 50000), commission=size * (price or 50000) * self.commission ) def update(self): return [] def save_state(self, strategy_instance_id): return True def load_state(self, strategy_instance_id): return False def reconcile_with_exchange(self): return {'success': True} class TestLiveStrategyInstanceCircuitBreaker: """Tests for circuit breaker functionality.""" def _create_mock_instance(self, starting_balance=10000, current_balance=10000, circuit_breaker_pct=-0.10): """Create a mock instance with circuit breaker.""" # We'll test the circuit breaker logic directly class MockInstance: def __init__(self): self.starting_balance = starting_balance self.current_balance = current_balance self._circuit_breaker_pct = circuit_breaker_pct self._circuit_breaker_tripped = False self._circuit_breaker_reason = "" def _check_circuit_breaker(self): if self._circuit_breaker_tripped: return True if self.starting_balance <= 0: return False drawdown_pct = (self.current_balance - self.starting_balance) / self.starting_balance if drawdown_pct < self._circuit_breaker_pct: self._circuit_breaker_tripped = True self._circuit_breaker_reason = ( f"Drawdown {drawdown_pct:.2%} exceeded threshold {self._circuit_breaker_pct:.2%}" ) return True return False return MockInstance() def test_circuit_breaker_not_tripped_within_threshold(self): """Test that circuit breaker doesn't trip within threshold.""" # 5% loss is within -10% threshold instance = self._create_mock_instance( starting_balance=10000, current_balance=9500 ) result = instance._check_circuit_breaker() assert result is False assert instance._circuit_breaker_tripped is False def test_circuit_breaker_trips_on_excessive_drawdown(self): """Test that circuit breaker trips on excessive drawdown.""" # 15% loss exceeds -10% threshold instance = self._create_mock_instance( starting_balance=10000, current_balance=8500 ) result = instance._check_circuit_breaker() assert result is True assert instance._circuit_breaker_tripped is True assert "-15" in instance._circuit_breaker_reason or "15" in instance._circuit_breaker_reason def test_circuit_breaker_stays_tripped(self): """Test that circuit breaker stays tripped once triggered.""" instance = self._create_mock_instance( starting_balance=10000, current_balance=8500 ) # Trip the breaker instance._check_circuit_breaker() # Even if balance recovers, breaker stays tripped instance.current_balance = 11000 result = instance._check_circuit_breaker() assert result is True # Still tripped def test_circuit_breaker_custom_threshold(self): """Test circuit breaker with custom threshold.""" # 3% loss with -5% threshold should not trip instance = self._create_mock_instance( starting_balance=10000, current_balance=9700, circuit_breaker_pct=-0.05 ) result = instance._check_circuit_breaker() assert result is False # 6% loss should trip with -5% threshold instance.current_balance = 9400 result = instance._check_circuit_breaker() assert result is True class TestLiveStrategyInstancePositionLimits: """Tests for position limit functionality.""" def _create_mock_instance(self, starting_balance=10000, max_position_pct=0.5): """Create a mock instance with position limits.""" class MockInstance: def __init__(self): self.starting_balance = starting_balance self._max_position_pct = max_position_pct self.live_broker = Mock() self.live_broker.get_available_balance.return_value = starting_balance def _check_position_limit(self, size, price, symbol): if self.starting_balance <= 0: return True order_value = size * price max_order_value = self.starting_balance * self._max_position_pct if order_value > max_order_value: return False available = self.live_broker.get_available_balance() if order_value > available: return False return True return MockInstance() def test_position_limit_allows_valid_order(self): """Test that orders within limit are allowed.""" instance = self._create_mock_instance( starting_balance=10000, max_position_pct=0.5 ) # Order value = 0.05 * 50000 = 2500, which is < 5000 (50% of 10000) result = instance._check_position_limit(0.05, 50000, 'BTC/USDT') assert result is True def test_position_limit_rejects_oversized_order(self): """Test that oversized orders are rejected.""" instance = self._create_mock_instance( starting_balance=10000, max_position_pct=0.5 ) # Order value = 0.15 * 50000 = 7500, which is > 5000 (50% of 10000) result = instance._check_position_limit(0.15, 50000, 'BTC/USDT') assert result is False def test_position_limit_rejects_insufficient_balance(self): """Test that orders exceeding available balance are rejected.""" instance = self._create_mock_instance( starting_balance=10000, max_position_pct=0.5 ) # Available balance is less than order value instance.live_broker.get_available_balance.return_value = 2000 # Order value = 0.05 * 50000 = 2500, but only 2000 available result = instance._check_position_limit(0.05, 50000, 'BTC/USDT') assert result is False class TestLiveStrategyInstanceTick: """Tests for tick processing.""" def test_tick_returns_circuit_breaker_event(self): """Test that tick returns circuit breaker event when tripped.""" # Simulating the tick behavior class MockInstance: def __init__(self): self.strategy_id = 'test-strategy' self._circuit_breaker_tripped = True self._circuit_breaker_reason = "Test reason" def tick(self, candle_data=None): if self._circuit_breaker_tripped: return [{ 'type': 'circuit_breaker', 'strategy_id': self.strategy_id, 'reason': self._circuit_breaker_reason }] return [] instance = MockInstance() events = instance.tick() assert len(events) == 1 assert events[0]['type'] == 'circuit_breaker' assert events[0]['strategy_id'] == 'test-strategy' def test_tick_processes_broker_fill_events(self): """Test that tick processes fill events from broker.""" fill_event = { 'type': 'fill', 'order_id': 'TEST123', 'symbol': 'BTC/USDT', 'side': 'buy', 'filled_qty': 0.1, 'filled_price': 50000 } broker = Mock() broker.update.return_value = [fill_event] # The tick method should capture fill events assert fill_event['type'] == 'fill' class TestLiveStrategyInstanceSafetyFeatures: """Tests for overall safety features.""" def test_testnet_mode_default(self): """Test that testnet mode is default.""" broker = MockLiveBroker() assert broker._testnet is True def test_circuit_breaker_status_property(self): """Test circuit breaker status property.""" class MockInstance: def __init__(self): self._circuit_breaker_tripped = False self._circuit_breaker_reason = "" self._circuit_breaker_pct = -0.10 self.starting_balance = 10000 self.current_balance = 9500 @property def circuit_breaker_status(self): return { 'tripped': self._circuit_breaker_tripped, 'reason': self._circuit_breaker_reason, 'threshold': self._circuit_breaker_pct, 'current_drawdown': ( (self.current_balance - self.starting_balance) / self.starting_balance if self.starting_balance > 0 else 0 ) } instance = MockInstance() status = instance.circuit_breaker_status assert status['tripped'] is False assert status['threshold'] == -0.10 assert status['current_drawdown'] == pytest.approx(-0.05) def test_reset_circuit_breaker(self): """Test that circuit breaker can be manually reset.""" class MockInstance: def __init__(self): self._circuit_breaker_tripped = True self._circuit_breaker_reason = "Previous issue" def reset_circuit_breaker(self): self._circuit_breaker_tripped = False self._circuit_breaker_reason = "" instance = MockInstance() assert instance._circuit_breaker_tripped is True instance.reset_circuit_breaker() assert instance._circuit_breaker_tripped is False assert instance._circuit_breaker_reason == "" class TestLiveStrategyInstanceIntegration: """Integration tests for LiveStrategyInstance behavior.""" def test_order_rejected_when_circuit_breaker_tripped(self): """Test that orders are rejected when circuit breaker is tripped.""" class MockInstance: def __init__(self): self._circuit_breaker_tripped = True self._max_position_pct = 0.5 self.starting_balance = 10000 self.live_broker = MockLiveBroker() def _check_circuit_breaker(self): return self._circuit_breaker_tripped def trade_order(self, trade_type, size, order_type, **kwargs): if self._check_circuit_breaker(): return None return self.live_broker.place_order( symbol='BTC/USDT', side=OrderSide.BUY if trade_type.lower() == 'buy' else OrderSide.SELL, order_type=OrderType.MARKET, size=size ) instance = MockInstance() result = instance.trade_order('buy', 0.1, 'MARKET') assert result is None def test_order_succeeds_when_safety_checks_pass(self): """Test that orders succeed when all safety checks pass.""" class MockInstance: def __init__(self): self._circuit_breaker_tripped = False self._max_position_pct = 0.5 self.starting_balance = 10000 self.live_broker = MockLiveBroker() def _check_circuit_breaker(self): return self._circuit_breaker_tripped def _check_position_limit(self, size, price, symbol): return True def get_current_price(self, **kwargs): return 50000 def trade_order(self, trade_type, size, order_type, **kwargs): if self._check_circuit_breaker(): return None price = self.get_current_price() if not self._check_position_limit(size, price, 'BTC/USDT'): return None return self.live_broker.place_order( symbol='BTC/USDT', side=OrderSide.BUY if trade_type.lower() == 'buy' else OrderSide.SELL, order_type=OrderType.MARKET, size=size ) instance = MockInstance() result = instance.trade_order('buy', 0.05, 'MARKET') assert result is not None assert result.success is True class TestLiveStrategyInstanceState: """Tests for state management.""" def test_save_context_includes_broker_state(self): """Test that save_context saves broker state.""" broker = Mock() broker.save_state = Mock(return_value=True) # Simulate save_context behavior broker.save_state('test-strategy-123') broker.save_state.assert_called_once_with('test-strategy-123') def test_testnet_property(self): """Test is_testnet property.""" class MockInstance: def __init__(self, testnet=True): self._testnet = testnet @property def is_testnet(self): return self._testnet testnet_instance = MockInstance(testnet=True) assert testnet_instance.is_testnet is True production_instance = MockInstance(testnet=False) assert production_instance.is_testnet is False