Files
trader-ml/tests/unit/test_risk_manager.py
Tika da30ef19ed Initial commit — Trading AI Secure project complet
Architecture Docker (8 services), FastAPI, TimescaleDB, Redis, Streamlit.
Stratégies : scalping, intraday, swing. MLEngine + RegimeDetector (HMM).
BacktestEngine + WalkForwardAnalyzer + Optuna optimizer.
Routes API complètes dont /optimize async.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 17:38:09 +00:00

315 lines
9.8 KiB
Python

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