Files
DOLPHIN/nautilus_dolphin/tests/test_circuit_breaker.py

155 lines
5.5 KiB
Python
Raw Normal View History

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