Team Ai
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 9mo agoView on Hugging Face
2likes
test_validators.py286 linesDownload Raw Back to tests
1"""
2Tests for Input Validators
3Tests the InputValidator class to ensure robust validation
4"""
5
6import pytest
7import pandas as pd
8import numpy as np
9from datetime import datetime, timedelta
10
11from utils.validators import InputValidator
12from utils.exceptions import ValidationException
13
14
15class TestDataFrameValidation:
16    """Tests for DataFrame validation"""
17    
18    def test_valid_dataframe(self):
19        """Test with valid DataFrame"""
20        df = pd.DataFrame({
21            'open': [100.0] * 20,
22            'high': [101.0] * 20,
23            'low': [99.0] * 20,
24            'close': [100.5] * 20,
25            'volume': [1000000] * 20
26        })
27        
28        is_valid, error = InputValidator.validate_dataframe(df, "BTCUSD")
29        assert is_valid is True
30        assert error is None
31    
32    def test_empty_dataframe(self):
33        """Test with empty DataFrame"""
34        df = pd.DataFrame()
35        
36        with pytest.raises(ValidationException) as exc_info:
37            InputValidator.validate_dataframe(df, "BTCUSD")
38        
39        assert exc_info.value.error_code == "VALIDATION_ERROR"
40        assert "empty" in exc_info.value.message.lower()
41    
42    def test_insufficient_rows(self):
43        """Test with too few rows"""
44        df = pd.DataFrame({
45            'open': [100.0] * 5,
46            'high': [101.0] * 5,
47            'low': [99.0] * 5,
48            'close': [100.5] * 5,
49            'volume': [1000000] * 5
50        })
51        
52        with pytest.raises(ValidationException) as exc_info:
53            InputValidator.validate_dataframe(df, "BTCUSD")
54        
55        assert "Insufficient data" in exc_info.value.message
56    
57    def test_missing_columns(self):
58        """Test with missing required columns"""
59        df = pd.DataFrame({
60            'open': [100.0] * 20,
61            'close': [100.5] * 20,
62            'volume': [1000000] * 20
63        })
64        
65        with pytest.raises(ValidationException) as exc_info:
66            InputValidator.validate_dataframe(df, "BTCUSD")
67        
68        assert "Missing required columns" in exc_info.value.message
69    
70    def test_nan_values(self):
71        """Test with NaN values"""
72        df = pd.DataFrame({
73            'open': [100.0, np.nan] + [100.0] * 18,
74            'high': [101.0] * 20,
75            'low': [99.0] * 20,
76            'close': [100.5] * 20,
77            'volume': [1000000] * 20
78        })
79        
80        with pytest.raises(ValidationException) as exc_info:
81            InputValidator.validate_dataframe(df, "BTCUSD")
82        
83        assert "NaN" in exc_info.value.message
84    
85    def test_negative_prices(self):
86        """Test with negative prices"""
87        df = pd.DataFrame({
88            'open': [-100.0] + [100.0] * 19,
89            'high': [101.0] * 20,
90            'low': [99.0] * 20,
91            'close': [100.5] * 20,
92            'volume': [1000000] * 20
93        })
94        
95        with pytest.raises(ValidationException) as exc_info:
96            InputValidator.validate_dataframe(df, "BTCUSD")
97        
98        assert "negative" in exc_info.value.message.lower()
99    
100    def test_invalid_ohlc_logic(self):
101        """Test with invalid OHLC logic (High < Low)"""
102        df = pd.DataFrame({
103            'open': [100.0] * 20,
104            'high': [99.0] * 20,  # Invalid: high < low
105            'low': [101.0] * 20,
106            'close': [100.5] * 20,
107            'volume': [1000000] * 20
108        })
109        
110        with pytest.raises(ValidationException) as exc_info:
111            InputValidator.validate_dataframe(df, "BTCUSD")
112        
113        assert "OHLC logic" in exc_info.value.message
114
115
116class TestBacktestParamValidation:
117    """Tests for backtest parameter validation"""
118    
119    def test_valid_params(self):
120        """Test with valid parameters"""
121        is_valid, error = InputValidator.validate_backtest_params(
122            initial_capital=10000,
123            risk_percent=2.0,
124            symbol="BTCUSD",
125            bars=500
126        )
127        assert is_valid is True
128        assert error is None
129    
130    def test_capital_too_small(self):
131        """Test with capital below minimum"""
132        with pytest.raises(ValidationException) as exc_info:
133            InputValidator.validate_backtest_params(
134                initial_capital=50,
135                risk_percent=2.0,
136                symbol="BTCUSD"
137            )
138        
139        assert "initial_capital too small" in exc_info.value.message
140    
141    def test_capital_too_large(self):
142        """Test with capital above maximum"""
143        with pytest.raises(ValidationException) as exc_info:
144            InputValidator.validate_backtest_params(
145                initial_capital=2e9,  # 2 billion
146                risk_percent=2.0,
147                symbol="BTCUSD"
148            )
149        
150        assert "initial_capital too large" in exc_info.value.message
151    
152    def test_risk_percent_too_low(self):
153        """Test with risk percent below minimum"""
154        with pytest.raises(ValidationException) as exc_info:
155            InputValidator.validate_backtest_params(
156                initial_capital=10000,
157                risk_percent=0.05,
158                symbol="BTCUSD"
159            )
160        
161        assert "risk_percent too low" in exc_info.value.message
162    
163    def test_risk_percent_too_high(self):
164        """Test with risk percent above maximum"""
165        with pytest.raises(ValidationException) as exc_info:
166            InputValidator.validate_backtest_params(
167                initial_capital=10000,
168                risk_percent=60.0,
169                symbol="BTCUSD"
170            )
171        
172        assert "risk_percent too high" in exc_info.value.message
173    
174    def test_invalid_symbol(self):
175        """Test with invalid symbol"""
176        with pytest.raises(ValidationException) as exc_info:
177            InputValidator.validate_backtest_params(
178                initial_capital=10000,
179                risk_percent=2.0,
180                symbol="INVALID@SYMBOL!"
181            )
182        
183        assert "invalid characters" in exc_info.value.message.lower()
184
185
186class TestSymbolValidation:
187    """Tests for symbol validation"""
188    
189    def test_valid_symbols(self):
190        """Test with valid symbols"""
191        valid_symbols = ["BTCUSD", "AAPL", "BTC-USD", "ES", "GLD"]
192        for symbol in valid_symbols:
193            assert InputValidator.validate_symbol(symbol) is True
194    
195    def test_empty_symbol(self):
196        """Test with empty symbol"""
197        with pytest.raises(ValidationException) as exc_info:
198            InputValidator.validate_symbol("")
199        
200        assert "empty" in exc_info.value.message.lower()
201    
202    def test_invalid_characters(self):
203        """Test with invalid characters"""
204        invalid_symbols = ["BTC@USD", "AAPL!", "BTC USD"]
205        for symbol in invalid_symbols:
206            with pytest.raises(ValidationException):
207                InputValidator.validate_symbol(symbol)
208    
209    def test_symbol_too_long(self):
210        """Test with symbol longer than 20 characters"""
211        with pytest.raises(ValidationException) as exc_info:
212            InputValidator.validate_symbol("A" * 25)
213        
214        assert "too long" in exc_info.value.message.lower()
215
216
217class TestDateRangeValidation:
218    """Tests for date range validation"""
219    
220    def test_valid_date_range(self):
221        """Test with valid date range"""
222        is_valid, error = InputValidator.validate_date_range(
223            "2023-01-01",
224            "2023-12-31"
225        )
226        assert is_valid is True
227        assert error is None
228    
229    def test_none_dates(self):
230        """Test with None dates"""
231        is_valid, error = InputValidator.validate_date_range(None, None)
232        assert is_valid is True
233        assert error is None
234    
235    def test_invalid_date_format(self):
236        """Test with invalid date format"""
237        with pytest.raises(ValidationException) as exc_info:
238            InputValidator.validate_date_range(
239                "01-01-2023",  # Wrong format
240                "2023-12-31"
241            )
242        
243        assert "format" in exc_info.value.message.lower()
244    
245    def test_start_after_end(self):
246        """Test with start date after end date"""
247        with pytest.raises(ValidationException) as exc_info:
248            InputValidator.validate_date_range(
249                "2023-12-31",
250                "2023-01-01"
251            )
252        
253        assert "before" in exc_info.value.message.lower()
254
255
256class TestGenericValidation:
257    """Tests for generic input validation"""
258    
259    def test_valid_type(self):
260        """Test with correct type"""
261        result = InputValidator.validate_input(100, int, "test_field")
262        assert result == 100
263    
264    def test_invalid_type(self):
265        """Test with incorrect type"""
266        with pytest.raises(ValidationException) as exc_info:
267            InputValidator.validate_input("100", int, "test_field")
268        
269        assert "must be int" in exc_info.value.message
270    
271    def test_none_not_allowed(self):
272        """Test with None when not allowed"""
273        with pytest.raises(ValidationException) as exc_info:
274            InputValidator.validate_input(None, int, "test_field", allow_none=False)
275        
276        assert "cannot be None" in exc_info.value.message
277    
278    def test_none_allowed(self):
279        """Test with None when allowed"""
280        result = InputValidator.validate_input(None, int, "test_field", allow_none=True)
281        assert result is None
282
283
284if __name__ == "__main__":
285    pytest.main([__file__, "-v"])
286