RSI Reversion
Catch oversold bounces, fade overbought spikes.
RSI is a 0-to-100 thermometer. Below 30 means everyone panic-sold and the price will probably bounce. Above 70 means everyone's cheering and a pullback is overdue. The bot fires BUY when RSI exits the oversold zone and SELL when it exits overbought.
RSI is a 0-to-100 thermometer that asks: how stretched is recent buying versus selling? Below 30 = panic-selling overshot. Above 70 = euphoria overshot. The bot fires BUY when RSI exits the oversold zone (recovery starts) and SELL when it exits overbought (cooling).
RSI = 100 − 100 / (1 + avgGain/avgLoss)
Real RSI Reversion bot, running on real Yahoo data when the symbol is available. Drag the params — the bot re-runs instantly.
This is the actual code the bot runs — not a re-explanation, not a simplified version. Whatever ships here is what executes when you press Run All in the workbench. Read it, copy it, fork it, build a better one.
const rsiBot: BotDef = {
id: "rsi-rev",
name: "RSI Reversion",
category: "trend",
glyph: "≈",
tagline: "Catch oversold bounces, fade overbought spikes.",
formula: "RSI = 100 − 100 / (1 + avgGain/avgLoss)",
params: [
{ key: "period", label: "Period", kind: "number", default: 14, min: 5, max: 30, step: 1 },
{ key: "buyBelow", label: "Buy below", kind: "number", default: 30, min: 10, max: 45, step: 1 },
{ key: "sellAbove", label: "Sell above", kind: "number", default: 70, min: 55, max: 90, step: 1 },
],
run: (ctx, p): BotResult => {
const period = Math.round(num(p, "period", 14));
const lo = num(p, "buyBelow", 30);
const hi = num(p, "sellAbove", 70);
const px = closes(ctx.candles);
const r = rsi(px, period);
const signals: Signal[] = [];
const sigArr: (1 | -1 | 0)[] = px.map(() => 0);
for (let i = 1; i < r.length; i++) {
const a = r[i - 1], b = r[i];
if (a == null || b == null) continue;
if (a < lo && b >= lo) {
signals.push({ i, kind: "buy", price: px[i], label: "exit oversold" });
sigArr[i] = 1;
} else if (a > hi && b <= hi) {
signals.push({ i, kind: "sell", price: px[i], label: "exit overbought" });
sigArr[i] = -1;
}
}
const bt = backtestLongOnly(px, sigArr);
const last = r[r.length - 1] ?? 50;
const state = last < lo ? "oversold" : last > hi ? "overbought" : "neutral";
return {
signals,
metrics: [
{ key: "rsi", label: "RSI", value: fmtNum(last, 1), tone: last < lo ? "bull" : last > hi ? "bear" : "neutral" },
{ key: "state", label: "State", value: state.toUpperCase(), tone: state === "oversold" ? "bull" : state === "overbought" ? "bear" : "neutral" },
{ key: "trades", label: "Trades", value: String(bt.trades), tone: "neutral" },
{ key: "ret", label: "Return", value: fmtPct(bt.equity[bt.equity.length - 1] - 1), tone: "info" },
],
pane: {
kind: "line",
series: [{ values: r, color: "var(--cyan)", label: "RSI" }],
refLines: [
{ value: hi, color: "var(--bear)", label: `${hi} OB` },
{ value: lo, color: "var(--bull)", label: `${lo} OS` },
{ value: 50, color: "var(--fg-faint)" },
],
height: 80,
},
summary: `RSI ${last.toFixed(1)} (${state}). ${signals.length} reversion triggers in window.`,
beginner:
"RSI is a 0-to-100 thermometer. Below 30 means everyone panicked and the price is probably going to bounce. Above 70 means everyone's cheering and a pullback is overdue.",
verdict: {
side: state === "oversold" ? "buy" : state === "overbought" ? "sell" : "hold",
text:
state === "oversold"
? "RSI is in the 'panic' zone — bounces happen here often."
: state === "overbought"
? "RSI is in the 'cheering' zone — fade or take profit."
: "RSI in the middle — no edge, sit tight.",
confidence: Math.abs(last - 50) / 50,
},
equity: bt.equity,
};
},
};id— unique key the workbench uses to find the bot.params— the sliders + inputs you see on the cell.run(ctx, p)— the function that gets called with candles + your params and returns the verdict.verdict— the BUY / SELL / HOLD pill at the top of the cell.metrics— the small stat boxes shown in the cell body.
- Copy the whole block above.
- On /quant, click + Import your bot in the bot library.
- Paste, hit save. It hot-loads into your workspace.
- Edit any param defaults or logic to your taste — it's now yours.
- ·Range-bound stocks and ETFs (REITs, utilities, sector rotators). RSI extremes mark the band edges.
- ·Single-name large caps that mean-revert intraday after a news pop.
- ·Combined with a trend filter — only take BUYs when the longer-term trend is up. Cuts whipsaws in half.
- ·Strong trends. RSI can sit above 70 for weeks during a parabolic move; the bot keeps firing SELLs that lose every time.
- ·Earnings-driven gaps. RSI smooths over them, missing the real signal.
- ·Crypto. The 14-period RSI is too slow for the volatility; bumping it to 6 helps but defeats the textbook calibration.
BUY when RSI just exited the oversold band (was < 30, now ≥ 30). SELL when it just exited overbought (was > 70, now ≤ 70). Confidence is the distance from 50 — the further into the extreme zone before the cross, the more conviction.
Should I use 30/70 or 20/80?+
Why does the bot say BUY when the price is still falling?+
Bet on snap-back when the price wanders too far.
Volatility envelopes around a moving average.
Momentum from EMA difference & its signal line.
Adaptive fair-value tracker. Smooth and self-correcting.
Buy when fast moving average crosses above slow.
The Turtle trade — buy n-day highs, sell n-day lows.