Module backtesting.lib

Collection of common building blocks, helper auxiliary functions and composable strategy classes for reuse.

Intended for simple missing-link procedures, not reinventing of better-suited, state-of-the-art, fast libraries, such as TA-Lib, Tulipy, PyAlgoTrade, NumPy, SciPy …

Please raise ideas for additions to this collection on the issue tracker.

Global variables

var OHLCV_AGG

Dictionary of rules for aggregating resampled OHLCV data frames, e.g.

df.resample('4H', label='right').agg(OHLCV_AGG).dropna()
var TRADES_AGG

Dictionary of rules for aggregating resampled trades data, e.g.

stats['_trades'].resample('1D', on='ExitTime',
                          label='right').agg(TRADES_AGG)

Functions

def barssince(condition, default=inf)

Return the number of bars since condition sequence was last True, or if never, return default.

>>> barssince(self.data.Close > self.data.Open)
3
def compute_stats(*, stats, data, trades=None, risk_free_rate=0.0)

(Re-)compute strategy performance metrics.

stats is the statistics series as returned by Backtest.run(). data is OHLC data as passed to the Backtest the stats were obtained in. trades can be a dataframe subset of stats._trades (e.g. only long trades). You can also tune risk_free_rate, used in calculation of Sharpe and Sortino ratios.

>>> stats = Backtest(GOOG, MyStrategy).run()
>>> only_long_trades = stats._trades[stats._trades.Size > 0]
>>> long_stats = compute_stats(stats=stats, trades=only_long_trades,
...                            data=GOOG, risk_free_rate=.02)
def cross(series1, series2)

Return True if series1 and series2 just crossed (above or below) each other.

>>> cross(self.data.Close, self.sma)
True
def crossover(series1, series2)

Return True if series1 just crossed over (above) series2.

>>> crossover(self.data.Close, self.sma)
True
def plot_heatmaps(heatmap, agg='max', *, ncols=3, plot_width=1200, filename='', open_browser=True)

Plots a grid of heatmaps, one for every pair of parameters in heatmap.

heatmap is a Series as returned by Backtest.optimize() when its parameter return_heatmap=True.

When projecting the n-dimensional heatmap onto 2D, the values are aggregated by 'max' function by default. This can be tweaked with agg parameter, which accepts any argument pandas knows how to aggregate by.

TODO

Lay heatmaps out lower-triangular instead of in a simple grid. Like sambo.plot.plot_objective() does.

def quantile(series, quantile=None)

If quantile() is None, return the quantile rank of the last value of series wrt former series values.

If quantile() is a value between 0 and 1, return the value of series at this quantile. If used to working with percentiles, just divide your percentile amount with 100 to obtain quantiles.

>>> quantile(self.data.Close[-20:], .1)
162.130
>>> quantile(self.data.Close)
0.13
def random_ohlc_data(example_data, *, frac=1.0, random_state=None)

OHLC data generator. The generated OHLC data has basic descriptive statistics similar to the provided example_data.

frac is a fraction of data to sample (with replacement). Values greater than 1 result in oversampling.

Such random data can be effectively used for stress testing trading strategy robustness, Monte Carlo simulations, significance testing, etc.

>>> from backtesting.test import EURUSD
>>> ohlc_generator = random_ohlc_data(EURUSD)
>>> next(ohlc_generator)  # returns new random data
...
>>> next(ohlc_generator)  # returns new random data
...
def resample_apply(rule, func, series, *args, agg=None, **kwargs)

Apply func (such as an indicator) to series, resampled to a time frame specified by rule. When called from inside Strategy.init(), the result (returned) series will be automatically wrapped in Strategy.I() wrapper method.

rule is a valid Pandas offset string indicating a time frame to resample series to.

func is the indicator function to apply on the resampled series.

series is a data series (or array), such as any of the Strategy.data series. Due to pandas resampling limitations, this only works when input series has a datetime index.

agg is the aggregation function to use on resampled groups of data. Valid values are anything accepted by pandas/resample/.agg(). Default value for dataframe input is OHLCV_AGG dictionary. Default value for series input is the appropriate entry from OHLCV_AGG if series has a matching name, or otherwise the value "last", which is suitable for closing prices, but you might prefer another (e.g. "max" for peaks, or similar).

Finally, any *args and **kwargs that are not already eaten by implicit Strategy.I() call are passed to func.

