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 lastTrue
, or if never, returndefault
.>>> 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 byBacktest.run()
.data
is OHLC data as passed to theBacktest
thestats
were obtained in.trades
can be a dataframe subset ofstats._trades
(e.g. only long trades). You can also tunerisk_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
ifseries1
andseries2
just crossed (above or below) each other.>>> cross(self.data.Close, self.sma) True
def crossover(series1, series2)
-
Return
True
ifseries1
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 byBacktest.optimize()
when its parameterreturn_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()
isNone
, return the quantile rank of the last value ofseries
wrt former series values.If
quantile()
is a value between 0 and 1, return the value ofseries
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) toseries
, resampled to a time frame specified byrule
. When called from insideStrategy.init()
, the result (returned) series will be automatically wrapped inStrategy.I()
wrapper method.rule
is a valid Pandas offset string indicating a time frame to resampleseries
to.func
is the indicator function to apply on the resampled series.series
is a data series (or array), such as any of theStrategy.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 bypandas/resample/.agg()
. Default value for dataframe input isOHLCV_AGG
dictionary. Default value for series input is the appropriate entry fromOHLCV_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 implicitStrategy.I()
call are passed tofunc
.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, passsatoshi=1e6
.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.spread
is the the constant bid-ask spread rate (relative to the price). E.g. set it to0.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 to0.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 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
spread
is now. If you want to keep the old behavior, simply setspread
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 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.If
finalize_trades
isTrue
, 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 callingSignalStrategy.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()
andsuper().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 whereverentry_size
is less than zero, followingOrder.size
semantics.If
exit_portion
is provided, a nonzero value closes portion the position (seeTrade.close()
) in the respective direction (positive values close long trades, negative short).If
plot
isTrue
, the signal entry/exit indicators are plotted whenBacktest.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()
andsuper().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