"""Tests for CircuitBreakerManager.""" import pytest from datetime import datetime, timedelta from nautilus_dolphin.nautilus.circuit_breaker import ( CircuitBreakerManager, CircuitBreakerReason ) class TestCircuitBreakerManager: def test_can_open_position_within_limits(self): """Test position can be opened when within limits.""" cb = CircuitBreakerManager(max_concurrent_positions=3) cb._day_start = datetime.now() can_trade, reason = cb.can_open_position("BTCUSDT", 10000.0) assert can_trade is True assert reason == "" def test_can_open_position_max_positions_reached(self): """Test position rejected when max positions reached.""" cb = CircuitBreakerManager(max_concurrent_positions=2) cb._day_start = datetime.now() cb._active_positions = {'pos1', 'pos2'} can_trade, reason = cb.can_open_position("ETHUSDT", 10000.0) assert can_trade is False assert "max_positions_reached" in reason def test_can_open_position_already_exists(self): """Test position rejected when asset already has position.""" cb = CircuitBreakerManager(max_concurrent_positions=10) cb._day_start = datetime.now() cb._asset_positions = {'BTCUSDT': 'pos1'} can_trade, reason = cb.can_open_position("BTCUSDT", 10000.0) assert can_trade is False assert "position_already_exists" in reason def test_daily_loss_limit_triggers_circuit_breaker(self): """Test circuit breaker trips on daily loss limit.""" cb = CircuitBreakerManager(daily_loss_limit_pct=10.0) cb.on_trading_day_start(10000.0) # Simulate losses that exceed 10% cb.on_position_closed('pos1', 'BTCUSDT', -500.0) cb.on_position_closed('pos2', 'ETHUSDT', -600.0) assert cb.is_tripped() is True assert cb._trip_reason == CircuitBreakerReason.DAILY_LOSS_LIMIT def test_order_size_sanity_check(self): """Test circuit breaker trips on oversized order.""" cb = CircuitBreakerManager(max_order_size_pct=50.0) cb._day_start = datetime.now() # Try to submit order for 60% of balance can_submit, reason = cb.can_submit_order(6000.0, 10000.0) assert can_submit is False assert "order_size_sanity_check_failed" in reason assert cb.is_tripped() is True assert cb._trip_reason == CircuitBreakerReason.ORDER_SIZE_SANITY def test_api_failure_tracking(self): """Test circuit breaker trips after consecutive API failures.""" cb = CircuitBreakerManager(max_api_failures=3) # Simulate 3 API failures cb.on_api_failure("Connection timeout") cb.on_api_failure("Connection timeout") cb.on_api_failure("Connection timeout") assert cb.is_tripped() is True assert cb._trip_reason == CircuitBreakerReason.API_FAILURE def test_manual_trip(self): """Test manual circuit breaker trip.""" cb = CircuitBreakerManager() cb.manual_trip("Emergency stop") assert cb.is_tripped() is True assert cb._trip_reason == CircuitBreakerReason.MANUAL def test_auto_reset_after_time(self): """Test circuit breaker auto-resets after configured time.""" cb = CircuitBreakerManager(auto_reset_hours=1.0) cb.manual_trip("Test trip") assert cb.is_tripped() is True # Manually set trip time to past cb._trip_time = datetime.now() - timedelta(hours=2) # Should auto-reset on next check assert cb.is_tripped() is False def test_get_status(self): """Test status report.""" cb = CircuitBreakerManager() cb.on_trading_day_start(10000.0) status = cb.get_status() assert 'is_tripped' in status assert 'active_positions' in status assert 'daily_pnl' in status assert status['active_positions'] == 0 class TestCircuitBreakerIntegration: def test_full_trading_scenario(self): """Test complete trading scenario with circuit breaker.""" cb = CircuitBreakerManager( daily_loss_limit_pct=10.0, max_concurrent_positions=2, max_order_size_pct=50.0 ) # Start trading day cb.on_trading_day_start(10000.0) # Should allow first position can_trade, _ = cb.can_open_position("BTCUSDT", 10000.0) assert can_trade is True # Open position cb.on_position_opened("pos1", "BTCUSDT") # Should allow second position can_trade, _ = cb.can_open_position("ETHUSDT", 10000.0) assert can_trade is True # Open second position cb.on_position_opened("pos2", "ETHUSDT") # Should reject third position (max 2) can_trade, reason = cb.can_open_position("SOLUSDT", 10000.0) assert can_trade is False assert "max_positions_reached" in reason # Close first position with profit cb.on_position_closed("pos1", "BTCUSDT", 500.0) # Now should allow new position can_trade, _ = cb.can_open_position("SOLUSDT", 10000.0) assert can_trade is True