- GuruFinance Insights
- Posts
- From a 60% Loss to 15% Profit: Taming the RSI by Adding Trend Filters
From a 60% Loss to 15% Profit: Taming the RSI by Adding Trend Filters
Learn AI in 5 minutes a day
This is the easiest way for a busy person wanting to learn AI in as little time as possible:
Sign up for The Rundown AI newsletter
They send you 5-minute email updates on the latest AI news and how to use it
You learn how to become 2x more productive by leveraging AI
🚀 Your Investing Journey Just Got Better: Premium Subscriptions Are Here! 🚀
It’s been 4 months since we launched our premium subscription plans at GuruFinance Insights, and the results have been phenomenal! Now, we’re making it even better for you to take your investing game to the next level. Whether you’re just starting out or you’re a seasoned trader, our updated plans are designed to give you the tools, insights, and support you need to succeed.
Here’s what you’ll get as a premium member:
Exclusive Trading Strategies: Unlock proven methods to maximize your returns.
In-Depth Research Analysis: Stay ahead with insights from the latest market trends.
Ad-Free Experience: Focus on what matters most—your investments.
Monthly AMA Sessions: Get your questions answered by top industry experts.
Coding Tutorials: Learn how to automate your trading strategies like a pro.
Masterclasses & One-on-One Consultations: Elevate your skills with personalized guidance.
Our three tailored plans—Starter Investor, Pro Trader, and Elite Investor—are designed to fit your unique needs and goals. Whether you’re looking for foundational tools or advanced strategies, we’ve got you covered.
Don’t wait any longer to transform your investment strategy. The last 4 months have shown just how powerful these tools can be—now it’s your turn to experience the difference.
The Relative Strength Index (RSI) is one of the most popular technical indicators, beloved by traders for its apparent simplicity in identifying potentially overbought or oversold conditions. Coupled with the intuitive concept of mean reversion — the idea that prices tend to return to their average over time — it forms the basis of many automated trading strategies. Buy when the RSI dips below 30 (oversold), sell when it climbs above 70 (overbought). Simple, right?
Maybe too simple. As a recent backtesting experiment dramatically illustrates, a basic RSI mean reversion strategy, while appealing on the surface, can lead to disastrous results when faced with real-world market dynamics, particularly strong trends. However, by adding a layer of intelligence — a trend filter — the exact same core idea was transformed from a catastrophic failure into a potentially viable strategy.
The Basic Strategy: Simple Logic, Flawed Premise?
The initial strategy was straightforward:
Go Long: Buy when the 14-period RSI crossed below the 30 level (indicating oversold conditions).
Go Short: Sell short when the 14-period RSI crossed above the 70 level (indicating overbought conditions).
Exit Long: Sell the long position if the RSI crossed back above 50 (neutral) or 70 (overbought).
Exit Short: Cover the short position if the RSI crossed back below 50 (neutral) or 30 (oversold).
The logic seems sound for a market oscillating back and forth. Buy the dips, sell the rips.
What Top Execs Read Before the Market Opens
The Daily Upside was built by investment pros to give execs the intel they need—no fluff, just sharp insights on trends, deals, and strategy. Join 1M+ professionals and subscribe for free.
Here’s how this basic strategy (including short selling) might be implemented using the backtrader Python library:
Python
class RsiMeanReversion(bt.Strategy):
"""
Implements an RSI Mean Reversion strategy WITH SHORT SELLING.
Buys when RSI goes below oversold level.
Sells (shorts) when RSI goes above overbought level.
Exits long when RSI goes above overbought or neutral level.
Exits short (covers) when RSI goes below neutral or oversold level.
"""
params = (
('rsi_period', 14), # Period for the RSI calculation
('oversold', 30), # RSI level considered oversold (for buying)
('overbought', 70), # RSI level considered overbought (for shorting)
('neutral', 50), # RSI level considered neutral for exit
('printlog', True), # Enable/disable logging
)
def __init__(self):
# Initialize the RSI indicator
self.rsi = bt.indicators.RelativeStrengthIndex(period=self.params.rsi_period)
self.order = None # To keep track of pending orders
self.buyprice = None
self.buycomm = None
self.dataclose = self.datas[0].close # Reference to the closing price
if self.params.printlog:
print(f"Strategy Parameters: RSI Period={self.params.rsi_period}, "
f"Oversold={self.params.oversold}, Overbought={self.params.overbought}, "
f"Neutral={self.params.neutral}")
def log(self, txt, dt=None, doprint=False):
''' Logging function for this strategy'''
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} {txt}')
def notify_order(self, order):
""" Handles order notifications """
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
if order.status in [order.Completed]:
if order.isbuy():
self.log(
f'BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}, Size: {order.executed.size:.4f}', doprint=True
)
# Check if closing a short or opening a long - not strictly necessary here
# but useful for more complex logic
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
elif order.issell():
self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}, Size: {order.executed.size:.4f}', doprint=True)
# Check if opening a short or closing a long
self.bar_executed = len(self) # Bar number when order was executed
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log(f'Order Canceled/Margin/Rejected: Status {order.getstatusname()}', doprint=True)
self.order = None # Reset order status after completion/failure
def notify_trade(self, trade):
""" Handles trade notifications """
if not trade.isclosed:
return
self.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}', doprint=True)
def next(self):
""" Core strategy logic executed per bar """
# Log the closing price and current RSI value for debugging/observation
# self.log(f'Close: {self.dataclose[0]:.2f}, RSI: {self.rsi[0]:.2f}, Position: {self.position.size}')
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check current position status
current_position_size = self.position.size
# --- Logic when FLAT (No position) ---
if current_position_size == 0:
# Check for LONG entry condition
if self.rsi < self.params.oversold:
self.log(f'BUY CREATE (Long Entry), Close={self.dataclose[0]:.2f}, RSI={self.rsi[0]:.2f}', doprint=True)
self.order = self.buy() # Place Buy order to go Long
# Check for SHORT entry condition
elif self.rsi > self.params.overbought:
self.log(f'SELL CREATE (Short Entry), Close={self.dataclose[0]:.2f}, RSI={self.rsi[0]:.2f}', doprint=True)
self.order = self.sell() # Place Sell order to go Short
# --- Logic when LONG (Position > 0) ---
elif current_position_size > 0:
# Check for LONG exit conditions:
# 1. RSI crosses ABOVE the overbought level
# 2. RSI crosses ABOVE the neutral level (mean reversion exit)
if self.rsi > self.params.overbought or self.rsi > self.params.neutral:
exit_condition = "Overbought" if self.rsi > self.params.overbought else "Neutral Reversion"
self.log(f'SELL CREATE (Long Exit - {exit_condition}), Close={self.dataclose[0]:.2f}, RSI={self.rsi[0]:.2f}', doprint=True)
self.order = self.sell() # Place Sell order to close Long position
# --- Logic when SHORT (Position < 0) ---
elif current_position_size < 0:
# Check for SHORT exit (Cover) conditions:
# 1. RSI crosses BELOW the oversold level
# 2. RSI crosses BELOW the neutral level (mean reversion exit)
# Let's exit if it drops below neutral OR oversold
if self.rsi < self.params.neutral or self.rsi < self.params.oversold:
exit_condition = "Oversold" if self.rsi < self.params.oversold else "Neutral Reversion"
self.log(f'BUY CREATE (Short Cover - {exit_condition}), Close={self.dataclose[0]:.2f}, RSI={self.rsi[0]:.2f}', doprint=True)
self.order = self.buy() # Place Buy order to close (cover) Short position
The Harsh Reality: Unfiltered Backtest Results
When this RsiMeanReversion strategy is backtested on historical data (3 years of BTC-USD daily data from 2020 to 2022), the results are devastating. Instead of generating profits, it produces:
Total Return: -60% (A significant loss of capital)
Maximum Drawdown: 350%
A 60% loss is bad enough, but a 350% drawdown is catastrophic. Drawdown measures the largest peak-to-trough decline in account equity. A drawdown exceeding 100% implies the use of leverage and indicates that at its worst point, the strategy lost not only all its initial capital but also a significant amount of borrowed funds, leading to margin calls and account wipeout.
Why does it fail so spectacularly? Because markets don’t always revert to the mean promptly. They often trend, sometimes strongly. The basic RSI strategy blindly bought dips in powerful downtrends (catching falling knives) and shorted peaks in roaring uptrends (standing in front of a freight train). Each signal fought the prevailing momentum, leading to accumulating losses and crippling drawdowns.
The Enhancement: Adding Market Context with Trend Filters
Recognizing that fighting the trend was the strategy’s Achilles’ heel, the next step is to introduce filters to gauge the market’s underlying direction and strength. The goal is to adapt the strategy: trade with the trend when it’s strong, and revert to mean reversion only when the market is directionless or ranging.
The following filters can be used:
Trend Direction: A 200-period Exponential Moving Average (EMA). Price trading above the EMA suggests an uptrend; below suggests a downtrend.
Trend Strength: The 14-period Average Directional Index (ADX). An ADX value above 25 is used to indicate a strong trend (either up or down).
The core RSI logic was then modified based on these filters:
If Strong Uptrend (Price > 200 EMA and ADX > 25): ONLY take LONG signals (RSI < 30). Ignore short signals (RSI > 70).
If Strong Downtrend (Price < 200 EMA and ADX > 25): ONLY take SHORT signals (RSI > 70). Ignore long signals (RSI < 30).
If Weak/Ranging Trend (ADX <= 25): Allow BOTH long (RSI < 30) and short (RSI > 70) signals, reverting to the original mean-reversion logic.
The exit rules remain the same mean-reversion style.
This enhanced logic is implemented in a new backtrader class:
Python
class RsiMeanReversionTrendFiltered(bt.Strategy):
"""
Implements an RSI Mean Reversion strategy filtered by trend strength (ADX)
and direction (EMA).
- Strong Trend (ADX > threshold):
- Uptrend (Close > EMA): Only take LONG entries on RSI oversold.
- Downtrend (Close < EMA): Only take SHORT entries on RSI overbought.
- Weak/Ranging Trend (ADX <= threshold):
- Take BOTH LONG (RSI oversold) and SHORT (RSI overbought) entries.
- Exits remain mean-reversion based (RSI crossing neutral/opposite threshold).
"""
params = (
('rsi_period', 14), # Period for the RSI calculation
('oversold', 30), # RSI level considered oversold (for buying)
('overbought', 70), # RSI level considered overbought (for shorting)
('neutral', 50), # RSI level considered neutral for exit
('ema_period', 200), # Period for the EMA trend filter
('adx_period', 14), # Period for ADX calculation
('adx_threshold', 25), # ADX level to distinguish strong trend
('printlog', True), # Enable/disable logging
)
def __init__(self):
# Indicators
self.rsi = bt.indicators.RelativeStrengthIndex(period=self.params.rsi_period)
self.ema = bt.indicators.ExponentialMovingAverage(period=self.params.ema_period)
self.adx = bt.indicators.AverageDirectionalMovementIndex(period=self.params.adx_period)
# Order tracking and price references
self.order = None
self.buyprice = None
self.buycomm = None
self.dataclose = self.datas[0].close
if self.params.printlog:
print("--- Strategy Parameters ---")
print(f" RSI Period: {self.params.rsi_period}")
print(f" RSI Oversold: {self.params.oversold}")
print(f" RSI Overbought: {self.params.overbought}")
print(f" RSI Neutral Exit: {self.params.neutral}")
print(f" EMA Period (Trend Dir): {self.params.ema_period}")
print(f" ADX Period (Trend Str): {self.params.adx_period}")
print(f" ADX Threshold: {self.params.adx_threshold}")
print("--------------------------")
def log(self, txt, dt=None, doprint=False):
''' Logging function for this strategy'''
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} {txt}')
def notify_order(self, order):
# (Keep the notify_order method exactly the same as the previous version)
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.log(
f'BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}, Size: {order.executed.size:.4f}', doprint=True
)
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
elif order.issell():
self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}, Size: {order.executed.size:.4f}', doprint=True)
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log(f'Order Canceled/Margin/Rejected: Status {order.getstatusname()}', doprint=True)
self.order = None
def notify_trade(self, trade):
# (Keep the notify_trade method exactly the same as the previous version)
if not trade.isclosed:
return
self.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}', doprint=True)
def next(self):
# Check if an order is pending
if self.order:
return
# Get current indicator values
current_rsi = self.rsi[0]
current_adx = self.adx.adx[0] # Access the main ADX line
current_close = self.dataclose[0]
current_ema = self.ema[0]
current_position_size = self.position.size
# Determine Trend State
is_strong_trend = current_adx > self.params.adx_threshold
is_uptrend = current_close > current_ema
trend_status_log = "" # For logging
# --- ENTRY LOGIC (only if flat) ---
if current_position_size == 0:
# --- Strong Trend Logic ---
if is_strong_trend:
# Strong Uptrend: Only look for LONG entries
if is_uptrend:
trend_status_log = f"Strong Uptrend (ADX={current_adx:.2f}, Close>EMA)"
if current_rsi < self.params.oversold:
self.log(f'{trend_status_log} - BUY CREATE (Long Entry), Close={current_close:.2f}, RSI={current_rsi:.2f}', doprint=True)
self.order = self.buy()
# else: self.log(f'{trend_status_log} - RSI {current_rsi:.2f} not oversold, no long entry.', doprint=False) # Optional debug log
# Strong Downtrend: Only look for SHORT entries
else: # Not is_uptrend implies downtrend here
trend_status_log = f"Strong Downtrend (ADX={current_adx:.2f}, Close<EMA)"
if current_rsi > self.params.overbought:
self.log(f'{trend_status_log} - SELL CREATE (Short Entry), Close={current_close:.2f}, RSI={current_rsi:.2f}', doprint=True)
self.order = self.sell()
# else: self.log(f'{trend_status_log} - RSI {current_rsi:.2f} not overbought, no short entry.', doprint=False) # Optional debug log
# --- Weak/Ranging Trend Logic ---
else: # Not is_strong_trend
trend_status_log = f"Weak/Ranging Trend (ADX={current_adx:.2f})"
# Allow LONG entry
if current_rsi < self.params.oversold:
self.log(f'{trend_status_log} - BUY CREATE (Long Entry), Close={current_close:.2f}, RSI={current_rsi:.2f}', doprint=True)
self.order = self.buy()
# Allow SHORT entry
elif current_rsi > self.params.overbought:
self.log(f'{trend_status_log} - SELL CREATE (Short Entry), Close={current_close:.2f}, RSI={current_rsi:.2f}', doprint=True)
self.order = self.sell()
# else: self.log(f'{trend_status_log} - RSI {current_rsi:.2f} between thresholds, no entry.', doprint=False) # Optional debug log
# --- EXIT LOGIC (Based on RSI, independent of trend filter for exit) ---
else: # Already in a position
# --- Exit LONG position ---
if current_position_size > 0:
if current_rsi > self.params.overbought or current_rsi > self.params.neutral:
exit_condition = "Overbought" if current_rsi > self.params.overbought else "Neutral Reversion"
self.log(f'SELL CREATE (Long Exit - {exit_condition}), Close={current_close:.2f}, RSI={current_rsi:.2f}', doprint=True)
self.order = self.sell() # Place Sell order to close Long
# --- Exit SHORT position ---
elif current_position_size < 0:
if current_rsi < self.params.neutral or current_rsi < self.params.oversold:
exit_condition = "Oversold" if current_rsi < self.params.oversold else "Neutral Reversion"
self.log(f'BUY CREATE (Short Cover - {exit_condition}), Close={current_close:.2f}, RSI={current_rsi:.2f}', doprint=True)
self.order = self.buy() # Place Buy order to close (cover) Short
The Transformation: Backtesting the Filtered Strategy
The RsiMeanReversionTrendFiltered strategy, now enhanced with trend awareness, is backtested on the same historical data. The results, now, showe a remarkable turnaround:
Total Return: +15% (Turning a significant loss into a respectable profit)
Maximum Drawdown: 35%
The improvement is staggering. The 60% loss turns into a 15% gain. More importantly, the catastrophic 350% drawdown is slashed by 90% to a much more manageable (though still significant because we don’t have even a basic risk management!) 35%. By simply avoiding entries that directly fight strong trends, the strategy preserves capital during unfavourable periods and capitalizes on opportunities more aligned with the market’s flow.
Comparing the Results: Side-by-Side

