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:
318
tests/unit/test_strategies.py
Normal file
318
tests/unit/test_strategies.py
Normal file
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
Tests Unitaires - Strategies.
|
||||
|
||||
Tests des stratégies de trading:
|
||||
- BaseStrategy
|
||||
- ScalpingStrategy
|
||||
- IntradayStrategy
|
||||
- SwingStrategy
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from src.strategies.base_strategy import BaseStrategy, Signal
|
||||
from src.strategies.scalping.scalping_strategy import ScalpingStrategy
|
||||
from src.strategies.intraday.intraday_strategy import IntradayStrategy
|
||||
from src.strategies.swing.swing_strategy import SwingStrategy
|
||||
|
||||
|
||||
class TestBaseStrategy:
|
||||
"""Tests de la classe BaseStrategy."""
|
||||
|
||||
def test_cannot_instantiate_abstract_class(self):
|
||||
"""Vérifie qu'on ne peut pas instancier BaseStrategy directement."""
|
||||
with pytest.raises(TypeError):
|
||||
BaseStrategy({})
|
||||
|
||||
def test_position_sizing_kelly(self, sample_config):
|
||||
"""Test calcul position sizing avec Kelly Criterion."""
|
||||
strategy = ScalpingStrategy(sample_config['strategy_params']['scalping_strategy'])
|
||||
|
||||
# Simuler historique
|
||||
strategy.win_rate = 0.6
|
||||
strategy.avg_win = 100
|
||||
strategy.avg_loss = -50
|
||||
|
||||
signal = Signal(
|
||||
symbol='EURUSD',
|
||||
direction='LONG',
|
||||
entry_price=1.1000,
|
||||
stop_loss=1.0950,
|
||||
take_profit=1.1100,
|
||||
confidence=0.8,
|
||||
timestamp=pd.Timestamp.now(),
|
||||
strategy='scalping',
|
||||
metadata={}
|
||||
)
|
||||
|
||||
position_size = strategy.calculate_position_size(
|
||||
signal=signal,
|
||||
portfolio_value=10000,
|
||||
current_volatility=0.02
|
||||
)
|
||||
|
||||
assert position_size > 0
|
||||
assert position_size < 10000 # Pas plus que le portfolio
|
||||
|
||||
|
||||
class TestScalpingStrategy:
|
||||
"""Tests de la stratégie Scalping."""
|
||||
|
||||
def test_initialization(self, sample_config):
|
||||
"""Test initialisation de la stratégie."""
|
||||
strategy = ScalpingStrategy(sample_config['strategy_params']['scalping_strategy'])
|
||||
|
||||
assert strategy.name == 'scalping'
|
||||
assert 'bb_period' in strategy.parameters
|
||||
assert 'rsi_period' in strategy.parameters
|
||||
|
||||
def test_calculate_indicators(self, sample_config, sample_ohlcv_data):
|
||||
"""Test calcul des indicateurs."""
|
||||
strategy = ScalpingStrategy(sample_config['strategy_params']['scalping_strategy'])
|
||||
|
||||
df = strategy.calculate_indicators(sample_ohlcv_data)
|
||||
|
||||
# Vérifier que tous les indicateurs sont présents
|
||||
assert 'bb_upper' in df.columns
|
||||
assert 'bb_lower' in df.columns
|
||||
assert 'bb_position' in df.columns
|
||||
assert 'rsi' in df.columns
|
||||
assert 'macd' in df.columns
|
||||
assert 'macd_hist' in df.columns
|
||||
assert 'atr' in df.columns
|
||||
|
||||
def test_analyze_generates_signal(self, sample_config, sample_ohlcv_data):
|
||||
"""Test génération de signal."""
|
||||
strategy = ScalpingStrategy(sample_config['strategy_params']['scalping_strategy'])
|
||||
|
||||
# Créer données oversold
|
||||
df = sample_ohlcv_data.copy()
|
||||
df = strategy.calculate_indicators(df)
|
||||
|
||||
signal = strategy.analyze(df)
|
||||
|
||||
# Signal peut être None ou Signal valide
|
||||
if signal is not None:
|
||||
assert isinstance(signal, Signal)
|
||||
assert signal.direction in ['LONG', 'SHORT']
|
||||
assert signal.stop_loss is not None
|
||||
assert signal.take_profit is not None
|
||||
assert 0 <= signal.confidence <= 1
|
||||
|
||||
def test_oversold_conditions(self, sample_config):
|
||||
"""Test détection conditions oversold."""
|
||||
strategy = ScalpingStrategy(sample_config['strategy_params']['scalping_strategy'])
|
||||
|
||||
# Créer données oversold artificielles
|
||||
dates = pd.date_range(start='2024-01-01', periods=100, freq='5min')
|
||||
df = pd.DataFrame(index=dates)
|
||||
|
||||
# Prix descendant puis rebond
|
||||
prices = np.linspace(1.1000, 1.0900, 100)
|
||||
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'] = 5000
|
||||
|
||||
df = strategy.calculate_indicators(df)
|
||||
|
||||
# Vérifier RSI oversold
|
||||
assert df['rsi'].iloc[-1] < 50 # Devrait être bas
|
||||
|
||||
|
||||
class TestIntradayStrategy:
|
||||
"""Tests de la stratégie Intraday."""
|
||||
|
||||
def test_initialization(self, sample_config):
|
||||
"""Test initialisation."""
|
||||
config = {
|
||||
'name': 'intraday',
|
||||
'timeframe': '1h',
|
||||
'risk_per_trade': 0.02,
|
||||
'max_holding_time': 28800,
|
||||
'max_trades_per_day': 20,
|
||||
'adaptive_params': {
|
||||
'ema_fast': 9,
|
||||
'ema_slow': 21,
|
||||
'ema_trend': 50,
|
||||
'adx_threshold': 25,
|
||||
}
|
||||
}
|
||||
|
||||
strategy = IntradayStrategy(config)
|
||||
|
||||
assert strategy.name == 'intraday'
|
||||
assert strategy.parameters['ema_fast'] == 9
|
||||
assert strategy.parameters['ema_slow'] == 21
|
||||
|
||||
def test_calculate_adx(self, sample_config, sample_ohlcv_data):
|
||||
"""Test calcul ADX."""
|
||||
config = {
|
||||
'name': 'intraday',
|
||||
'timeframe': '1h',
|
||||
'adaptive_params': {
|
||||
'ema_fast': 9,
|
||||
'ema_slow': 21,
|
||||
'ema_trend': 50,
|
||||
}
|
||||
}
|
||||
|
||||
strategy = IntradayStrategy(config)
|
||||
df = strategy.calculate_indicators(sample_ohlcv_data)
|
||||
|
||||
# Vérifier ADX calculé
|
||||
assert 'adx' in df.columns
|
||||
assert 'pos_di' in df.columns
|
||||
assert 'neg_di' in df.columns
|
||||
|
||||
# ADX devrait être entre 0 et 100
|
||||
adx_values = df['adx'].dropna()
|
||||
assert (adx_values >= 0).all()
|
||||
assert (adx_values <= 100).all()
|
||||
|
||||
def test_ema_crossover_detection(self, sample_config):
|
||||
"""Test détection croisement EMA."""
|
||||
config = {
|
||||
'name': 'intraday',
|
||||
'timeframe': '1h',
|
||||
'adaptive_params': {
|
||||
'ema_fast': 9,
|
||||
'ema_slow': 21,
|
||||
'ema_trend': 50,
|
||||
'adx_threshold': 25,
|
||||
}
|
||||
}
|
||||
|
||||
strategy = IntradayStrategy(config)
|
||||
|
||||
# Créer données avec croisement
|
||||
dates = pd.date_range(start='2024-01-01', periods=100, freq='1H')
|
||||
df = pd.DataFrame(index=dates)
|
||||
|
||||
# Tendance haussière
|
||||
df['close'] = np.linspace(1.0900, 1.1100, 100)
|
||||
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'] = 5000
|
||||
|
||||
df = strategy.calculate_indicators(df)
|
||||
|
||||
# Vérifier que EMA fast > EMA slow en fin de tendance
|
||||
assert df['ema_fast'].iloc[-1] > df['ema_slow'].iloc[-1]
|
||||
|
||||
|
||||
class TestSwingStrategy:
|
||||
"""Tests de la stratégie Swing."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test initialisation."""
|
||||
config = {
|
||||
'name': 'swing',
|
||||
'timeframe': '4h',
|
||||
'adaptive_params': {
|
||||
'sma_short': 20,
|
||||
'sma_long': 50,
|
||||
'fibonacci_lookback': 50,
|
||||
}
|
||||
}
|
||||
|
||||
strategy = SwingStrategy(config)
|
||||
|
||||
assert strategy.name == 'swing'
|
||||
assert strategy.parameters['sma_short'] == 20
|
||||
assert strategy.parameters['sma_long'] == 50
|
||||
|
||||
def test_fibonacci_levels(self, sample_ohlcv_data):
|
||||
"""Test calcul niveaux Fibonacci."""
|
||||
config = {
|
||||
'name': 'swing',
|
||||
'timeframe': '4h',
|
||||
'adaptive_params': {
|
||||
'sma_short': 20,
|
||||
'sma_long': 50,
|
||||
'fibonacci_lookback': 50,
|
||||
}
|
||||
}
|
||||
|
||||
strategy = SwingStrategy(config)
|
||||
df = strategy.calculate_indicators(sample_ohlcv_data)
|
||||
|
||||
# Vérifier niveaux Fibonacci
|
||||
assert 'fib_236' in df.columns
|
||||
assert 'fib_382' in df.columns
|
||||
assert 'fib_500' in df.columns
|
||||
assert 'fib_618' in df.columns
|
||||
assert 'fib_786' in df.columns
|
||||
|
||||
# Vérifier ordre des niveaux
|
||||
last_row = df.iloc[-1]
|
||||
assert last_row['fib_high'] >= last_row['fib_236']
|
||||
assert last_row['fib_236'] >= last_row['fib_382']
|
||||
assert last_row['fib_382'] >= last_row['fib_500']
|
||||
assert last_row['fib_500'] >= last_row['fib_618']
|
||||
assert last_row['fib_618'] >= last_row['fib_786']
|
||||
assert last_row['fib_786'] >= last_row['fib_low']
|
||||
|
||||
def test_get_strategy_info(self):
|
||||
"""Test récupération infos stratégie."""
|
||||
config = {
|
||||
'name': 'swing',
|
||||
'timeframe': '4h',
|
||||
'adaptive_params': {}
|
||||
}
|
||||
|
||||
strategy = SwingStrategy(config)
|
||||
info = strategy.get_strategy_info()
|
||||
|
||||
assert 'name' in info
|
||||
assert 'type' in info
|
||||
assert 'timeframe' in info
|
||||
assert 'indicators' in info
|
||||
assert info['type'] == 'swing'
|
||||
|
||||
|
||||
class TestSignal:
|
||||
"""Tests de la classe Signal."""
|
||||
|
||||
def test_signal_creation(self):
|
||||
"""Test création d'un signal."""
|
||||
signal = Signal(
|
||||
symbol='EURUSD',
|
||||
direction='LONG',
|
||||
entry_price=1.1000,
|
||||
stop_loss=1.0950,
|
||||
take_profit=1.1100,
|
||||
confidence=0.75,
|
||||
timestamp=pd.Timestamp.now(),
|
||||
strategy='scalping',
|
||||
metadata={'rsi': 25, 'bb_position': 0.1}
|
||||
)
|
||||
|
||||
assert signal.symbol == 'EURUSD'
|
||||
assert signal.direction == 'LONG'
|
||||
assert signal.confidence == 0.75
|
||||
assert 'rsi' in signal.metadata
|
||||
|
||||
def test_signal_risk_reward(self):
|
||||
"""Test calcul risk/reward d'un signal."""
|
||||
signal = Signal(
|
||||
symbol='EURUSD',
|
||||
direction='LONG',
|
||||
entry_price=1.1000,
|
||||
stop_loss=1.0950, # 50 pips risk
|
||||
take_profit=1.1100, # 100 pips reward
|
||||
confidence=0.75,
|
||||
timestamp=pd.Timestamp.now(),
|
||||
strategy='scalping',
|
||||
metadata={}
|
||||
)
|
||||
|
||||
risk = abs(signal.entry_price - signal.stop_loss)
|
||||
reward = abs(signal.take_profit - signal.entry_price)
|
||||
rr_ratio = reward / risk
|
||||
|
||||
assert rr_ratio == 2.0 # R:R de 2:1
|
||||
Reference in New Issue
Block a user