initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
154
nautilus_dolphin/tests/test_circuit_breaker.py
Executable file
154
nautilus_dolphin/tests/test_circuit_breaker.py
Executable file
@@ -0,0 +1,154 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user