Skip to main content
The Backtest class is the entry point for running and optimizing trading strategies. Initialize it with historical data and a strategy class, then call run(), optimize(), or plot().
from backtesting import Backtest, Strategy

Constructor

Backtest(data, strategy, *, cash=10000, spread=0.0, commission=0.0, margin=1.0,
         trade_on_close=False, hedging=False, exclusive_orders=False, finalize_trades=False)
data
pd.DataFrame
required
OHLCV price data. Must contain columns Open, High, Low, and Close. A Volume column is optional — if omitted, it is set to NaN. The index must be a pd.DatetimeIndex (recommended) or a monotonic range index.
# Minimal setup if you only have close prices
df['Open'] = df['High'] = df['Low'] = df['Close']
All OHLC values must be non-null. Strip missing rows with df.dropna() or fill them with df.interpolate() before passing the data.
strategy
Type[Strategy]
required
A Strategy subclass (the class itself, not an instance). The class must implement init() and next() methods.
# Correct — pass the class
Backtest(data, SmaCross)

# Wrong — do not instantiate
Backtest(data, SmaCross())
cash
float
default:"10000"
Initial cash balance. All trades are sized relative to this starting capital.
Fractional trading is not supported. If any asset price exceeds the initial cash, the backtest will warn you. Consider increasing cash or using backtesting.lib.FractionalBacktest.
spread
float
default:"0.0"
Constant bid-ask spread as a fraction of the entry price. Applied once at order entry. For example, 0.0002 models a 0.02% spread typical in commission-less forex trading.
spread is applied at entry only. Use commission for fees that apply at both entry and exit.
commission
float | tuple[float, float] | callable
default:"0.0"
Commission rate applied at both trade entry and exit. Accepts three forms:
  • float — relative commission as a fraction of order value. E.g. 0.01 for 1%.
  • (fixed, relative) tuple — fixed cash amount plus a relative fraction. E.g. (100, 0.01) for $100 + 1%.
  • callable(order_size: int, price: float) -> float — custom commission function. order_size is negative for short orders. Useful for modeling rebates or tiered fee schedules.
Negative commission values represent market-maker rebates.
Before v0.4.0, commission was applied only once (like spread is now). To preserve old behavior, use spread instead.
margin
float
default:"1.0"
Required margin ratio for leveraged trading. The ratio is 1 / leverage. For example, 0.02 enables 50:1 leverage. No distinction is made between initial and maintenance margin.Must be between 0 (exclusive) and 1 (inclusive).
trade_on_close
bool
default:"false"
When True, market orders fill at the current bar’s closing price instead of the next bar’s open. Useful for strategies that decide to trade at bar close.
hedging
bool
default:"false"
When True, allows simultaneous long and short positions. When False (default), opposite-facing orders close existing positions in FIFO order.
exclusive_orders
bool
default:"false"
When True, placing a new order automatically cancels all pending orders and closes the current position, ensuring at most one trade is active at any time.
finalize_trades
bool
default:"false"
When True, any trades still open at the end of the backtest are closed on the last bar and included in the computed statistics. When False, a warning is issued for any remaining open trades.

Backtest.run()

Backtest.run(**kwargs) -> pd.Series
Run the backtest and return a pd.Series of performance statistics. Keyword arguments override strategy class-level parameter defaults for this run:
bt = Backtest(data, SmaCross)
stats = bt.run(n1=10, n2=30)
The backtest begins on the first bar where all declared indicators have non-NaN values. A strategy using a 200-bar moving average will not start trading until bar 201. This affects Exposure Time, Duration, and trade counts.

Return value

