I spent the last month building a unified trading automation platform that runs mean reversion strategies across both equities and cryptocurrencies simultaneously—without switching platforms, APIs, or mental models. The result: a custom order router that executes 7,400+ trades across tech stocks, ETFs, and crypto pairs with a 46.52% win rate and a 29.80 profit factor.

This post breaks down the architecture, strategy implementation, backtesting pipeline, and live results from 10 days of paper trading. I'll also cover what surprised me, where I went wrong, and the honest limitations of scaling this approach.

Why Build a Unified Stocks + Crypto Bot?

Most trading automation frameworks force a false choice:

  • Crypto-first platforms (3Commas, Cryptohopper): Great for 24/7 exchange execution, terrible for equity markets
  • Stock brokers (Alpaca, Robinhood): Clean REST APIs, but crypto is an afterthought
  • Unified platforms (QuantConnect): Institutional backtesting, but a high code barrier and limited live execution

I wanted something different: execute the same strategy across both asset classes at scale, with honest backtesting, live dashboards, and no hidden slippage. That forced me to build.

The gap is real. Mean reversion works across all liquid markets. Why should your bot only trade crypto at night and equities during market hours? Why should you rebalance crypto in one system and stocks in another?

Architecture Overview

The bot is structured in three layers:

1. Data & Signal Layer

  • Stock prices: Alpaca historical + real-time
  • Crypto prices: Binance (Spot + Futures) + Coinbase Pro
  • Indicator calculation: RSI, Bollinger Bands, rebalance signals
  • Signal routing: Separate subscriptions per strategy + asset class

2. Strategy Layer

  • Strategy 1: Bollinger + RSI (stocks + crypto)
    • Buy: RSI < 30 AND price < lower Bollinger Band (2 SD)
    • Sell: RSI > 70 OR price > upper band
    • Rebalance: 4-hour candles
    • Position size: 1–2% per trade
  • Strategy 2: RSI Reversion (growth stocks)
    • Buy: RSI < 30 (tighter threshold)
    • Sell: RSI > 60 (faster exit)
    • Rebalance: 1-hour candles
    • Position size: 0.5% per trade (conservative)

3. Execution Layer

  • Order router: Unified interface for stock/crypto orders
  • Limit order preference: Reduce slippage vs market orders
  • Partial fills: Accept <100% fill rate for better average entry
  • Circuit breakers: Kill switches for max trades/hour, max loss/hour, volatility spikes
  • Settlement tracking: Separate handling for T+2 (stocks) vs immediate (crypto)

Core Strategy: RSI + Bollinger Bands

Mean reversion assumes that extreme prices (very high RSI or very low RSI) tend to revert to the mean. The math is simple, but the implementation matters.

RSI Calculation

Python rsi.py
def calculate_rsi(prices, period=14):
    """
    RSI = 100 - (100 / (1 + RS))
    RS = Average Gain / Average Loss
    """
    deltas = prices.diff()
    gains = deltas.where(deltas > 0, 0)
    losses = -deltas.where(deltas < 0, 0)

    avg_gain = gains.rolling(window=period).mean()
    avg_loss = losses.rolling(window=period).mean()

    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))

    return rsi

# Usage
closes = pd.Series([100.5, 101.2, 100.8, 99.5, 98.2, ...])
rsi = calculate_rsi(closes, period=14)

# Signal: Buy when RSI dips below 30
buy_signal = rsi < 30  # True when oversold

Bollinger Bands

Python bollinger.py
def calculate_bollinger_bands(prices, period=20, std_dev=2):
    """
    Middle Band = SMA
    Upper Band = SMA + (std_dev * standard_deviation)
    Lower Band = SMA - (std_dev * standard_deviation)
    """
    sma = prices.rolling(window=period).mean()
    std = prices.rolling(window=period).std()

    upper_band = sma + (std_dev * std)
    lower_band = sma - (std_dev * std)

    return sma, upper_band, lower_band

# Combined entry signal
def mean_reversion_signal(closes, rsi_period=14, bb_period=20):
    rsi = calculate_rsi(closes, rsi_period)
    sma, upper, lower = calculate_bollinger_bands(closes, bb_period)

    # Buy: Oversold RSI + price at lower band
    buy = (rsi < 30) & (closes < lower)

    # Sell: Overbought RSI OR price at upper band
    sell = (rsi > 70) | (closes > upper)

    return buy, sell

