'How I Built an Overnight Strategy Tournament System for Algorithmic Paper Trading'
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
- 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.
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.shStrategy 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."""
passMACD 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:
- MACD Crossover
- RSI Overbought/Oversold
- Bollinger Band Squeeze
- EMA Ribbon
- Volume Spike + Price Confirmation
- 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 resultsScoring 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
- Drop a file in
strategies/. - Inherit from
Strategy. - Implement
compute_signalsandsubmit_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
passThe 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.