Back to projects December 2025 - February 2026

rsi2bot: Testing RSI(2) Mean Reversion Behind a Fail-Closed Trading Agent

A paper-by-default equities agent for the official Robinhood Agentic Trading MCP server. The backtester imports the same strategy, sizing, and risk modules as the live path, and under the tested universe and period RSI(2) mean reversion roughly matched buy and hold.

  • MCP Agents
  • Trading Safety
  • Python
  • Backtesting
  • Risk Engineering
rsi2bot: Testing RSI(2) Mean Reversion Behind a Fail-Closed Trading Agent

rsi2bot asks a narrow question: does Connors-style RSI(2) mean reversion still beat a simple buy-and-hold benchmark on liquid US large caps once liquidity filters, modeled spreads, and position caps are applied? Under the tested universe, period, and backtest assumptions, it did not show a compelling advantage. The README records the strategy as having decayed since its heyday and as roughly matching buy and hold over the last decade.

The rest of the project is the machinery that makes that comparison trustworthy and the live path safe to leave connected. The bot runs against Robinhood’s official Agentic Trading MCP server, is paper by default, and has no live profit and loss to report. I designed the research question, the backtest methodology, and the safety requirements the bot had to meet, then used Claude Code to implement against those constraints. The code is open at github.com/hpagni/rsi2bot.

The strategy, and why it stays small

The trading logic is RSI(2) mean reversion, one decision per day near the close. Buy when a name is in an established uptrend (close above its 200-day SMA) but short-term oversold (RSI(2) below 10). Exit when price reverts above the 5-day SMA, when a hard ATR stop is hit, or after a 7-day time stop. rsi2bot/strategy.py is deliberately pure: it reads bars, returns typed signals, and performs no I/O, no sizing, and no order placement.

Each trade risks a fixed 0.75% of equity, converted to shares through the ATR stop distance, then clamped by per-position (10% of equity), gross exposure (1.0x, no margin borrowing), participation (0.5% of average daily volume), a five-position cap, and settled-cash caps. Given how thin the measured edge is, sizing is set to lose small rather than to press.

One code path for research and live

The backtester imports the same strategy, sizing, and risk modules the live path uses, so there is no separate research implementation that can drift from the traded logic. Fills are next-bar and pessimistic: NBBO mid plus half the EDGE-estimated spread. The paper broker fills the same way, so paper results carry the modeled costs instead of flattering them.

The parameters are few and are literature defaults (RSI period 2, entry threshold 10, SMA 200 and 5, ATR 14 with a 2x stop), and the README documents the decision not to grid-search them. Tuning a thin edge would mostly fit the sample.

Most candidates are rejected before the signal is checked

Before the strategy sees a symbol, it has to clear hard liquidity and cost gates: price at least $10, 20-day median dollar volume at least $50M, an EDGE-estimated round-trip spread of 10 bps or less, and at least 220 bars of history so no indicator fires during its warmup window. After a valid signal, the expected edge must clear three times the modeled round-trip cost or the trade is rejected. Every gate result, pass or fail, is written to an append-only JSON-lines audit ledger with the reason string attached.

Funnel diagram showing the watchlist narrowing through liquidity gates, the RSI2 and SMA200 signal, the cost gate, and portfolio caps down to a sized order

The daily decision funnel. A symbol must survive every gate, in order, before a sized order exists.

Fail-closed live gating

Because an agent can reach a brokerage interface, live execution stays disabled unless several explicit safeguards pass. The controls are layered so that no single mistake, file, or process state moves real money.

Two-factor live opt-in. Live trading requires both mode = "live" in the config and the environment variable RH_TRADING_LIVE=1. The env flag is deliberately not a config field, so an agent that can write files still cannot flip the switch. If either factor is missing, the broker factory refuses to build the live broker and falls back to paper with a loud banner.

A kill switch that survives restarts. If session losses breach the daily limit (default -2% of start-of-day equity, hard cap -3%), the bot flattens everything and halts new entries. The tripped state is persisted to disk, so killing and restarting the process resumes the halt rather than resetting it. If the state file exists but is unreadable, the risk manager trips the switch rather than assume it is safe to trade.

Hard caps that config cannot loosen. Config.validate() raises on any setting past the ceilings: per-trade risk above 1%, position above 15%, kill switch looser than -3%, any margin in live mode. The caps are constants in the source, not tunable knobs.

Execution discipline. Marketable-limit orders only, never plain market orders, which Robinhood collars at 5%. Software ATR stops on every position, because the equities beta may not guarantee broker-side stops. T+1 settlement tracking on cash accounts, so the bot refuses to spend unsettled sale proceeds. A KILL file or env var stops entries immediately, checked on every cycle.

Layer diagram of the fail-closed controls: two-factor live opt-in, ATR hard stops, disk-persisted kill switch, T+1 settlement blocking, marketable-limit orders, and the append-only audit ledger

Six independent layers between a signal and real money. Each one fails closed on ambiguity.

Tests written as invariants

The test suite is 43 deterministic pytest tests, all passing, written as safety invariants rather than line-coverage filler. The live gate fails closed when the env flag is absent. A tripped kill switch survives a simulated process restart. The indicator math carries no look-ahead, checked against an independent hand-coded oracle. Plain market orders are rejected before they reach the wire. Sizing respects every cap simultaneously.

Bar chart of the 43-test suite broken down by category: indicators, sizing, strategy, risk and brokers, backtest

The 43 tests by category, each pinning a specific safety or correctness invariant.

What transfers

The MCP tool surface that lets an agent place an order has the same shape as one that lets an agent send an email or provision a server, and the controls carry over: two-factor opt-in where no single writable artifact enables the dangerous mode, state that persists across restarts so a crash cannot clear a halt, and every decision logged with its reason before the action fires. The strategy result carries over too. The question resolved negatively under the tested assumptions, and the repo reports it that way instead of tuning parameters until the equity curve improved.