This tutorial shows some of the features of *backtesting.py*, a Python framework for backtesting trading strategies.

*Backtesting.py* is a small and lightweight, blazing fast backtesting framework that uses state-of-the-art Python structures and procedures (Python 3.6+, Pandas, NumPy, Bokeh). It has a very small and simple API that is easy to remember and quickly shape towards meaningful results. The library *doesn't* really support stock picking or trading strategies that rely on arbitrage or multi-asset portfolio rebalancing; instead, it works with an individual tradeable asset at a time and is best suited for optimizing position entrance and exit signal strategies, decisions upon values of technical indicators, and it's also a versatile interactive trade visualization and statistics tool.

*You bring your own data.* Backtesting ingests _all kinds of
OHLC
data_ (stocks, forex, futures, crypto, ...) as a
pandas.DataFrame
with columns `'Open'`

, `'High'`

, `'Low'`

, `'Close'`

and (optionally) `'Volume'`

. Such data is widely obtainable (see:
pandas-datareader,
Quandl,
findatapy).
Besides these, your data frames can have *additional columns* which are accessible in your strategies in a similar manner.

DataFrame should ideally be indexed with a *datetime index* (convert it with `pd.to_datetime()`

), otherwise a simple range index will do.

In [1]:

```
# Example OHLC daily data for Google Inc.
from backtesting.test import GOOG
GOOG.tail()
```

Let's create our first strategy to backtest on these Google data, a simple moving average (MA) cross-over strategy.

*Backtesting.py* doesn't ship its own set of *technical analysis indicators*. Users favoring TA should probably refer to functions from proven indicator libraries, such as
TA-Lib or
Tulipy,
but for this example, we can define a simple helper moving average function ourselves:

In [2]:

```
import pandas as pd
def SMA(values, n):
"""
Return simple moving average of `values`, at
each step taking into account `n` previous values.
"""
return pd.Series(values).rolling(n).mean()
```

A new strategy needs to extend
`Strategy`

class and override its two abstract methods:
`init()`

and
`next()`

.

Method `init()`

is invoked before the strategy is run. Within it, one ideally precomputes in efficient, vectorized manner whatever indicators and signals the strategy depends on.

Method `next()`

is then iteratively called by the
`Backtest`

instance, once for each data point (data frame row), simulating the incremental availability of each new full candlestick bar.

Note, *backtesting.py* cannot make decisions / trades *within* candlesticks — any new orders are executed on the next candle's *open* (or the current candle's *close* if
`trade_on_close=True`

).
If you find yourself wishing to trade within candlesticks (e.g. daytrading), you instead need to begin with more fine-grained (e.g. hourly) data.

In [3]:

```
from backtesting import Strategy
from backtesting.lib import crossover
class SmaCross(Strategy):
# Define the two MA lags as *class variables*
# for later optimization
n1 = 10
n2 = 20
def init(self):
# Precompute the two moving averages
self.sma1 = self.I(SMA, self.data.Close, self.n1)
self.sma2 = self.I(SMA, self.data.Close, self.n2)
def next(self):
# If sma1 crosses above sma2, close any existing
# short trades, and buy the asset
if crossover(self.sma1, self.sma2):
self.position.close()
self.buy()
# Else, if sma1 crosses below sma2, close any existing
# long trades, and sell the asset
elif crossover(self.sma2, self.sma1):
self.position.close()
self.sell()
```

In `init()`

as well as in `next()`

, the data the strategy is simulated on is available as an instance variable
`self.data`

.

In `init()`

