Mean-reversion is a popular trading strategy that assumes asset prices tend to revert to their historical mean. In the context of Bollinger Bands, the strategy often uses the lower and upper bands as triggers for trading:
Entry:
Buy: When the closing price falls below the lower Bollinger Band, the market is deemed “oversold” and is expected to revert toward the mean (the middle band).
(Optional Short Entry): Similarly, when the price rises above the upper Bollinger Band, the market is considered “overbought.”
Exit:
Close Long: The position is exited when the price moves back above the middle band.
(Optional Cover Short): For short positions, the exit occurs when the price falls back below the middle band.
Basic Strategy Code:
Here’s how I implemented the basic mean-reversion strategy using Backtrader:
class BBMeanReversion(bt.Strategy):
params = (
('bb_period', 20),
('bb_devfactor', 2.0),
('printlog', True),
)
def __init__(self):
self.dataclose = self.datas[0].close
self.bband = bt.indicators.BollingerBands(
self.datas[0],
period=self.params.bb_period,
devfactor=self.params.bb_devfactor
)
def next(self):
# Entry condition: Only enter if not in market
if not self.position:
if self.dataclose[0] < self.bband.lines.bot[0]:
self.order = self.buy()
# Exit condition: if in a long position and price crosses above the mid band
else:
if self.dataclose[0] > self.bband.lines.mid[0]:
self.order = self.close()Performance Observations:
When applied on a one-year dataset, the basic Bollinger Bands
mean-reversion strategy can yield a high return. However, over other
years the same strategy might result in modest returns or even losses.
This inconsistency is common with straightforward mean-reversion
approaches because markets can experience strong trends during which
price “extremes” do not reliably reverse. For example let’s look at the
cumulative performance from 2020 onward:
let’s look at some statistics:
Starting Portfolio Value: 100000.00
2025-04-12 - (BB Period 20, BB DevFact 2.0) Ending Value 92930.49
Final Portfolio Value: 92930.49
Sharpe Ratio: 0.008
Total Return: -7.33%
Annualized Return: -0.95%
Max Drawdown: 53.27%
Total Trades: 28
Winning Trades: 18
Losing Trades: 10
Win Rate: 64.29%
Avg Winning Trade: 4947.69
Avg Losing Trade: -9612.79
Profit Factor: 0.9264571927698776
Overall Performance:
Starting with $100,000, the portfolio ended at approximately
$92,930—reflecting a total return of -7.33% and an annualized loss of
about -0.95%. This indicates that the strategy, despite a relatively
high win ratio, did not manage to generate net profits over the
period.
Risk Metrics:
The Sharpe Ratio is almost negligible at 0.008, suggesting that the
returns were not sufficient to compensate for the risk taken.
Additionally, the maximum drawdown reached over 53%, a significant
decline that points to high volatility and periods of substantial
losses.
Trade-Level Insights:
Although 18 out of 28 trades were winners (win rate of 64.29%), the
average winning trade ($4,947.69) was considerably smaller than the
average losing trade (-$9,612.79). This imbalance contributed to a
profit factor of only 0.93, meaning that losing trades cost more than
the gains from winning trades.
To address the limitations of a basic mean-reversion strategy, multiple filters can be introduced:
ADX Filter: The Average Directional Index (ADX) indicates the strength of a trend. By requiring the ADX to be below a threshold (e.g., 25), the strategy avoids taking positions during strong trending periods, where mean reversion is less likely.
RSI Filter: The Relative Strength Index (RSI) adds an extra layer of confirmation. It ensures that long entries occur in an oversold state (RSI below 30) and, if short selling is enabled, short entries occur in an overbought condition (RSI above 70).
These filters help ensure that trades are initiated only when the conditions favor mean-reversion rather than trend continuation.
Enhanced Strategy Code:
Below is the modified version of the basic strategy. Notice that both the entry and exit conditions now incorporate the ADX and RSI filters:
class EnhancedBBMeanReversion(bt.Strategy):
params = (
('bb_period', 20),
('bb_devfactor', 2.0),
('adx_period', 14),
('adx_threshold', 25),
('rsi_period', 14),
('rsi_lower', 30),
('rsi_upper', 70),
('printlog', True),
)
def __init__(self):
self.dataclose = self.datas[0].close
self.bband = bt.indicators.BollingerBands(
self.datas[0],
period=self.params.bb_period,
devfactor=self.params.bb_devfactor
)
# ADX Filter for non-trending conditions
self.adx = bt.indicators.ADX(self.datas[0], period=self.params.adx_period)
# RSI Filter for overbought/oversold conditions
self.rsi = bt.indicators.RSI(self.datas[0], period=self.params.rsi_period)
def next(self):
# Skip if ADX indicates a trending market
if self.adx[0] >= self.params.adx_threshold:
self.log(f"Market trending (ADX {self.adx[0]:.2f} >= {self.params.adx_threshold}), skipping new entries.")
return
# Long Entry Condition with multiple confirmations:
# Price below lower band and RSI indicates oversold conditions.
if not self.position:
if (self.dataclose[0] < self.bband.lines.bot[0] and
self.rsi[0] < self.params.rsi_lower):
self.log(f'LONG ENTRY SIGNAL: Close: {self.dataclose[0]:.2f}, Lower BB: {self.bband.lines.bot[0]:.2f}, RSI: {self.rsi[0]:.2f}')
self.order = self.buy()
# Exit Condition: long positions exit when price crosses above the middle band.
else:
if self.position.size > 0 and self.dataclose[0] > self.bband.lines.mid[0]:
self.log(f'EXIT LONG SIGNAL: Close: {self.dataclose[0]:.2f}, Mid BB: {self.bband.lines.mid[0]:.2f}')
self.order = self.close()Key Enhancements:
Refined Entry Criteria:
The enhanced strategy now incorporates both ADX and RSI filters to
fine-tune entry signals. First, it verifies that
self.adx[0] < self.params.adx_threshold, ensuring that
trades are only considered when the market is not strongly trending.
Then, it checks that the RSI is in an oversold condition (i.e.,
self.rsi[0] < self.params.rsi_lower) before executing a
buy order. This dual-filter approach prevents entering trades in
environments prone to prolonged trends or oversold conditions that might
not revert as expected.
Optimized Exit Conditions:
The exit logic similarly benefits from the ADX filter by requiring that
the market remains non-trending before closing a position. This helps
avoid premature exits during volatile periods and reduces the risk of
locking in losses if the trend temporarily reverses. In essence, the
exit is triggered only when the price crosses above the middle Bollinger
Band under favorable (i.e., non-trending) market conditions.
Overall Impact on Performance:
While the basic mean-reversion strategy can deliver high returns in
periods conducive to reversion, it typically suffers from significant
variability and higher risk during trending markets. The enhanced
version, by integrating both ADX and RSI filters, may yield slightly
lower returns during peak mean-reversion years, but it offers
substantially improved stability. This results in smaller drawdowns and
a more robust risk profile over time. Let’s examine the performance
metrics from 2020 onward:
let’s check the new result statistics:
Starting Portfolio Value: 100000.00
2025-04-12 - (BB Period 20, BB DevFact 2.0, ADX Thresh 25, RSI [< 30 / > 70]) Ending Value 157725.55
Final Portfolio Value: 157725.55
Sharpe Ratio: 0.033
Total Return: 45.57%
Annualized Return: 6.13%
Max Drawdown: 14.98%
Total Trades: 6
Winning Trades: 4
Losing Trades: 2
Win Rate: 66.67%
Avg Winning Trade: 15023.39
Avg Losing Trade: -1184.00
Profit Factor: 25.377326687383313
Overall Return:
The ending portfolio value of $157,725.55 translates to a total return
of 45.57% with an annualized return of 6.13%—a strong performance
compared to the previous negative return.
Risk-Adjusted Return:
Although the Sharpe Ratio remains modest at 0.033, the dramatic
reduction in maximum drawdown—from 53.27% to 14.98%—indicates that the
enhanced filtering not only improved returns but also significantly
reduced downside risk.
Trade Efficiency:
With only 6 trades executed:
Win Rate: 66.67% (4 winners vs. 2 losers) suggests that the filters helped in identifying higher-quality trades.
Trade Sizing: The average winning trade delivered approximately $15,023, whereas the average loss was limited to about $1,184. This imbalance (reflected in a profit factor of 25.38) shows that the winners more than compensated for the losses.
Method Enhancements:
The additional filters (ADX threshold at 25 and RSI conditions at [<
30 / > 70]) appear to have refined entry and exit signals, allowing
the strategy to avoid unfavorable conditions and capture higher
conviction trades.
Introducing filters such as ADX and RSI to the Bollinger Bands mean-reversion strategy provides a more refined trading signal—one that avoids unfavorable conditions and captures high-conviction entries. While the basic version might produce high returns in the right market environment, it can also suffer substantial losses in trending periods. The enhanced strategy, on the other hand, demonstrates that combining multiple indicators can lead to a more stable and attractive risk-adjusted performance.
By integrating both ADX and RSI filters, traders can achieve a balance between capturing high-probability mean-reversion trades and maintaining strict risk control—resulting in fewer trades but with significantly improved outcomes.