The Problem With Testing Trading Strategies
As a trader, testing multiple strategies is brutal. Manual backtesting is slow, inconsistent, and you can't run 10 strategies simultaneously while you sleep. Most people end up with gut-feel decisions dressed up as analysis.
I built TradeSight to fix this: an overnight strategy tournament that runs your strategies in parallel, ranks them by actual performance metrics, and hands you a leaderboard in the morning.
The Tournament Concept
The core idea is simple — pit your strategies against each other on real market data (paper trading via Alpaca), rank them by Sharpe ratio + win rate, and let the best ones survive. Think evolutionary pressure, but for trading algos.
Each night:
- All registered strategies fetch OHLCV data for a watchlist of tickers
- Each strategy computes buy/sell signals using its own indicator logic
- Paper orders are submitted to Alpaca
- At morning close, results are scored and ranked on the dashboard
Architecture
Four components keep this simple:
- Flask Dashboard — configure tournaments, view live positions, leaderboard
- Strategy Runner — executes strategies in parallel (threading, not async — simpler for this use case)
- Alpaca Integration — paper trading API for order submission and position tracking
- Cron Automation — launches the tournament at market close, scores at open
TradeSight/
├── app/
│ ├── dashboard.py # Flask routes
│ ├── runner.py # Strategy execution engine
│ └── alpaca_client.py # Alpaca API wrapper
├── strategies/
│ ├── base.py # Strategy base class
│ ├── macd_strategy.py
│ └── rsi_strategy.py
└── cron/
└── overnight_run.sh
Defining a Strategy
Every strategy inherits from Strategy and implements two methods:
from strategies.base import Strategy
import numpy as np
class MACDStrategy(Strategy):
name = "MACD Crossover"
def compute_signals(self, ohlcv_data):
"""Return array of 1 (buy), -1 (sell), 0 (hold)."""
close = ohlcv_data['close'].values
# EMA calculations
ema12 = self._ema(close, 12)
ema26 = self._ema(close, 26)
macd_line = ema12 - ema26
signal_line = self._ema(macd_line, 9)
# Crossover detection
signals = np.zeros(len(close))
for i in range(1, len(macd_line)):
if macd_line[i] > signal_line[i] and macd_line[i-1] <= signal_line[i-1]:
signals[i] = 1 # bullish crossover
elif macd_line[i] < signal_line[i] and macd_line[i-1] >= signal_line[i-1]:
signals[i] = -1 # bearish crossover
return signals
def submit_orders(self, signals, symbol, qty=10):
"""Submit paper orders based on signals."""
latest = signals[-1]
if latest == 1:
self.alpaca.submit_order(symbol=symbol, qty=qty, side='buy', type='market')
elif latest == -1:
self.alpaca.submit_order(symbol=symbol, qty=qty, side='sell', type='market')
TradeSight currently ships with 6 strategies out of the box:
- MACD Crossover
- RSI Overbought/Oversold
- Bollinger Band Squeeze
- EMA Ribbon
- Volume Spike + Price Confirmation
- Mean Reversion (Z-score based)
The Paper Trading Loop
def run_tournament(symbols, strategies):
results = []
for strategy in strategies:
strategy_results = []
for symbol in symbols:
# Fetch OHLCV from Alpaca
bars = alpaca.get_bars(symbol, TimeFrame.Minute, limit=200)
ohlcv = pd.DataFrame([b._raw for b in bars])
# Get signals
signals = strategy.compute_signals(ohlcv)
# Submit paper orders
strategy.submit_orders(signals, symbol)
# Track for scoring
strategy_results.append({
'symbol': symbol,
'signal': signals[-1],
'entry_price': ohlcv['close'].iloc[-1]
})
results.append({'strategy': strategy.name, 'trades': strategy_results})
return results
Scoring and the Leaderboard
After positions close, each strategy is scored:
def score_strategy(trades):
winning_trades = [t for t in trades if t['pnl'] > 0]
win_rate = len(winning_trades) / len(trades) if trades else 0
returns = [t['pnl_pct'] for t in trades]
sharpe = (np.mean(returns) / np.std(returns)) * np.sqrt(252) if returns else 0
# Combined score: weights Sharpe more than win rate
score = (sharpe * 0.6) + (win_rate * 0.4)
return {'sharpe': sharpe, 'win_rate': win_rate, 'score': score}
The Flask dashboard shows a live leaderboard, current open positions, and historical tournament results so you can watch strategies improve (or die) over time.
Adding Your Own Strategy
Drop a file in strategies/, inherit from Strategy, implement compute_signals and submit_orders. That's it. The tournament runner auto-discovers all strategy classes on startup.
class MyCustomStrategy(Strategy):
name = "My Custom Strategy"
def compute_signals(self, ohlcv_data):
# Your logic here
pass
def submit_orders(self, signals, symbol, qty=10):
# Your execution logic here
pass
What I Learned
Running overnight tournaments for a few weeks taught me more about strategy behavior than months of manual backtesting. The biggest insight: strategies that look good on paper often collapse on live data because of execution timing. Paper trading isn't perfect, but it's a lot closer to reality than backtesting on historical data with perfect hindsight.
The tournament format also surfaces something important: consistency beats peak performance. A strategy that wins 60% of tournaments is more valuable than one that wins one tournament spectacularly and fails the rest.
GitHub
The full source is at github.com/rmbell09-lang/tradesight. Stars and PRs welcome — especially new strategy implementations.
Top comments (0)