Execution Models
Overview
A backtest is only as good as its execution model. If you assume zero trading costs and perfect fills, your results will be wildly optimistic. This chapter covers slippage and commission models – the two components that bridge the gap between theoretical signals and realistic performance.
Slippage Models
Slippage is the difference between the expected fill price and the actual fill price. It arises from the bid-ask spread, market impact, and latency. The SlippageModel dataclass supports three modes:
Fixed Slippage
A constant dollar amount per share, independent of price. Suitable for liquid large-cap stocks with tight spreads.
from puffin.backtest import SlippageModel
# Fixed slippage: $0.01 per share
SlippageModel(fixed=0.01)
Percentage Slippage
A fraction of the fill price. Better for modeling slippage across stocks at different price levels or for less liquid names.
# Percentage slippage: 0.1% of price
SlippageModel(pct=0.001)
Combined Slippage
Use both components together for a more realistic model that captures a base spread plus proportional market impact.
# Combined: $0.005 fixed + 0.05% of price
SlippageModel(fixed=0.005, pct=0.0005)
Under the hood, SlippageModel.calculate(price) returns fixed + price * pct. For buy orders, the slippage is added to the fill price; for sell orders, it is subtracted.
Commission Models
Commissions are the fees charged by your broker for executing trades. The CommissionModel dataclass supports three fee structures:
Flat Fee
A fixed dollar amount per order, regardless of size. Common with discount brokers.
from puffin.backtest import CommissionModel
# Flat fee per order
CommissionModel(flat=5.0)
Per-Share Fee
A fee proportional to the number of shares traded. Used by some institutional brokers (e.g., Interactive Brokers’ tiered pricing).
# Per-share fee: $0.005 per share
CommissionModel(per_share=0.005)
Percentage Fee
A fee proportional to the total trade value. Common in non-US equity markets and for some crypto exchanges.
# Percentage of trade value: 0.1%
CommissionModel(pct=0.001)
The calculation is flat + per_share * qty + price * qty * pct. You can combine all three components for a complex fee schedule.
Start with SlippageModel(fixed=0.01) and CommissionModel(flat=1.0) as reasonable defaults for US equities. Many retail brokers now offer commission-free trading, but slippage still exists.
Performance Metrics
After a backtest completes, call result.metrics() to get a dictionary of performance statistics:
metrics = result.metrics()
# {
# 'total_return': 0.15,
# 'annualized_return': 0.12,
# 'annualized_volatility': 0.18,
# 'sharpe_ratio': 1.2,
# 'max_drawdown': -0.08,
# 'win_rate': 0.55,
# 'profit_factor': 1.8,
# 'total_trades': 42,
# }
Key Metrics Explained
| Metric | Description | What to look for |
|---|---|---|
| Total return | Cumulative percentage gain/loss | Positive, obviously |
| Annualized return | Compounded annual growth rate | Compare to benchmark (S&P ~10%) |
| Annualized volatility | Standard deviation of returns, annualized | Lower is better at equal return |
| Sharpe ratio | Risk-adjusted return (excess return / volatility) | > 1.0 is good, > 2.0 is excellent |
| Max drawdown | Largest peak-to-trough decline | How much pain you must endure |
| Win rate | Fraction of winning trades | Context-dependent (trend followers are often < 50%) |
| Profit factor | Gross profits / gross losses | > 1.0 means profitable, > 1.5 is solid |
| Total trades | Number of executed fills | Too few = not statistically significant |
No single metric tells the full story. A high Sharpe ratio with only 5 trades is not meaningful. Always consider metrics together with the equity curve and drawdown chart.
Visualization
The BacktestResult object provides a built-in plotting method that shows the equity curve and drawdown together:
result.plot() # Equity curve + drawdown chart
This generates a two-panel chart: the top panel shows portfolio value over time, and the bottom panel shows the drawdown (percentage decline from peak equity) in red. Look for long, deep drawdowns – they indicate periods where the strategy is losing money and may signal regime changes.
Exercises
- Run a backtest with zero slippage and zero commission, then re-run with
SlippageModel(fixed=0.02)andCommissionModel(per_share=0.005). Compare the Sharpe ratios. - Experiment with percentage-based slippage. At what slippage level does a profitable strategy become unprofitable?
- After running a backtest, examine the equity curve using
result.plot(). Identify the largest drawdown period – what market event caused it?