{ "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": [ " \n", "
\n", " \n", " Loading BokehJS ...\n", "
\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 \"
\\n\"+\n \"

\\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 \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\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} encountered errors 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 }