First investing simulating with backtrader

backtrader.png

What is backtrader?

A feature-rich Python framework for backtesting and trading

backtrader allows you to focus on writing reusable trading strategies, indicators and analyzers instead of having to spend time building infrastructure.

Imagine that you hire a trader. In your first communication you describe to her/him, how much money you want to invest, your financial goals, where you want to invest, but also you design an investing strategy (which depends on your goal and investor profile).

Backtrader allows you to simulate the performance of your strategy!

Since I've been playing with yfinance to download and analyze stockmarket data, my workflow continues from there.

Let's start loading the libraries

import yfinance as yf
import pandas as pd
import backtrader as bt
from datetime import datetime

and defining (or reusing) the functions to retrieve, clean and enhance the data with financial features

def retrieve_clean_data(symbol: str, start_date: str, end_date: str):
    """
    Retrieve the stockmarket data:
    - symbol: Stock abbreviation, e.g. "AAPL", "MSFT"
    - start_date: in format YYYY-MM-DD
    - end_date: in format YYYY-MM-DD

    Returns a pandas DataFrame
    """
    data = yf.download(symbol, start=start_date, end=end_date)

    if isinstance(data.columns, pd.MultiIndex):
        data.columns = data.columns.get_level_values(0)

    data = data.rename(columns={
        "Open": "open",
        "High": "high",
        "Low": "low",
        "Close": "close",
        "Volume": "volume"
    })

    data.index = pd.to_datetime(data.index)
    return data.sort_index()


def feature_engineering(df: pd.DataFrame) -> pd.DataFrame:
    """
    Feature Engineering. Creates the features:
    - return: decimal equivalent of percentage change
    - ma10: moving average over 10 days
    - target: variable indicating raising (1) or falling (0)
      of the share price.
    """
    df['return'] = df['close'].pct_change()
    df['ma10'] = df['close'].rolling(window=10).mean()
    df["target"] = (df["return"] > 0).astype(int)
    df.dropna(inplace=True)
    return df

With the above, I shall be able of getting our data.

The next step is to define our investment strategy, which would be a backtrader.Strategy class.

class FirstStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        self.dataclose = self.datas[0].close

    def next(self):
        self.log('Close, %.2f' % self.dataclose[0])

        if self.dataclose[0] < self.dataclose[-1]:
            # current close less than previous close
            if self.dataclose[-1] < self.dataclose[-2]:
                # previous close less than the previous close
                # BUY, BUY, BUY!!! (with all possible default parameters)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()

How does it work? Our trader will go through the data (note that at a given point in the data, the trader denotes with index zero, [0], the current moment in the analysis) using the next method. In the next method, we decide to buy shares if the stock price (at close) has decline the last two days.

The __init__ method defines the dataclose instance using the closing prices from the data. The log method prints the current date of the simulation, together with a given text.

Finally, the main part of the simulation.

def main():
    symbol = "META"
    start_date = datetime(year=2021,
                          month=1,
                          day=1)
    end_date = datetime(year=2026,
                        month=2,
                        day=1)
    df = retrieve_clean_data(symbol, start_date, end_date)
    df = feature_engineering(df)
    # Convert DataFrame to backtrader data feed
    # Ensure the DataFrame has the required columns
    data = bt.feeds.PandasData(
        dataname=df,
        datetime=None,  # Adjust this to match your date column name
        open='open',
        high='high',
        low='low',
        close='close',
        volume='volume',
        openinterest=-1  # -1 indicates no open interest
    )

    # Now our bt Cerebro
    cerebro = bt.Cerebro()
    cerebro.addstrategy(FirstStrategy)
    cerebro.broker.set_cash(5_000)
    print(f"Initial portfolio value: {cerebro.broker.getvalue():.2f}")
    cerebro.adddata(data)
    cerebro.run()
    print(f"Final portfolio investment: {cerebro.broker.getvalue():.2f}")


if __name__ == "__main__":
    main()

The relevant thing is that we instantiate a Cerebro() (which plays the role of our broker!), add the strategy and set the amount of our investment.

Then, we run our simulation and retrieve the final value of our investment.

The result:

.
.
.
2026-01-02, BUY CREATE, 650.41
2026-01-05, Close, 658.79
2026-01-06, Close, 660.62
2026-01-07, Close, 648.69
2026-01-08, Close, 646.06
2026-01-08, BUY CREATE, 646.06
2026-01-09, Close, 653.06
2026-01-12, Close, 641.97
2026-01-13, Close, 631.09
2026-01-13, BUY CREATE, 631.09
2026-01-14, Close, 615.52
2026-01-14, BUY CREATE, 615.52
2026-01-15, Close, 620.80
2026-01-16, Close, 620.25
2026-01-20, Close, 604.12
2026-01-20, BUY CREATE, 604.12
2026-01-21, Close, 612.96
2026-01-22, Close, 647.63
2026-01-23, Close, 658.76
2026-01-26, Close, 672.36
2026-01-27, Close, 672.97
2026-01-28, Close, 668.73
2026-01-29, Close, 738.31
2026-01-30, Close, 716.50
Final portfolio investment: 12948.17

It worked! And the investment has increased 150% 🤑

BUT WAIT!!!

Do you remember the visualization of share prices?

yfinance-multi-data-plot.png

Note that the performance of the META shares is about 130%. I'll be returning to this point after further reading and analysis.

Author: Oscar Castillo-Felisola

Created: 2026-04-02 Thu 14:59