Library of Composable Base Strategies¶

This tutorial will show how to reuse composable base trading strategies that are part of backtesting.py software distribution. It is, henceforth, assumed you're already familiar with basic package usage.

We'll extend the same moving average cross-over strategy as in Quick Start User Guide, but we'll rewrite it as a vectorized signal strategy and add trailing stop-loss.

Again, we'll use our helper moving average function.

In [1]:
from backtesting.test import SMA
/opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
Loading BokehJS ...
"}}; function display_loaded(error = null) { const el = document.getElementById("d85a8806-7af2-4947-a530-458df8a08879"); if (el != null) { const html = (() => { if (typeof root.Bokeh === "undefined") { if (error == null) { return "BokehJS is loading ..."; } else { return "BokehJS failed to load."; } } else { const prefix = `BokehJS ${root.Bokeh.version}`; if (error == null) { return `${prefix} successfully loaded.`; } else { return `${prefix} encountered errors while loading and may not function as expected.`; } } })(); el.innerHTML = html; if (error != null) { const wrapper = document.createElement("div"); wrapper.style.overflow = "auto"; wrapper.style.height = "5em"; wrapper.style.resize = "vertical"; const content = document.createElement("div"); content.style.fontFamily = "monospace"; content.style.whiteSpace = "pre-wrap"; content.style.backgroundColor = "rgb(255, 221, 221)"; content.textContent = error.stack ?? error.toString(); wrapper.append(content); el.append(wrapper); } } else if (Date.now() < root._bokeh_timeout) { setTimeout(() => display_loaded(error), 100); } } function run_callbacks() { try { root._bokeh_onload_callbacks.forEach(function(callback) { if (callback != null) callback(); }); } finally { delete root._bokeh_onload_callbacks } console.debug("Bokeh: all callbacks have finished"); } function load_libs(css_urls, js_urls, callback) { if (css_urls == null) css_urls = []; if (js_urls == null) js_urls = []; root._bokeh_onload_callbacks.push(callback); if (root._bokeh_is_loading > 0) { console.debug("Bokeh: BokehJS is being loaded, scheduling callback at", now()); return null; } if (js_urls == null || js_urls.length === 0) { run_callbacks(); return null; } console.debug("Bokeh: BokehJS not loaded, scheduling load and callback at", now()); root._bokeh_is_loading = css_urls.length + js_urls.length; function on_load() { root._bokeh_is_loading--; if (root._bokeh_is_loading === 0) { console.debug("Bokeh: all BokehJS libraries/stylesheets loaded"); run_callbacks() } } function on_error(url) { console.error("failed to load " + url); } for (let i = 0; i < css_urls.length; i++) { const url = css_urls[i]; const element = document.createElement("link"); element.onload = on_load; element.onerror = on_error.bind(null, url); element.rel = "stylesheet"; element.type = "text/css"; element.href = url; console.debug("Bokeh: injecting link tag for BokehJS stylesheet: ", url); document.body.appendChild(element); } for (let i = 0; i < js_urls.length; i++) { const url = js_urls[i]; const element = document.createElement('script'); element.onload = on_load; element.onerror = on_error.bind(null, url); element.async = false; element.src = url; console.debug("Bokeh: injecting script tag for BokehJS library: ", url); document.head.appendChild(element); } }; function inject_raw_css(css) { const element = document.createElement("style"); element.appendChild(document.createTextNode(css)); document.body.appendChild(element); } const js_urls = ["https://cdn.bokeh.org/bokeh/release/bokeh-3.7.2.min.js", "https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.7.2.min.js", "https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.7.2.min.js", "https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.7.2.min.js", "https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.7.2.min.js"]; const css_urls = []; const inline_js = [ function(Bokeh) { Bokeh.set_log_level("info"); }, function(Bokeh) { } ]; function run_inline_js() { if (root.Bokeh !== undefined || force === true) { try { for (let i = 0; i < inline_js.length; i++) { inline_js[i].call(root, root.Bokeh); } } catch (error) {display_loaded(error);throw error; }if (force === true) { display_loaded(); }} else if (Date.now() < root._bokeh_timeout) { setTimeout(run_inline_js, 100); } else if (!root._bokeh_failed_load) { console.log("Bokeh: BokehJS failed to load within specified timeout."); root._bokeh_failed_load = true; } else if (force !== true) { const cell = $(document.getElementById("d85a8806-7af2-4947-a530-458df8a08879")).parents('.cell').data().cell; cell.output_area.append_execute_result(NB_LOAD_WARNING) } } if (root._bokeh_is_loading === 0) { console.debug("Bokeh: BokehJS loaded, going straight to plotting"); run_inline_js(); } else { load_libs(css_urls, js_urls, function() { console.debug("Bokeh: BokehJS plotting callback run at", now()); run_inline_js(); }); } }(window));

Part of this software distribution is backtesting.lib module that contains various reusable utilities for strategy development. Some of those utilities are composable base strategies we can extend and build upon.

We import and extend two of those strategies here:

  • SignalStrategy which decides upon a single signal vector whether to buy into a position, akin to vectorized backtesting engines, and
  • TrailingStrategy which automatically trails the current price with a stop-loss order some multiple of average true range (ATR) away.
In [2]:
import pandas as pd
from backtesting.lib import SignalStrategy, TrailingStrategy


class SmaCross(SignalStrategy,
               TrailingStrategy):
    n1 = 10
    n2 = 25
    
    def init(self):
        # In init() and in next() it is important to call the
        # super method to properly initialize the parent classes
        super().init()
        
        # Precompute the two moving averages
        sma1 = self.I(SMA, self.data.Close, self.n1)
        sma2 = self.I(SMA, self.data.Close, self.n2)
        
        # Where sma1 crosses sma2 upwards. Diff gives us [-1,0, *1*]
        signal = (pd.Series(sma1) > sma2).astype(int).diff().fillna(0)
        signal = signal.replace(-1, 0)  # Upwards/long only
        
        # Use 95% of available liquidity (at the time) on each order.
        # (Leaving a value of 1. would instead buy a single share.)
        entry_size = signal * .95
                
        # Set order entry sizes using the method provided by 
        # `SignalStrategy`. See the docs.
        self.set_signal(entry_size=entry_size)
        
        # Set trailing stop-loss to 2x ATR using
        # the method provided by `TrailingStrategy`
        self.set_trailing_sl(2)

Note, since the strategies in lib may require their own intialization and next-tick logic, be sure to always call super().init() and super().next() in your overridden methods.

Let's see how the example strategy fares on historical Google data.

In [3]:
from backtesting import Backtest
from backtesting.test import GOOG

bt = Backtest(GOOG, SmaCross, commission=.002)

bt.run()
bt.plot()
Backtest.run:   0%|          | 0/2147 [00:00
                                                       

Out[3]:
GridPlot(
id = 'p1396', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p1050', ...), 0, 0), (figure(id='p1150', ...), 1, 0), (figure(id='p1003', ...), 2, 0), (figure(id='p1208', ...), 3, 0), (figure(id='p1327', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p1395', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

Notice how managing risk with a trailing stop-loss secures our gains and limits our losses.

For other strategies of the sort, and other reusable utilities in general, see backtesting.lib module reference.

Learn more by exploring further examples or find more framework options in the full API reference.