, we declare and **compute indicators indirectly by wrapping them in
self.I()**.
The wrapper is passed a function (our

`SMA`

function) along with any arguments to call it with (our In `next()`

, we simply check if the faster moving average just crossed over the slower one. If it did and upwards, we close the possible short position and go long; if it did and downwards, we close the open long position and go short. Note, we don't adjust order size, so *Backtesting.py* assumes *maximal possible position*. We use
`backtesting.lib.crossover()`

function instead of writing more obscure and confusing conditions, such as:

In [4]:

```
%%script echo
def next(self):
if (self.sma1[-2] < self.sma2[-2] and
self.sma1[-1] > self.sma2[-1]):
self.position.close()
self.buy()
elif (self.sma1[-2] > self.sma2[-2] and # Ugh!
self.sma1[-1] < self.sma2[-1]):
self.position.close()
self.sell()
```

In `init()`

, the whole series of points was available, whereas **in next(), the length of self.data and all declared indicators is adjusted** on each

`next()`

call so that `array[-1]`

(e.g. `self.data.Close[-1]`

or `self.sma1[-1]`

) always contains the most recent value, `array[-2]`

the previous value, etc. (ordinary Python indexing of ascending-sorted 1D arrays).**Note**: `self.data`

and any indicators wrapped with `self.I`

(e.g. `self.sma1`

) are NumPy arrays for performance reasons. If you prefer pandas Series or DataFrame objects, use `Strategy.data.<column>.s`

or `Strategy.data.df`

accessors respectively. You could also construct the series manually, e.g. `pd.Series(self.data.Close, index=self.data.index)`

.

We might avoid `self.position.close()`

calls if we primed the
`Backtest`

instance with `Backtest(..., exclusive_orders=True)`

.

In [5]:

```
from backtesting import Backtest
bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002)
stats = bt.run()
stats
```

Out[5]:

Start 2004-08-19 00:00:00 End 2013-03-01 00:00:00 Duration 3116 days 00:00:00 Exposure Time [%] 97.07 Equity Final [$] 68221.97 Equity Peak [$] 68991.22 Return [%] 582.22 Buy & Hold Return [%] 703.46 Return (Ann.) [%] 25.27 Volatility (Ann.) [%] 38.38 Sharpe Ratio 0.66 Sortino Ratio 1.29 Calmar Ratio 0.76 Max. Drawdown [%] -33.08 Avg. Drawdown [%] -5.58 Max. Drawdown Duration 688 days 00:00:00 Avg. Drawdown Duration 41 days 00:00:00 # Trades 94 Win Rate [%] 54.26 Best Trade [%] 57.12 Worst Trade [%] -16.63 Avg. Trade [%] 2.07 Max. Trade Duration 121 days 00:00:00 Avg. Trade Duration 33 days 00:00:00 Profit Factor 2.19 Expectancy [%] 2.61 SQN 1.99 _strategy SmaCross _equity_curve Equ... _trades Size EntryB... dtype: object

`Backtest.run()`

method returns a pandas Series of simulation results and statistics associated with our strategy. We see that this simple strategy makes almost 600% return in the period of 9 years, with maximum drawdown 33%, and with longest drawdown period spanning almost two years ...

`Backtest.plot()`

method provides the same insights in a more visual form.

In [6]:

```
bt.plot()
```

Out[6]:

id = '1450', …)

We hard-coded the two lag parameters (`n1`

and `n2`

) into our strategy above. However, the strategy may work better with 15–30 or some other cross-over. **We declared the parameters as optimizable by making them class variables**.

We optimize the two parameters by calling
`Backtest.optimize()`

method with each parameter a keyword argument pointing to its pool of possible values to test. Parameter `n1`

is tested for values in range between 5 and 30 and parameter `n2`

for values between 10 and 70, respectively. Some combinations of values of the two parameters are invalid, i.e. `n1`

should not be *larger than* or equal to `n2`

. We limit admissible parameter combinations with an *ad hoc* constraint function, which takes in the parameters and returns `True`

(i.e. admissible) whenever `n1`

is less than `n2`

. Additionally, we search for such parameter combination that maximizes return over the observed period. We could instead choose to optimize any other key from the returned `stats`

series.

In [7]:

```
%%time
stats = bt.optimize(n1=range(5, 30, 5),
n2=range(10, 70, 5),
maximize='Equity Final [$]',
constraint=lambda param: param.n1 < param.n2)
stats
```

CPU times: user 177 ms, sys: 16.1 ms, total: 193 ms Wall time: 2.41 s

Out[7]:

Start 2004-08-19 00:00:00 End 2013-03-01 00:00:00 Duration 3116 days 00:00:00 Exposure Time [%] 99.07 Equity Final [$] 103949.43 Equity Peak [$] 108327.72 Return [%] 939.49 Buy & Hold Return [%] 703.46 Return (Ann.) [%] 31.61 Volatility (Ann.) [%] 44.74 Sharpe Ratio 0.71 Sortino Ratio 1.49 Calmar Ratio 0.72 Max. Drawdown [%] -44.00 Avg. Drawdown [%] -6.14 Max. Drawdown Duration 690 days 00:00:00 Avg. Drawdown Duration 43 days 00:00:00 # Trades 153 Win Rate [%] 51.63 Best Trade [%] 61.56 Worst Trade [%] -19.78 Avg. Trade [%] 1.55 Max. Trade Duration 83 days 00:00:00 Avg. Trade Duration 21 days 00:00:00 Profit Factor 1.98 Expectancy [%] 1.98 SQN 1.60 _strategy SmaCross(n1=10,n... _equity_curve Eq... _trades Size Entry... dtype: object

We can look into `stats['_strategy']`

to access the Strategy *instance* and its optimal parameter values (10 and 15).

In [8]:

```
stats._strategy
```

Out[8]:

<Strategy SmaCross(n1=10,n2=15)>

In [9]:

```
bt.plot(plot_volume=False, plot_pl=False)
```