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>
This commit is contained in:
314
tests/unit/test_risk_manager.py
Normal file
314
tests/unit/test_risk_manager.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user