Files
strategy-lab/to_explore/pyquantnews/28_FlowEffectsBacktrader.ipynb
David Brazda e3da60c647 daily update
2024-10-21 20:57:56 +02:00

340 lines
10 KiB
Plaintext

{
"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
}