run() returns a pd.Series with the following fields:
Start
pd.Timestamp
Datetime of the first data bar.
End
pd.Timestamp
Datetime of the last data bar.
Duration
pd.Timedelta
Total duration of the backtest period.
Exposure Time [%]
float
Percentage of bars where at least one trade was active.
Equity Final [$]
float
Final account equity at the end of the backtest.
Equity Peak [$]
float
Maximum equity reached during the backtest.
Return [%]
float
Total return on initial cash.
Buy & Hold Return [%]
float
Return of a passive buy-and-hold strategy over the same period, for comparison.
Return (Ann.) [%]
float
Annualized return.
Volatility (Ann.) [%]
float
Annualized volatility of equity returns.
CAGR [%]
float
Compound Annual Growth Rate.
Sharpe Ratio
float
Risk-adjusted return relative to volatility (assumes risk-free rate of 0).
Sortino Ratio
float
Like Sharpe, but penalizes only downside volatility.
Calmar Ratio
float
Annualized return divided by maximum drawdown.
Alpha [%]
float
Excess return over buy-and-hold.
Beta
float
Sensitivity of strategy returns relative to the underlying asset.
Max. Drawdown [%]
float
Largest peak-to-trough decline in equity, as a percentage.
Avg. Drawdown [%]
float
Average drawdown across all drawdown periods.
Max. Drawdown Duration
pd.Timedelta
Longest time spent in a drawdown.
Avg. Drawdown Duration
pd.Timedelta
Average duration of drawdown periods.
# Trades
int
Total number of completed trades.
Win Rate [%]
float
Percentage of trades that were profitable.
Best Trade [%]
float
Return of the single best trade.
Worst Trade [%]
float
Return of the single worst trade.
Avg. Trade [%]
float
Average return per trade.
Max. Trade Duration
pd.Timedelta
Duration of the longest trade.
Avg. Trade Duration
pd.Timedelta
Average duration per trade.
Profit Factor
float
Ratio of gross profit to gross loss.
Expectancy [%]
float
Average expected return per trade, accounting for win rate and average trade size.
SQN
float
Van Tharp’s System Quality Number. Values above 1.6 indicate a good system.
Kelly Criterion
float
Optimal fraction of capital to risk per trade according to the Kelly formula.
_strategy
Strategy
The strategy instance used in the run, including all parameter values.
_equity_curve
pd.DataFrame
Full equity curve as a DataFrame, indexed by datetime.
_trades
pd.DataFrame
DataFrame of all completed trades with entry/exit prices, sizes, P&L, and tags.

Backtest.optimize()

Backtest.optimize(
    *,
    maximize='SQN',
    method='grid',
    max_tries=None,
    constraint=None,
    return_heatmap=False,
    return_optimization=False,
    random_state=None,
    **kwargs
) -> pd.Series | tuple
Search for the parameter combination that maximizes a performance metric. Pass strategy parameters as keyword arguments with lists of candidate values.
best_stats = bt.optimize(
    n1=[5, 10, 15],
    n2=[10, 20, 40],
    constraint=lambda p: p.n1 < p.n2,
)
**kwargs
list-like
Strategy parameter names mapped to sequences of candidate values. The optimizer searches over combinations of these values.
bt.optimize(sma1=[5, 10, 15], sma2=[10, 20, 40])
maximize
str | callable
default:"'SQN'"
The metric to maximize. Either:
  • A string key matching a field in the run() result series (e.g. 'Return [%]', 'Sharpe Ratio').
  • A callable func(stats: pd.Series) -> float that accepts a run() result and returns a score. The higher the score, the better.
method
str
default:"'grid'"
Optimization method:
  • 'grid' — exhaustive (or randomized if max_tries is set) search over the Cartesian product of all parameter combinations.
  • 'sambo' — model-based optimization using SAMBO. Requires the sambo package (pip install sambo). Efficient for large parameter spaces.
max_tries
int | float | None
default:"None"
Maximum number of strategy evaluations:
  • None — exhaustive grid search (all combinations). For method='sambo', defaults to 200.
  • float in (0, 1] — fraction of the full grid space to sample randomly.
  • int — absolute maximum number of evaluations.
constraint
callable | None
default:"None"
A function that accepts a dict-like object of parameter values and returns True if the combination is worth testing. Use this to prune invalid combinations.
constraint=lambda p: p.fast_period < p.slow_period
return_heatmap
bool
default:"false"
When True, the method returns a tuple (stats, heatmap) where heatmap is a pd.Series with a MultiIndex of all tested parameter combinations and their maximize scores. Use backtesting.lib.plot_heatmaps() to visualize.
return_optimization
bool
default:"false"
When True and method='sambo', also returns the raw scipy.optimize.OptimizeResult object for further inspection with SAMBO’s plotting tools. Requires return_heatmap=True if you want all three return values.
return_optimization=True is only valid when method='sambo'.
random_state
int | None
default:"None"
Integer seed for reproducible randomized grid search or SAMBO optimization.

