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)
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
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
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
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%})")
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)