""" Tests Unitaires - RiskManager. Tests complets du Risk Manager incluant: - Pattern Singleton - Validation pré-trade - Gestion positions - Métriques de risque - Circuit breakers """ import pytest from datetime import datetime from src.core.risk_manager import RiskManager, Position, RiskMetrics class TestRiskManagerSingleton: """Tests du pattern Singleton.""" def test_singleton_same_instance(self): """Vérifie que deux appels retournent la même instance.""" rm1 = RiskManager() rm2 = RiskManager() assert rm1 is rm2 def test_singleton_shared_state(self, risk_manager): """Vérifie que l'état est partagé entre instances.""" rm1 = risk_manager rm1.portfolio_value = 15000.0 rm2 = RiskManager() assert rm2.portfolio_value == 15000.0 class TestRiskManagerInitialization: """Tests d'initialisation.""" def test_initialize_with_config(self, sample_config): """Vérifie l'initialisation avec configuration.""" rm = RiskManager() rm.initialize(sample_config['risk_limits']) assert rm.initial_capital == 10000.0 assert rm.portfolio_value == 10000.0 assert rm.peak_value == 10000.0 assert len(rm.positions) == 0 def test_config_loaded_correctly(self, risk_manager, sample_config): """Vérifie que la configuration est chargée.""" assert risk_manager.config == sample_config['risk_limits'] class TestTradeValidation: """Tests de validation pré-trade.""" def test_validate_trade_success(self, risk_manager): """Test validation d'un trade valide.""" is_valid, error = risk_manager.validate_trade( symbol='EURUSD', quantity=1000, price=1.1000, stop_loss=1.0950, take_profit=1.1100, strategy='intraday' ) assert is_valid is True assert error is None def test_validate_trade_no_stop_loss(self, risk_manager): """Test rejet si pas de stop-loss.""" is_valid, error = risk_manager.validate_trade( symbol='EURUSD', quantity=1000, price=1.1000, stop_loss=None, take_profit=1.1100, strategy='intraday' ) assert is_valid is False assert 'stop-loss' in error.lower() def test_validate_trade_excessive_risk(self, risk_manager): """Test rejet si risque trop élevé.""" is_valid, error = risk_manager.validate_trade( symbol='EURUSD', quantity=100000, # Très grande position price=1.1000, stop_loss=1.0000, # Stop très loin take_profit=1.2000, strategy='intraday' ) assert is_valid is False assert 'risk' in error.lower() def test_validate_trade_position_too_large(self, risk_manager): """Test rejet si position trop grande.""" is_valid, error = risk_manager.validate_trade( symbol='EURUSD', quantity=20000, # > 10% du portfolio price=1.1000, stop_loss=1.0950, take_profit=1.1100, strategy='intraday' ) assert is_valid is False assert 'position size' in error.lower() def test_validate_trade_bad_risk_reward(self, risk_manager): """Test rejet si R:R ratio insuffisant.""" is_valid, error = risk_manager.validate_trade( symbol='EURUSD', quantity=1000, price=1.1000, stop_loss=1.0950, # 50 pips risk take_profit=1.1020, # 20 pips reward (R:R = 0.4) strategy='intraday' ) assert is_valid is False assert 'risk/reward' in error.lower() class TestPositionManagement: """Tests de gestion des positions.""" def test_add_position(self, risk_manager): """Test ajout d'une position.""" position = Position( symbol='EURUSD', quantity=1000, entry_price=1.1000, current_price=1.1000, stop_loss=1.0950, take_profit=1.1100, strategy='intraday', entry_time=datetime.now(), unrealized_pnl=0.0, risk_amount=50.0 ) risk_manager.add_position(position) assert 'EURUSD' in risk_manager.positions assert risk_manager.positions['EURUSD'] == position assert risk_manager.total_trades == 1 def test_update_position(self, risk_manager): """Test mise à jour d'une position.""" position = Position( symbol='EURUSD', quantity=1000, entry_price=1.1000, current_price=1.1000, stop_loss=1.0950, take_profit=1.1100, strategy='intraday', entry_time=datetime.now(), unrealized_pnl=0.0, risk_amount=50.0 ) risk_manager.add_position(position) risk_manager.update_position('EURUSD', 1.1050) assert risk_manager.positions['EURUSD'].current_price == 1.1050 assert risk_manager.positions['EURUSD'].unrealized_pnl == 50.0 def test_close_position_profit(self, risk_manager): """Test fermeture position avec profit.""" position = Position( symbol='EURUSD', quantity=1000, entry_price=1.1000, current_price=1.1000, stop_loss=1.0950, take_profit=1.1100, strategy='intraday', entry_time=datetime.now(), unrealized_pnl=0.0, risk_amount=50.0 ) risk_manager.add_position(position) initial_value = risk_manager.portfolio_value pnl = risk_manager.close_position('EURUSD', 1.1100, 'take_profit') assert pnl == 100.0 assert 'EURUSD' not in risk_manager.positions assert risk_manager.portfolio_value == initial_value + 100.0 assert risk_manager.winning_trades == 1 def test_close_position_loss(self, risk_manager): """Test fermeture position avec perte.""" position = Position( symbol='EURUSD', quantity=1000, entry_price=1.1000, current_price=1.1000, stop_loss=1.0950, take_profit=1.1100, strategy='intraday', entry_time=datetime.now(), unrealized_pnl=0.0, risk_amount=50.0 ) risk_manager.add_position(position) initial_value = risk_manager.portfolio_value pnl = risk_manager.close_position('EURUSD', 1.0950, 'stop_loss') assert pnl == -50.0 assert risk_manager.portfolio_value == initial_value - 50.0 assert risk_manager.losing_trades == 1 class TestRiskMetrics: """Tests des métriques de risque.""" def test_get_risk_metrics(self, risk_manager): """Test calcul des métriques de risque.""" metrics = risk_manager.get_risk_metrics() assert isinstance(metrics, RiskMetrics) assert metrics.total_risk >= 0 assert metrics.current_drawdown >= 0 assert 0 <= metrics.risk_utilization <= 1 def test_calculate_drawdown(self, risk_manager): """Test calcul du drawdown.""" risk_manager.peak_value = 12000.0 risk_manager.portfolio_value = 10800.0 dd = risk_manager._calculate_current_drawdown() assert dd == 0.10 # 10% drawdown def test_calculate_var(self, risk_manager): """Test calcul VaR.""" # Ajouter historique de P&L risk_manager.pnl_history = [100, -50, 75, -30, 120, -80, 90, -40, 110, -60] * 3 var = risk_manager._calculate_var(confidence=0.95) assert var > 0 class TestCircuitBreakers: """Tests des circuit breakers.""" def test_halt_on_max_drawdown(self, risk_manager): """Test arrêt si drawdown maximum atteint.""" risk_manager.peak_value = 10000.0 risk_manager.portfolio_value = 8400.0 # 16% drawdown risk_manager.check_circuit_breakers() assert risk_manager.trading_halted is True assert 'drawdown' in risk_manager.halt_reason.lower() def test_halt_on_daily_loss(self, risk_manager): """Test arrêt si perte journalière excessive.""" # Simuler grosse perte journalière risk_manager.portfolio_value = 10000.0 risk_manager.daily_trades = [ {'time': datetime.now(), 'strategy': 'test'} ] risk_manager.pnl_history = [-400] # -4% en un jour risk_manager.check_circuit_breakers() assert risk_manager.trading_halted is True def test_resume_trading(self, risk_manager): """Test reprise du trading.""" risk_manager.halt_trading("Test halt") assert risk_manager.trading_halted is True risk_manager.resume_trading() assert risk_manager.trading_halted is False assert risk_manager.halt_reason is None class TestStatistics: """Tests des statistiques.""" def test_get_statistics(self, risk_manager): """Test récupération des statistiques.""" stats = risk_manager.get_statistics() assert 'portfolio_value' in stats assert 'total_return' in stats assert 'win_rate' in stats assert 'total_trades' in stats def test_win_rate_calculation(self, risk_manager): """Test calcul du win rate.""" risk_manager.winning_trades = 6 risk_manager.losing_trades = 4 risk_manager.total_trades = 10 stats = risk_manager.get_statistics() assert stats['win_rate'] == 0.6