DEV Community

Ray
Ray

Posted on

"How I Built an Overnight Strategy Tournament System for Algorithmic Paper Trading"

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:

  1. All registered strategies fetch OHLCV data for a watchlist of tickers
  2. Each strategy computes buy/sell signals using its own indicator logic
  3. Paper orders are submitted to Alpaca
  4. 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
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)