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.