The design philosophy of backtesting.py: Clean, pandas-native research
In quantitative trading research, heavy event-driven backtesting frameworks often impose substantial operational friction. Configuring custom feed classes, managing line buffers, and writing hundreds of lines of boilerplate can slow exploratory research on single instruments.
backtesting.py was designed to streamline this process. It prioritizes three practical mechanics:
- Pandas-native inputs: Backtests consume standard
pandas.DataFrameobjects directly without bespoke data wrappers or feed adapters. - Minimalist
Strategyinterface: Strategy logic requires only two lifecycle methods:init()for one-time indicator precomputation andnext()for bar-by-bar decision logic. - Interactive Bokeh visualizations: Simulation runs produce self-contained HTML charts with zoomable candlesticks, indicator overlays, volume bars, and individual trade markers.
┌────────────────────────────────────────────────────────┐
│ Standard pandas DataFrame │
│ [Open, High, Low, Close, Volume] │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Strategy Lifecycle │
│ ├── init(): Vectorized indicator wrapping via I() │
│ └── next(): Bar-by-bar procedural order dispatch │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Backtest Execution │
│ ├── Next-bar open fill matching │
│ ├── Cash, position, and commission tracking │
│ └── Output: pandas.Series statistics + Bokeh HTML │
└────────────────────────────────────────────────────────┘The engine focuses exclusively on single-asset historical OHLCV simulation. It does not attempt multi-asset portfolio accounting or high-frequency order matching. Instead, it offers an approachable environment for testing technical strategies on daily or intraday bars.
Data requirements and the I() indicator wrapper
Working effectively with backtesting.py requires adhering to its input schema and understanding how indicator calculations are vectorized through self.I().
Strict column formatting requirements
backtesting.py expects historical price bars structured as a pandas DataFrame with specific conventions:
- The index must be a sorted, monotonic
DatetimeIndex. - Price columns must use exact Title Case:
Open,High,Low,Close, and optionallyVolume. - Values must be numeric floats without unhandled
NaNentries in price fields.
Passing lowercase column names (e.g., open, close) triggers a runtime ValueError. You can normalize raw data before testing:
import pandas as pd
def prepare_ohlcv(df: pd.DataFrame) -> pd.DataFrame:
"""Normalize column names and enforce a monotonic DatetimeIndex."""
data = df.copy()
mapping = {c: c.capitalize() for c in data.columns if c.lower() in ["open", "high", "low", "close", "volume"]}
data = data.rename(columns=mapping)
required = ["Open", "High", "Low", "Close"]
for col in required:
if col not in data.columns:
raise KeyError(f"Required column '{col}' missing from DataFrame.")
if not isinstance(data.index, pd.DatetimeIndex):
data.index = pd.to_datetime(data.index)
return data.sort_index().dropna(subset=required)The self.I() indicator wrapper
Recomputing technical indicators inside the iterative next() loop is computationally inefficient. Conversely, computing indicators outside the strategy risks lookahead leakage if series slicing is mismanaged.
backtesting.py solves this via self.I(). Declared inside Strategy.init(), self.I() precomputes indicators across the full historical series once using vectorized operations, then manages causal, lookahead-free indexing during next().
def SMA(series: pd.Series, period: int) -> pd.Series:
"""Simple moving average helper."""
return pd.Series(series).rolling(period).mean()
# Inside Strategy.init():
# self.fast_ma = self.I(SMA, self.data.Close.s, 10)
# self.slow_ma = self.I(SMA, self.data.Close.s, 30)[!NOTE] Within
Strategy.next(), indicators registered throughself.I()expose sliding-array indexing where[-1]returns the current bar's indicator value and[-2]retrieves the prior bar. This preserves historical causality while maintaining vectorized execution speeds.
Building a complete strategy: Moving Average Crossover
Here is an end-to-end moving average crossover strategy with ATR-based stop-loss placement and fractional position sizing.
Strategy lifecycle and execution mechanics
By default, backtesting.py implements causal execution timing:
- Signals are evaluated on bar
tclosing prices duringnext(). - Orders staged during
next()execute on the open of bart+1(trade_on_close=False). - Setting
trade_on_close=Truefills orders immediately at bartclose, which can introduce lookahead bias unless specifically simulating market-on-close orders.
import numpy as np
import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import GOOG
def calculate_sma(series: pd.Series, period: int) -> pd.Series:
return pd.Series(series).rolling(period).mean()
def calculate_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
tr1 = high - low
tr2 = (high - close.shift(1)).abs()
tr3 = (low - close.shift(1)).abs()
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
return tr.rolling(period).mean()
class SmaCrossWithAtrStop(Strategy):
n_fast = 10
n_slow = 30
atr_period = 14
atr_mult = 2.0
def init(self):
# Register indicators with self.I()
price = self.data.Close.s
self.sma_fast = self.I(calculate_sma, price, self.n_fast)
self.sma_slow = self.I(calculate_sma, price, self.n_slow)
self.atr = self.I(calculate_atr, self.data.High.s, self.data.Low.s, price, self.atr_period)
def next(self):
# Skip bars until indicators finish warming up
if np.isnan(self.sma_slow[-1]) or np.isnan(self.atr[-1]):
return
current_close = self.data.Close[-1]
atr_val = self.atr[-1]
# Entry logic: fast MA crosses above slow MA
if crossover(self.sma_fast, self.sma_slow):
if self.position.is_short:
self.position.close()
if not self.position:
sl_price = current_close - (atr_val * self.atr_mult)
# Fractional sizing: 0.95 allocates 95% of available portfolio equity
self.buy(size=0.95, sl=sl_price)
# Exit logic: fast MA crosses below slow MA
elif crossover(self.slow_ma, self.fast_ma):
if self.position.is_long:
self.position.close()Position sizing conventions
In self.buy() and self.sell():
- Values between 0 and 1 (exclusive): Represents a fraction of total equity (e.g.,
size=0.95uses 95% of equity). - Values
\ge 1: Interpreted as an exact integer quantity of units or shares (e.g.,size=100).
Running Backtest.run() and interpreting the output Series
To run the simulation, pass the formatted DataFrame and strategy class to Backtest, set starting cash and commissions, and call .run().
bt = Backtest(
GOOG,
SmaCrossWithAtrStop,
cash=10_000,
commission=0.002, # 0.2% commission per trade
trade_on_close=False # Next-bar Open fills
)
stats = bt.run()
print(stats)Interpreting key performance metrics
bt.run() returns a pandas.Series containing standard portfolio attribution statistics:
| Metric | Output Format | Quantitative Meaning |
|---|---|---|
Return [%] | Percentage | Cumulative total strategy return over the backtest duration. |
Buy & Hold Return [%] | Percentage | Benchmark return from holding the asset from start to finish. |
Max. Drawdown [%] | Percentage | Peak-to-trough decline in portfolio equity. |
Sharpe Ratio | Float | Annualized excess return divided by annualized return volatility. |
Win Rate [%] | Percentage | Percentage of closed trades that produced positive net PnL. |
Profit Factor | Float | Ratio of gross winning profits to gross losing losses (> 1.0 indicates profitability). |
# Trades | Integer | Total count of closed round-trip trades. |
Inspecting closed trades and generating charts
To audit individual trades, inspect the trade log stored in stats._trades:
# View individual trade fills
print(stats._trades[["EntryTime", "ExitTime", "EntryPrice", "ExitPrice", "PnL", "ReturnPct"]].head())
# Render standalone interactive chart
bt.plot(filename="crossover_report.html", open_browser=False)The resulting HTML file provides zoomable candlestick series, volume overlays, entry/exit arrows, and equity curves.
Parameter optimization and heatmaps
backtesting.py includes a built-in grid search optimizer via Backtest.optimize(). It evaluates parameter combinations across multiple CPU cores using Python's standard multiprocessing.
stats_opt, heatmap = bt.optimize(
n_fast=range(5, 25, 5),
n_slow=range(20, 60, 10),
atr_mult=[1.5, 2.0, 2.5],
maximize="Sharpe Ratio",
constraint=lambda p: p.n_fast < p.n_slow,
max_tries=100,
return_heatmap=True
)
print("Best Sharpe Ratio:", stats_opt["Sharpe Ratio"])
print("Optimal Configuration:\n", stats_opt._strategy)Visualizing parameter sensitivity
When return_heatmap=True is enabled, the method returns a multi-index Series containing the objective metric across trials. Unstacking this Series highlights the stability of the parameter space:
heatmap_2d = heatmap.groupby(["n_fast", "n_slow"]).mean().unstack()
print(heatmap_2d)n_slow 20 30 40 50
n_fast
5 0.421503 0.584102 0.612091 0.540122
10 NaN 0.710344 0.684511 0.631209
15 NaN NaN 0.590123 0.562301
20 NaN NaN NaN 0.481230[!WARNING]
Mitigating in-sample curve fitting
Maximizing returns across dense parameter grids frequently produces overfitting. An isolated performance peak surrounded by deteriorating metrics is typically a statistical artifact of market noise. Target broad parameter plateaus where adjacent settings produce consistent risk-adjusted returns, and always validate selections on holdout out-of-sample data.
License and architecture considerations: backtesting.py vs. OpenBacktesting
Selecting backtesting software requires considering legal terms and system architecture alongside API convenience. backtesting.py and OpenBacktesting are independent projects with distinct engineering objectives.
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ backtesting.py │ │ OpenBacktesting │
├─────────────────────────────────┤ ├─────────────────────────────────┤
│ • License: GNU AGPL-3.0 │ │ • License: Apache-2.0 │
│ • Focus: Single-script research │ │ • Focus: Inspectable audit │
│ • Interactive Bokeh HTML plots │ │ • Pluggable execution models │
│ • Ephemeral in-memory trials │ │ • SQLite trial persistence │
│ • Single-asset OHLCV modeling │ │ • Deterministic bar-by-bar │
└─────────────────────────────────┘ └─────────────────────────────────┘Licensing model: Copyleft vs. Permissive
- backtesting.py (GNU AGPL-3.0): The Affero General Public License is a strong copyleft license. If you modify or embed
backtesting.pywithin software made available over a network (such as a proprietary research server or web-based quant platform), the AGPL requires releasing the complete source code under the same license. For commercial hedge funds and proprietary desks, this often requires legal review. - OpenBacktesting (Apache-2.0): The Apache license is commercially permissive. Firms and individual quants can run proprietary strategies, integrate custom execution pipelines, or build closed-source applications without copyleft disclosure mandates.
Architecture and feature comparison
| Architectural Dimension | backtesting.py (v0.3.x) | OpenBacktesting (v0.2.1) |
|---|---|---|
| Primary Focus | Rapid single-asset prototyping & Bokeh charts | Deterministic, inspectable strategy auditing |
| Open Source License | GNU AGPL-3.0 (Copyleft) | Apache-2.0 (Permissive) |
| Python Runtime | Python 3.7+ | Python >=3.12, <3.15 |
| Core Dependencies | NumPy, pandas, Bokeh | NumPy, pandas, pandas-ta-classic |
| Supported Order Types | Market, Limit, Stop | Market, Limit, Stop, Stop-Limit, Trailing Stop, OCO |
| Indicator Ecosystem | User-defined functions wrapped via self.I() | Built-in pandas-ta-classic catalog + custom functions |
| Cost & Slippage | Proportional commission percentage | Pluggable percentage, fixed-fee, and explicit price slippage |
| Parameter Search | Multiprocessing Grid & skopt | Grid Search and SAMBO (Bayesian Optimization) |
| Persistence & Audit | In-memory Series and static Bokeh HTML | Durable SQLite trial storage, JSON/CSV exports, local Studio |
| Live Execution | None (Historical simulation only) | None (Historical simulation only) |
Neither library provides live broker connectivity, low-latency execution, or real-time data feeds; both are strictly offline simulation engines. Furthermore, they are independent codebases without 1-to-1 code compatibility or direct migration tools.
Strategy audit checklist
Before relying on backtest metrics for any strategy, verify the following experimental conditions:
- Execution timing: Ensure orders stage at bar
tclose and execute on bart+1open (trade_on_close=False). - Data monotonicity: Check that timestamps advance strictly forward without duplicate rows or missing price columns.
- Indicator warm-up: Verify that trading logic suppresses execution while indicator lookbacks contain
NaNvalues. - Friction modeling: Include realistic commission fees (e.g., 0.1%–0.2%) and slippage penalties.
- Parameter robustness: Confirm that optimal settings sit on smooth parameter plateaus rather than isolated spikes.
- Out-of-sample testing: Evaluate the strategy on unseen historical periods before drawing conclusions.
- License alignment: Verify that the framework license fits your operational and commercial deployment requirements.
Conclusion and resources
backtesting.py offers a streamlined, pandas-native workflow for single-asset technical research. Its clean init() and next() lifecycle, vectorized indicator wrapper self.I(), and interactive Bokeh charts make it well suited for rapid strategy prototyping.
When your research requires deeper execution modeling—such as multi-order OCO brackets, trailing stops, persistent SQLite trial databases, and permissive Apache-2.0 licensing—modern frameworks like OpenBacktesting offer complementary, inspectable architectures.
Explore related backtesting guides and documentation:
- Python Backtesting Libraries Compared: A Practical Selection Guide: A side-by-side comparison of Backtrader, backtesting.py, VectorBT, and OpenBacktesting.
- Good Strategies Start with Evidence: Inspectable Strategy Research: Foundational principles for reproducible, evidence-based backtesting.
- OpenBacktesting Documentation: Official quickstart guides and execution engine specifications.
