|
9 | 9 | SizeFactor, |
10 | 10 | ValueFactor, |
11 | 11 | VolatilityFactor, |
| 12 | + BollingerBandsFactor, |
12 | 13 | ) |
13 | 14 |
|
14 | 15 |
|
@@ -195,3 +196,31 @@ def test_volatility_calculation(self): |
195 | 196 | assert ( |
196 | 197 | spearman_corr < -0.5 |
197 | 198 | ), f"volatility factor should be negatively correlated with realized volatility (spearman={spearman_corr})" |
| 199 | + |
| 200 | +class TestBollingerBandsFactor: |
| 201 | + """Test Bollinger Bands factor calculations.""" |
| 202 | + |
| 203 | + def test_bollinger_basic(self, sample_prices): |
| 204 | + """Test basic Bollinger Bands z-score calculation.""" |
| 205 | + factor = BollingerBandsFactor(lookback=20, num_std=2.0) |
| 206 | + result = factor.compute(sample_prices) |
| 207 | + |
| 208 | + # Must be a DataFrame with same shape as prices |
| 209 | + assert isinstance(result, pd.DataFrame) |
| 210 | + assert not result.empty |
| 211 | + assert set(result.columns) == set(sample_prices.columns) |
| 212 | + |
| 213 | + # Values should be finite where enough data exists |
| 214 | + valid_values = result.iloc[factor.lookback:].values.flatten() |
| 215 | + assert np.all(np.isfinite(valid_values)), "NaNs found after lookback period" |
| 216 | + |
| 217 | + # Mean of z-scores should be roughly centered near 0 |
| 218 | + mean_z = np.nanmean(valid_values) |
| 219 | + assert abs(mean_z) < 0.5, f"Z-scores not centered: mean={mean_z}" |
| 220 | + |
| 221 | + def test_bollinger_lookback_validation(self, sample_prices): |
| 222 | + """Ensure _validate_data raises for insufficient data.""" |
| 223 | + short_data = sample_prices.iloc[:5] |
| 224 | + factor = BollingerBandsFactor(lookback=10) |
| 225 | + with pytest.raises(ValueError): |
| 226 | + factor.compute(short_data) |
0 commit comments