{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "04574bc2",
   "metadata": {},
   "source": [
    "<div style=\"background-color:#000;\"><img src=\"pqn.png\"></img></div>"
   ]
  },
  {
   "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": [
    "<a href=\"https://pyquantnews.com/\">PyQuant News</a> 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 <a href=\"https://gettingstartedwithpythonforquantfinance.com/\">get started with Python for quant finance</a>. 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
}
