rsi2bot: An Agentic Trading Safety Engine
A paper-by-default equities trading agent for the official Robinhood Agentic Trading MCP server, built on the premise that an LLM-accessible broker is a hostile surface: fail-closed live gating, a disk-persisted kill switch, hard cost gates, and a 43-test suite that asserts the safety invariants.
Robinhood now runs an official Agentic Trading MCP server, the same tool-calling protocol AI agents use to act on the world. That sentence should make any engineer slightly nervous. An agent with a broker connection can move real money, and the failure modes are not hypothetical: a config typo, a restarted process that forgets it was halted, a market order that fills 5% away from the quote. rsi2bot is my answer to that nervousness. It is a short-term equity trading bot whose actual product is the safety machinery around the trade, built on one governing idea: treat an LLM-accessible broker as a hostile surface, and make every path to real money fail closed.
The code is open at github.com/hpagni/rsi2bot. One thing to say up front, because it matters more than anything else on this page: the bot is paper-by-default and there is no live profit-and-loss to report. Nothing here claims trading returns. What it claims, and what the tests prove, is that the system cannot trade real money by accident, cannot exceed its risk limits by misconfiguration, and cannot forget a halt by restarting.
A small strategy, on purpose
The trading logic is a Connors-style 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. The full rule set lives in rsi2bot/strategy.py, which is deliberately pure: it reads bars, returns typed signals, and performs no I/O, no sizing, and no order placement.
Position sizing is volatility-scaled and small. 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. The README states the uncomfortable truth plainly: RSI(2) reversion on liquid large caps has decayed since the strategy’s heyday and has roughly matched buy-and-hold over the last decade. The system is engineered to lose small and survive, not to promise alpha.
Most trades die before the signal is even 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. Even 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.

The daily decision funnel. A symbol must survive every gate, in order, before a single sized order exists.
The safety stack is the product
The controls are layered so that no single mistake, file, or process state can move real money. The interesting design decisions live in rsi2bot/config.py and rsi2bot/risk_manager.py.
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 a config file alone can never enable live trading; 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. It fails closed in the other direction too: if the persisted 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, visible to any reviewer, 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. And a manual halt: a KILL file or env var stops entries immediately, checked live on every cycle.

Six independent layers between a signal and real money. Each one fails closed on ambiguity.
Tests that assert invariants, not coverage
The test suite is 43 deterministic pytest tests, all passing, and they are 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 rather than against itself. Plain market orders are rejected before they reach the wire. Sizing respects every cap simultaneously. These are the properties I would want a code reviewer to demand of any agent that touches a brokerage account, so the suite demands them on every run.

The 43 tests by category. Each category exists to pin a specific safety or correctness invariant.
Backtesting without lying to yourself
The backtester imports the exact same strategy, sizing, and risk modules the live path uses, so there is no “backtest version” of the logic that can drift from reality. Fills are next-bar and pessimistic: NBBO mid plus half the EDGE-estimated spread, never the print you wish you got. The paper broker fills the same way, so paper results carry the modeled costs rather than flattering them.
The parameters follow the same philosophy. There are few of them, they are literature defaults (RSI period 2, entry threshold 10, SMA 200 and 5, ATR 14 with a 2x stop), and the README documents a refusal to grid-search them. Optimized parameters on a thin, decayed edge would just be overfitting with extra steps, and the honest move is to say so in the repo rather than publish a curve-fit equity chart.
Why this matters beyond trading
Strip out the tickers and this project is a case study in a discipline that is about to matter everywhere: putting engineering controls around AI agents that take real-world actions. The MCP tool surface that lets an agent place an order is the same shape as the surface that lets an agent send an email, provision a server, or sign a contract. The patterns here transfer directly: two-factor opt-in where no single writable artifact can enable the dangerous mode, state that persists across restarts so a crash cannot launder a halt, every decision logged with its reason before the action fires, and a test suite that treats “the gate fails closed” as a hard invariant rather than a hope.
The honest summary: rsi2bot has produced zero live dollars and was never the point. It is a working demonstration that when you give an agent the keys to something real, the safety engineering is not overhead around the interesting part. It is the interesting part.