1
Fork 0
crypto_bot_training/Session_04/backtesting_overview.ipynb
2025-06-21 15:58:39 +02:00

592 lines
29 KiB
Text

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Backtesting.py Library Tutorial\n",
"\n",
"## Introduction to Backtesting.py\n",
"\n",
"Backtesting.py is a Python framework for inferring viability of trading strategies on historical data. It provides a simple, fast, and flexible way to backtest trading strategies with just a few lines of code.\n",
"\n",
"### Key Features:\n",
"- Simple and intuitive API\n",
"- Fast vectorized operations\n",
"- Built-in performance metrics\n",
"- Interactive charts and plots\n",
"- Support for various order types\n",
"- Portfolio optimization capabilities"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Installation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic Imports and Setup"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/alex/Dev/cryptoTraining/venv/lib/python3.13/site-packages/backtesting/_plotting.py:55: UserWarning: Jupyter Notebook detected. Setting Bokeh output to notebook. This may not work in Jupyter clients without JavaScript support, such as old IDEs. Reset with `backtesting.set_bokeh_output(notebook=False)`.\n",
" warnings.warn('Jupyter Notebook detected. '\n"
]
},
{
"data": {
"text/html": [
" <style>\n",
" .bk-notebook-logo {\n",
" display: block;\n",
" width: 20px;\n",
" height: 20px;\n",
" background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAOkSURBVDiNjZRtaJVlGMd/1/08zzln5zjP1LWcU9N0NkN8m2CYjpgQYQXqSs0I84OLIC0hkEKoPtiH3gmKoiJDU7QpLgoLjLIQCpEsNJ1vqUOdO7ppbuec5+V+rj4ctwzd8IIbbi6u+8f1539dt3A78eXC7QizUF7gyV1fD1Yqg4JWz84yffhm0qkFqBogB9rM8tZdtwVsPUhWhGcFJngGeWrPzHm5oaMmkfEg1usvLFyc8jLRqDOMru7AyC8saQr7GG7f5fvDeH7Ej8CM66nIF+8yngt6HWaKh7k49Soy9nXurCi1o3qUbS3zWfrYeQDTB/Qj6kX6Ybhw4B+bOYoLKCC9H3Nu/leUTZ1JdRWkkn2ldcCamzrcf47KKXdAJllSlxAOkRgyHsGC/zRday5Qld9DyoM4/q/rUoy/CXh3jzOu3bHUVZeU+DEn8FInkPBFlu3+nW3Nw0mk6vCDiWg8CeJaxEwuHS3+z5RgY+YBR6V1Z1nxSOfoaPa4LASWxxdNp+VWTk7+4vzaou8v8PN+xo+KY2xsw6une2frhw05CTYOmQvsEhjhWjn0bmXPjpE1+kplmmkP3suftwTubK9Vq22qKmrBhpY4jvd5afdRA3wGjFAgcnTK2s4hY0/GPNIb0nErGMCRxWOOX64Z8RAC4oCXdklmEvcL8o0BfkNK4lUg9HTl+oPlQxdNo3Mg4Nv175e/1LDGzZen30MEjRUtmXSfiTVu1kK8W4txyV6BMKlbgk3lMwYCiusNy9fVfvvwMxv8Ynl6vxoByANLTWplvuj/nF9m2+PDtt1eiHPBr1oIfhCChQMBw6Aw0UulqTKZdfVvfG7VcfIqLG9bcldL/+pdWTLxLUy8Qq38heUIjh4XlzZxzQm19lLFlr8vdQ97rjZVOLf8nclzckbcD4wxXMidpX30sFd37Fv/GtwwhzhxGVAprjbg0gCAEeIgwCZyTV2Z1REEW8O4py0wsjeloKoMr6iCY6dP92H6Vw/oTyICIthibxjm/DfN9lVz8IqtqKYLUXfoKVMVQVVJOElGjrnnUt9T9wbgp8AyYKaGlqingHZU/uG2NTZSVqwHQTWkx9hxjkpWDaCg6Ckj5qebgBVbT3V3NNXMSiWSDdGV3hrtzla7J+duwPOToIg42ChPQOQjspnSlp1V+Gjdged7+8UN5CRAV7a5EdFNwCjEaBR27b3W890TE7g24NAP/mMDXRWrGoFPQI9ls/MWO2dWFAar/xcOIImbbpA3zgAAAABJRU5ErkJggg==);\n",
" }\n",
" </style>\n",
" <div>\n",
" <a href=\"https://bokeh.org\" target=\"_blank\" class=\"bk-notebook-logo\"></a>\n",
" <span id=\"d0182c90-d4c8-447c-9d7f-7596d736d7bd\">Loading BokehJS ...</span>\n",
" </div>\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/javascript": "'use strict';\n(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\nconst JS_MIME_TYPE = 'application/javascript';\n const HTML_MIME_TYPE = 'text/html';\n const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n const CLASS_NAME = 'output_bokeh rendered_html';\n\n /**\n * Render data to the DOM node\n */\n function render(props, node) {\n const script = document.createElement(\"script\");\n node.appendChild(script);\n }\n\n /**\n * Handle when an output is cleared or removed\n */\n function handleClearOutput(event, handle) {\n function drop(id) {\n const view = Bokeh.index.get_by_id(id)\n if (view != null) {\n view.model.document.clear()\n Bokeh.index.delete(view)\n }\n }\n\n const cell = handle.cell;\n\n const id = cell.output_area._bokeh_element_id;\n const server_id = cell.output_area._bokeh_server_id;\n\n // Clean up Bokeh references\n if (id != null) {\n drop(id)\n }\n\n if (server_id !== undefined) {\n // Clean up Bokeh references\n const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n cell.notebook.kernel.execute(cmd_clean, {\n iopub: {\n output: function(msg) {\n const id = msg.content.text.trim()\n drop(id)\n }\n }\n });\n // Destroy server and session\n const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n cell.notebook.kernel.execute(cmd_destroy);\n }\n }\n\n /**\n * Handle when a new output is added\n */\n function handleAddOutput(event, handle) {\n const output_area = handle.output_area;\n const output = handle.output;\n\n // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n return\n }\n\n const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n // store reference to embed id on output_area\n output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n }\n if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n const bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n const script_attrs = bk_div.children[0].attributes;\n for (let i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n }\n\n function register_renderer(events, OutputArea) {\n\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n const toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[toinsert.length - 1]);\n element.append(toinsert);\n return toinsert\n }\n\n /* Handle when an output is cleared or removed */\n events.on('clear_output.CodeCell', handleClearOutput);\n events.on('delete.Cell', handleClearOutput);\n\n /* Handle when a new output is added */\n events.on('output_added.OutputArea', handleAddOutput);\n\n /**\n * Register the mime type and append_mime function with output_area\n */\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n /* Is output safe? */\n safe: true,\n /* Index of renderer in `output_area.display_order` */\n index: 0\n });\n }\n\n // register the mime type if in Jupyter Notebook environment and previously unregistered\n if (root.Jupyter !== undefined) {\n const events = require('base/js/events');\n const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n }\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"<div style='background-color: #fdd'>\\n\"+\n \"<p>\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"</p>\\n\"+\n \"<ul>\\n\"+\n \"<li>re-rerun `output_notebook()` to attempt to load from CDN again, or</li>\\n\"+\n \"<li>use INLINE resources instead, as so:</li>\\n\"+\n \"</ul>\\n\"+\n \"<code>\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"</code>\\n\"+\n \"</div>\"}};\n\n function display_loaded(error = null) {\n const el = document.getElementById(\"d0182c90-d4c8-447c-9d7f-7596d736d7bd\");\n if (el != null) {\n const html = (() => {\n if (typeof root.Bokeh === \"undefined\") {\n if (error == null) {\n return \"BokehJS is loading ...\";\n } else {\n return \"BokehJS failed to load.\";\n }\n } else {\n const prefix = `BokehJS ${root.Bokeh.version}`;\n if (error == null) {\n return `${prefix} successfully loaded.`;\n } else {\n return `${prefix} <b>encountered errors</b> while loading and may not function as expected.`;\n }\n }\n })();\n el.innerHTML = html;\n\n if (error != null) {\n const wrapper = document.createElement(\"div\");\n wrapper.style.overflow = \"auto\";\n wrapper.style.height = \"5em\";\n wrapper.style.resize = \"vertical\";\n const content = document.createElement(\"div\");\n content.style.fontFamily = \"monospace\";\n content.style.whiteSpace = \"pre-wrap\";\n content.style.backgroundColor = \"rgb(255, 221, 221)\";\n content.textContent = error.stack ?? error.toString();\n wrapper.append(content);\n el.append(wrapper);\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(() => display_loaded(error), 100);\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.7.3.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n try {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n\n } catch (error) {display_loaded(error);throw error;\n }if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"d0182c90-d4c8-447c-9d7f-7596d736d7bd\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));",
"application/vnd.bokehjs_load.v0+json": ""
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"from backtesting import Backtest, Strategy\n",
"from backtesting.lib import crossover\n",
"from backtesting.test import SMA, GOOG # Sample data and indicators\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# Display all columns in pandas\n",
"pd.set_option('display.max_columns', None)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Understanding the Data Format"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Data structure:\n",
" Open High Low Close Volume\n",
"2004-08-19 100.00 104.06 95.96 100.34 22351900\n",
"2004-08-20 101.01 109.08 100.50 108.31 11428600\n",
"2004-08-23 110.75 113.48 109.05 109.40 9137200\n",
"2004-08-24 111.24 111.60 103.57 104.87 7631300\n",
"2004-08-25 104.96 108.00 103.88 106.00 4598900\n",
"\n",
"Data shape: (2148, 5)\n",
"Date range: 2004-08-19 00:00:00 to 2013-03-01 00:00:00\n"
]
}
],
"source": [
"# Load sample Google stock data\n",
"data = GOOG.copy()\n",
"print(\"Data structure:\")\n",
"print(data.head())\n",
"print(f\"\\nData shape: {data.shape}\")\n",
"print(f\"Date range: {data.index[0]} to {data.index[-1]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Required Data Format:**\n",
"- DataFrame with DateTime index\n",
"- Columns: 'Open', 'High', 'Low', 'Close', 'Volume' (OHLCV)\n",
"- Data should be sorted chronologically"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Creating Your First Strategy"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class SMACrossStrategy(Strategy):\n",
" \"\"\"\n",
" Simple Moving Average Crossover Strategy\n",
" Buy when fast SMA crosses above slow SMA\n",
" Sell when fast SMA crosses below slow SMA\n",
" \"\"\"\n",
" \n",
" # Strategy parameters\n",
" fast_sma = 10\n",
" slow_sma = 30\n",
" \n",
" def init(self):\n",
" \"\"\"Initialize indicators\"\"\"\n",
" # Calculate moving averages\n",
" self.sma_fast = self.I(SMA, self.data.Close, self.fast_sma)\n",
" self.sma_slow = self.I(SMA, self.data.Close, self.slow_sma)\n",
" \n",
" def next(self):\n",
" \"\"\"Define trading logic for each bar\"\"\"\n",
" # Buy signal: fast SMA crosses above slow SMA\n",
" if crossover(self.sma_fast, self.sma_slow):\n",
" self.buy()\n",
" \n",
" # Sell signal: fast SMA crosses below slow SMA\n",
" elif crossover(self.sma_slow, self.sma_fast):\n",
" self.sell()\n",
"\n",
"print(\"Strategy class created successfully!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Running a Backtest"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Create and run backtest\n",
"bt = Backtest(data, SMACrossStrategy, cash=10000, commission=.002)\n",
"results = bt.run()\n",
"\n",
"# Display results\n",
"print(\"Backtest Results:\")\n",
"print(results)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Key Strategy Methods\n",
"\n",
"### The `init()` Method"
]
},
{
"cell_type": "code",
"execution_count": 260,
"metadata": {},
"outputs": [],
"source": [
"def init(self):\n",
" \"\"\"\n",
" Called once at the beginning of backtesting\n",
" Used to:\n",
" - Initialize indicators using self.I()\n",
" - Set up any variables needed throughout the strategy\n",
" - Prepare data transformations\n",
" \"\"\"\n",
" self.sma = self.I(SMA, self.data.Close, 20)\n",
" # self.rsi = self.I(lambda x, n: RSI(x, n), self.data.Close, 14)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### The `next()` Method"
]
},
{
"cell_type": "code",
"execution_count": 261,
"metadata": {},
"outputs": [],
"source": [
"def next(self):\n",
" \"\"\"\n",
" Called for each bar of data\n",
" Contains the main trading logic\n",
" Available methods:\n",
" - self.buy() / self.sell() - Market orders\n",
" - self.position - Current position info\n",
" - self.data - Current price data\n",
" \"\"\"\n",
" if self.data.Close[-1] > self.sma[-1]:\n",
" self.buy()\n",
" elif self.data.Close[-1] < self.sma[-1]:\n",
" self.sell()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Position Management"
]
},
{
"cell_type": "code",
"execution_count": 262,
"metadata": {},
"outputs": [],
"source": [
"class PositionSizingStrategy(Strategy):\n",
" def init(self):\n",
" self.sma = self.I(SMA, self.data.Close, 20)\n",
" \n",
" def next(self):\n",
" # Check current position\n",
" if not self.position: # No position\n",
" if self.data.Close[-1] > self.sma[-1]:\n",
" # Buy with specific size (50% of equity)\n",
" self.buy(size=0.5)\n",
" \n",
" elif self.position.is_long: # Long position\n",
" if self.data.Close[-1] < self.sma[-1]:\n",
" self.position.close() # Close position\n",
" \n",
" # Access position information\n",
" if self.position:\n",
" print(f\"Position size: {self.position.size}\")\n",
" print(f\"Position value: {self.position.pl}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Advanced Order Types"
]
},
{
"cell_type": "code",
"execution_count": 263,
"metadata": {},
"outputs": [],
"source": [
"class AdvancedOrderStrategy(Strategy):\n",
" def init(self):\n",
" self.sma = self.I(SMA, self.data.Close, 20)\n",
" \n",
" def next(self):\n",
" current_price = self.data.Close[-1]\n",
" \n",
" # Market order\n",
" if current_price > self.sma[-1] and not self.position:\n",
" self.buy()\n",
" \n",
" # Limit order (buy below current price)\n",
" elif not self.position:\n",
" self.buy(limit=current_price * 0.98)\n",
" \n",
" # Stop-loss order\n",
" elif self.position.is_long:\n",
" self.sell(stop=current_price * 0.95)\n",
" \n",
" # Take-profit order\n",
" elif self.position.is_long:\n",
" self.sell(limit=current_price * 1.10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Performance Metrics"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Run backtest and analyze results\n",
"bt = Backtest(data, SMACrossStrategy, cash=10000, commission=.002)\n",
"results = bt.run()\n",
"\n",
"print(\"Key Performance Metrics:\")\n",
"print(f\"Total Return: {results['Return [%]']:.2f}%\")\n",
"print(f\"Buy & Hold Return: {results['Buy & Hold Return [%]']:.2f}%\")\n",
"print(f\"Max Drawdown: {results['Max. Drawdown [%]']:.2f}%\")\n",
"print(f\"Sharpe Ratio: {results['Sharpe Ratio']:.2f}\")\n",
"print(f\"Number of Trades: {results['# Trades']}\")\n",
"print(f\"Win Rate: {results['Win Rate [%]']:.2f}%\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualization"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Plot backtest results\n",
"bt.plot(show_legend=False, resample=False, plot_pl=True, relative_equity=False)\n",
"\n",
"# The plot includes:\n",
"# - Price chart with entry/exit points\n",
"# - Portfolio equity curve\n",
"# - Drawdown periods\n",
"# - Trade markers"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Parameter Optimization"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Optimize strategy parameters\n",
"optimization_results = bt.optimize(\n",
" fast_sma=range(5, 20, 2), # Test fast SMA from 5 to 18\n",
" slow_sma=range(20, 50, 5), # Test slow SMA from 20 to 45\n",
" maximize='Sharpe Ratio', # Optimization objective\n",
" constraint=lambda p: p.fast_sma < p.slow_sma # Constraint\n",
")\n",
"\n",
"print(\"Optimal Parameters:\")\n",
"print(optimization_results)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Complete Example: RSI Strategy"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def RSI(close, period=14):\n",
" \"\"\"Calculate RSI indicator - fixed for backtesting library\"\"\"\n",
" # Convert to pandas Series if it's an _Array object\n",
" if hasattr(close, '__array__'):\n",
" close = pd.Series(close)\n",
" \n",
" delta = close.diff()\n",
" gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()\n",
" loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()\n",
" rs = gain / loss\n",
" rsi = 100 - (100 / (1 + rs))\n",
" \n",
" # Return as numpy array for backtesting compatibility\n",
" return rsi.values\n",
"\n",
"class RSIStrategy(Strategy):\n",
" \"\"\"\n",
" RSI Mean Reversion Strategy\n",
" Buy when RSI < 30 (oversold)\n",
" Sell when RSI > 70 (overbought)\n",
" \"\"\"\n",
" \n",
" rsi_period = 14\n",
" oversold = 30\n",
" overbought = 70\n",
" \n",
" def init(self):\n",
" self.rsi = self.I(RSI, self.data.Close, self.rsi_period)\n",
" \n",
" def next(self):\n",
" # Buy when oversold and no position\n",
" if self.rsi[-1] < self.oversold and not self.position:\n",
" self.buy()\n",
" \n",
" # Sell when overbought and have long position\n",
" elif self.rsi[-1] > self.overbought and self.position.is_long:\n",
" self.position.close()\n",
"\n",
"# Run RSI strategy\n",
"bt_rsi = Backtest(data, RSIStrategy, cash=10000, commission=.002)\n",
"results_rsi = bt_rsi.run()\n",
"\n",
"print(\"RSI Strategy Results:\")\n",
"print(f\"Return: {results_rsi['Return [%]']:.2f}%\")\n",
"print(f\"Sharpe Ratio: {results_rsi['Sharpe Ratio']:.2f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Best Practices and Tips\n",
"\n",
"### 1. Data Quality"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Always check for data issues\n",
"def validate_data(df):\n",
" print(f\"Missing values: {df.isnull().sum().sum()}\")\n",
" print(f\"Duplicate dates: {df.index.duplicated().sum()}\")\n",
" \n",
" # Check for unrealistic price movements\n",
" returns = df['Close'].pct_change()\n",
" extreme_moves = returns[abs(returns) > 0.2]\n",
" if len(extreme_moves) > 0:\n",
" print(f\"Extreme price movements detected: {len(extreme_moves)}\")\n",
"\n",
"validate_data(data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2. Avoiding Look-Ahead Bias"
]
},
{
"cell_type": "code",
"execution_count": 269,
"metadata": {},
"outputs": [],
"source": [
"# WRONG - Uses future data\n",
"def bad_strategy_next(self):\n",
" if self.data.Close[-1] > self.data.Close[0:].mean(): # Uses all data\n",
" self.buy()\n",
"\n",
"# CORRECT - Only uses past data\n",
"def good_strategy_next(self):\n",
" if self.data.Close[-1] > self.data.Close[:-1].mean(): # Excludes current bar\n",
" self.buy()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 3. Transaction Costs"
]
},
{
"cell_type": "code",
"execution_count": 270,
"metadata": {},
"outputs": [],
"source": [
"# Always include realistic transaction costs\n",
"bt = Backtest(\n",
" data, \n",
" Strategy, \n",
" cash=10000,\n",
" commission=0.002, # 0.2% per trade\n",
" trade_on_close=True, # More realistic execution\n",
" exclusive_orders=True # Prevent multiple orders per bar\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Common Pitfalls to Avoid\n",
"\n",
"1. **Overfitting**: Don't optimize too many parameters\n",
"2. **Look-ahead bias**: Only use historical data available at each point\n",
"3. **Survivorship bias**: Include delisted stocks in your universe\n",
"4. **Ignoring transaction costs**: Always include realistic fees\n",
"5. **In-sample testing**: Always test on out-of-sample data\n",
"\n",
"## Next Steps\n",
"\n",
"- Explore more complex strategies (multi-asset, machine learning)\n",
"- Learn about walk-forward analysis\n",
"- Study risk management techniques\n",
"- Implement portfolio-level backtesting\n",
"- Use alternative data sources\n",
"\n",
"## Resources\n",
"\n",
"- [Official Documentation](https://kernc.github.io/backtesting.py/)\n",
"- [GitHub Repository](https://github.com/kernc/backtesting.py)\n",
"- [Example Strategies](https://kernc.github.io/backtesting.py/doc/examples/)\n",
"\n",
"Happy backtesting! 📈"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}