Skip to main content
The Backtest class is the simulation engine. You instantiate it with your data and strategy, call run() to simulate trading, and optionally call optimize() to find the best parameters.

Constructor

from backtesting import Backtest

bt = Backtest(
    data,
    strategy,
    cash=10_000,
    commission=.002,
    exclusive_orders=True,
)

Parameters

data
pd.DataFrame
required
A pandas DataFrame with columns Open, High, Low, Close, and optionally Volume. The index should be a DatetimeIndex. See Data for details.
strategy
Type[Strategy]
required
A Strategy subclass (not an instance). The engine instantiates it internally.
cash
float
default:"10000"
Starting account balance in cash units.
spread
float
default:"0.0"
Constant bid-ask spread as a fraction of price. For example, 0.0002 models a 0.2‰ forex spread. Applied once at trade entry.
commission
float | tuple[float, float] | Callable
default:"0.0"
Broker commission. Applied at both entry and exit.
  • Float: fraction of trade value. E.g. 0.002 for 0.2%.
  • Tuple (fixed, relative): fixed cash amount plus relative fraction. E.g. (100, 0.01) for $100 + 1%.
  • Callable func(order_size, price) -> float: full custom commission model. Note that order_size is negative for short orders. Negative values model market-maker rebates.
margin
float
default:"1.0"
Required margin ratio for leveraged accounts. Set to 1/leverage. For example, 0.02 for 50:1 leverage. No distinction is made between initial and maintenance margin.
trade_on_close
bool
default:"False"
If True, market orders fill at the current bar’s close instead of the next bar’s open.
hedging
bool
default:"False"
If True, allows simultaneous long and short positions. If False, placing a trade in the opposite direction first closes existing trades in FIFO order.
exclusive_orders
bool
default:"False"
If True, each new order automatically cancels pending orders and closes all open trades before opening the new one. Keeps at most one trade active at any time.
finalize_trades
bool
default:"False"
If True, all trades still open at the end of the dataset are closed on the last bar and included in the statistics. By default, open trades are excluded from stats and a warning is issued.

Running a backtest

Call run() to execute the simulation. Keyword arguments override strategy class variables for this run only.
stats = bt.run()

# Override strategy parameters for this run
stats = bt.run(n1=5, n2=50)
run() returns a pd.Series with all performance statistics:
Start                     2004-08-19 00:00:00
End                       2013-03-01 00:00:00
Duration                   3116 days 00:00:00
Exposure Time [%]                    96.74115
Equity Final [$]                     51422.99
Equity Peak [$]                      75787.44
Return [%]                           414.2299
Buy & Hold Return [%]               703.45824
Return (Ann.) [%]                    21.18026
Volatility (Ann.) [%]                36.49391
CAGR [%]                             14.15984
Sharpe Ratio                          0.58038
Sortino Ratio                         1.08479
Calmar Ratio                          0.44144
Alpha [%]                           394.37391
Beta                                  0.03803
Max. Drawdown [%]                   -47.98013
Avg. Drawdown [%]                    -5.92585
Max. Drawdown Duration      584 days 00:00:00
Avg. Drawdown Duration       41 days 00:00:00
# Trades                                   66
Win Rate [%]                          46.9697
Best Trade [%]                       53.59595
Worst Trade [%]                     -18.39887
Avg. Trade [%]                        2.53172
Max. Trade Duration         183 days 00:00:00
Avg. Trade Duration          46 days 00:00:00
Profit Factor                         2.16795
Expectancy [%]                        3.27481
SQN                                   1.07662
Kelly Criterion                       0.15187
_strategy                            SmaCross
_equity_curve                           Eq...
_trades                       Size  EntryB...
dtype: object
The series also contains three private keys with raw data:
KeyContent
_strategyThe strategy instance that was run
_equity_curveDataFrame of equity, drawdown percent, and drawdown duration over time
_tradesDataFrame of all closed trades with size, entry/exit prices, P&L, and commissions
# Inspect all closed trades
print(stats['_trades'])

# Plot equity curve
stats['_equity_curve']['Equity'].plot()
Results depend on the indicator warm-up period. If you use a 200-bar SMA, the simulation begins on bar 201. Strategies with longer indicators have fewer bars to trade on, which affects statistics like return and trade count.

