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,
spread=0.0,
commission=0.0,
margin=1.0,
trade_on_close=False,
hedging=False,
exclusive_orders=False,
finalize_trades=False)-
Backtest a particular (parameterized) strategy on particular data.
Initialize a backtest. Requires data and a strategy to test. After initialization, you can call method
Backtest.run()to run a backtest instance, orBacktest.optimize()to optimize it.datais apd.DataFramewith 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).
strategyis aStrategysubclass (not an instance).cashis the initial cash to start with.spreadis the the constant bid-ask spread rate (relative to the price). E.g. set it to0.0002for commission-less forex trading where the average spread is roughly 0.2‰ of the asking price.commissionis the commission rate. E.g. if your broker's commission is 1% of order value, set commission to0.01. The commission is applied twice: at trade entry and at trade exit. Besides one single floating value,commissioncan also be a tuple of floating values(fixed, relative). E.g. set it to(100, .01)if your broker charges minimum $100 + 1%. Additionally,commissioncan be a callablefunc(order_size: int, price: float) -> float(note, order size is negative for short orders), which can be used to model more complex commission structures. Negative commission values are interpreted as market-maker's rebates.Note
Before v0.4.0, the commission was only applied once, like
spreadis now. If you want to keep the old behavior, simply setspreadinstead.Note
With nonzero
commission, long and short orders will be placed at an adjusted price that is slightly higher or lower (respectively) than the current price. See e.g. #153, #538, #633.marginis 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_closeisTrue, market orders will be filled with respect to the current bar's closing price instead of the next bar's open.If
hedgingisTrue, allow trades in both directions simultaneously. IfFalse, the opposite-facing orders first close existing trades in a FIFO manner.If
exclusive_ordersisTrue, each new order auto-closes the previous trade/position, making at most a single trade (long or short) in effect at each time.If
finalize_tradesisTrue, the trades that are still active and ongoing at the end of the backtest will be closed on the last bar and will contribute to the computed backtest statistics.Tip: Fractional trading
See also
FractionalBacktestif you want to trade fractional units (of e.g. bitcoin).Subclasses
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.Seriesof the best run.maximizeis 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.methodis the optimization method. Currently two methods are supported:"grid"which does an exhaustive (or randomized) search over the cartesian product of parameter combinations, and"sambo"which finds close-to-optimal strategy parameters using model-based optimization, making at mostmax_triesevaluations.
max_triesis the maximal number of strategy runs to perform. Ifmethod="grid", this results in randomized grid search. Ifmax_triesis 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="sambo",max_triesis set to 200.constraintis a function that accepts a dict-like object of parameters (with values) and returnsTruewhen the combination is admissible to test with. By default, any parameters combination is considered admissible.If
return_heatmapisTrue, besides returning the result series, an additionalpd.Seriesis 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_optimizationis True andmethod = 'sambo', in addition to result series (and maybe heatmap), return rawscipy.optimize.OptimizeResultfor further inspection, e.g. with SAMBO's plotting tools.If you want reproducible optimization results, set
random_stateto 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:
best_stats = backtest.optimize(sma1=[5, 10, 15], sma2=[10, 20, 40], constraint=lambda p: p.sma1 < p.sma2) 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
resultsis provided, it should be a particular resultpd.Seriessuch as returned byBacktest.run()orBacktest.optimize(), otherwise the last run's results are used.filenameis the path to save the interactive HTML plot to. By default, a strategy/parameter-dependent file is created in the current working directory.plot_widthis 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_equityisTrue, the resulting plot will contain an equity (initial cash plus assets) graph section. This is the same asplot_returnplus initial 100%.If
plot_returnisTrue, the resulting plot will contain a cumulative return graph section. This is the same asplot_equityminus initial 100%.If
plot_plisTrue, the resulting plot will contain a profit/loss (P/L) indicator section.If
plot_volumeisTrue, the resulting plot will contain a trade volume section.If
plot_drawdownisTrue, the resulting plot will contain a separate drawdown graph section.If
plot_tradesisTrue, the stretches between trade entries and trade exits are marked by hash-marked tractor beams.If
smooth_equityisTrue, the equity graph will be interpolated between fixed points at trade closing times, unaffected by any interim asset volatility.If
relative_equityisTrue, scale and label equity graph axis with return percent, not absolute cash-equivalent values.If
superimposeisTrue, 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.superimposecan 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
resampleisTrue, 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 exceededor similar. Equity & dropdown curves and individual trades data is, likewise, reasonably aggregated.resamplecan 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_indicatorsisTrue, the indicators below the OHLC chart are plotted in reverse order of declaration.If
show_legendisTrue, the resulting plot graphs will contain labeled legends.If
open_browserisTrue, the resultingfilenamewill be opened in the default web browser. def run(self, **kwargs)-
Run the backtest. Returns
pd.Serieswith 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 [%] 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: objectWarning
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
Tradeis closed.You can modify contingent orders through
Trade.slandTrade.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.plminus used margin). A value greater than or equal to 1 indicates an absolute number of units. prop slprop 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 tagprop tp
Methods
def cancel(self)-
Cancel the order.
class Position-
Currently held asset position, available as
Strategy.positionwithinStrategy.next(). Can be used in boolean contexts, e.g.if self.position: ... # we have a position, either long or shortInstance 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
portionof 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:datais not a DataFrame, but a custom structure that serves customized numpy arrays for reasons of performance and convenience. Besides OHLCV columns,.indexand length, it offers.pipproperty, the smallest price unit of change.- Within
Strategy.init(),dataarrays are available in full length, as passed intoBacktest(for precomputing indicators and such). However, withinStrategy.next(),dataarrays are only as long as the current iteration, simulating gradual price point revelation. In each call ofStrategy.next()(iteratively called byBacktestinternally), 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.saccessor (e.g.data.Close.s). If you need the whole of data as a DataFrame, use.dfaccessor (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 (or a tuple of such arrays in case of, e.g., MACD indicator), but one that is revealed gradually in
Strategy.next()much likeStrategy.datais. Returnsnp.ndarrayof indicator values.funcis 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
nameoverrides it. Iffuncreturns a tuple of arrays,namecan be a sequence of strings, and its size must agree with the number of arrays returned.If
plotisTrue, the indicator is plotted on the resultingBacktest.plot().If
overlayisTrue, 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.colorcan be string hex RGB triplet or X11 color name. By default, the next available color is assigned.If
scatterisTrue, the plotted indicator marker will be a circle instead of a connected line segment (default).Additional
*argsand**kwargsare passed tofuncand 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)Warning
Rolling indicators may front-pad warm-up values with NaNs. In this case, the backtest will only begin on the first bar when all declared indicators have non-NaN values (e.g. bar 201 for a strategy that uses a 200-bar MA). This can affect results.
def buy(self, *, size=.9999, limit=None, stop=None, sl=None, tp=None, tag=None)-
Place a new long order and return it. For explanation of parameters, see
Orderand its properties. Unless you're runningBacktest(..., trade_on_close=True), market orders are filled on next bar's open, whereas other order types (limit, stop-limit, stop-market) are filled when the respective conditions are met.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.datainstance (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 and return it. For explanation of parameters, see
Orderand its properties.Caution
Keep in mind that
self.sell(size=.1)doesn't close existingself.buy(size=.1)trade unless:- the backtest was run with
exclusive_orders=True, - the underlying asset price is equal in both cases and
the backtest was run with
spread = commission = 0.
Use
Trade.close()orPosition.close()to explicitly exit trades.See also
Strategy.buy().Note
If you merely want to close an existing long position, use
Position.close()orTrade.close(). - the backtest was run with
class Trade-
When an
Orderis filled, it results in an activeTrade. Find active trades inStrategy.tradesand 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. Commissions are reflected only after the Trade is closed.
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 tagprop 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
Orderto closeportionof the trade at next market price.