Skip to content

Commit a650680

Browse files
feat: Add Bollinger Bands factor
Implements BollingerBandsFactor to compute z-score of price vs rolling mean and std. Includes tests and integration into factors module. Closes #3
1 parent dc8608a commit a650680

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pandas as pd
2+
from .base import Factor
3+
4+
5+
class BollingerBandsFactor(Factor):
6+
"""
7+
Compute Bollinger Bands z-score:
8+
z = (price - rolling_mean) / rolling_std
9+
"""
10+
11+
def __init__(self, name: str = "bollinger_bands", lookback: int = 20, num_std: float = 2.0):
12+
super().__init__(name=name, lookback=lookback)
13+
self.num_std = num_std
14+
15+
def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
16+
# Validate data
17+
self._validate_data(prices)
18+
19+
# Rolling statistics
20+
rolling_mean = prices.rolling(self.lookback).mean()
21+
rolling_std = prices.rolling(self.lookback).std()
22+
23+
# Bollinger z-score
24+
zscore = (prices - rolling_mean) / rolling_std
25+
26+
# Save results
27+
self._values = zscore
28+
29+
return zscore

tests/test_factors.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
SizeFactor,
1010
ValueFactor,
1111
VolatilityFactor,
12+
BollingerBandsFactor,
1213
)
1314

1415

@@ -195,3 +196,31 @@ def test_volatility_calculation(self):
195196
assert (
196197
spearman_corr < -0.5
197198
), 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

Comments
 (0)