{ "cells": [ { "cell_type": "markdown", "id": "04574bc2", "metadata": {}, "source": [ "
" ] }, { "cell_type": "markdown", "id": "8c027594", "metadata": {}, "source": [ "This notebook demonstrates backtesting a trading strategy using Backtrader and OpenBB SDK for data acquisition. It outlines creating a trading strategy and running backtests to simulate performance over historical data. The code includes setting up a backtest, downloading data, defining strategy logic, and assessing results with performance metrics. This is useful for evaluating trading strategies' robustness and optimizing them for better risk-adjusted returns." ] }, { "cell_type": "code", "execution_count": null, "id": "3bee1431", "metadata": { "lines_to_next_cell": 1 }, "outputs": [], "source": [ "import datetime as dt\n", "import pandas as pd\n", "from openbb_terminal.sdk import openbb\n", "import quantstats as qs\n", "import backtrader as bt" ] }, { "cell_type": "markdown", "id": "c0e61059", "metadata": {}, "source": [ "Define a function to fetch stock data from OpenBB SDK and convert it for Backtrader use" ] }, { "cell_type": "code", "execution_count": null, "id": "132f6e66", "metadata": { "lines_to_next_cell": 1 }, "outputs": [], "source": [ "def openbb_data_to_bt_data(symbol, start_date, end_date):\n", " \"\"\"Fetch and convert stock data for Backtrader\n", " \n", " Parameters\n", " ----------\n", " symbol : str\n", " Stock symbol to fetch\n", " start_date : str\n", " Start date for data in 'YYYY-MM-DD' format\n", " end_date : str\n", " End date for data in 'YYYY-MM-DD' format\n", " \n", " Returns\n", " -------\n", " bt.feeds.YahooFinanceCSVData\n", " Formatted data for Backtrader\n", " \"\"\"\n", " \n", " # Fetch stock data from OpenBB SDK\n", " df = openbb.stocks.load(symbol, start_date=start_date, end_date=end_date)\n", "\n", " # Save data to a CSV file\n", " fn = f\"{symbol.lower()}.csv\"\n", " df.to_csv(fn)\n", " \n", " # Return data formatted for Backtrader\n", " return bt.feeds.YahooFinanceCSVData(\n", " dataname=fn,\n", " fromdate=dt.datetime.strptime(start_date, '%Y-%m-%d'),\n", " todate=dt.datetime.strptime(end_date, '%Y-%m-%d')\n", " )" ] }, { "cell_type": "markdown", "id": "6c3c0564", "metadata": {}, "source": [ "Determine the last day of a given month" ] }, { "cell_type": "code", "execution_count": null, "id": "cc17ebfc", "metadata": { "lines_to_next_cell": 1 }, "outputs": [], "source": [ "def last_day_of_month(any_day):\n", " \"\"\"Calculate the last day of the month\n", " \n", " Parameters\n", " ----------\n", " any_day : datetime.date\n", " Any date within the target month\n", " \n", " Returns\n", " -------\n", " int\n", " Last day of the month\n", " \"\"\"\n", " \n", " # Move to the next month and then back to the last day of the current month\n", " next_month = any_day.replace(day=28) + dt.timedelta(days=4)\n", " return (next_month - dt.timedelta(days=next_month.day)).day" ] }, { "cell_type": "markdown", "id": "64b04705", "metadata": {}, "source": [ "Define a trading strategy to execute monthly flows" ] }, { "cell_type": "code", "execution_count": null, "id": "0729cd4a", "metadata": { "lines_to_next_cell": 1 }, "outputs": [], "source": [ "class MonthlyFlows(bt.Strategy):\n", " \"\"\"Strategy to trade based on monthly flows\n", " \n", " Parameters\n", " ----------\n", " end_of_month : int, optional\n", " Day of the month to start buying (default is 23)\n", " start_of_month : int, optional\n", " Day of the month to start selling (default is 7)\n", " \n", " Attributes\n", " ----------\n", " order : bt.Order or None\n", " Current order in the market\n", " dataclose : bt.LineSeries\n", " Closing prices of the data feed\n", " \"\"\"\n", " \n", " params = (\n", " (\"end_of_month\", 23),\n", " (\"start_of_month\", 7),\n", " )\n", " \n", " def __init__(self):\n", " self.order = None\n", " self.dataclose = self.datas[0].close\n", " \n", " def notify_order(self, order):\n", " \"\"\"Handle order notifications\"\"\"\n", " \n", " # No more orders\n", " self.order = None \n", " \n", " def next(self):\n", " \"\"\"Execute strategy logic for each step in the backtest\"\"\"\n", " \n", " # Get today's date, day of month, and last day of current month\n", " dt_ = self.datas[0].datetime.date(0)\n", " dom = dt_.day\n", " ldm = last_day_of_month(dt_)\n", " \n", " # If an order is pending, exit\n", " if self.order:\n", " return\n", " \n", " # Check if we are in the market\n", " if not self.position:\n", " \n", " # We're in the first week of the month, sell\n", " if dom <= self.params.start_of_month:\n", " self.order = self.order_target_percent(target=-1)\n", " print(f\"Created SELL of {self.order.size} at {self.data_close[0]} on day {dom}\")\n", " \n", " # We're in the last week of the month, buy\n", " if dom >= self.params.end_of_month:\n", " self.order = self.order_target_percent(target=1)\n", " print(f\"Created BUY of {self.order.size} {self.data_close[0]} on day {dom}\")\n", " \n", " # We are not in the market\n", " else:\n", " \n", " # If we're long\n", " if self.position.size > 0:\n", " if not self.params.end_of_month <= dom <= ldm:\n", " print(f\"Created CLOSE of {self.position.size} at {self.data_close[0]} on day {dom}\")\n", " self.order = self.order_target_percent(target=0.0)\n", " \n", " # If we're short\n", " if self.position.size < 0:\n", " if not 1 <= dom <= self.params.start_of_month:\n", " print(f\"Created CLOSE of {self.position.size} at {self.data_close[0]} on day {dom}\")\n", " self.order = self.order_target_percent(target=0.0)" ] }, { "cell_type": "markdown", "id": "e8fbbc83", "metadata": {}, "source": [ "Run the strategy using Backtrader" ] }, { "cell_type": "code", "execution_count": null, "id": "b47c7a71", "metadata": {}, "outputs": [], "source": [ "data = openbb_data_to_bt_data(\"TLT\", start_date=\"2002-01-01\", end_date=\"2022-06-30\")" ] }, { "cell_type": "code", "execution_count": null, "id": "d53cbfee", "metadata": {}, "outputs": [], "source": [ "cerebro = bt.Cerebro(stdstats=False)\n", "cerebro.adddata(data)\n", "cerebro.broker.setcash(1000.0)\n", "cerebro.addstrategy(MonthlyFlows)\n", "cerebro.addobserver(bt.observers.Value)\n", "cerebro.addanalyzer(bt.analyzers.Returns, _name=\"returns\")\n", "cerebro.addanalyzer(bt.analyzers.TimeReturn, _name=\"time_return\")" ] }, { "cell_type": "code", "execution_count": null, "id": "94ea3665", "metadata": {}, "outputs": [], "source": [ "backtest_result = cerebro.run()" ] }, { "cell_type": "markdown", "id": "257cc57c", "metadata": {}, "source": [ "Convert backtest results into a pandas DataFrame" ] }, { "cell_type": "code", "execution_count": null, "id": "27a1ec64", "metadata": {}, "outputs": [], "source": [ "returns_dict = backtest_result[0].analyzers.time_return.get_analysis()\n", "returns_df = (\n", " pd.DataFrame(\n", " list(returns_dict.items()),\n", " columns=[\"date\", \"return\"]\n", " )\n", " .set_index(\"date\")\n", ")" ] }, { "cell_type": "markdown", "id": "7b6e5887", "metadata": {}, "source": [ "Fetch benchmark data for comparison" ] }, { "cell_type": "code", "execution_count": null, "id": "86c540be", "metadata": {}, "outputs": [], "source": [ "bench = openbb.stocks.load(\"TLT\", start_date=\"2002-01-01\", end_date=\"2022-06-30\")[\"Adj Close\"]" ] }, { "cell_type": "markdown", "id": "3c39d605", "metadata": {}, "source": [ "Assess results using QuantStats metrics" ] }, { "cell_type": "code", "execution_count": null, "id": "2dec6aa2", "metadata": {}, "outputs": [], "source": [ "qs.reports.metrics(\n", " returns_df,\n", " benchmark=bench,\n", " mode=\"full\"\n", ")" ] }, { "cell_type": "markdown", "id": "5b590090", "metadata": {}, "source": [ "PyQuant News is where finance practitioners level up with Python for quant finance, algorithmic trading, and market data analysis. Looking to get started? Check out the fastest growing, top-selling course to get started with Python for quant finance. For educational purposes. Not investment advise. Use at your own risk." ] } ], "metadata": { "jupytext": { "cell_metadata_filter": "-all", "main_language": "python", "notebook_metadata_filter": "-all" } }, "nbformat": 4, "nbformat_minor": 5 }