Return value

Depending on the flags set:
FlagsReturns
(default)pd.Series — stats of the best run
return_heatmap=True(pd.Series, pd.Series) — best stats + heatmap
return_heatmap=True, return_optimization=True(pd.Series, pd.Series, OptimizeResult)

Backtest.plot()

Backtest.plot(
    *,
    results=None,
    filename=None,
    plot_width=None,
    plot_equity=True,
    plot_return=False,
    plot_pl=True,
    plot_volume=True,
    plot_drawdown=False,
    plot_trades=True,
    smooth_equity=False,
    relative_equity=True,
    superimpose=True,
    resample=True,
    reverse_indicators=False,
    show_legend=True,
    open_browser=True,
)
Generate an interactive Bokeh HTML chart of the last backtest run.
results
pd.Series | None
default:"None"
A specific result series from run() or optimize(). If None, the most recent run() result is used.
filename
str | None
default:"None"
File path for the output HTML file. Defaults to a strategy/parameter-dependent filename in the current working directory.
plot_width
int | None
default:"None"
Width of the chart in pixels. When None, the chart spans 100% of the browser width.
plot_equity
bool
default:"true"
Show an equity curve section (cash plus open position value). Equivalent to plot_return plus the initial 100%.
plot_return
bool
default:"false"
Show a cumulative return section. Equivalent to plot_equity minus the initial 100%.
plot_pl
bool
default:"true"
Show a profit/loss indicator section.
plot_volume
bool
default:"true"
Show a trade volume section.
plot_drawdown
bool
default:"false"
Show a separate drawdown graph section.
plot_trades
bool
default:"true"
Mark trade entry-to-exit spans on the chart with hash-marked tractor beams.
smooth_equity
bool
default:"false"
Interpolate the equity curve between trade closing points, removing intra-trade volatility from the visual.
relative_equity
bool
default:"true"
Label the equity axis with return percentages instead of absolute cash values.
superimpose
bool | str
default:"true"
Overlay larger-timeframe candlesticks on the price chart. When True, the downsampling frequency is chosen automatically (monthly for daily data, daily for hourly, etc.). Pass a Pandas offset string like '5min' to override. Requires a datetime index.
resample
bool | str
default:"true"
Resample the OHLC data to keep the Bokeh canvas under 10,000 candles for performance. Pass a Pandas offset string to force a specific frequency. Requires a datetime index.
reverse_indicators
bool
default:"false"
Plot sub-chart indicators in reverse declaration order.
show_legend
bool
default:"true"
Display labeled legends on each plot section.
open_browser
bool
default:"true"
Open the generated HTML file in the default web browser after saving.

Complete example

import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import GOOG
import talib

class SmaCross(Strategy):
    n1 = 10
    n2 = 20

    def init(self):
        self.sma1 = self.I(talib.SMA, self.data.Close, self.n1)
        self.sma2 = self.I(talib.SMA, self.data.Close, self.n2)

    def next(self):
        if crossover(self.sma1, self.sma2):
            self.buy()
        elif crossover(self.sma2, self.sma1):
            self.sell()

# Initialize
bt = Backtest(
    GOOG,
    SmaCross,
    cash=10_000,
    commission=0.002,
)

# Run a single backtest
stats = bt.run()
print(stats)

# Optimize parameters
best_stats, heatmap = bt.optimize(
    n1=range(5, 30, 5),
    n2=range(10, 70, 5),
    constraint=lambda p: p.n1 < p.n2,
    maximize='Sharpe Ratio',
    return_heatmap=True,
)
print(best_stats['_strategy'])

# Plot results
bt.plot(smooth_equity=True, relative_equity=False)

Build docs developers (and LLMs) love