340 lines
10 KiB
Plaintext
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
|
|
}
|