A professional-grade quantitative trading system for statistical arbitrage across any cointegrated asset pair
Features • Installation • Quick Start • Documentation • Results
The Universal Pairs Trading System is a sophisticated mean-reversion strategy framework designed for cointegrated asset pairs. Built with institutional-grade risk management and comprehensive statistical validation, it works seamlessly across stocks, ETFs, futures, commodities, and cryptocurrencies.
Pairs trading is a market-neutral statistical arbitrage strategy that exploits temporary price divergences between two historically correlated assets. When the spread deviates from its historical mean, the strategy goes long the undervalued asset and short the overvalued one, profiting when the spread reverts.
- Cointegration Testing - Engle-Granger methodology
- Stationarity Analysis - Augmented Dickey-Fuller test
- Half-Life Calculation - Mean-reversion speed estimation
- Rolling Cointegration - Dynamic regime detection
- Dynamic Z-Score Thresholds - Volatility-adjusted entry/exit
- Regime Detection - Distinguish mean-reverting vs. trending markets
- Momentum Filters - Avoid catching falling knives
- Position Pyramiding - Scale into high-confidence trades
- Multiple Exit Strategies - Mean-reversion, stop-loss, and regime-based
- ATR-Based Position Sizing - Volatility-adjusted exposure
- Dynamic Stop-Loss/Take-Profit - Adaptive risk parameters
- Portfolio Heat Management - Maximum drawdown protection
- Circuit Breakers - Automatic trading halt on catastrophic losses
- Kelly Criterion - Optimal position sizing (optional)
- Realistic Transaction Costs - Commission, slippage, and market impact
- Walk-Forward Optimization - Out-of-sample validation
- Monte Carlo Simulation - Risk assessment via bootstrapping
- Comprehensive Metrics - Sharpe, Sortino, Calmar, drawdown, win rate
- Trade Journal Export - Detailed CSV for further analysis
- Publication-quality charts with institutional aesthetics
- Equity curves with drawdown
- Trade distribution analysis
- Monthly returns heatmap
- Rolling performance metrics
- Regime classification plots
- Python 3.8 or higher
- pip package manager
# Clone the repository
git clone https://github.com/yourusername/universal-pairs-trading.git
cd universal-pairs-trading
# Create virtual environment (recommended)
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txtpandas>=1.5.0
numpy>=1.23.0
matplotlib>=3.6.0
seaborn>=0.12.0
yfinance>=0.2.0
statsmodels>=0.14.0
scipy>=1.10.0
python main.pySelect from pre-configured examples:
- SPY-QQQ - S&P 500 vs Nasdaq ETFs
- GLD-SLV - Gold vs Silver
- CL-HO - Crude Oil crack spread (futures)
- AAPL-MSFT - Tech giants
- Multi-Pair Comparison - Portfolio analysis
- Custom Pair - Enter your own
from main import run_pairs_strategy
# Example 1: Basic usage
result = run_pairs_strategy(
asset1='SPY',
asset2='QQQ',
pair_name='SPY-QQQ',
start_date='2015-01-01',
initial_capital=500_000
)
# Example 2: Your original oil crack spread
result = run_pairs_strategy(
asset1='CL=F',
asset2='HO=F',
pair_name='Oil-Crack-Spread',
start_date='2010-01-01',
asset_type='futures'
)
# Example 3: Multi-pair portfolio
from main import run_multiple_pairs
pairs = [
('SPY', 'QQQ', 'S&P-Nasdaq'),
('GLD', 'SLV', 'Gold-Silver'),
('GLD', 'GDX', 'Gold-Miners')
]
comparison = run_multiple_pairs(pairs, start_date='2015-01-01')universal-pairs-trading/
├── config.py # Configuration management
├── data_handler.py # Data acquisition & validation
├── strategy.py # Signal generation logic
├── backtester.py # Backtesting engine
├── risk_manager.py # Risk management system
├── visualization.py # Charting and reporting
├── main.py # Orchestration script
├── requirements.txt # Dependencies
└── results/ # Output directory (auto-created)
from config import Config
# Default configuration
config = Config(asset1='SPY', asset2='QQQ')
# Custom configuration
config = Config(
asset1='GLD',
asset2='SLV',
start_date='2015-01-01',
initial_capital=1_000_000
)
# From preset
config = Config.from_preset('oil-crack')from data_handler import PairsDataHandler
handler = PairsDataHandler(config, 'SPY', 'QQQ')
# Fetch and validate
df = handler.fetch_data()
spread = handler.compute_spread(method='log')
# Statistical tests
handler.test_cointegration()
handler.test_stationarity(spread)
handler.calculate_half_life(spread)
handler.calculate_hedge_ratio()
# Rolling analysis
rolling_coint = handler.calculate_rolling_cointegration(window=252)from strategy import PairsMeanReversionStrategy
strategy = PairsMeanReversionStrategy(config)
signals = strategy.generate_signals(df, spread, rolling_coint)
# Analyze trades
trades = strategy.get_trade_list()
analysis = strategy.analyze_trades()from backtester import PairsBacktester
backtester = PairsBacktester(config)
results = backtester.run_backtest(signals, initial_capital=500_000)
metrics = backtester.calculate_performance_metrics()
# Advanced analysis
mc_results = backtester.monte_carlo_simulation(n_simulations=1000)
benchmark = backtester.benchmark_comparison(benchmark_ticker='SPY')from risk_manager import PairsRiskManager
risk_mgr = PairsRiskManager(config)
risk_adjusted = risk_mgr.apply_risk_filters(df, signals, asset_type='etf')
# Risk metrics
var_95 = risk_mgr.calculate_var(returns, confidence=0.95)
cvar_95 = risk_mgr.calculate_cvar(returns, confidence=0.95)
kelly_pct = risk_mgr.calculate_kelly_criterion(win_rate, avg_win, avg_loss)from visualization import PairsVisualizer
viz = PairsVisualizer(config, pair_name='SPY-QQQ')
# Generate plots
viz.plot_price_series(df, save_path='results/prices.png')
viz.plot_spread_analysis(signals, save_path='results/spread.png')
viz.plot_equity_curve(results, save_path='results/equity.png')
viz.plot_trade_distribution(trades, save_path='results/trades.png')
viz.plot_monthly_returns_heatmap(results, save_path='results/monthly.png')- Calculate z-score of spread:
z = (spread - mean) / std - Adjust thresholds dynamically based on volatility regime
- Confirm favorable market regime (mean-reverting vs. trending)
- Verify momentum indicators align with entry direction
- Optional: Only trade during cointegrated periods
Entry Signals:
- Long: z-score < -2.0σ (spread oversold)
- Short: z-score > +2.0σ (spread overbought)
- Mean Reversion: |z-score| < 0.5σ (primary exit)
- Stop Loss: z-score reverses direction sharply
- Regime Change: Market shifts to volatile/trending
- Time-Based: Maximum holding period exceeded
- ATR Stops: Dynamic stop-loss based on volatility
Using ATR-based risk management:
Position Size = (Capital × Risk%) / (ATR × Stop Multiple)
Constraints:
- Max 30% of capital per position
- Max 10% total portfolio heat
- Volatility-adjusted scaling during extreme regimes
Two price series can be correlated but still drift apart permanently. Cointegration ensures:
- A stable long-term equilibrium relationship exists
- Deviations are temporary and statistically predictable
- Mean-reversion is not just correlation noise
- P-Value < 0.05: Statistically significant cointegration
- ADF Statistic < Critical Value: Spread is stationary
- Half-Life < 60 days: Fast enough for practical trading
- Hedge Ratio (β): Optimal units of Asset2 per unit of Asset1
config.strategy.window = 30 # Z-score calculation window
config.strategy.z_entry_long = -2.0 # Long entry threshold
config.strategy.z_entry_short = 2.0 # Short entry threshold
config.strategy.z_exit = 0.5 # Exit threshold
config.strategy.use_regime_filter = True # Enable regime detection
config.strategy.use_dynamic_thresholds = True # Volatility-adjusted
config.strategy.scale_in_enabled = True # Position pyramidingconfig.risk.risk_per_trade = 0.02 # 2% risk per trade
config.risk.max_position_size = 0.30 # Max 30% per position
config.risk.atr_period = 14 # ATR calculation period
config.risk.atr_stop_multiple = 2.5 # Stop at 2.5× ATR
config.risk.atr_target_multiple = 4.0 # Target at 4.0× ATRconfig.backtest.transaction_cost_pct = 0.0005 # 5 bps per side
config.backtest.slippage_pct = 0.0002 # 2 bps slippage
config.backtest.commission_per_contract = 2.50 # For futuresbacktester.walk_forward_analysis(
df=df,
strategy_func=generate_signals,
train_window=504, # 2 years
test_window=126 # 6 months
)mc_results = backtester.monte_carlo_simulation(
n_simulations=1000,
n_trades=100
)
print(f"Probability of Profit: {mc_results['summary']['Probability_of_Profit']:.1f}%")
print(f"5th Percentile Return: {mc_results['summary']['Percentile_5_Return']:.2f}%")
print(f"95th Percentile Return: {mc_results['summary']['Percentile_95_Return']:.2f}%")- SPY vs QQQ (S&P 500 vs Nasdaq)
- EFA vs EEM (Developed vs Emerging Markets)
- XLE vs XLF (Energy vs Financials)
- GLD vs SLV (Gold vs Silver)
- GLD vs GDX (Gold vs Gold Miners)
- CL vs HO (Crude Oil crack spread)
- CL vs NG (Crude vs Natural Gas)
- USO vs UNG (Oil vs Gas ETFs)
- BTC-USD vs ETH-USD
- BTC-USD vs BNB-USD
- Tech vs Healthcare
- Utilities vs Industrials
This software is for educational and research purposes only.
- Past performance does not guarantee future results
- Trading involves substantial risk of loss
- Cointegration relationships can break down unexpectedly
- Always backtest thoroughly on out-of-sample data
- Use appropriate position sizing and risk management
- Consult a licensed financial advisor before trading
Extend the PairsMeanReversionStrategy class:
class MyCustomStrategy(PairsMeanReversionStrategy):
def _generate_entry_signals(self, df, rolling_coint=None):
# Your custom entry logic
df['Signal'] = 0
# Example: Add RSI filter
df['RSI'] = self.calculate_rsi(df['Spread'], period=14)
long_condition = (
(df['Z_Score'] < -2.0) &
(df['RSI'] < 30) & # Oversold
(df['Regime'] == 'Mean_Reverting')
)
df.loc[long_condition, 'Signal'] = 1
return df# Pseudo-code for live trading integration
from live_trading import BrokerAPI
broker = BrokerAPI(api_key='your_key')
# Get current positions
positions = broker.get_positions()
# Generate signals
signals = strategy.generate_signals(df_latest, spread_latest)
# Execute trades
if signals['Signal'].iloc[-1] == 1:
broker.submit_order(
symbol='SPY',
qty=calculated_position_size,
side='buy'
)
broker.submit_order(
symbol='QQQ',
qty=calculated_position_size * hedge_ratio,
side='sell'
)Contributions are welcome! Please feel free to submit a Pull Request.
# Fork and clone the repository
git clone https://github.com/yourusername/universal-pairs-trading.git
cd universal-pairs-trading
# Create feature branch
git checkout -b feature/amazing-feature
# Make changes and test
python -m pytest tests/
# Commit and push
git commit -m "Add amazing feature"
git push origin feature/amazing-featureIf you use this system in your research, please cite:
@software{robbins2026pairs,
author = {Robbins, Alexander},
title = {Universal Pairs Trading System: A Professional-Grade Statistical Arbitrage Framework},
year = {2026},
publisher = {GitHub},
url = {https://github.com/XanderRobbins/universal-pairs-trading}
}Alexander Robbins
University of Florida | Math, CS, Economics
📧 robbins.a@ufl.edu
🔗 GitHub | LinkedIn | Website
This project is licensed under the MIT License - see the LICENSE file for details.
- University of Florida Department of Mathematics
- Open-source Python community (pandas, numpy, matplotlib, statsmodels)