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.
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.
| Strategy | Signal Type | Primary Indicator |
|---|---|---|
| MACD Crossover | Momentum | 12/26 EMA delta vs 9-period signal |
| MACD + SMA Filter | Momentum + Trend | MACD cross only above 50-day SMA |
| RSI Mean Reversion | Mean Reversion | RSI <30 buy / >70 sell |
| Bollinger Breakout | Volatility | Price crossing upper/lower band |
| Bollinger + Volume Confirm | Volatility + Volume | Breakout only with volume >1.5x avg |
| EMA Crossover (9/21) | Trend | Fast/slow EMA crossover |
| VWAP Reversion | Mean Reversion | Price deviation from intraday VWAP |
| ATR Momentum | Volatility | Price range expansion relative to ATR |
| Confluence Strategy | Multi-signal | MACD + RSI + SMA agreement |
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.
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.
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 Regime | VIX Range | Typical Winner | Why |
|---|---|---|---|
| Low volatility trend | <15 | Confluence | Multi-signal agreement = fewer false entries |
| Normal | 15–20 | MACD + SMA Filter | Trend filter cuts whipsaws |
| High volatility | 20–30 | MACD Crossover current | Momentum dominates in chaotic price action |
| Extreme / crash | >30 | ATR Momentum | Volatility 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.
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.
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
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.