RSIMeanRev
RSI-based mean reversion. buy oversold, sell overbought
RSIMeanRev trades against Wilder’s Relative Strength Index extremes. When RSI drops below the oversold threshold, it goes long. When RSI rises above overbought, it goes short.
Import
from horizon.quant import RSIMeanRev
Signature
RSIMeanRev(
window: int = 14,
oversold: float = 30.0,
overbought: float = 70.0,
vol_window: int = 60,
edge_bps: float = 35.0,
horizon_days: int = 3,
)
windowintoversoldfloatoverboughtfloatvol_windowintedge_bpsfloathorizon_daysintFeatures it uses
features = {
"rsi": RSI(window=window),
"vol": RealizedVol(window=vol_window),
}
See RSI for the Wilder’s RSI math.
Signal logic
def evaluate(self, f, universe):
signals = []
horizon = timedelta(days=self.horizon_days)
for m in universe:
rsi = f.rsi[m.id]
vol = f.vol[m.id]
if isnan(rsi):
continue
vol_bps = max((vol if not isnan(vol) else 0.20) * 10_000, 50)
if rsi < self.oversold:
signals.append(Signal(
market_id=m.id,
direction=Direction.Increase,
confidence=min(1.0, (self.oversold - rsi) / 30),
expected_edge_bps=self.edge_bps,
expected_expected_vol_bps=vol_bps,
horizon=horizon,
reason=f"RSI={rsi:.1f} oversold",
))
elif rsi > self.overbought:
signals.append(Signal(
market_id=m.id,
direction=Direction.Decrease,
confidence=min(1.0, (rsi - self.overbought) / 30),
expected_edge_bps=self.edge_bps,
expected_expected_vol_bps=vol_bps,
horizon=horizon,
reason=f"RSI={rsi:.1f} overbought",
))
return signals
- RSI under 30 → long (bet on bounce)
- RSI > 70 → short (bet on pullback)
- Confidence scales with how deeply RSI has moved past the threshold
- No signal in the 30-70 dead zone
Use it
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticGBM
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.portfolio import KellyOptimizer
from horizon.quant import RSIMeanRev
from horizon.risk import RiskProfile
result = hz.run(
mode="backtest",
strategies=[RSIMeanRev(window=14, oversold=30, overbought=70, edge_bps=40)],
asset_classes=[Equity],
universe=StaticUniverse([
Market(id=t, asset_class=AssetClass.Equity)
for t in ["AAPL", "MSFT", "NVDA"]
]),
portfolio=KellyOptimizer(kelly_fraction=0.25, max_gross_leverage=1.0),
risk=RiskProfile.moderate(),
data_source=SyntheticGBM(["AAPL", "MSFT", "NVDA"], n_bars=252, seed=42),
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
When to use
When NOT to use
Parameter tuning
Risk companions
Same as BollingerMeanRev: mean reversion needs tight stops:
from horizon.risk import RiskConfig, StopLoss, TimeStop
risk = RiskConfig(
stop_loss=StopLoss(per_position_pct=0.05),
max_gross_leverage=1.0,
)
A time stop is especially useful here:
from datetime import timedelta
stop_loss = StopLoss(per_position_pct=0.05)
time_stop = TimeStop(max_hold_time=timedelta(days=5))
If the bounce doesn’t happen in 5 days, close the position. Mean reversion that doesn’t revert within the expected horizon is not mean reversion. it’s a new trend against you.
Ensemble with MovingAverageCross
A classic pairing is RSI mean reversion + MA cross trend-following:
from horizon.quant import Ensemble, MovingAverageCrossStrategy, RSIMeanRev
strategy = Ensemble(
strategies=[
MovingAverageCrossStrategy(fast=10, slow=30),
RSIMeanRev(window=14, oversold=30, overbought=70),
],
weights=[0.5, 0.5],
name="trend_plus_rsi",
)
MA cross catches trends; RSI catches counter-trend bounces within those trends. Natural regime diversification.
Pitfalls
Source
horizon/quant/timeseries.py. ~40 lines.