Backtesting.py: A Reproducible Python Strategy Testing Guide
Backtesting.py is an open-source Python framework for simulating trading rules on historical data. It is useful for inspecting a single instrument's signals and trades. The examples below demonstrate its API; they do not establish that a moving-average crossover is profitable or suitable for live trading.
Install and Record the Environment
Use an isolated Python environment, then install the package named backtesting:
python -m pip install backtesting
The project repository documents installation and its AGPL-3.0 license. Open-source availability does not include market-data subscriptions or remove license obligations when distributing software. Record the installed package versions with your results so another run can use the same environment.
python -m pip show backtesting pandas numpy bokeh
Prepare Data Before Writing Signals
The library accepts a pandas DataFrame with Open, High, Low, and Close; Volume is optional. Its bundled GOOG fixture is historical Google equity data, not a live download or a combined forex dataset. The official quick-start guide introduces this fixture and the basic strategy lifecycle.
For your own data, check chronological order, duplicates, missing prices, timestamp conventions, and corporate-action adjustments. Preserve the original data file. If an adapter returns multi-level columns, normalize them deliberately and verify the selected ticker before testing. A successfully loaded DataFrame does not establish that its observations were available at the simulated decision time.
Run the SMA Crossover Example
A Strategy subclass defines init() for setup and next() for decisions as bars become available. This example closes the current position and opens the opposite direction when the two moving averages cross. It is a long-and-short example, not a long-only buy-and-hold replacement.
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG
class SmaCross(Strategy):
n1 = 10
n2 = 20
def init(self):
close = self.data.Close
self.sma1 = self.I(SMA, close, self.n1)
self.sma2 = self.I(SMA, close, self.n2)
def next(self):
if crossover(self.sma1, self.sma2):
self.position.close()
self.buy()
elif crossover(self.sma2, self.sma1):
self.position.close()
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002)
output = bt.run()
bt.plot()
The constructor order is data first, strategy class second. commission=.002 means 0.2% at entry and again at exit in the current API. With default timing, market orders normally fill at the following bar's open. The default does not close outstanding positions at the test end; finalize_trades=True changes that policy. See the Backtest API.
The example leaves order size at the library default and has no stop-loss. Before using another asset, specify the intended sizing, shorting constraints, and cost assumptions. In particular, replacing the ticker does not automatically reproduce that market's contract multiplier, financing, or borrow availability.
Inspect the Output Instead of Copying a Return Claim
The examples do not present verified investment returns. Print the actual dates, return, and trades from your own run:
print(GOOG.index.min(), GOOG.index.max())
print(output[["Return [%]", "Max. Drawdown [%]", "# Trades"]])
print(output["_trades"].head())
The underscore-prefixed trade table is useful for an audit but may change across versions. Reconstruct several trades independently from the input bars. Confirm when a crossover becomes known, when the order fills, and whether an open final position is included in each statistic. Do not compare a tutorial's displayed return with your run until the data, version, costs, and end-of-test policy match.
A different moving-average window also changes the warm-up period. Compare strategies over a common evaluation window when making performance claims.
Choose a Strategy Base Deliberately
The composable strategy documentation describes reusable bases:
| Base | Purpose | Detail to check |
|---|---|---|
Strategy | Explicit bar-by-bar order logic | You define state, sizing, and exits |
SignalStrategy | Entry-size and exit-portion arrays | A repeated nonzero signal may request repeated orders |
TrailingStrategy | ATR-based trailing stops | Cooperative subclasses need the appropriate super() calls |
SignalStrategy.set_signal() is not a pair of “buy condition” and “short condition” arguments. Its second array represents exits. A boolean condition that remains true for several bars is also different from a one-bar crossover event. Keep the explicit SmaCross version until the desired behavior is clear; compare trade lists when moving to a helper class.
Optimize With the Method You Actually Selected
The following continues after the SMA example. It searches the specified parameter grid and returns the best run for the selected objective:
stats = bt.optimize(
n1=range(5, 30, 5),
n2=range(10, 70, 5),
maximize='Sharpe Ratio',
constraint=lambda p: p.n1 < p.n2
)
The default method is grid, not SAMBO. Select method='sambo' explicitly for model-based search. A heatmap is not returned automatically: request return_heatmap=True and unpack the additional result. These options are documented in the optimization API and the parameter heatmap tutorial.
Use the grid to inspect sensitivity, not only to select a winner. A narrow optimum may be fragile, but a broad plateau is not proof of an edge either. Predeclare the search space and number of trials. Freeze the selected rules and evaluate them on later data that did not influence selection. If you change rules after inspecting that period, it is no longer an untouched holdout.
Add an External Indicator Correctly
self.I() registers indicator output for gradual exposure during the simulation. It does not require every indicator function to accept a pandas Series, and it cannot detect whether your calculation uses future information.
The optional ta library's RSI function is ta.momentum.rsi, not ta.rsi. Its documented input is a pandas Series. The following class continues in the same Python session and registers and plots RSI without placing trades:
import pandas as pd
from ta.momentum import rsi
class RsiDemo(Strategy):
def init(self):
close = pd.Series(self.data.Close, index=self.data.index)
self.rsi = self.I(rsi, close, 14)
def next(self):
pass
Install ta separately if using this example. Preserve input length and order, inspect initial missing values, and compare a few values with the indicator's calculation. Avoid centered rolling windows, negative shifts, or full-sample normalization in trading features: precomputing an array in init() does not make those operations causal.
Model Limits That Affect the Conclusion
A fixed spread or increased commission can test sensitivity to costs; it does not reproduce order-book queues, latency, partial fills, or market impact. OHLC bars cannot settle every within-bar ordering question. If stops and targets can be touched on the same bar, inspect the engine's treatment and test finer data where appropriate.
The library's core Backtest is for one instrument. Running several independent tests is different from a portfolio simulation with shared cash and concurrent positions. Likewise, fast vectorized calculations do not establish that an engine models high-frequency execution. Choose a different model when those mechanisms determine the strategy's result.
What is the correct installation command?
Use python -m pip install backtesting. The package name is backtesting, even though the project is commonly called backtesting.py.
Why are my results different from a tutorial?
Compare package versions, exact data, dates, costs, order size, warm-up, and final-position handling. Reconcile the trade list before treating a difference as a strategy finding.
Does the optimizer automatically use SAMBO and produce a heatmap?
No. The default search method is grid. SAMBO and heatmap output require explicit options. Optimization output remains an in-sample selection result.
Can backtesting.py place live orders?
This framework simulates historical trades. Live execution requires a separate integration and validation of data, order, and position-state behavior.
