Module backtesting.backtesting
Core framework data structures. Objects from this module can also be imported from the top-level module directly, e.g.
from backtesting import Backtest, Strategy
Classes
class Backtest (data, strategy, *, cash=10000, commission=0.0, margin=1.0, trade_on_close=False, hedging=False, exclusive_orders=False)
-
Backtest a particular (parameterized) strategy on particular data.
Upon initialization, call method
Backtest.run()
to run a backtest instance, orBacktest.optimize()
to optimize it.Initialize a backtest. Requires data and a strategy to test.
data
is apd.DataFrame
with columns:Open
,High
,Low
,Close
, and (optionally)Volume
. If any columns are missing, set them to what you have available, e.g.df['Open'] = df['High'] = df['Low'] = df['Close']
The passed data frame can contain additional columns that can be used by the strategy (e.g. sentiment info). DataFrame index can be either a datetime index (timestamps) or a monotonic range index (i.e. a sequence of periods).
strategy
is aStrategy
subclass (not an instance).cash
is the initial cash to start with.commission
is the commission ratio. E.g. if your broker's commission is 1% of trade value, set commission to0.01
. Note, if you wish to account for bid-ask spread, you can approximate doing so by increasing the commission, e.g. set it to0.0002
for commission-less forex trading where the average spread is roughly 0.2‰ of asking price.margin
is the required margin (ratio) of a leveraged account. No difference is made between initial and maintenance margins. To run the backtest using e.g. 50:1 leverge that your broker allows, set margin to0.02
(1 / leverage).If
trade_on_close
isTrue
, market orders will be filled with respect to the current bar's closing price instead of the next bar's open.If
hedging
isTrue
, allow trades in both directions simultaneously. IfFalse
, the opposite-facing orders first close existing trades in a FIFO manner.If
exclusive_orders
isTrue
, each new order auto-closes the previous trade/position, making at most a single trade (long or short) in effect at each time.Methods
def optimize(self, *, maximize='SQN', method='grid', max_tries=None, constraint=None, return_heatmap=False, return_optimization=False, random_state=None, **kwargs)
-
Optimize strategy parameters to an optimal combination. Returns result
pd.Series
of the best run.maximize
is a string key from theBacktest.run()
-returned results series, or a function that accepts this series object and returns a number; the higher the better. By default, the method maximizes Van Tharp's System Quality Number.method
is the optimization method. Currently two methods are supported:"grid"
which does an exhaustive (or randomized) search over the cartesian product of parameter combinations, and"skopt"
which finds close-to-optimal strategy parameters using model-based optimization, making at mostmax_tries
evaluations.
max_tries
is the maximal number of strategy runs to perform. Ifmethod="grid"
, this results in randomized grid search. Ifmax_tries
is a floating value between (0, 1], this sets the number of runs to approximately that fraction of full grid space. Alternatively, if integer, it denotes the absolute maximum number of evaluations. If unspecified (default), grid search is exhaustive, whereas formethod="skopt"
,max_tries
is set to 200.constraint
is a function that accepts a dict-like object of parameters (with values) and returnsTrue
when the combination is admissible to test with. By default, any parameters combination is considered admissible.If
return_heatmap
isTrue
, besides returning the result series, an additionalpd.Series
is returned with a multiindex of all admissible parameter combinations, which can be further inspected or projected onto 2D to plot a heatmap (seeplot_heatmaps()
).If
return_optimization
is True andmethod = 'skopt'
, in addition to result series (and maybe heatmap), return rawscipy.optimize.OptimizeResult
for further inspection, e.g. with scikit-optimize plotting tools.If you want reproducible optimization results, set
random_state
to a fixed integer random seed.Additional keyword arguments represent strategy arguments with list-like collections of possible values. For example, the following code finds and returns the "best" of the 7 admissible (of the 9 possible) parameter combinations:
backtest.optimize(sma1=[5, 10, 15], sma2=[10, 20, 40], constraint=lambda p: p.sma1 < p.sma2)
TODO
Improve multiprocessing/parallel execution on Windos with start method 'spawn'.
def plot(self, *, 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)
-
Plot the progression of the last backtest run.
If
results
is provided, it should be a particular resultpd.Series
such as returned byBacktest.run()
orBacktest.optimize()
, otherwise the last run's results are used.filename
is the path to save the interactive HTML plot to. By default, a strategy/parameter-dependent file is created in the current working directory.plot_width
is the width of the plot in pixels. If None (default), the plot is made to span 100% of browser width. The height is currently non-adjustable.If
plot_equity
isTrue
, the resulting plot will contain an equity (initial cash plus assets) graph section. This is the same asplot_return
plus initial 100%.If
plot_return
isTrue
, the resulting plot will contain a cumulative return graph section. This is the same asplot_equity
minus initial 100%.If
plot_pl
isTrue
, the resulting plot will contain a profit/loss (P/L) indicator section.If
plot_volume
isTrue
, the resulting plot will contain a trade volume section.If
plot_drawdown
isTrue
, the resulting plot will contain a separate drawdown graph section.If
plot_trades
isTrue
, the stretches between trade entries and trade exits are marked by hash-marked tractor beams.If
smooth_equity
isTrue
, the equity graph will be interpolated between fixed points at trade closing times, unaffected by any interim asset volatility.If
relative_equity
isTrue
, scale and label equity graph axis with return percent, not absolute cash-equivalent values.If
superimpose
isTrue
, superimpose larger-timeframe candlesticks over the original candlestick chart. Default downsampling rule is: monthly for daily data, daily for hourly data, hourly for minute data, and minute for (sub-)second data.superimpose
can also be a valid Pandas offset string, such as'5T'
or'5min'
, in which case this frequency will be used to superimpose. Note, this only works for data with a datetime index.If
resample
isTrue
, the OHLC data is resampled in a way that makes the upper number of candles for Bokeh to plot limited to 10_000. This may, in situations of overabundant data, improve plot's interactive performance and avoid browser'sJavascript Error: Maximum call stack size exceeded
or similar. Equity & dropdown curves and individual trades data is, likewise, reasonably aggregated.resample
can also be a Pandas offset string, such as'5T'
or'5min'
, in which case this frequency will be used to resample, overriding above numeric limitation. Note, all this only works for data with a datetime index.If
reverse_indicators
isTrue
, the indicators below the OHLC chart are plotted in reverse order of declaration.If
show_legend
isTrue
, the resulting plot graphs will contain labeled legends.If
open_browser
isTrue
, the resultingfilename
will be opened in the default web browser. def run(self, **kwargs)
-
Run the backtest. Returns
pd.Series
with results and statistics.Keyword arguments are interpreted as strategy parameters.
>>> Backtest(GOOG, SmaCross).run() Start 2004-08-19 00:00:00 End 2013-03-01 00:00:00 Duration 3116 days 00:00:00 Exposure Time [%] 93.9944 Equity Final [$] 51959.9 Equity Peak [$] 75787.4 Return [%] 419.599 Buy & Hold Return [%] 703.458 Return (Ann.) [%] 21.328 Volatility (Ann.) [%] 36.5383 Sharpe Ratio 0.583718 Sortino Ratio 1.09239 Calmar Ratio 0.444518 Max. Drawdown [%] -47.9801 Avg. Drawdown [%] -5.92585 Max. Drawdown Duration 584 days 00:00:00 Avg. Drawdown Duration 41 days 00:00:00 # Trades 65 Win Rate [%] 46.1538 Best Trade [%] 53.596 Worst Trade [%] -18.3989 Avg. Trade [%] 2.35371 Max. Trade Duration 183 days 00:00:00 Avg. Trade Duration 46 days 00:00:00 Profit Factor 2.08802 Expectancy [%] 8.79171 SQN 0.916893 Kelly Criterion 0.6134 _strategy SmaCross _equity_curve Eq... _trades Size EntryB... dtype: object
Warning
You may obtain different results for different strategy parameters. E.g. if you use 50- and 200-bar SMA, the trading simulation will begin on bar 201. The actual length of delay is equal to the lookback period of the
Strategy.I()
indicator which lags the most. Obviously, this can affect results.
class Order
-
Place new orders through
Strategy.buy()
andStrategy.sell()
. Query existing orders throughStrategy.orders
.When an order is executed or filled, it results in a
Trade
.If you wish to modify aspects of a placed but not yet filled order, cancel it and place a new one instead.
All placed orders are Good 'Til Canceled.
Instance variables
prop is_contingent
-
True for contingent orders, i.e. OCO stop-loss and take-profit bracket orders placed upon an active trade. Remaining contingent orders are canceled when their parent
Trade
is closed.You can modify contingent orders through
Trade.sl
andTrade.tp
. prop is_long
-
True if the order is long (order size is positive).
prop is_short
-
True if the order is short (order size is negative).
prop limit
-
Order limit price for limit orders, or None for market orders, which are filled at next available price.
prop size
-
Order size (negative for short orders).
If size is a value between 0 and 1, it is interpreted as a fraction of current available liquidity (cash plus
Position.pl
minus used margin). A value greater than or equal to 1 indicates an absolute number of units. prop sl
prop stop
-
Order stop price for stop-limit/stop-market order, otherwise None if no stop was set, or the stop price has already been hit.
prop tag
prop tp
Methods
def cancel(self)
-
Cancel the order.
class Position
-
Currently held asset position, available as
Strategy.position
withinStrategy.next()
. Can be used in boolean contexts, e.g.if self.position: ... # we have a position, either long or short
Instance variables
prop is_long
-
True if the position is long (position size is positive).
prop is_short
-
True if the position is short (position size is negative).
prop pl
-
Profit (positive) or loss (negative) of the current position in cash units.
prop pl_pct
-
Profit (positive) or loss (negative) of the current position in percent.
prop size
-
Position size in units of asset. Negative if position is short.
Methods
def close(self, portion=1.0)
-
Close portion of position by closing
portion
of each active trade. SeeTrade.close()
.
class Strategy
-
A trading strategy base class. Extend this class and override methods
Strategy.init()
andStrategy.next()
to define your own strategy.Subclasses
Instance variables
prop closed_trades
-
List of settled trades (see
Trade
). prop data
-
Price data, roughly as passed into
Backtest
, but with two significant exceptions:data
is not a DataFrame, but a custom structure that serves customized numpy arrays for reasons of performance and convenience. Besides OHLCV columns,.index
and length, it offers.pip
property, the smallest price unit of change.- Within
Strategy.init()
,data
arrays are available in full length, as passed intoBacktest
(for precomputing indicators and such). However, withinStrategy.next()
,data
arrays are only as long as the current iteration, simulating gradual price point revelation. In each call ofStrategy.next()
(iteratively called byBacktest
internally), the last array value (e.g.data.Close[-1]
) is always the most recent value. - If you need data arrays (e.g.
data.Close
) to be indexed Pandas series, you can call their.s
accessor (e.g.data.Close.s
). If you need the whole of data as a DataFrame, use.df
accessor (i.e.data.df
).
prop equity
-
Current account equity (cash plus assets).
prop orders
-
List of orders (see
Order
) waiting for execution. prop position
-
Instance of
Position
. prop trades
-
List of active trades (see
Trade
).
Methods
def I(self, func, *args, name=None, plot=True, overlay=None, color=None, scatter=False, **kwargs)
-
Declare an indicator. An indicator is just an array of values, but one that is revealed gradually in
Strategy.next()
much likeStrategy.data
is. Returnsnp.ndarray
of indicator values.func
is a function that returns the indicator array(s) of same length asStrategy.data
.In the plot legend, the indicator is labeled with function name, unless
name
overrides it.If
plot
isTrue
, the indicator is plotted on the resultingBacktest.plot()
.If
overlay
isTrue
, the indicator is plotted overlaying the price candlestick chart (suitable e.g. for moving averages). IfFalse
, the indicator is plotted standalone below the candlestick chart. By default, a heuristic is used which decides correctly most of the time.color
can be string hex RGB triplet or X11 color name. By default, the next available color is assigned.If
scatter
isTrue
, the plotted indicator marker will be a circle instead of a connected line segment (default).Additional
*args
and**kwargs
are passed tofunc
and can be used for parameters.For example, using simple moving average function from TA-Lib:
def init(): self.sma = self.I(ta.SMA, self.data.Close, self.n_sma)
def buy(self, *, size=.9999, limit=None, stop=None, sl=None, tp=None, tag=None)
-
Place a new long order. For explanation of parameters, see
Order
and its properties.See
Position.close()
andTrade.close()
for closing existing positions.See also
Strategy.sell()
. def init(self)
-
Initialize the strategy. Override this method. Declare indicators (with
Strategy.I()
). Precompute what needs to be precomputed or can be precomputed in a vectorized fashion before the strategy starts.If you extend composable strategies from
backtesting.lib
, make sure to call:super().init()
def next(self)
-
Main strategy runtime method, called as each new
Strategy.data
instance (row; full candlestick bar) becomes available. This is the main method where strategy decisions upon data precomputed inStrategy.init()
take place.If you extend composable strategies from
backtesting.lib
, make sure to call:super().next()
def sell(self, *, size=.9999, limit=None, stop=None, sl=None, tp=None, tag=None)
-
Place a new short order. For explanation of parameters, see
Order
and its properties.See also
Strategy.buy()
.Note
If you merely want to close an existing long position, use
Position.close()
orTrade.close()
.
class Trade
-
When an
Order
is filled, it results in an activeTrade
. Find active trades inStrategy.trades
and closed, settled trades inStrategy.closed_trades
.Instance variables
prop entry_bar
-
Candlestick bar index of when the trade was entered.
prop entry_price
-
Trade entry price.
prop entry_time
-
Datetime of when the trade was entered.
prop exit_bar
-
Candlestick bar index of when the trade was exited (or None if the trade is still active).
prop exit_price
-
Trade exit price (or None if the trade is still active).
prop exit_time
-
Datetime of when the trade was exited.
prop is_long
-
True if the trade is long (trade size is positive).
prop is_short
-
True if the trade is short (trade size is negative).
prop pl
-
Trade profit (positive) or loss (negative) in cash units.
prop pl_pct
-
Trade profit (positive) or loss (negative) in percent.
prop size
-
Trade size (volume; negative for short trades).
prop sl
-
Stop-loss price at which to close the trade.
This variable is writable. By assigning it a new price value, you create or modify the existing SL order. By assigning it
None
, you cancel it. prop tag
prop tp
-
Take-profit price at which to close the trade.
This property is writable. By assigning it a new price value, you create or modify the existing TP order. By assigning it
None
, you cancel it. prop value
-
Trade total value in cash (volume × price).
Methods
def close(self, portion=1.0)
-
Place new
Order
to closeportion
of the trade at next market price.