Skip to content
Single .md

RSI Mean Reversion

Mean reversion strategy with RSI. When the RSI is oversold (below 30), the market has overshot to the downside and the strategy buys expecting a bounce. When overbought (above 70), the market has overshot to the upside and the strategy sells expecting a correction. The exit occurs when the RSI returns to the neutral line (50).

Serves as a starting point for understanding mean reversion. The implementation handles Wilder smoothing and neutral-zone exits.

python
# No import needed: `Indicator` is a pre-injected global (native indicator kernel).
# A fixed lookback window keeps every bar O(1) in history length — see the note below.
LOOKBACK = 300  # window >> period → O(1) per bar; converges to the full-history RSI

DECLARATION = {
    "type": "strategy",
    "inputs": [
        {"name": "period", "type": "int", "default": 14},
        {"name": "oversold", "type": "float", "default": 30.0},
        {"name": "overbought", "type": "float", "default": 70.0},
    ],
    "plots": [
        {"name": "rsi", "title": "RSI", "source": "rsi",
         "type": "line", "color": "#A78BFA", "width": 2},
    ],
    "pane": "new",
    "scale": "right",
    "levels": [
        {"name": "Overbought", "value": 70, "color": "#EF4444", "width": 1, "style": "dashed"},
        {"name": "Midline",    "value": 50, "color": "#64748B", "width": 1, "style": "dotted"},
        {"name": "Oversold",   "value": 30, "color": "#22C55E", "width": 1, "style": "dashed"},
    ],
}

def main(df=None, sdk=None, params={}):
    # Strict Mode requires an accepted entrypoint (main / on_bar / on_*_tick /
    # a Strategy class). This 3-branch dispatcher wires the per-bar backtest,
    # the chart (study) call, and the declaration probe. on_bar_strategy on its
    # own is NOT an entrypoint — without this main() the runner rejects the script.
    params = params or {}
    if sdk is not None:
        return on_bar_strategy(sdk, params)
    if df is not None:
        return _build_chart(df, params)
    return DECLARATION


def _build_chart(df, params):
    # df= branch runs once for the chart, so a whole-series call is fine here.
    # Indicator.rsi accepts the DataFrame directly and reads source='close'.
    period = int((params or {}).get("period", 14))
    return {**DECLARATION, "series": {"rsi": Indicator.rsi(df, period)}}


def on_bar_strategy(sdk, params):
    period = int(params.get("period", 14))
    oversold = float(params.get("oversold", 30))
    overbought = float(params.get("overbought", 70))

    if len(sdk.candles) < period + 2:
        return

    # Bounded window → O(1) in history length. Indicator.rsi (native, no import)
    # accepts the candle dicts and picks 'close'. Take the last value.
    rows = sdk.candles[-LOOKBACK:]
    rsi = Indicator.rsi(rows, period)[-1]
    if rsi is None:
        return

    if sdk.position == 0:
        if rsi < oversold:
            sdk.buy(action="buy_to_open", qty=1, order_type="market")
        elif rsi > overbought:
            sdk.sell(action="sell_short_to_open", qty=1, order_type="market")
    elif sdk.position > 0 and rsi >= 50:
        sdk.sell(action="sell_to_close", qty=abs(sdk.position), order_type="market")
    elif sdk.position < 0 and rsi <= 50:
        sdk.buy(action="buy_to_cover", qty=abs(sdk.position), order_type="market")

Keep every bar O(1). The Indicator.rsi(rows, …) call above reads a bounded sdk.candles[-LOOKBACK:] slice, so each frame is constant-time in history length. Do not pass the whole history — Indicator.rsi([c["close"] for c in sdk.candles], period) is O(n) per bar and O(n²) over the backtest. As history grows, a late frame overruns the ~800 ms per-bar budget, which can desync the request/response protocol into a fatal ProtocolError that aborts the whole run — see Compute indicators incrementally.

A window comfortably larger than the period (≈10×; LOOKBACK = 300 here) converges to the full-history RSI. If you need a value bit-identical to a full-history recompute, keep an exact accumulator in sdk.state instead (a Wilder avg_gain/avg_loss pair updated from the newest close) — see Compute indicators incrementally. Indicator is a pre-injected global; there is nothing to import.

Visual representation of the RSI reversion logic. Focuses on oversold/overbought extremes and the 50-level exit.


When to use

  • Sideways or range-bound markets. Ideal scenario for mean reversion.
  • Assets that tend to revert to the mean. Blue-chip stocks, range-bound crypto.

What to expect

  • High individual win rate (approximately 60 to 70%), but occasional large losses in strong trends.
  • Sensitive to thresholds. oversold=30, overbought=70 are robust defaults.