For example, if we have a typical moving average function SMA(values, lookback_period), hourly data source, and need to apply the moving average MA(10) on a daily time frame, but don't want to plot the resulting indicator, we can do:

class System(Strategy):
    def init(self):
        self.sma = resample_apply(
            'D', SMA, self.data.Close, 10, plot=False)

The above short snippet is roughly equivalent to:

class System(Strategy):
    def init(self):
        # Strategy exposes <code>self.data</code> as raw NumPy arrays.
        # Let's convert closing prices back to pandas Series.
        close = self.data.Close.s

        # Resample to daily resolution. Aggregate groups
        # using their last value (i.e. closing price at the end
        # of the day). Notice `label='right'`. If it were set to
        # 'left' (default), the strategy would exhibit
        # look-ahead bias.
        daily = close.resample('D', label='right').agg('last')

        # We apply SMA(10) to daily close prices,
        # then reindex it back to original hourly index,
        # forward-filling the missing values in each day.
        # We make a separate function that returns the final
        # indicator array.
        def SMA(series, n):
            from backtesting.test import SMA
            return SMA(series, n).reindex(close.index).ffill()

        # The result equivalent to the short example above:
        self.sma = self.I(SMA, daily, 10, plot=False)

Classes

class FractionalBacktest (data, *args, satoshi=100000000, **kwargs)

A backtesting.Backtest that supports fractional share trading by simple composition. It applies roughly the transformation:

df = (df / satoshi).assign(Volume=df.Volume * satoshi)

as unchallenged in this FAQ entry on GitHub, then passes data, args*, and **kwargs to its super.

Parameter satoshi tells the amount of scaling to do. E.g. for μBTC trading, pass satoshi=1e6.

Initialize a backtest. Requires data and a strategy to test.

data is a pd.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 a Strategy subclass (not an instance).

cash is the initial cash to start with.

spread is the the constant bid-ask spread rate (relative to the price). E.g. set it to 0.0002 for commission-less forex trading where the average spread is roughly 0.2‰ of the asking price.

commission is the commission rate. E.g. if your broker's commission is 1% of order value, set commission to 0.01. The commission is applied twice: at trade entry and at trade exit. Besides one single floating value, commission can also be a tuple of floating values (fixed, relative). E.g. set it to (100, .01) if your broker charges minimum $100 + 1%. Additionally, commission can be a callable func(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 spread is now. If you want to keep the old behavior, simply set spread instead.

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.

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 to 0.02 (1 / leverage).

If trade_on_close is True, market orders will be filled with respect to the current bar's closing price instead of the next bar's open.

If hedging is True, allow trades in both directions simultaneously. If False, the opposite-facing orders first close existing trades in a FIFO manner.

If exclusive_orders is True, 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_trades is True, 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.

Ancestors

Inherited members

class SignalStrategy

A simple helper strategy that operates on position entry/exit signals. This makes the backtest of the strategy simulate a vectorized backtest. See tutorials for usage examples.

To use this helper strategy, subclass it, override its Strategy.init() method, and set the signal vector by calling SignalStrategy.set_signal() method from within it.

class ExampleStrategy(SignalStrategy):
    def init(self):
        super().init()
        self.set_signal(sma1 > sma2, sma1 < sma2)

Remember to call super().init() and super().next() in your overridden methods.

Ancestors

Methods

def set_signal(self, entry_size, exit_portion=None, *, plot=True)

Set entry/exit signal vectors (arrays).

A long entry signal is considered present wherever entry_size is greater than zero, and a short signal wherever entry_size is less than zero, following Order.size semantics.

If exit_portion is provided, a nonzero value closes portion the position (see Trade.close()) in the respective direction (positive values close long trades, negative short).

If plot is True, the signal entry/exit indicators are plotted when Backtest.plot() is called.

Inherited members

class TrailingStrategy

A strategy with automatic trailing stop-loss, trailing the current price at distance of some multiple of average true range (ATR). Call TrailingStrategy.set_trailing_sl() to set said multiple (6 by default). See tutorials for usage examples.

Remember to call super().init() and super().next() in your overridden methods.

Ancestors

Methods

def set_atr_periods(self, periods=100)

Set the lookback period for computing ATR. The default value of 100 ensures a stable ATR.

def set_trailing_sl(self, n_atr=6)

Sets the future trailing stop-loss as some multiple (n_atr) average true bar ranges away from the current price.

Inherited members