📅 April 4, 2026 python algo-trading backtesting open-source

How TradeSight's Overnight Strategy Tournament Works

Every night at 2 AM, TradeSight runs all 9 of its built-in strategies against 6 months of historical data and scores them. The top performer gets more capital allocation the next trading day. By morning, you have a ranked leaderboard and a winner — without touching anything.

This post explains the tournament mechanics: what gets scored, how composite scores are calculated, and why the winner isn't always the strategy with the highest return.

TL;DR: Return alone is a bad fitness function. TradeSight scores on Sharpe ratio, max drawdown, win rate, and profit factor — then weights them to penalize high-variance strategies even if they look profitable on raw P&L.

The 9 Strategies

Each strategy is a standalone Python module under strategies/ with a consistent interface: it takes OHLCV data, returns a list of trades. No shared state, no side effects between strategies during the tournament run.

StrategySignal TypePrimary Indicator
MACD CrossoverMomentum12/26 EMA delta vs 9-period signal
MACD + SMA FilterMomentum + TrendMACD cross only above 50-day SMA
RSI Mean ReversionMean ReversionRSI <30 buy / >70 sell
Bollinger BreakoutVolatilityPrice crossing upper/lower band
Bollinger + Volume ConfirmVolatility + VolumeBreakout only with volume >1.5x avg
EMA Crossover (9/21)TrendFast/slow EMA crossover
VWAP ReversionMean ReversionPrice deviation from intraday VWAP
ATR MomentumVolatilityPrice range expansion relative to ATR
Confluence StrategyMulti-signalMACD + RSI + SMA agreement

Running the Tournament

The tournament runner iterates over each strategy, fetches data for the configured ticker universe, runs the backtest, and collects metrics. The whole thing runs under a cron job:

# crontab entry
0 2 * * 1-5 cd /path/to/tradesight && python run_tournament.py >> logs/tournament.log 2>&1
from tournament import TournamentRunner

runner = TournamentRunner(
    strategies=load_all_strategies(),
    tickers=["AAPL", "MSFT", "NVDA", "JPM", "META", "AMZN"],
    lookback_days=180
)

results = runner.run()
winner = results.ranked[0]
print(f"Winner: {winner.name} — composite score {winner.score:.3f}")

Each strategy runs the same data, same date range, same tickers. No lookahead, no survivorship bias in the ticker selection — the universe is fixed upfront.

The Scoring Function

This is where most tournament implementations go wrong. If you rank by return, you get the strategy that got lucky on one big trade. TradeSight uses a composite score:

def composite_score(metrics: StrategyMetrics) -> float:
    """
    Weights tuned empirically over 3 months of live paper trading.
    Penalizes high variance aggressively — a consistent +8% beats
    a volatile +15% with deep drawdowns.
    """
    sharpe_score    = normalize(metrics.sharpe_ratio,    min=-2, max=3)   * 0.35
    drawdown_score  = normalize(-metrics.max_drawdown,   min=-0.4, max=0) * 0.25
    winrate_score   = normalize(metrics.win_rate,        min=0.3, max=0.8) * 0.20
    pfactor_score   = normalize(metrics.profit_factor,   min=0.5, max=3.0) * 0.20

    return sharpe_score + drawdown_score + winrate_score + pfactor_score

The weights reflect real risk tolerance. Sharpe ratio gets 35% because it captures return per unit of risk. Max drawdown gets 25% because a strategy you'd abandon mid-drawdown is useless regardless of its long-term backtest. Win rate and profit factor split the remainder.

Why the Winner Changes Week to Week

This is the part that surprised people when we first shared TradeSight. The Confluence Strategy — which combines MACD, RSI, and SMA agreement — almost always wins during trending, low-volatility regimes. But when VIX spikes above 20, MACD Crossover takes the top spot because momentum signals work better in high-volatility environments where mean reversion breaks down.

Market RegimeVIX RangeTypical WinnerWhy
Low volatility trend<15ConfluenceMulti-signal agreement = fewer false entries
Normal15–20MACD + SMA FilterTrend filter cuts whipsaws
High volatility20–30MACD Crossover currentMomentum dominates in chaotic price action
Extreme / crash>30ATR MomentumVolatility expansion signals more reliable than direction signals

This is regime-dependent performance in action. A static capital allocation — "always run the Confluence strategy" — underperforms because it's optimized for one regime. The tournament handles this automatically.

Capital Allocation After Each Tournament

After ranking, TradeSight redistributes capital across the live strategies for the next trading day:

def allocate_capital(ranked_results, total_capital=10000):
    """
    Top scorer gets 50%, second gets 30%, third gets 20%.
    Everything below rank 3 runs at zero allocation (monitoring only).
    """
    allocations = {}
    weights = [0.50, 0.30, 0.20]
    for i, result in enumerate(ranked_results[:3]):
        allocations[result.name] = total_capital * weights[i]
    return allocations

This is deliberately simple. Fancier allocation schemes (Kelly criterion, mean-variance optimization) add fragility. The tournament already does the heavy lifting by identifying which strategy fits the current regime.

Running It Yourself

git clone https://github.com/rmbell09-lang/tradesight
cd tradesight
pip install -r requirements.txt
cp .env.example .env  # add Alpaca paper trading keys

# Run a single tournament manually
python run_tournament.py --tickers AAPL MSFT NVDA --lookback 90

# Or let cron handle it nightly
crontab -e  # add the 2 AM line from above

Open source, MIT licensed

Fork it, add your own strategies, plug in your own scoring weights. github.com/rmbell09-lang/tradesight

The code is structured so adding a new strategy is one file and one import — the tournament runner discovers them automatically. If you build something interesting, open a PR.