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.
references/details.md
1# backtesting-frameworks — detailed worked examples23## Implementation Patterns45### Pattern 1: Event-Driven Backtester67```python8from abc import ABC, abstractmethod9from dataclasses import dataclass, field10from datetime import datetime11from decimal import Decimal12from enum import Enum13from typing import Dict, List, Optional14import pandas as pd15import numpy as np1617class OrderSide(Enum):18BUY = "buy"19SELL = "sell"2021class OrderType(Enum):22MARKET = "market"23LIMIT = "limit"24STOP = "stop"2526@dataclass27class Order:28symbol: str29side: OrderSide30quantity: Decimal31order_type: OrderType32limit_price: Optional[Decimal] = None33stop_price: Optional[Decimal] = None34timestamp: Optional[datetime] = None3536@dataclass37class Fill:38order: Order39fill_price: Decimal40fill_quantity: Decimal41commission: Decimal42slippage: Decimal43timestamp: datetime4445@dataclass46class Position:47symbol: str48quantity: Decimal = Decimal("0")49avg_cost: Decimal = Decimal("0")50realized_pnl: Decimal = Decimal("0")5152def update(self, fill: Fill) -> None:53if fill.order.side == OrderSide.BUY:54new_quantity = self.quantity + fill.fill_quantity55if new_quantity != 0:56self.avg_cost = (57(self.quantity * self.avg_cost + fill.fill_quantity * fill.fill_price)58/ new_quantity59)60self.quantity = new_quantity61else:62self.realized_pnl += fill.fill_quantity * (fill.fill_price - self.avg_cost)63self.quantity -= fill.fill_quantity6465@dataclass66class Portfolio:67cash: Decimal68positions: Dict[str, Position] = field(default_factory=dict)6970def get_position(self, symbol: str) -> Position:71if symbol not in self.positions:72self.positions[symbol] = Position(symbol=symbol)73return self.positions[symbol]7475def process_fill(self, fill: Fill) -> None:76position = self.get_position(fill.order.symbol)77position.update(fill)7879if fill.order.side == OrderSide.BUY:80self.cash -= fill.fill_price * fill.fill_quantity + fill.commission81else:82self.cash += fill.fill_price * fill.fill_quantity - fill.commission8384def get_equity(self, prices: Dict[str, Decimal]) -> Decimal:85equity = self.cash86for symbol, position in self.positions.items():87if position.quantity != 0 and symbol in prices:88equity += position.quantity * prices[symbol]89return equity9091class Strategy(ABC):92@abstractmethod93def on_bar(self, timestamp: datetime, data: pd.DataFrame) -> List[Order]:94pass9596@abstractmethod97def on_fill(self, fill: Fill) -> None:98pass99100class ExecutionModel(ABC):101@abstractmethod102def execute(self, order: Order, bar: pd.Series) -> Optional[Fill]:103pass104105class SimpleExecutionModel(ExecutionModel):106def __init__(self, slippage_bps: float = 10, commission_per_share: float = 0.01):107self.slippage_bps = slippage_bps108self.commission_per_share = commission_per_share109110def execute(self, order: Order, bar: pd.Series) -> Optional[Fill]:111if order.order_type == OrderType.MARKET:112base_price = Decimal(str(bar["open"]))113114# Apply slippage115slippage_mult = 1 + (self.slippage_bps / 10000)116if order.side == OrderSide.BUY:117fill_price = base_price * Decimal(str(slippage_mult))118else:119fill_price = base_price / Decimal(str(slippage_mult))120121commission = order.quantity * Decimal(str(self.commission_per_share))122slippage = abs(fill_price - base_price) * order.quantity123124return Fill(125order=order,126fill_price=fill_price,127fill_quantity=order.quantity,128commission=commission,129slippage=slippage,130timestamp=bar.name131)132return None133134class Backtester:135def __init__(136self,137strategy: Strategy,138execution_model: ExecutionModel,139initial_capital: Decimal = Decimal("100000")140):141self.strategy = strategy142self.execution_model = execution_model143self.portfolio = Portfolio(cash=initial_capital)144self.equity_curve: List[tuple] = []145self.trades: List[Fill] = []146147def run(self, data: pd.DataFrame) -> pd.DataFrame:148"""Run backtest on OHLCV data with DatetimeIndex."""149pending_orders: List[Order] = []150151for timestamp, bar in data.iterrows():152# Execute pending orders at today's prices153for order in pending_orders:154fill = self.execution_model.execute(order, bar)155if fill:156self.portfolio.process_fill(fill)157self.strategy.on_fill(fill)158self.trades.append(fill)159160pending_orders.clear()161162# Get current prices for equity calculation163prices = {data.index.name or "default": Decimal(str(bar["close"]))}164equity = self.portfolio.get_equity(prices)165self.equity_curve.append((timestamp, float(equity)))166167# Generate new orders for next bar168new_orders = self.strategy.on_bar(timestamp, data.loc[:timestamp])169pending_orders.extend(new_orders)170171return self._create_results()172173def _create_results(self) -> pd.DataFrame:174equity_df = pd.DataFrame(self.equity_curve, columns=["timestamp", "equity"])175equity_df.set_index("timestamp", inplace=True)176equity_df["returns"] = equity_df["equity"].pct_change()177return equity_df178```179180### Pattern 2: Vectorized Backtester (Fast)181182```python183import pandas as pd184import numpy as np185from typing import Callable, Dict, Any186187class VectorizedBacktester:188"""Fast vectorized backtester for simple strategies."""189190def __init__(191self,192initial_capital: float = 100000,193commission: float = 0.001, # 0.1%194slippage: float = 0.0005 # 0.05%195):196self.initial_capital = initial_capital197self.commission = commission198self.slippage = slippage199200def run(201self,202prices: pd.DataFrame,203signal_func: Callable[[pd.DataFrame], pd.Series]204) -> Dict[str, Any]:205"""206Run backtest with signal function.207208Args:209prices: DataFrame with 'close' column210signal_func: Function that returns position signals (-1, 0, 1)211212Returns:213Dictionary with results214"""215# Generate signals (shifted to avoid look-ahead)216signals = signal_func(prices).shift(1).fillna(0)217218# Calculate returns219returns = prices["close"].pct_change()220221# Calculate strategy returns with costs222position_changes = signals.diff().abs()223trading_costs = position_changes * (self.commission + self.slippage)224225strategy_returns = signals * returns - trading_costs226227# Build equity curve228equity = (1 + strategy_returns).cumprod() * self.initial_capital229230# Calculate metrics231results = {232"equity": equity,233"returns": strategy_returns,234"signals": signals,235"metrics": self._calculate_metrics(strategy_returns, equity)236}237238return results239240def _calculate_metrics(241self,242returns: pd.Series,243equity: pd.Series244) -> Dict[str, float]:245"""Calculate performance metrics."""246total_return = (equity.iloc[-1] / self.initial_capital) - 1247annual_return = (1 + total_return) ** (252 / len(returns)) - 1248annual_vol = returns.std() * np.sqrt(252)249sharpe = annual_return / annual_vol if annual_vol > 0 else 0250251# Drawdown252rolling_max = equity.cummax()253drawdown = (equity - rolling_max) / rolling_max254max_drawdown = drawdown.min()255256# Win rate257winning_days = (returns > 0).sum()258total_days = (returns != 0).sum()259win_rate = winning_days / total_days if total_days > 0 else 0260261return {262"total_return": total_return,263"annual_return": annual_return,264"annual_volatility": annual_vol,265"sharpe_ratio": sharpe,266"max_drawdown": max_drawdown,267"win_rate": win_rate,268"num_trades": int((returns != 0).sum())269}270271# Example usage272def momentum_signal(prices: pd.DataFrame, lookback: int = 20) -> pd.Series:273"""Simple momentum strategy: long when price > SMA, else flat."""274sma = prices["close"].rolling(lookback).mean()275return (prices["close"] > sma).astype(int)276277# Run backtest278# backtester = VectorizedBacktester()279# results = backtester.run(price_data, lambda p: momentum_signal(p, 50))280```281282### Pattern 3: Walk-Forward Optimization283284```python285from typing import Callable, Dict, List, Tuple, Any286import pandas as pd287import numpy as np288from itertools import product289290class WalkForwardOptimizer:291"""Walk-forward analysis with anchored or rolling windows."""292293def __init__(294self,295train_period: int,296test_period: int,297anchored: bool = False,298n_splits: int = None299):300"""301Args:302train_period: Number of bars in training window303test_period: Number of bars in test window304anchored: If True, training always starts from beginning305n_splits: Number of train/test splits (auto-calculated if None)306"""307self.train_period = train_period308self.test_period = test_period309self.anchored = anchored310self.n_splits = n_splits311312def generate_splits(313self,314data: pd.DataFrame315) -> List[Tuple[pd.DataFrame, pd.DataFrame]]:316"""Generate train/test splits."""317splits = []318n = len(data)319320if self.n_splits:321step = (n - self.train_period) // self.n_splits322else:323step = self.test_period324325start = 0326while start + self.train_period + self.test_period <= n:327if self.anchored:328train_start = 0329else:330train_start = start331332train_end = start + self.train_period333test_end = min(train_end + self.test_period, n)334335train_data = data.iloc[train_start:train_end]336test_data = data.iloc[train_end:test_end]337338splits.append((train_data, test_data))339start += step340341return splits342343def optimize(344self,345data: pd.DataFrame,346strategy_func: Callable,347param_grid: Dict[str, List],348metric: str = "sharpe_ratio"349) -> Dict[str, Any]:350"""351Run walk-forward optimization.352353Args:354data: Full dataset355strategy_func: Function(data, **params) -> results dict356param_grid: Parameter combinations to test357metric: Metric to optimize358359Returns:360Combined results from all test periods361"""362splits = self.generate_splits(data)363all_results = []364optimal_params_history = []365366for i, (train_data, test_data) in enumerate(splits):367# Optimize on training data368best_params, best_metric = self._grid_search(369train_data, strategy_func, param_grid, metric370)371optimal_params_history.append(best_params)372373# Test with optimal params374test_results = strategy_func(test_data, **best_params)375test_results["split"] = i376test_results["params"] = best_params377all_results.append(test_results)378379print(f"Split {i+1}/{len(splits)}: "380f"Best {metric}={best_metric:.4f}, params={best_params}")381382return {383"split_results": all_results,384"param_history": optimal_params_history,385"combined_equity": self._combine_equity_curves(all_results)386}387388def _grid_search(389self,390data: pd.DataFrame,391strategy_func: Callable,392param_grid: Dict[str, List],393metric: str394) -> Tuple[Dict, float]:395"""Grid search for best parameters."""396best_params = None397best_metric = -np.inf398399# Generate all parameter combinations400param_names = list(param_grid.keys())401param_values = list(param_grid.values())402403for values in product(*param_values):404params = dict(zip(param_names, values))405results = strategy_func(data, **params)406407if results["metrics"][metric] > best_metric:408best_metric = results["metrics"][metric]409best_params = params410411return best_params, best_metric412413def _combine_equity_curves(414self,415results: List[Dict]416) -> pd.Series:417"""Combine equity curves from all test periods."""418combined = pd.concat([r["equity"] for r in results])419return combined420```421422### Pattern 4: Monte Carlo Analysis423424```python425import numpy as np426import pandas as pd427from typing import Dict, List428429class MonteCarloAnalyzer:430"""Monte Carlo simulation for strategy robustness."""431432def __init__(self, n_simulations: int = 1000, confidence: float = 0.95):433self.n_simulations = n_simulations434self.confidence = confidence435436def bootstrap_returns(437self,438returns: pd.Series,439n_periods: int = None440) -> np.ndarray:441"""442Bootstrap simulation by resampling returns.443444Args:445returns: Historical returns series446n_periods: Length of each simulation (default: same as input)447448Returns:449Array of shape (n_simulations, n_periods)450"""451if n_periods is None:452n_periods = len(returns)453454simulations = np.zeros((self.n_simulations, n_periods))455456for i in range(self.n_simulations):457# Resample with replacement458simulated_returns = np.random.choice(459returns.values,460size=n_periods,461replace=True462)463simulations[i] = simulated_returns464465return simulations466467def analyze_drawdowns(468self,469returns: pd.Series470) -> Dict[str, float]:471"""Analyze drawdown distribution via simulation."""472simulations = self.bootstrap_returns(returns)473474max_drawdowns = []475for sim_returns in simulations:476equity = (1 + sim_returns).cumprod()477rolling_max = np.maximum.accumulate(equity)478drawdowns = (equity - rolling_max) / rolling_max479max_drawdowns.append(drawdowns.min())480481max_drawdowns = np.array(max_drawdowns)482483return {484"expected_max_dd": np.mean(max_drawdowns),485"median_max_dd": np.median(max_drawdowns),486f"worst_{int(self.confidence*100)}pct": np.percentile(487max_drawdowns, (1 - self.confidence) * 100488),489"worst_case": max_drawdowns.min()490}491492def probability_of_loss(493self,494returns: pd.Series,495holding_periods: List[int] = [21, 63, 126, 252]496) -> Dict[int, float]:497"""Calculate probability of loss over various holding periods."""498results = {}499500for period in holding_periods:501if period > len(returns):502continue503504simulations = self.bootstrap_returns(returns, period)505total_returns = (1 + simulations).prod(axis=1) - 1506prob_loss = (total_returns < 0).mean()507results[period] = prob_loss508509return results510511def confidence_interval(512self,513returns: pd.Series,514periods: int = 252515) -> Dict[str, float]:516"""Calculate confidence interval for future returns."""517simulations = self.bootstrap_returns(returns, periods)518total_returns = (1 + simulations).prod(axis=1) - 1519520lower = (1 - self.confidence) / 2521upper = 1 - lower522523return {524"expected": total_returns.mean(),525"lower_bound": np.percentile(total_returns, lower * 100),526"upper_bound": np.percentile(total_returns, upper * 100),527"std": total_returns.std()528}529```530531## Performance Metrics532533```python534def calculate_metrics(returns: pd.Series, rf_rate: float = 0.02) -> Dict[str, float]:535"""Calculate comprehensive performance metrics."""536# Annualization factor (assuming daily returns)537ann_factor = 252538539# Basic metrics540total_return = (1 + returns).prod() - 1541annual_return = (1 + total_return) ** (ann_factor / len(returns)) - 1542annual_vol = returns.std() * np.sqrt(ann_factor)543544# Risk-adjusted returns545sharpe = (annual_return - rf_rate) / annual_vol if annual_vol > 0 else 0546547# Sortino (downside deviation)548downside_returns = returns[returns < 0]549downside_vol = downside_returns.std() * np.sqrt(ann_factor)550sortino = (annual_return - rf_rate) / downside_vol if downside_vol > 0 else 0551552# Calmar ratio553equity = (1 + returns).cumprod()554rolling_max = equity.cummax()555drawdowns = (equity - rolling_max) / rolling_max556max_drawdown = drawdowns.min()557calmar = annual_return / abs(max_drawdown) if max_drawdown != 0 else 0558559# Win rate and profit factor560wins = returns[returns > 0]561losses = returns[returns < 0]562win_rate = len(wins) / len(returns[returns != 0]) if len(returns[returns != 0]) > 0 else 0563profit_factor = wins.sum() / abs(losses.sum()) if losses.sum() != 0 else np.inf564565return {566"total_return": total_return,567"annual_return": annual_return,568"annual_volatility": annual_vol,569"sharpe_ratio": sharpe,570"sortino_ratio": sortino,571"calmar_ratio": calmar,572"max_drawdown": max_drawdown,573"win_rate": win_rate,574"profit_factor": profit_factor,575"num_trades": int((returns != 0).sum())576}577```578