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

Published: (March 29, 2026 at 07:26 AM EDT)
4 min read
Source: Dev.to

Source: Dev.to

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 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.

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.

Project Structure

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

Strategy Base Class Example

# strategies/base.py
from abc import ABC, abstractmethod

class Strategy(ABC):
    name: str

    def __init__(self, alpaca):
        self.alpaca = alpaca

    @abstractmethod
    def compute_signals(self, ohlcv_data):
        """Return array of 1 (buy), -1 (sell), 0 (hold)."""
        pass

    @abstractmethod
    def submit_orders(self, signals, symbol, qty=10):
        """Submit paper orders based on signals."""
        pass

MACD Crossover Implementation

# strategies/macd_strategy.py
import numpy as np
from strategies.base import Strategy

class MACDStrategy(Strategy):
    name = "MACD Crossover"

    def compute_signals(self, ohlcv_data):
        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  # bearish crossover
        return signals

    def submit_orders(self, signals, symbol, qty=10):
        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')

Built‑in Strategies

TradeSight ships with six ready‑to‑use strategies:

  1. MACD Crossover
  2. RSI Overbought/Oversold
  3. Bollinger Band Squeeze
  4. EMA Ribbon
  5. Volume Spike + Price Confirmation
  6. Mean Reversion (Z‑score based)

Running a Tournament

import pandas as pd
import numpy as np
from alpaca_trade_api import TimeFrame

def run_tournament(symbols, strategies, alpaca):
    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 Strategies

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 displays a live leaderboard, current open positions, and historical tournament results so you can watch strategies improve (or die) over time.


Adding a Custom Strategy

  1. Drop a file in strategies/.
  2. Inherit from Strategy.
  3. Implement compute_signals and submit_orders.
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

The tournament runner auto‑discovers all strategy classes on startup.


Insights from Running Overnight Tournaments

Running overnight tournaments for a few weeks taught me more about strategy behavior than months of manual backtesting.

  • Execution timing matters – strategies that look good on paper often collapse on live data because of order latency and slippage. Paper trading isn’t perfect, but it’s much closer to reality than backtesting with perfect hindsight.
  • Consistency beats peak performance – a strategy that wins 60 % of tournaments is more valuable than one that wins spectacularly once and fails the rest.

Source Code

The full source is available at github.com/rmbell09-lang/tradesight. Stars and PRs are welcome — especially new strategy implementations.

0 views
Back to Blog

Related posts

Read more »