Key Lessons and Caveats
This comparison underscores several critical points for strategy developers and traders:
Context is Crucial: Indicators like RSI rarely work well in isolation. Understanding the broader market context, especially the prevailing trend, is vital.
Simplicity Can Be Deceptive: While simple rules are appealing, they often lack the robustness to handle diverse market conditions.
Filters Can Add Value: Intelligent filters, like the EMA and ADX used here, can significantly improve a strategy’s risk-adjusted returns by adapting its behaviour.
Drawdown Matters Immensely: A strategy is only viable if its drawdowns are survivable, both financially and psychologically. Reducing drawdown is often more important than maximizing raw returns.
Backtesting is Essential (But Not Perfect): This experiment highlights the power of backtesting to reveal flaws and test enhancements. However, remember that these results are specific to the tested period, asset, and parameters. There’s always a risk of overfitting, and past performance doesn’t guarantee future results.
Conclusion
The journey from a -60% return and 350% drawdown to a +15% return and 35% drawdown showcases the power of adding adaptive logic to a simple trading concept. The basic RSI mean reversion strategy, applied blindly, was a recipe for disaster in trending markets, as shown in our hypothetical backtest. By incorporating basic trend direction and strength filters, the strategy was able to navigate market regimes more intelligently, dramatically improving its performance and survivability in this specific example. It serves as a powerful reminder that successful trading often lies not just in finding signals, but in understanding when and when not to act on them.