DEV Community

Ray
Ray

Posted on

"How My Trading Bot Evolves Its Own RSI Strategy Overnight (Without Me Touching It)"

How My Trading Bot Evolves Its Own RSI Strategy Overnight (Without Me Touching It)

Most algo trading systems have a fixed strategy. You tune the parameters, backtest, deploy. Done. The strategy stays static until you manually update it.

TradeSight does something different: every night, while markets are closed, it runs a tournament. 20+ RSI parameter combinations compete against historical data. The winner gets promoted to live paper trading the next day. No manual intervention.

Here's exactly how the overnight evolution loop works.


Why RSI Parameters Matter More Than You Think

RSI (Relative Strength Index) has two critical parameters most traders treat as fixed:

  • Period (usually 14): how many bars to average
  • Overbought/oversold thresholds (usually 70/30): when to trade

The problem: optimal RSI parameters aren't universal. A 14-period RSI with 70/30 thresholds worked in 2010. It may not work the same way now. Markets change. What worked last month might underperform this month.

The insight: if you can test enough combinations automatically and rank them by risk-adjusted performance, you can adapt.


The Tournament Architecture

Market Close (4 PM ET)
        ↓
Fetch Historical Bars (Alpaca API, last 60 days)
        ↓
Generate Strategy Grid (25 combinations)
        ↓
Run Backtests in Parallel
        ↓
Rank by Sharpe Ratio
        ↓
Promote Winner to Live Config
        ↓
Market Open (9:30 AM ET)
Enter fullscreen mode Exit fullscreen mode

The whole loop runs via a scheduled cron job and writes results to a SQLite tournament database.


Generating the Strategy Grid

from itertools import product

def generate_rsi_grid():
    periods = [9, 11, 14, 18, 21]
    oversold = [25, 28, 30, 32, 35]

    strategies = []
    for period, sold_threshold in product(periods, oversold):
        bought_threshold = 100 - sold_threshold  # symmetric
        strategies.append({
            "period": period,
            "oversold": sold_threshold,
            "overbought": bought_threshold,
            "name": f"RSI_{period}_{sold_threshold}"
        })

    return strategies  # 25 combinations
Enter fullscreen mode Exit fullscreen mode

Each combination is a full strategy with buy signal (RSI crosses below oversold) and sell signal (RSI crosses above overbought or 5-day timeout).


The Backtest Engine

For each strategy, the backtest iterates over 60 days of daily bars:

def backtest_rsi_strategy(bars_df, period, oversold, overbought, capital=10000):
    rsi = compute_rsi(bars_df['close'], period)

    position = None
    trades = []
    portfolio_value = [capital]

    for i in range(period, len(bars_df)):
        current_rsi = rsi.iloc[i]
        price = bars_df['close'].iloc[i]

        if position is None and current_rsi < oversold:
            # Buy signal
            shares = int(capital * 0.95 / price)
            position = {"shares": shares, "entry_price": price, "entry_day": i}

        elif position is not None:
            days_held = i - position["entry_day"]

            if current_rsi > overbought or days_held >= 5:
                # Sell signal
                pnl = (price - position["entry_price"]) * position["shares"]
                trades.append({"pnl": pnl, "days": days_held})
                capital += pnl
                position = None

        portfolio_value.append(capital)

    return trades, portfolio_value
Enter fullscreen mode Exit fullscreen mode

Simple and fast — each backtest runs in milliseconds.


Sharpe Ratio Ranking

Not all returns are equal. A strategy that returns 8% with low volatility beats one returning 12% with wild swings.

import numpy as np

def sharpe_ratio(portfolio_values, risk_free_rate=0.05):
    """Annualized Sharpe ratio from daily portfolio values."""
    daily_returns = np.diff(portfolio_values) / portfolio_values[:-1]

    if len(daily_returns) < 2 or daily_returns.std() == 0:
        return 0.0

    excess_returns = daily_returns - (risk_free_rate / 252)
    annualized_sharpe = (excess_returns.mean() / excess_returns.std()) * np.sqrt(252)

    return round(annualized_sharpe, 3)

def rank_strategies(results):
    ranked = sorted(results, key=lambda x: x["sharpe"], reverse=True)
    return ranked
Enter fullscreen mode Exit fullscreen mode

A Sharpe above 1.0 is considered acceptable. Above 2.0 is strong. The tournament promotes whatever scores highest, with a floor of 0.5 (if nothing beats 0.5 Sharpe, the previous winner stays).


Promoting the Winner

def promote_winner(strategy):
    """Write winning strategy params to live config."""
    config_path = Path("~/Projects/TradeSight/config/live_strategy.json")

    current = json.loads(config_path.read_text()) if config_path.exists() else {}

    config_path.write_text(json.dumps({
        **current,
        "rsi_period": strategy["period"],
        "rsi_oversold": strategy["oversold"],
        "rsi_overbought": strategy["overbought"],
        "promoted_at": datetime.utcnow().isoformat(),
        "tournament_sharpe": strategy["sharpe"],
        "tournament_return": strategy["total_return"]
    }, indent=2))

    print(f"Promoted: {strategy['name']} (Sharpe: {strategy['sharpe']}, Return: {strategy['total_return']:.1%})")
Enter fullscreen mode Exit fullscreen mode

The live trading engine reads this file at market open. No restarts needed — just a config change.


Real Tournament Results

Running this on S&P 500 large-cap stocks over a 30-day period, here's what the last 5 tournaments promoted:

Date Winner Period Oversold Sharpe 30d Return
Mar 21 RSI_9_28 9 28 1.84 +7.2%
Mar 22 RSI_11_30 11 30 1.61 +5.9%
Mar 23 RSI_9_28 9 28 1.92 +8.1%
Mar 24 RSI_14_32 14 32 1.44 +4.8%
Mar 25 RSI_9_28 9 28 1.78 +6.4%

The 9-period / 28-threshold combination has won 3 of 5 recent tournaments on this dataset — suggesting a faster RSI with a tighter oversold threshold is currently better-suited to market conditions. With the default 14/30, that edge doesn't show up.


What This Isn't

This is paper trading. Real money is not involved. Past backtest performance doesn't guarantee future results. The tournament runs on historical data that includes lookahead bias risks if you're not careful about data splits.

The goal isn't to get rich — it's to build infrastructure that continuously adapts, and to do it in a way that's transparent (every tournament result is logged) and auditable (all parameters stored, not a black box).


Try It

TradeSight is MIT-licensed and runs entirely on your machine against Alpaca's free paper trading API.

GitHub: github.com/rmbell09-lang/tradesight

The tournament system lives in tournament/overnight_optimizer.py. Pull requests welcome — especially for adding new strategy types beyond RSI.

Top comments (0)