Files
DOLPHIN/nautilus_dolphin/tests/test_adaptive_circuit_breaker.py
hjnormey 01c19662cb 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.
2026-04-21 16:58:38 +02:00

272 lines
10 KiB
Python
Executable File

"""
Tests for Adaptive Circuit Breaker v5.
"""
import unittest
from datetime import datetime
from unittest.mock import patch, MagicMock
import numpy as np
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import (
AdaptiveCircuitBreaker, ACBConfig, ACBPositionSizer, get_acb_cut_for_date
)
class TestACBConfig(unittest.TestCase):
"""Test ACB configuration."""
def test_default_config(self):
"""Test default configuration values."""
config = ACBConfig()
# Check cut rates (v5 configuration)
self.assertEqual(config.CUT_RATES[0], 0.00)
self.assertEqual(config.CUT_RATES[1], 0.15)
self.assertEqual(config.CUT_RATES[2], 0.45)
self.assertEqual(config.CUT_RATES[3], 0.55)
self.assertEqual(config.CUT_RATES[4], 0.75)
self.assertEqual(config.CUT_RATES[5], 0.80)
# Check thresholds
self.assertEqual(config.FUNDING_VERY_BEARISH, -0.0001)
self.assertEqual(config.DVOL_EXTREME, 80)
self.assertEqual(config.FNG_EXTREME_FEAR, 25)
class TestAdaptiveCircuitBreaker(unittest.TestCase):
"""Test Adaptive Circuit Breaker functionality."""
def setUp(self):
"""Set up test fixtures."""
self.acb = AdaptiveCircuitBreaker()
def test_signal_calculation_no_stress(self):
"""Test signal calculation with no stress factors."""
factors = {
'funding_btc': 0.0001, # Positive funding (bullish)
'dvol_btc': 40.0, # Low volatility
'fng': 60, # Greed (bullish)
'taker': 1.1 # Buying pressure
}
result = self.acb._calculate_signals(factors)
self.assertEqual(result['signals'], 0.0)
self.assertEqual(result['severity'], 0)
def test_signal_calculation_funding_only(self):
"""Test signal calculation with funding stress only."""
factors = {
'funding_btc': -0.00015, # Very bearish funding
'dvol_btc': 40.0,
'fng': 60,
'taker': 1.1
}
result = self.acb._calculate_signals(factors)
self.assertEqual(result['signals'], 1.0) # 1 signal from funding
self.assertEqual(result['severity'], 2)
def test_signal_calculation_multiple_stress(self):
"""Test signal calculation with multiple stress factors."""
factors = {
'funding_btc': -0.00015, # Very bearish funding (1.0 signals, sev 2)
'dvol_btc': 85.0, # Extreme volatility (1.0 signals, sev 2)
'fng': 20, # Extreme fear (confirmed, 1.0 signals, sev 2)
'taker': 0.75 # Selling pressure (1.0 signals, sev 1)
}
result = self.acb._calculate_signals(factors)
# Should have 4 signals (funding + dvol + fng + taker)
self.assertGreaterEqual(result['signals'], 3.0)
self.assertGreater(result['severity'], 0)
def test_cut_mapping(self):
"""Test signal to cut rate mapping."""
test_cases = [
(0.0, 0.00), # 0 signals -> 0% cut
(0.5, 0.00), # 0.5 signals -> 0% cut (below threshold)
(1.0, 0.15), # 1 signal -> 15% cut
(1.5, 0.15), # 1.5 signals -> 15% cut
(2.0, 0.45), # 2 signals -> 45% cut
(2.5, 0.45), # 2.5 signals -> 45% cut
(3.0, 0.55), # 3 signals -> 55% cut
(4.0, 0.75), # 4 signals -> 75% cut
(5.0, 0.80), # 5 signals -> 80% cut
]
for signals, expected_cut in test_cases:
cut = self.acb._get_cut_from_signals(signals)
self.assertEqual(cut, expected_cut,
f"Failed for signals={signals}: got {cut}, expected {expected_cut}")
def test_apply_cut_to_position_size(self):
"""Test applying cut to position size."""
base_size = 1000.0
# Test with no stress (0% cut)
with patch.object(self.acb, 'get_cut_for_date') as mock_get:
mock_get.return_value = {'cut': 0.0, 'signals': 0.0}
final_size, info = self.acb.apply_cut_to_position_size(base_size, '2026-02-06')
self.assertEqual(final_size, base_size)
# Test with moderate stress (45% cut)
with patch.object(self.acb, 'get_cut_for_date') as mock_get:
mock_get.return_value = {'cut': 0.45, 'signals': 2.0}
final_size, info = self.acb.apply_cut_to_position_size(base_size, '2026-02-06')
self.assertAlmostEqual(final_size, base_size * 0.55, places=10) # 1000 * (1 - 0.45)
# Test with extreme stress (80% cut)
with patch.object(self.acb, 'get_cut_for_date') as mock_get:
mock_get.return_value = {'cut': 0.80, 'signals': 5.0}
final_size, info = self.acb.apply_cut_to_position_size(base_size, '2026-02-06')
self.assertAlmostEqual(final_size, base_size * 0.20, places=10) # 1000 * (1 - 0.80)
def test_caching(self):
"""Test that results are cached."""
date_str = '2026-02-06'
# First call should cache
with patch.object(self.acb, '_load_external_factors') as mock_load:
mock_load.return_value = {
'funding_btc': -0.0001,
'dvol_btc': 60,
'fng': 40,
'taker': 0.95
}
result1 = self.acb.get_cut_for_date(date_str)
self.assertEqual(self.acb._stats['total_calls'], 1)
self.assertEqual(self.acb._stats['cache_hits'], 0)
# Second call should use cache
result2 = self.acb.get_cut_for_date(date_str)
self.assertEqual(self.acb._stats['total_calls'], 2)
self.assertEqual(self.acb._stats['cache_hits'], 1)
# Results should be identical
self.assertEqual(result1['cut'], result2['cut'])
def test_stats_tracking(self):
"""Test statistics tracking."""
# Clear stats
self.acb.reset_stats()
# Simulate some calls
with patch.object(self.acb, '_load_external_factors') as mock_load:
mock_load.return_value = {
'funding_btc': -0.0001,
'dvol_btc': 60,
'fng': 40,
'taker': 0.95
}
self.acb.get_cut_for_date('2026-02-06')
self.acb.get_cut_for_date('2026-02-07')
self.acb.get_cut_for_date('2026-02-06') # Should be cached
stats = self.acb.get_stats()
self.assertEqual(stats['total_calls'], 3)
self.assertEqual(stats['cache_hits'], 1)
self.assertGreater(stats['cache_hit_rate'], 0)
class TestACBPositionSizer(unittest.TestCase):
"""Test ACB Position Sizer."""
def setUp(self):
"""Set up test fixtures."""
self.sizer = ACBPositionSizer()
def test_enabled_disabled(self):
"""Test enabling/disabling ACB."""
self.assertTrue(self.sizer.is_enabled())
self.sizer.disable()
self.assertFalse(self.sizer.is_enabled())
# When disabled, should return base size unchanged
size, info = self.sizer.calculate_size(1000.0, '2026-02-06')
self.assertEqual(size, 1000.0)
self.assertFalse(info['enabled'])
self.sizer.enable()
self.assertTrue(self.sizer.is_enabled())
def test_calculate_size_with_acb(self):
"""Test calculate size with ACB enabled."""
base_size = 1000.0
with patch.object(self.sizer.acb, 'get_cut_for_date') as mock_get:
mock_get.return_value = {
'cut': 0.15,
'signals': 1.0,
'factors': {}
}
final_size, info = self.sizer.calculate_size(base_size, '2026-02-06')
self.assertAlmostEqual(final_size, base_size * 0.85, places=10) # 15% cut
self.assertEqual(info['base_size'], base_size)
self.assertAlmostEqual(info['final_size'], final_size, places=10)
self.assertAlmostEqual(info['reduction_pct'], 15.0, places=10)
class TestIntegration(unittest.TestCase):
"""Integration tests."""
def test_feb_6_scenario(self):
"""
Test Feb 6, 2026 scenario (actual crash day).
Expected signals:
- Funding: -0.000137 (very bearish)
- DVOL: 58.9 (elevated)
- FNG: 14 (extreme fear)
Expected: 3+ signals -> 55% cut
"""
acb = AdaptiveCircuitBreaker()
# Mock the external factors for Feb 6
with patch.object(acb, '_load_external_factors') as mock_load:
mock_load.return_value = {
'funding_btc': -0.000137, # Very bearish
'dvol_btc': 58.9, # Elevated
'fng': 14, # Extreme fear
'taker': 0.85, # Mild selling
'available': True
}
result = acb.get_cut_for_date('2026-02-06')
# Should detect 3+ signals
self.assertGreaterEqual(result['signals'], 2.0)
# Should apply 55% or higher cut
self.assertGreaterEqual(result['cut'], 0.45)
def test_normal_day_scenario(self):
"""Test normal market day scenario."""
acb = AdaptiveCircuitBreaker()
with patch.object(acb, '_load_external_factors') as mock_load:
mock_load.return_value = {
'funding_btc': 0.00005, # Slightly positive
'dvol_btc': 45.0, # Normal volatility
'fng': 55, # Neutral/greed
'taker': 1.05, # Slight buying
'available': True
}
result = acb.get_cut_for_date('2026-01-15')
# Should detect 0-1 signals
self.assertLessEqual(result['signals'], 1.0)
# Should apply 0% or 15% cut
self.assertLessEqual(result['cut'], 0.15)
if __name__ == '__main__':
unittest.main()