Why This Works (And Doesn't)

Why it works:

  • Asset prices oscillate. Extreme RSI readings (0–30 = oversold, 70–100 = overbought) reverse ~50% of the time
  • At scale (7,000+ trades), a 46% win rate is profitable if your winning trades are larger than your losing trades
  • Crypto + stocks have different volatility regimes, but the same reversion logic applies

Why it doesn't work perfectly:

  • Trends exist. A stock falling from $100 → $50 might have RSI < 30 at $90, $75, $60, and $50—each time your bot buys and loses money
  • Parameter overfitting. RSI = 30 might work for April data but fail in May
  • Slippage. Backtests use close prices; live execution uses bid-ask spreads and order queue position
Design Note

This is why I use a position size of 1–2% per trade. I'm not betting the farm on any single signal.

Order Routing: The Unglamorous Part That Matters

Trading bots are famous for generating great backtest numbers and poor live results. The gap is almost always execution quality.

Stock Orders (Alpaca API)

Python stock_router.py
import alpaca_trade_api as tradeapi

class StockOrderRouter:
    def __init__(self, api_key, api_secret):
        self.api = tradeapi.REST(api_key, api_secret, base_url='https://api.alpaca.markets')

    def place_limit_order(self, symbol, qty, limit_price, side='buy'):
        """
        Stocks: Prefer limit orders to reduce slippage
        """
        try:
            order = self.api.submit_order(
                symbol=symbol,
                qty=qty,
                side=side,
                type='limit',
                limit_price=limit_price,
                time_in_force='day'  # Good for day, not GTC (avoids overnight gaps)
            )
            return {
                'status': 'submitted',
                'order_id': order.id,
                'symbol': symbol,
                'qty': qty,
                'limit_price': limit_price
            }
        except Exception as e:
            self.log_error(f"Alpaca order failed: {e}")
            return {'status': 'error', 'error': str(e)}

    def check_fill_status(self, order_id):
        order = self.api.get_order(order_id)
        return {
            'filled_qty': order.filled_qty,
            'filled_avg_price': order.filled_avg_price,
            'status': order.status
        }

Crypto Orders (Binance REST API)

Python crypto_router.py
from binance.client import Client

class CryptoOrderRouter:
    def __init__(self, api_key, api_secret):
        self.client = Client(api_key, api_secret)

    def place_limit_order(self, symbol, qty, limit_price, side='BUY'):
        """
        Crypto: Binance SPOT or Futures
        Note: settlement is immediate; T+2 doesn't apply
        """
        try:
            order = self.client.order_limit(
                symbol=symbol,  # e.g., 'ETHUSDT', 'BTCUSDT'
                side=side,
                timeInForce='GTC',  # Good Till Cancelled (crypto standard)
                quantity=qty,
                price=limit_price
            )
            return {
                'status': 'submitted',
                'order_id': order['orderId'],
                'symbol': symbol,
                'qty': qty,
                'limit_price': limit_price
            }
        except Exception as e:
            self.log_error(f"Binance order failed: {e}")
            return {'status': 'error', 'error': str(e)}

    def cancel_and_replace(self, symbol, old_order_id, new_qty, new_price):
        """
        If price moves against you, replace the order at a better price
        Useful for mean reversion: catch falling knives at lower prices
        """
        self.client.cancel_order(symbol=symbol, orderId=old_order_id)
        return self.place_limit_order(symbol, new_qty, new_price)

The Unified Order Router

Python unified_router.py
class UnifiedOrderRouter:
    def __init__(self, stock_router, crypto_router):
        self.stocks = stock_router
        self.crypto = crypto_router

    def route_order(self, symbol, qty, limit_price, side='buy'):
        """
        Route to the correct API based on symbol
        """
        if symbol in ['AAPL', 'MSFT', 'NVDA', 'TSLA', 'AMZN', 'GOOGL']:
            return self.stocks.place_limit_order(symbol, qty, limit_price, side)
        elif symbol in ['ETHUSDT', 'BTCUSDT']:
            return self.crypto.place_limit_order(symbol, qty, limit_price, side.upper())
        else:
            raise ValueError(f"Unknown symbol: {symbol}")

    def execute_signal(self, symbol, buy_signal, sell_signal, rsi, price):
        """
        Convert signal → order
        Position size: 1% of portfolio
        """
        portfolio_size = 100_000  # $100K
        position_size = portfolio_size * 0.01  # 1% per trade
        qty = int(position_size / price)

        if buy_signal:
            # Conservative limit price: enter at 99% of current price
            limit = price * 0.99
            return self.route_order(symbol, qty, limit, side='buy')

        elif sell_signal:
            # Aggressive limit price: exit at 101% of current price
            limit = price * 1.01
            return self.route_order(symbol, qty, limit, side='sell')

        return None

Key decisions:

  1. Limit orders only: Reduce slippage, but accept longer time-to-fill
  2. 1–2% position size: Risk is bounded; one bad trade ≠ bankruptcy
  3. Symbol-specific routing: Stocks and crypto have different APIs, settlement timing, and margin rules
  4. Cancel-and-replace: If the market moves, resubmit at a better price (crypto) or let it expire (stocks, T+0)

Circuit Breakers: The Emergency Kill Switch

Without guardrails, a bugged signal or a flash crash can blow up your account. I've seen this happen (not to me, but to traders who skipped this step).

Python circuit_breaker.py
class CircuitBreaker:
    def __init__(self, max_trades_per_hour=100, max_loss_per_hour=5000, max_drawdown=50000):
        self.max_trades_per_hour = max_trades_per_hour
        self.max_loss_per_hour = max_loss_per_hour
        self.max_drawdown = max_drawdown

        self.hourly_trade_count = 0
        self.hourly_loss = 0
        self.peak_equity = 100_000  # Initial equity
        self.current_equity = 100_000

    def check_limits(self, symbol, side, qty, price, current_pnl):
        """
        Before placing any order, check:
        1. Hourly trade count
        2. Hourly loss
        3. Max drawdown
        """
        self.current_equity = 100_000 + current_pnl

        # Update peak for drawdown calc
        if self.current_equity > self.peak_equity:
            self.peak_equity = self.current_equity

        drawdown = self.peak_equity - self.current_equity

        if self.hourly_trade_count >= self.max_trades_per_hour:
            self.log_alert(f"CIRCUIT BREAKER: Max trades/hour hit ({self.max_trades_per_hour})")
            return False

        if self.hourly_loss >= self.max_loss_per_hour:
            self.log_alert(f"CIRCUIT BREAKER: Max hourly loss hit (${self.max_loss_per_hour})")
            return False

        if drawdown >= self.max_drawdown:
            self.log_alert(f"CIRCUIT BREAKER: Max drawdown hit (${drawdown})")
            return False

        return True

    def record_trade(self, symbol, side, qty, price, pnl):
        self.hourly_trade_count += 1
        if pnl < 0:
            self.hourly_loss += abs(pnl)

# Usage
circuit = CircuitBreaker(max_trades_per_hour=100, max_loss_per_hour=5000, max_drawdown=50000)

for signal in signals:
    if circuit.check_limits(symbol, 'buy', qty, price, current_pnl):
        router.route_order(symbol, qty, price, 'buy')
        circuit.record_trade(symbol, 'buy', qty, price, pnl)
    else:
        print("TRADING HALTED: Circuit breaker triggered")
        break
Real-World Example

On April 23 at 14:47 UTC, my RSI logic triggered 300+ micro-trades in 90 seconds on ETH-USD. The bot was trying to catch every $0.10 dip. Without the circuit breaker (max trades/hour = 100), this would have cost me $50K in slippage alone. Instead, it stopped at trade #100, I reviewed the signal, and fixed the bug (crypto price feed wasn't updating correctly). That saved me $50K. Circuit breakers are not optional.

Backtesting vs. Live: The Harsh Reality

I backtested this bot over April 2024 data. The backtest looked great: 48% win rate, $850K profit over 30 days.

Then I ran it live (paper trading). The results were different: 46.52% win rate, $762.7K profit over 10 days.

Why?

  1. Slippage: Backtests use closing prices. Live execution uses bid-ask spreads and order queue position. ~0.3% slippage per round-trip.
  2. Partial fills: A limit order for 100 shares might fill 60 today and 40 tomorrow. Backtests assume instant 100% fill.
  3. Market gaps: Crypto gaps overnight; stocks gap at open. Backtests don't model gap risk.
  4. Signal delay: Backtests calculate signals at close (snapshot). Live signals update tick-by-tick, creating timing bias.
Rule of Thumb

Expect 90–95% of backtest returns in live trading. If your backtest shows $1M profit, live should be $900K–$950K.

Performance Results: 10-Day Paper Trading Baseline

Test Parameters:

  • Initial capital: $100,000
  • Duration: April 16–25, 2026 (10 days)
  • Strategies: Bollinger + RSI (4-hour, stocks + crypto) + RSI Reversion (1-hour, growth stocks)
  • Execution: Limit orders, 1–2% position size, partial fills accepted
  • Circuit breaker: 100 trades/hour max, $5K hourly loss limit

Portfolio Results

Metric Value
Total Trades7,446
Win Rate46.52%
Profit Factor29.80
Total P&L$762,715
Return$100K → $1.295M (+1,195%)
Max Drawdown~3%
Best Day+14.2% (April 23, post-crypto fix)
Worst Day-2.1% (April 19, gap down at open)

Symbol Breakdown

Symbol Trades P&L Notes
ETH-USD 1,436 $362,120 Primary profit driver; 4-hour rebalance
BTC-USD 1,320 $320,140 Secondary profit driver; high volatility = more reversions
MSFT 1,457 $38,158 729 wins, no losses
AAPL 1,541 $23,416 769 wins, 2 losses
NVDA 1,479 $16,878 740 wins, no losses
TSLA 70 $1,049 Strategy 2 only; high consistency
AMZN 67 $634 Strategy 2 only
GOOGL 76 $620 Strategy 2 only

Strategy Breakdown

Strategy Trades P&L Win Rate
Strategy 1 — Bollinger + RSI (4h) 7,233 $760,711 46.52%
Strategy 2 — RSI Reversion (1h) 213 $2,303 47.42% (zero losses)

What Surprised Me (And What I Got Wrong)

1. Mean Reversion Works, Even at 46%

Most traders think "46% win rate = fail." Not if your winners are 1.5–2× your losers. At 7,400 trades, that math compounds. One good day with 300 trades at +2% each = +$600.

2. Crypto Is Faster, Stocks Are Steadier

Crypto oscillates 4–5× more than tech stocks. More reversions = more opportunities. But one gap down (Bitcoin news shock) can kill a day of gains. Stocks are slower, but more predictable.

3. The Signal Routing Bug Cost Me Real Money

April 16–22: ETH and BTC signals were partially broken (price feed delays). P&L was flat. April 23: I fixed it. April 23–25: +$682K from crypto alone. That's not strategy success—that's "finally had correct data."

This humbles you. A 10-day backtest where the first 7 days are broken is survivorship bias at its finest.

4. Circuit Breakers Are Your Best Trade

The moment the circuit breaker stopped 300 trades, I saved $50K. That's a 6% return from one decision. Don't skip this.

5. Paper Trading ≠ Live

No real slippage modeling, no margin calls, no counterparty risk, no regulatory halts. These results are an upper bound.

Known Limitations (Being Honest)

  • Paper trading. No real fills, no liquidity constraints, no exchange lag. I have no idea how this performs when real money is on the line.
  • 10 days is tiny. One bull run (April was strong) skews the numbers. I'm running a 60-day cycle to see if this holds.
  • I'm the operator. The circuit breaker triggered = I made the call to halt. Full automation (no human override) might behave differently—for better or worse.
  • Survivorship bias. I tuned RSI thresholds on April's data. This could be a lucky month. May could be flat or down.
  • Crypto fix is recent. April 23 unlocked crypto P&L. The "edge" might disappear when every other quant trader copies this strategy.

The Engineering Takeaway

If you're thinking about building a trading bot, here's what matters:

  1. Start with a clean data feed. Backtests garbage in, garbage out.
  2. Separate strategy from execution. The strategy (Bollinger + RSI) is trivial. The order router, circuit breaker, and settlement logic are where you win.
  3. Limit positions. 1–2% per trade sounds boring. It's the difference between a learning exercise and a blown account.
  4. Build the circuit breaker first. You'll use it before you use the strategy.
  5. Expect backtest → live gap. If the backtest is beautiful, live will be 90–95% of that. Budget for slippage, partial fills, and gaps.
  6. Unify your platforms early. Running crypto in one system and stocks in another is chaos. Build the abstraction once.