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

10 KiB

No description has been provided for this image

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.

In [ ]:
import datetime as dt
import pandas as pd
from openbb_terminal.sdk import openbb
import quantstats as qs
import backtrader as bt

Define a function to fetch stock data from OpenBB SDK and convert it for Backtrader use

In [ ]:
def openbb_data_to_bt_data(symbol, start_date, end_date):
    """Fetch and convert stock data for Backtrader
    
    Parameters
    ----------
    symbol : str
        Stock symbol to fetch
    start_date : str
        Start date for data in 'YYYY-MM-DD' format
    end_date : str
        End date for data in 'YYYY-MM-DD' format
    
    Returns
    -------
    bt.feeds.YahooFinanceCSVData
        Formatted data for Backtrader
    """
    
    # Fetch stock data from OpenBB SDK
    df = openbb.stocks.load(symbol, start_date=start_date, end_date=end_date)

    # Save data to a CSV file
    fn = f"{symbol.lower()}.csv"
    df.to_csv(fn)
    
    # Return data formatted for Backtrader
    return bt.feeds.YahooFinanceCSVData(
        dataname=fn,
        fromdate=dt.datetime.strptime(start_date, '%Y-%m-%d'),
        todate=dt.datetime.strptime(end_date, '%Y-%m-%d')
    )

Determine the last day of a given month

In [ ]:
def last_day_of_month(any_day):
    """Calculate the last day of the month
    
    Parameters
    ----------
    any_day : datetime.date
        Any date within the target month
    
    Returns
    -------
    int
        Last day of the month
    """
    
    # Move to the next month and then back to the last day of the current month
    next_month = any_day.replace(day=28) + dt.timedelta(days=4)
    return (next_month - dt.timedelta(days=next_month.day)).day

Define a trading strategy to execute monthly flows

In [ ]:
class MonthlyFlows(bt.Strategy):
    """Strategy to trade based on monthly flows
    
    Parameters
    ----------
    end_of_month : int, optional
        Day of the month to start buying (default is 23)
    start_of_month : int, optional
        Day of the month to start selling (default is 7)
    
    Attributes
    ----------
    order : bt.Order or None
        Current order in the market
    dataclose : bt.LineSeries
        Closing prices of the data feed
    """
    
    params = (
        ("end_of_month", 23),
        ("start_of_month", 7),
    )
    
    def __init__(self):
        self.order = None
        self.dataclose = self.datas[0].close
        
    def notify_order(self, order):
        """Handle order notifications"""
        
        # No more orders
        self.order = None    
    
    def next(self):
        """Execute strategy logic for each step in the backtest"""
        
        # Get today's date, day of month, and last day of current month
        dt_ = self.datas[0].datetime.date(0)
        dom = dt_.day
        ldm = last_day_of_month(dt_)
        
        # If an order is pending, exit
        if self.order:
            return
        
        # Check if we are in the market
        if not self.position:
            
            # We're in the first week of the month, sell
            if dom <= self.params.start_of_month:
                self.order = self.order_target_percent(target=-1)
                print(f"Created SELL of {self.order.size} at {self.data_close[0]} on day {dom}")
            
            # We're in the last week of the month, buy
            if dom >= self.params.end_of_month:
                self.order = self.order_target_percent(target=1)
                print(f"Created BUY of {self.order.size} {self.data_close[0]} on day {dom}")
        
        # We are not in the market
        else:
            
            # If we're long
            if self.position.size > 0:
                if not self.params.end_of_month <= dom <= ldm:
                    print(f"Created CLOSE of {self.position.size} at {self.data_close[0]} on day {dom}")
                    self.order = self.order_target_percent(target=0.0)
            
            # If we're short
            if self.position.size < 0:
                if not 1 <= dom <= self.params.start_of_month:
                    print(f"Created CLOSE of {self.position.size} at {self.data_close[0]} on day {dom}")
                    self.order = self.order_target_percent(target=0.0)

Run the strategy using Backtrader

In [ ]:
data = openbb_data_to_bt_data("TLT", start_date="2002-01-01", end_date="2022-06-30")
In [ ]:
cerebro = bt.Cerebro(stdstats=False)
cerebro.adddata(data)
cerebro.broker.setcash(1000.0)
cerebro.addstrategy(MonthlyFlows)
cerebro.addobserver(bt.observers.Value)
cerebro.addanalyzer(bt.analyzers.Returns, _name="returns")
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name="time_return")
In [ ]:
backtest_result = cerebro.run()

Convert backtest results into a pandas DataFrame

In [ ]:
returns_dict = backtest_result[0].analyzers.time_return.get_analysis()
returns_df = (
    pd.DataFrame(
        list(returns_dict.items()),
        columns=["date", "return"]
    )
    .set_index("date")
)

Fetch benchmark data for comparison

In [ ]:
bench = openbb.stocks.load("TLT", start_date="2002-01-01", end_date="2022-06-30")["Adj Close"]

Assess results using QuantStats metrics

In [ ]:
qs.reports.metrics(
    returns_df,
    benchmark=bench,
    mode="full"
)

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.