Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Implement and analyze trading strategy backtests using Python frameworks like Backtrader, Zipline, or VectorBT.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: backtesting-frameworks3description: Build robust backtesting systems for trading strategies with proper handling of look-ahead bias, survivorship bias, and transaction costs. Use when developing trading algorithms, validating strategies, or building backtesting infrastructure.4---56# Backtesting Frameworks78Build robust, production-grade backtesting systems that avoid common pitfalls and produce reliable strategy performance estimates.910## When to Use This Skill1112- Developing trading strategy backtests13- Building backtesting infrastructure14- Validating strategy performance15- Avoiding common backtesting biases16- Implementing walk-forward analysis17- Comparing strategy alternatives1819## Core Concepts2021### 1. Backtesting Biases2223| Bias | Description | Mitigation |24| ---------------- | ------------------------- | ----------------------- |25| **Look-ahead** | Using future information | Point-in-time data |26| **Survivorship** | Only testing on survivors | Use delisted securities |27| **Overfitting** | Curve-fitting to history | Out-of-sample testing |28| **Selection** | Cherry-picking strategies | Pre-registration |29| **Transaction** | Ignoring trading costs | Realistic cost models |3031### 2. Proper Backtest Structure3233```34Historical Data35│36▼37┌─────────────────────────────────────────┐38│ Training Set │39│ (Strategy Development & Optimization) │40└─────────────────────────────────────────┘41│42▼43┌─────────────────────────────────────────┐44│ Validation Set │45│ (Parameter Selection, No Peeking) │46└─────────────────────────────────────────┘47│48▼49┌─────────────────────────────────────────┐50│ Test Set │51│ (Final Performance Evaluation) │52└─────────────────────────────────────────┘53```5455### 3. Walk-Forward Analysis5657```58Window 1: [Train──────][Test]59Window 2: [Train──────][Test]60Window 3: [Train──────][Test]61Window 4: [Train──────][Test]62─────▶ Time63```6465## Implementation Patterns6667### Pattern 1: Event-Driven Backtester6869```python70from abc import ABC, abstractmethod71from dataclasses import dataclass, field72from datetime import datetime73from decimal import Decimal74from enum import Enum75from typing import Dict, List, Optional76import pandas as pd77import numpy as np7879class OrderSide(Enum):80BUY = "buy"81SELL = "sell"8283class OrderType(Enum):84MARKET = "market"85LIMIT = "limit"86STOP = "stop"8788@dataclass89class Order:90symbol: str91side: OrderSide92quantity: Decimal93order_type: OrderType94limit_price: Optional[Decimal] = None95stop_price: Optional[Decimal] = None96timestamp: Optional[datetime] = None9798@dataclass99class Fill:100order: Order101fill_price: Decimal102fill_quantity: Decimal103commission: Decimal104slippage: Decimal105timestamp: datetime106107@dataclass108class Position:109symbol: str110quantity: Decimal = Decimal("0")111avg_cost: Decimal = Decimal("0")112realized_pnl: Decimal = Decimal("0")113114def update(self, fill: Fill) -> None:115if fill.order.side == OrderSide.BUY:116new_quantity = self.quantity + fill.fill_quantity117if new_quantity != 0:118self.avg_cost = (119(self.quantity * self.avg_cost + fill.fill_quantity * fill.fill_price)120/ new_quantity121)122self.quantity = new_quantity123else:124self.realized_pnl += fill.fill_quantity * (fill.fill_price - self.avg_cost)125self.quantity -= fill.fill_quantity126127@dataclass128class Portfolio:129cash: Decimal130positions: Dict[str, Position] = field(default_factory=dict)131132def get_position(self, symbol: str) -> Position:133if symbol not in self.positions:134self.positions[symbol] = Position(symbol=symbol)135return self.positions[symbol]136137def process_fill(self, fill: Fill) -> None:138position = self.get_position(fill.order.symbol)139position.update(fill)140141if fill.order.side == OrderSide.BUY:142self.cash -= fill.fill_price * fill.fill_quantity + fill.commission143else:144self.cash += fill.fill_price * fill.fill_quantity - fill.commission145146def get_equity(self, prices: Dict[str, Decimal]) -> Decimal:147equity = self.cash148for symbol, position in self.positions.items():149if position.quantity != 0 and symbol in prices:150equity += position.quantity * prices[symbol]151return equity152153class Strategy(ABC):154@abstractmethod155def on_bar(self, timestamp: datetime, data: pd.DataFrame) -> List[Order]:156pass157158@abstractmethod159def on_fill(self, fill: Fill) -> None:160pass161162class ExecutionModel(ABC):163@abstractmethod164def execute(self, order: Order, bar: pd.Series) -> Optional[Fill]:165pass166167class SimpleExecutionModel(ExecutionModel):168def __init__(self, slippage_bps: float = 10, commission_per_share: float = 0.01):169self.slippage_bps = slippage_bps170self.commission_per_share = commission_per_share171172def execute(self, order: Order, bar: pd.Series) -> Optional[Fill]:173if order.order_type == OrderType.MARKET:174base_price = Decimal(str(bar["open"]))175176# Apply slippage177slippage_mult = 1 + (self.slippage_bps / 10000)178if order.side == OrderSide.BUY:179fill_price = base_price * Decimal(str(slippage_mult))180else:181fill_price = base_price / Decimal(str(slippage_mult))182183commission = order.quantity * Decimal(str(self.commission_per_share))184slippage = abs(fill_price - base_price) * order.quantity185186return Fill(187order=order,188fill_price=fill_price,189fill_quantity=order.quantity,190commission=commission,191slippage=slippage,192timestamp=bar.name193)194return None195196class Backtester:197def __init__(198self,199strategy: Strategy,200execution_model: ExecutionModel,201initial_capital: Decimal = Decimal("100000")202):203self.strategy = strategy204self.execution_model = execution_model205self.portfolio = Portfolio(cash=initial_capital)206self.equity_curve: List[tuple] = []207self.trades: List[Fill] = []208209def run(self, data: pd.DataFrame) -> pd.DataFrame:210"""Run backtest on OHLCV data with DatetimeIndex."""211pending_orders: List[Order] = []212213for timestamp, bar in data.iterrows():214# Execute pending orders at today's prices215for order in pending_orders:216fill = self.execution_model.execute(order, bar)217if fill:218self.portfolio.process_fill(fill)219self.strategy.on_fill(fill)220self.trades.append(fill)221222pending_orders.clear()223224# Get current prices for equity calculation225prices = {data.index.name or "default": Decimal(str(bar["close"]))}226equity = self.portfolio.get_equity(prices)227self.equity_curve.append((timestamp, float(equity)))228229# Generate new orders for next bar230new_orders = self.strategy.on_bar(timestamp, data.loc[:timestamp])231pending_orders.extend(new_orders)232233return self._create_results()234235def _create_results(self) -> pd.DataFrame:236equity_df = pd.DataFrame(self.equity_curve, columns=["timestamp", "equity"])237equity_df.set_index("timestamp", inplace=True)238equity_df["returns"] = equity_df["equity"].pct_change()239return equity_df240```241242### Pattern 2: Vectorized Backtester (Fast)243244```python245import pandas as pd246import numpy as np247from typing import Callable, Dict, Any248249class VectorizedBacktester:250"""Fast vectorized backtester for simple strategies."""251252def __init__(253self,254initial_capital: float = 100000,255commission: float = 0.001, # 0.1%256slippage: float = 0.0005 # 0.05%257):258self.initial_capital = initial_capital259self.commission = commission260self.slippage = slippage261262def run(263self,264prices: pd.DataFrame,265signal_func: Callable[[pd.DataFrame], pd.Series]266) -> Dict[str, Any]:267"""268Run backtest with signal function.269270Args:271prices: DataFrame with 'close' column272signal_func: Function that returns position signals (-1, 0, 1)273274Returns:275Dictionary with results276"""277# Generate signals (shifted to avoid look-ahead)278signals = signal_func(prices).shift(1).fillna(0)279280# Calculate returns281returns = prices["close"].pct_change()282283# Calculate strategy returns with costs284position_changes = signals.diff().abs()285trading_costs = position_changes * (self.commission + self.slippage)286287strategy_returns = signals * returns - trading_costs288289# Build equity curve290equity = (1 + strategy_returns).cumprod() * self.initial_capital291292# Calculate metrics293results = {294"equity": equity,295"returns": strategy_returns,296"signals": signals,297"metrics": self._calculate_metrics(strategy_returns, equity)298}299300return results301302def _calculate_metrics(303self,304returns: pd.Series,305equity: pd.Series306) -> Dict[str, float]:307"""Calculate performance metrics."""308total_return = (equity.iloc[-1] / self.initial_capital) - 1309annual_return = (1 + total_return) ** (252 / len(returns)) - 1310annual_vol = returns.std() * np.sqrt(252)311sharpe = annual_return / annual_vol if annual_vol > 0 else 0312313# Drawdown314rolling_max = equity.cummax()315drawdown = (equity - rolling_max) / rolling_max316max_drawdown = drawdown.min()317318# Win rate319winning_days = (returns > 0).sum()320total_days = (returns != 0).sum()321win_rate = winning_days / total_days if total_days > 0 else 0322323return {324"total_return": total_return,325"annual_return": annual_return,326"annual_volatility": annual_vol,327"sharpe_ratio": sharpe,328"max_drawdown": max_drawdown,329"win_rate": win_rate,330"num_trades": int((returns != 0).sum())331}332333# Example usage334def momentum_signal(prices: pd.DataFrame, lookback: int = 20) -> pd.Series:335"""Simple momentum strategy: long when price > SMA, else flat."""336sma = prices["close"].rolling(lookback).mean()337return (prices["close"] > sma).astype(int)338339# Run backtest340# backtester = VectorizedBacktester()341# results = backtester.run(price_data, lambda p: momentum_signal(p, 50))342```343344### Pattern 3: Walk-Forward Optimization345346```python347from typing import Callable, Dict, List, Tuple, Any348import pandas as pd349import numpy as np350from itertools import product351352class WalkForwardOptimizer:353"""Walk-forward analysis with anchored or rolling windows."""354355def __init__(356self,357train_period: int,358test_period: int,359anchored: bool = False,360n_splits: int = None361):362"""363Args:364train_period: Number of bars in training window365test_period: Number of bars in test window366anchored: If True, training always starts from beginning367n_splits: Number of train/test splits (auto-calculated if None)368"""369self.train_period = train_period370self.test_period = test_period371self.anchored = anchored372self.n_splits = n_splits373374def generate_splits(375self,376data: pd.DataFrame377) -> List[Tuple[pd.DataFrame, pd.DataFrame]]:378"""Generate train/test splits."""379splits = []380n = len(data)381382if self.n_splits:383step = (n - self.train_period) // self.n_splits384else:385step = self.test_period386387start = 0388while start + self.train_period + self.test_period <= n:389if self.anchored:390train_start = 0391else:392train_start = start393394train_end = start + self.train_period395test_end = min(train_end + self.test_period, n)396397train_data = data.iloc[train_start:train_end]398test_data = data.iloc[train_end:test_end]399400splits.append((train_data, test_data))401start += step402403return splits404405def optimize(406self,407data: pd.DataFrame,408strategy_func: Callable,409param_grid: Dict[str, List],410metric: str = "sharpe_ratio"411) -> Dict[str, Any]:412"""413Run walk-forward optimization.414415Args:416data: Full dataset417strategy_func: Function(data, **params) -> results dict418param_grid: Parameter combinations to test419metric: Metric to optimize420421Returns:422Combined results from all test periods423"""424splits = self.generate_splits(data)425all_results = []426optimal_params_history = []427428for i, (train_data, test_data) in enumerate(splits):429# Optimize on training data430best_params, best_metric = self._grid_search(431train_data, strategy_func, param_grid, metric432)433optimal_params_history.append(best_params)434435# Test with optimal params436test_results = strategy_func(test_data, **best_params)437test_results["split"] = i438test_results["params"] = best_params439all_results.append(test_results)440441print(f"Split {i+1}/{len(splits)}: "442f"Best {metric}={best_metric:.4f}, params={best_params}")443444return {445"split_results": all_results,446"param_history": optimal_params_history,447"combined_equity": self._combine_equity_curves(all_results)448}449450def _grid_search(451self,452data: pd.DataFrame,453strategy_func: Callable,454param_grid: Dict[str, List],455metric: str456) -> Tuple[Dict, float]:457"""Grid search for best parameters."""458best_params = None459best_metric = -np.inf460461# Generate all parameter combinations462param_names = list(param_grid.keys())463param_values = list(param_grid.values())464465for values in product(*param_values):466params = dict(zip(param_names, values))467results = strategy_func(data, **params)468469if results["metrics"][metric] > best_metric:470best_metric = results["metrics"][metric]471best_params = params472473return best_params, best_metric474475def _combine_equity_curves(476self,477results: List[Dict]478) -> pd.Series:479"""Combine equity curves from all test periods."""480combined = pd.concat([r["equity"] for r in results])481return combined482```483484### Pattern 4: Monte Carlo Analysis485486```python487import numpy as np488import pandas as pd489from typing import Dict, List490491class MonteCarloAnalyzer:492"""Monte Carlo simulation for strategy robustness."""493494def __init__(self, n_simulations: int = 1000, confidence: float = 0.95):495self.n_simulations = n_simulations496self.confidence = confidence497498def bootstrap_returns(499self,500returns: pd.Series,501n_periods: int = None502) -> np.ndarray:503"""504Bootstrap simulation by resampling returns.505506Args:507returns: Historical returns series508n_periods: Length of each simulation (default: same as input)509510Returns:511Array of shape (n_simulations, n_periods)512"""513if n_periods is None:514n_periods = len(returns)515516simulations = np.zeros((self.n_simulations, n_periods))517518for i in range(self.n_simulations):519# Resample with replacement520simulated_returns = np.random.choice(521returns.values,522size=n_periods,523replace=True524)525simulations[i] = simulated_returns526527return simulations528529def analyze_drawdowns(530self,531returns: pd.Series532) -> Dict[str, float]:533"""Analyze drawdown distribution via simulation."""534simulations = self.bootstrap_returns(returns)535536max_drawdowns = []537for sim_returns in simulations:538equity = (1 + sim_returns).cumprod()539rolling_max = np.maximum.accumulate(equity)540drawdowns = (equity - rolling_max) / rolling_max541max_drawdowns.append(drawdowns.min())542543max_drawdowns = np.array(max_drawdowns)544545return {546"expected_max_dd": np.mean(max_drawdowns),547"median_max_dd": np.median(max_drawdowns),548f"worst_{int(self.confidence*100)}pct": np.percentile(549max_drawdowns, (1 - self.confidence) * 100550),551"worst_case": max_drawdowns.min()552}553554def probability_of_loss(555self,556returns: pd.Series,557holding_periods: List[int] = [21, 63, 126, 252]558) -> Dict[int, float]:559"""Calculate probability of loss over various holding periods."""560results = {}561562for period in holding_periods:563if period > len(returns):564continue565566simulations = self.bootstrap_returns(returns, period)567total_returns = (1 + simulations).prod(axis=1) - 1568prob_loss = (total_returns < 0).mean()569results[period] = prob_loss570571return results572573def confidence_interval(574self,575returns: pd.Series,576periods: int = 252577) -> Dict[str, float]:578"""Calculate confidence interval for future returns."""579simulations = self.bootstrap_returns(returns, periods)580total_returns = (1 + simulations).prod(axis=1) - 1581582lower = (1 - self.confidence) / 2583upper = 1 - lower584585return {586"expected": total_returns.mean(),587"lower_bound": np.percentile(total_returns, lower * 100),588"upper_bound": np.percentile(total_returns, upper * 100),589"std": total_returns.std()590}591```592593## Performance Metrics594595```python596def calculate_metrics(returns: pd.Series, rf_rate: float = 0.02) -> Dict[str, float]:597"""Calculate comprehensive performance metrics."""598# Annualization factor (assuming daily returns)599ann_factor = 252600601# Basic metrics602total_return = (1 + returns).prod() - 1603annual_return = (1 + total_return) ** (ann_factor / len(returns)) - 1604annual_vol = returns.std() * np.sqrt(ann_factor)605606# Risk-adjusted returns607sharpe = (annual_return - rf_rate) / annual_vol if annual_vol > 0 else 0608609# Sortino (downside deviation)610downside_returns = returns[returns < 0]611downside_vol = downside_returns.std() * np.sqrt(ann_factor)612sortino = (annual_return - rf_rate) / downside_vol if downside_vol > 0 else 0613614# Calmar ratio615equity = (1 + returns).cumprod()616rolling_max = equity.cummax()617drawdowns = (equity - rolling_max) / rolling_max618max_drawdown = drawdowns.min()619calmar = annual_return / abs(max_drawdown) if max_drawdown != 0 else 0620621# Win rate and profit factor622wins = returns[returns > 0]623losses = returns[returns < 0]624win_rate = len(wins) / len(returns[returns != 0]) if len(returns[returns != 0]) > 0 else 0625profit_factor = wins.sum() / abs(losses.sum()) if losses.sum() != 0 else np.inf626627return {628"total_return": total_return,629"annual_return": annual_return,630"annual_volatility": annual_vol,631"sharpe_ratio": sharpe,632"sortino_ratio": sortino,633"calmar_ratio": calmar,634"max_drawdown": max_drawdown,635"win_rate": win_rate,636"profit_factor": profit_factor,637"num_trades": int((returns != 0).sum())638}639```640641## Best Practices642643### Do's644645- **Use point-in-time data** - Avoid look-ahead bias646- **Include transaction costs** - Realistic estimates647- **Test out-of-sample** - Always reserve data648- **Use walk-forward** - Not just train/test649- **Monte Carlo analysis** - Understand uncertainty650651### Don'ts652653- **Don't overfit** - Limit parameters654- **Don't ignore survivorship** - Include delisted655- **Don't use adjusted data carelessly** - Understand adjustments656- **Don't optimize on full history** - Reserve test set657- **Don't ignore capacity** - Market impact matters658