Optimizing parameters

optimize() searches for the strategy parameter combination that maximizes a given metric.
stats, heatmap = bt.optimize(
    n1=range(5, 30, 5),
    n2=range(10, 70, 10),
    constraint=lambda p: p.n1 < p.n2,
    maximize='SQN',
    return_heatmap=True,
)

Parameters

**kwargs
required
Strategy parameter names mapped to sequences of candidate values. For example, n1=[5, 10, 15] tests three values for the n1 parameter.
maximize
str | Callable
default:"'SQN'"
The metric to maximize. Either a string key from the run() result Series (e.g. 'SQN', 'Sharpe Ratio', 'Return [%]') or a callable that accepts the result Series and returns a number — the higher the better.
method
str
default:"'grid'"
Optimization method. 'grid' performs exhaustive search over all parameter combinations. 'sambo' uses model-based optimization, which requires the sambo package and is more efficient when the parameter space is large.
max_tries
int | float | None
Maximum number of strategy runs. For method='grid', triggers randomized grid search. A float between 0 and 1 sets it as a fraction of the full grid. Defaults to exhaustive for 'grid' and 200 for 'sambo'.
constraint
Callable[[dict], bool] | None
A function that receives a dict-like object of parameter values and returns True if the combination is admissible. Use this to skip invalid combinations.
constraint=lambda p: p.n1 < p.n2
return_heatmap
bool
default:"False"
If True, return a tuple (stats, heatmap). The heatmap is a pd.Series with a MultiIndex of all tested parameter combinations and their objective values. Pass it to backtesting.lib.plot_heatmaps() to visualize.
random_state
int | None
Integer seed for reproducible randomized optimization results.
optimize() returns the pd.Series result of the best run found. With return_heatmap=True, it returns a tuple.

Plotting results

plot() opens an interactive Bokeh chart in the browser.
bt.plot()
The chart shows the price candlesticks, declared indicators, trade entry/exit markers, and the equity curve with drawdown periods highlighted.

Key parameters

results
pd.Series | None
A specific result Series from run() or optimize(). If not provided, the last run() result is used.
filename
str | None
Path to save the HTML file. Defaults to a strategy-specific name in the current directory.
plot_equity
bool
default:"True"
Include an equity curve section.
plot_drawdown
bool
default:"False"
Include a separate drawdown section.
open_browser
bool
default:"True"
Open the chart in the default browser after saving.
superimpose
bool | str
default:"True"
Superimpose a higher-timeframe candlestick chart. Pass a Pandas offset string like '1W' to control the timeframe.

Complete example

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):
        price = self.data.Close
        self.ma1 = self.I(SMA, price, self.n1)
        self.ma2 = self.I(SMA, price, self.n2)

    def next(self):
        if crossover(self.ma1, self.ma2):
            self.buy()
        elif crossover(self.ma2, self.ma1):
            self.sell()


# 1. Construct
bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002)

# 2. Run
stats = bt.run()
print(stats['Return [%]'])
print(stats['Sharpe Ratio'])
print(stats['# Trades'])

# 3. Inspect trades
trades_df = stats['_trades']
print(trades_df.head())

# 4. Optimize
best_stats, heatmap = bt.optimize(
    n1=range(5, 30, 5),
    n2=range(10, 70, 10),
    constraint=lambda p: p.n1 < p.n2,
    return_heatmap=True,
)

# 5. Plot
bt.plot(results=best_stats)

Commission types

# 0.2% of trade value, applied at entry and exit
bt = Backtest(data, MyStrategy, commission=0.002)
Before v0.4.0, commission was applied only once (like spread is now). If you need the old behavior, set spread instead of commission.

Fractional trading

The default Backtest class requires whole-unit position sizes. If you want to trade fractional units (e.g. Bitcoin), use backtesting.lib.FractionalBacktest:
from backtesting.lib import FractionalBacktest

bt = FractionalBacktest(btc_data, MyStrategy, cash=10_000)
stats = bt.run()
For assets like Bitcoin with high per-unit prices, you can also work around the whole-unit requirement by converting your data to micro-units (e.g. µBTC or satoshis) and increasing initial cash accordingly.

Build docs developers (and LLMs) love