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:
Tika
2026-03-08 17:38:09 +00:00
commit da30ef19ed
111 changed files with 31723 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Tests unitaires pour le module ML."""

View File

@@ -0,0 +1,523 @@
"""
Tests Unitaires - FeatureEngineering.
Tests de la création de features pour ML.
"""
import pytest
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from src.ml.feature_engineering import FeatureEngineering
class TestFeatureEngineeringInitialization:
"""Tests d'initialisation."""
def test_initialization_default(self):
"""Test initialisation par défaut."""
fe = FeatureEngineering()
assert fe.config == {}
assert len(fe.feature_names) == 0
def test_initialization_with_config(self):
"""Test initialisation avec config."""
config = {'param1': 'value1'}
fe = FeatureEngineering(config)
assert fe.config == config
class TestFeatureCreation:
"""Tests de création de features."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * (1 + np.random.uniform(0, 0.001, 300))
df['low'] = df[['open', 'close']].min(axis=1) * (1 - np.random.uniform(0, 0.001, 300))
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_create_all_features(self, sample_data):
"""Test création de toutes les features."""
fe = FeatureEngineering()
features_df = fe.create_all_features(sample_data)
assert isinstance(features_df, pd.DataFrame)
assert len(features_df) > 0
assert len(fe.feature_names) > 0
def test_features_count(self, sample_data):
"""Test que le nombre de features est correct."""
fe = FeatureEngineering()
features_df = fe.create_all_features(sample_data)
# Devrait créer 100+ features
assert len(fe.feature_names) >= 100
def test_no_nan_in_features(self, sample_data):
"""Test qu'il n'y a pas de NaN dans les features."""
fe = FeatureEngineering()
features_df = fe.create_all_features(sample_data)
# Après dropna, ne devrait pas y avoir de NaN
assert features_df.isna().sum().sum() == 0
class TestPriceFeatures:
"""Tests des features basées sur les prix."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_price_features_created(self, sample_data):
"""Test que les features de prix sont créées."""
fe = FeatureEngineering()
df = fe._create_price_features(sample_data.copy())
assert 'returns' in df.columns
assert 'log_returns' in df.columns
assert 'high_low_ratio' in df.columns
assert 'close_open_ratio' in df.columns
assert 'price_position' in df.columns
def test_returns_calculation(self, sample_data):
"""Test calcul des returns."""
fe = FeatureEngineering()
df = fe._create_price_features(sample_data.copy())
# Vérifier que returns est calculé correctement
expected_returns = sample_data['close'].pct_change()
pd.testing.assert_series_equal(
df['returns'].dropna(),
expected_returns.dropna(),
check_names=False
)
def test_price_position_range(self, sample_data):
"""Test que price_position est entre 0 et 1."""
fe = FeatureEngineering()
df = fe._create_price_features(sample_data.copy())
price_pos = df['price_position'].dropna()
assert (price_pos >= 0).all()
assert (price_pos <= 1).all()
class TestTechnicalIndicators:
"""Tests des indicateurs techniques."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_moving_averages_created(self, sample_data):
"""Test création des moyennes mobiles."""
fe = FeatureEngineering()
df = fe._create_technical_indicators(sample_data.copy())
# Vérifier SMA
for period in [5, 10, 20, 50, 100, 200]:
assert f'sma_{period}' in df.columns
assert f'ema_{period}' in df.columns
def test_rsi_calculation(self, sample_data):
"""Test calcul RSI."""
fe = FeatureEngineering()
df = fe._create_technical_indicators(sample_data.copy())
# Vérifier RSI
for period in [7, 14, 21]:
assert f'rsi_{period}' in df.columns
# RSI devrait être entre 0 et 100
rsi = df[f'rsi_{period}'].dropna()
assert (rsi >= 0).all()
assert (rsi <= 100).all()
def test_macd_calculation(self, sample_data):
"""Test calcul MACD."""
fe = FeatureEngineering()
df = fe._create_technical_indicators(sample_data.copy())
assert 'macd' in df.columns
assert 'macd_signal' in df.columns
assert 'macd_hist' in df.columns
def test_bollinger_bands(self, sample_data):
"""Test calcul Bollinger Bands."""
fe = FeatureEngineering()
df = fe._create_technical_indicators(sample_data.copy())
for period in [20, 50]:
assert f'bb_upper_{period}' in df.columns
assert f'bb_middle_{period}' in df.columns
assert f'bb_lower_{period}' in df.columns
assert f'bb_width_{period}' in df.columns
assert f'bb_position_{period}' in df.columns
# Vérifier ordre: upper > middle > lower
upper = df[f'bb_upper_{period}'].dropna()
middle = df[f'bb_middle_{period}'].dropna()
lower = df[f'bb_lower_{period}'].dropna()
assert (upper >= middle).all()
assert (middle >= lower).all()
def test_atr_calculation(self, sample_data):
"""Test calcul ATR."""
fe = FeatureEngineering()
df = fe._create_technical_indicators(sample_data.copy())
for period in [7, 14, 21]:
assert f'atr_{period}' in df.columns
# ATR devrait être positif
atr = df[f'atr_{period}'].dropna()
assert (atr > 0).all()
class TestStatisticalFeatures:
"""Tests des features statistiques."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_statistical_features_created(self, sample_data):
"""Test création features statistiques."""
fe = FeatureEngineering()
df = fe._create_statistical_features(sample_data.copy())
for period in [10, 20, 50]:
assert f'mean_{period}' in df.columns
assert f'std_{period}' in df.columns
assert f'skew_{period}' in df.columns
assert f'kurt_{period}' in df.columns
assert f'zscore_{period}' in df.columns
def test_zscore_calculation(self, sample_data):
"""Test calcul z-score."""
fe = FeatureEngineering()
df = fe._create_statistical_features(sample_data.copy())
# Z-score devrait avoir moyenne ~0 et std ~1
zscore = df['zscore_20'].dropna()
assert abs(zscore.mean()) < 0.5
assert abs(zscore.std() - 1.0) < 0.5
class TestVolatilityFeatures:
"""Tests des features de volatilité."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_volatility_features_created(self, sample_data):
"""Test création features volatilité."""
fe = FeatureEngineering()
# Ajouter returns d'abord
df = sample_data.copy()
df['returns'] = df['close'].pct_change()
df = fe._create_volatility_features(df)
for period in [10, 20, 50]:
assert f'volatility_{period}' in df.columns
assert 'parkinson_vol' in df.columns
assert 'gk_vol' in df.columns
assert 'vol_ratio' in df.columns
def test_volatility_positive(self, sample_data):
"""Test que la volatilité est positive."""
fe = FeatureEngineering()
df = sample_data.copy()
df['returns'] = df['close'].pct_change()
df = fe._create_volatility_features(df)
vol = df['volatility_20'].dropna()
assert (vol > 0).all()
class TestVolumeFeatures:
"""Tests des features de volume."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_volume_features_created(self, sample_data):
"""Test création features volume."""
fe = FeatureEngineering()
df = fe._create_volume_features(sample_data.copy())
for period in [5, 10, 20]:
assert f'volume_ma_{period}' in df.columns
assert 'volume_ratio' in df.columns
assert 'volume_change' in df.columns
assert 'obv' in df.columns
assert 'vwap' in df.columns
class TestTimeFeatures:
"""Tests des features temporelles."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test avec index datetime."""
dates = pd.date_range(start='2024-01-01', periods=300, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 300)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 300)
return df
def test_time_features_created(self, sample_data):
"""Test création features temporelles."""
fe = FeatureEngineering()
df = fe._create_time_features(sample_data.copy())
assert 'hour' in df.columns
assert 'hour_sin' in df.columns
assert 'hour_cos' in df.columns
assert 'day_of_week' in df.columns
assert 'dow_sin' in df.columns
assert 'dow_cos' in df.columns
assert 'month' in df.columns
assert 'month_sin' in df.columns
assert 'month_cos' in df.columns
def test_cyclic_encoding_range(self, sample_data):
"""Test que l'encodage cyclique est dans [-1, 1]."""
fe = FeatureEngineering()
df = fe._create_time_features(sample_data.copy())
for col in ['hour_sin', 'hour_cos', 'dow_sin', 'dow_cos', 'month_sin', 'month_cos']:
values = df[col].dropna()
assert (values >= -1).all()
assert (values <= 1).all()
class TestFeatureImportance:
"""Tests de feature importance."""
@pytest.fixture
def sample_features(self):
"""Génère des features de test."""
np.random.seed(42)
n_samples = 1000
n_features = 20
features = pd.DataFrame(
np.random.randn(n_samples, n_features),
columns=[f'feature_{i}' for i in range(n_features)]
)
return features
@pytest.fixture
def sample_target(self):
"""Génère une target de test."""
np.random.seed(42)
return pd.Series(np.random.randn(1000))
def test_get_feature_importance(self, sample_features, sample_target):
"""Test calcul feature importance."""
fe = FeatureEngineering()
importance = fe.get_feature_importance(
sample_features,
sample_target,
method='mutual_info'
)
assert isinstance(importance, pd.DataFrame)
assert 'feature' in importance.columns
assert 'importance' in importance.columns
assert len(importance) == len(sample_features.columns)
def test_select_top_features(self, sample_features, sample_target):
"""Test sélection top features."""
fe = FeatureEngineering()
top_features = fe.select_top_features(
sample_features,
sample_target,
n_features=10
)
assert isinstance(top_features, list)
assert len(top_features) == 10
assert all(f in sample_features.columns for f in top_features)
class TestFeatureEngineeringIntegration:
"""Tests d'intégration."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=500, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 500)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * (1 + np.random.uniform(0, 0.001, 500))
df['low'] = df[['open', 'close']].min(axis=1) * (1 - np.random.uniform(0, 0.001, 500))
df['volume'] = np.random.randint(1000, 10000, 500)
return df
def test_full_workflow(self, sample_data):
"""Test workflow complet."""
fe = FeatureEngineering()
# 1. Créer toutes les features
features_df = fe.create_all_features(sample_data)
assert len(features_df) > 0
assert len(fe.feature_names) >= 100
# 2. Vérifier pas de NaN
assert features_df.isna().sum().sum() == 0
# 3. Créer target
target = features_df['returns'].shift(-1).dropna()
features_for_ml = features_df.iloc[:-1]
# 4. Feature importance
importance = fe.get_feature_importance(
features_for_ml[fe.feature_names],
target,
method='correlation'
)
assert len(importance) > 0
# 5. Sélectionner top features
top_features = fe.select_top_features(
features_for_ml[fe.feature_names],
target,
n_features=50
)
assert len(top_features) == 50

View File

@@ -0,0 +1,473 @@
"""
Tests Unitaires - RegimeDetector.
Tests de la détection de régimes de marché avec HMM.
"""
import pytest
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from src.ml.regime_detector import RegimeDetector
class TestRegimeDetectorInitialization:
"""Tests d'initialisation du RegimeDetector."""
def test_initialization_default(self):
"""Test initialisation avec paramètres par défaut."""
detector = RegimeDetector()
assert detector.n_regimes == 4
assert detector.random_state == 42
assert detector.is_fitted is False
assert len(detector.feature_names) == 0
def test_initialization_custom_regimes(self):
"""Test initialisation avec nombre de régimes personnalisé."""
detector = RegimeDetector(n_regimes=3)
assert detector.n_regimes == 3
def test_regime_names_defined(self):
"""Test que les noms de régimes sont définis."""
detector = RegimeDetector()
assert len(detector.REGIME_NAMES) == 4
assert 'Trending Up' in detector.REGIME_NAMES.values()
assert 'Trending Down' in detector.REGIME_NAMES.values()
assert 'Ranging' in detector.REGIME_NAMES.values()
assert 'High Volatility' in detector.REGIME_NAMES.values()
class TestRegimeDetectorFitting:
"""Tests d'entraînement du modèle."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=200, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 200)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 200)
return df
def test_fit_success(self, sample_data):
"""Test entraînement réussi."""
detector = RegimeDetector()
detector.fit(sample_data)
assert detector.is_fitted is True
assert len(detector.feature_names) > 0
def test_fit_creates_features(self, sample_data):
"""Test que fit crée les features."""
detector = RegimeDetector()
detector.fit(sample_data)
# Vérifier que les features attendues sont créées
expected_features = ['returns', 'volatility', 'trend', 'range', 'volume_change', 'momentum']
for feature in expected_features:
assert feature in detector.feature_names
def test_fit_with_insufficient_data(self):
"""Test avec données insuffisantes."""
detector = RegimeDetector()
# Données trop courtes
dates = pd.date_range(start='2024-01-01', periods=10, freq='1H')
df = pd.DataFrame({
'close': np.random.randn(10),
'open': np.random.randn(10),
'high': np.random.randn(10),
'low': np.random.randn(10),
'volume': np.random.randint(1000, 10000, 10)
}, index=dates)
# Devrait lever une erreur ou gérer gracieusement
try:
detector.fit(df)
# Si pas d'erreur, vérifier que le modèle n'est pas fitted
# ou qu'il y a un warning
except Exception as e:
# Acceptable
pass
class TestRegimeDetectorPrediction:
"""Tests de prédiction des régimes."""
@pytest.fixture
def fitted_detector(self, sample_data):
"""Retourne un détecteur entraîné."""
detector = RegimeDetector()
detector.fit(sample_data)
return detector
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=200, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 200)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 200)
return df
def test_predict_regime_returns_array(self, fitted_detector, sample_data):
"""Test que predict_regime retourne un array."""
regimes = fitted_detector.predict_regime(sample_data)
assert isinstance(regimes, np.ndarray)
assert len(regimes) > 0
def test_predict_regime_values_valid(self, fitted_detector, sample_data):
"""Test que les régimes prédits sont valides."""
regimes = fitted_detector.predict_regime(sample_data)
# Tous les régimes doivent être entre 0 et n_regimes-1
assert (regimes >= 0).all()
assert (regimes < fitted_detector.n_regimes).all()
def test_predict_current_regime(self, fitted_detector, sample_data):
"""Test prédiction du régime actuel."""
current_regime = fitted_detector.predict_current_regime(sample_data)
assert isinstance(current_regime, (int, np.integer))
assert 0 <= current_regime < fitted_detector.n_regimes
def test_predict_without_fitting(self, sample_data):
"""Test prédiction sans entraînement préalable."""
detector = RegimeDetector()
with pytest.raises(ValueError, match="not fitted"):
detector.predict_regime(sample_data)
def test_get_regime_probabilities(self, fitted_detector, sample_data):
"""Test obtention des probabilités."""
probabilities = fitted_detector.get_regime_probabilities(sample_data)
assert isinstance(probabilities, np.ndarray)
assert probabilities.shape[1] == fitted_detector.n_regimes
# Vérifier que les probabilités somment à 1
prob_sums = probabilities.sum(axis=1)
np.testing.assert_array_almost_equal(prob_sums, np.ones(len(prob_sums)), decimal=5)
class TestRegimeDetectorStatistics:
"""Tests des statistiques de régimes."""
@pytest.fixture
def fitted_detector(self, sample_data):
"""Retourne un détecteur entraîné."""
detector = RegimeDetector()
detector.fit(sample_data)
return detector
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=200, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 200)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 200)
return df
def test_get_regime_name(self, fitted_detector):
"""Test récupération du nom d'un régime."""
for regime in range(fitted_detector.n_regimes):
name = fitted_detector.get_regime_name(regime)
assert isinstance(name, str)
assert len(name) > 0
def test_get_regime_statistics(self, fitted_detector, sample_data):
"""Test calcul des statistiques."""
stats = fitted_detector.get_regime_statistics(sample_data)
assert 'regime_counts' in stats
assert 'regime_percentages' in stats
assert 'current_regime' in stats
assert 'current_regime_name' in stats
# Vérifier que les pourcentages somment à 1
total_pct = sum(stats['regime_percentages'].values())
assert abs(total_pct - 1.0) < 0.01
class TestRegimeDetectorAdaptation:
"""Tests d'adaptation des paramètres."""
def test_adapt_strategy_parameters(self):
"""Test adaptation des paramètres selon le régime."""
detector = RegimeDetector()
base_params = {
'min_confidence': 0.6,
'risk_per_trade': 0.02
}
# Tester pour chaque régime
for regime in range(4):
adapted = detector.adapt_strategy_parameters(regime, base_params)
assert 'min_confidence' in adapted
assert 'risk_per_trade' in adapted
# Les paramètres doivent être modifiés
assert adapted != base_params
def test_adapt_trending_up(self):
"""Test adaptation pour régime Trending Up."""
detector = RegimeDetector()
base_params = {
'min_confidence': 0.6,
'risk_per_trade': 0.02
}
adapted = detector.adapt_strategy_parameters(0, base_params) # 0 = Trending Up
# Devrait être plus agressif
assert adapted['min_confidence'] < base_params['min_confidence']
assert adapted['risk_per_trade'] > base_params['risk_per_trade']
def test_adapt_high_volatility(self):
"""Test adaptation pour régime High Volatility."""
detector = RegimeDetector()
base_params = {
'min_confidence': 0.6,
'risk_per_trade': 0.02
}
adapted = detector.adapt_strategy_parameters(3, base_params) # 3 = High Volatility
# Devrait être plus conservateur
assert adapted['min_confidence'] > base_params['min_confidence']
assert adapted['risk_per_trade'] < base_params['risk_per_trade']
def test_should_trade_in_regime(self):
"""Test décision de trading selon régime."""
detector = RegimeDetector()
# Scalping devrait trader en Ranging
assert detector.should_trade_in_regime(2, 'scalping') is True
# Scalping ne devrait pas trader en High Volatility
assert detector.should_trade_in_regime(3, 'scalping') is False
# Intraday devrait trader en Trending
assert detector.should_trade_in_regime(0, 'intraday') is True
assert detector.should_trade_in_regime(1, 'intraday') is True
# Intraday ne devrait pas trader en Ranging
assert detector.should_trade_in_regime(2, 'intraday') is False
class TestRegimeDetectorFeatures:
"""Tests de calcul des features."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=200, freq='1H')
np.random.seed(42)
returns = np.random.normal(0.0001, 0.01, 200)
prices = 1.1000 * np.exp(np.cumsum(returns))
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 200)
return df
def test_calculate_features(self, sample_data):
"""Test calcul des features."""
detector = RegimeDetector()
features = detector._calculate_features(sample_data)
assert isinstance(features, pd.DataFrame)
assert len(features) > 0
# Vérifier présence des features
expected_features = ['returns', 'volatility', 'trend', 'range', 'volume_change', 'momentum']
for feature in expected_features:
assert feature in features.columns
def test_features_no_nan(self, sample_data):
"""Test que les features n'ont pas de NaN après nettoyage."""
detector = RegimeDetector()
features = detector._calculate_features(sample_data)
# Après dropna, ne devrait pas y avoir de NaN
assert features.isna().sum().sum() == 0
def test_normalize_features(self):
"""Test normalisation des features."""
detector = RegimeDetector()
# Créer features test
X = np.random.randn(100, 6)
X_normalized = detector._normalize_features(X)
# Vérifier que la moyenne est proche de 0 et std proche de 1
assert abs(X_normalized.mean()) < 0.1
assert abs(X_normalized.std() - 1.0) < 0.1
class TestRegimeDetectorEdgeCases:
"""Tests des cas limites."""
def test_with_missing_columns(self):
"""Test avec colonnes manquantes."""
detector = RegimeDetector()
# DataFrame incomplet
df = pd.DataFrame({
'close': np.random.randn(100)
# Manque open, high, low, volume
})
with pytest.raises(KeyError):
detector.fit(df)
def test_with_constant_prices(self):
"""Test avec prix constants."""
detector = RegimeDetector()
dates = pd.date_range(start='2024-01-01', periods=200, freq='1H')
df = pd.DataFrame(index=dates)
df['close'] = 1.1000 # Prix constant
df['open'] = 1.1000
df['high'] = 1.1000
df['low'] = 1.1000
df['volume'] = 1000
# Devrait gérer gracieusement (ou lever erreur appropriée)
try:
detector.fit(df)
# Si réussit, vérifier que le modèle est fitted
assert detector.is_fitted
except Exception:
# Acceptable si erreur appropriée
pass
def test_regime_name_invalid(self):
"""Test avec numéro de régime invalide."""
detector = RegimeDetector()
# Régime hors limites
name = detector.get_regime_name(999)
# Devrait retourner un nom par défaut
assert 'Regime' in name
class TestRegimeDetectorIntegration:
"""Tests d'intégration."""
@pytest.fixture
def sample_data(self):
"""Génère des données de test."""
dates = pd.date_range(start='2024-01-01', periods=500, freq='1H')
np.random.seed(42)
# Créer différents régimes
regimes = []
prices = []
base_price = 1.1000
for i in range(500):
if i < 125: # Trending Up
regime = 0
price = base_price * (1 + i * 0.0001)
elif i < 250: # Trending Down
regime = 1
price = base_price * (1 - (i - 125) * 0.0001)
elif i < 375: # Ranging
regime = 2
price = base_price + 0.001 * np.sin(i / 10)
else: # High Volatility
regime = 3
price = base_price + 0.01 * np.random.randn()
regimes.append(regime)
prices.append(price)
df = pd.DataFrame(index=dates)
df['close'] = prices
df['open'] = df['close'].shift(1).fillna(df['close'].iloc[0])
df['high'] = df[['open', 'close']].max(axis=1) * 1.001
df['low'] = df[['open', 'close']].min(axis=1) * 0.999
df['volume'] = np.random.randint(1000, 10000, 500)
return df
def test_full_workflow(self, sample_data):
"""Test workflow complet."""
detector = RegimeDetector(n_regimes=4)
# 1. Fit
detector.fit(sample_data)
assert detector.is_fitted
# 2. Predict
regimes = detector.predict_regime(sample_data)
assert len(regimes) > 0
# 3. Current regime
current = detector.predict_current_regime(sample_data)
assert 0 <= current < 4
# 4. Statistics
stats = detector.get_regime_statistics(sample_data)
assert 'current_regime' in stats
# 5. Adaptation
adapted = detector.adapt_strategy_parameters(current, {'min_confidence': 0.6})
assert 'min_confidence' in adapted
# 6. Should trade
should_trade = detector.should_trade_in_regime(current, 'intraday')
assert isinstance(should_trade, bool)