Skip to content
All library documents

Preparing Custom Data for Lumibot’s Pandas Backtester

Article Lumibot

Summary

This guide explains how advanced users can run Lumibot backtests with their own historical data. It supports intraday and daily testing and describes assets including stocks, futures, cryptocurrency, and foreign exchange. Input data must be converted into a specific time-indexed dataframe with open, high, low, close, and volume fields; the timestamps must be timezone-aware. Each dataset is associated with an Asset and Data object, then supplied to the Pandas backtester with a start and end date. The guide notes that the default timezone is America/New_York and that minute and daily data use matching timestep settings.

The examples show loading data from CSV, assembling the objects, and running a strategy, with optional environment variables for the date range. The guide also summarizes options expiration handling: equity and ETF options are physically settled, index options are cash settled, and out-of-the-money contracts expire. It frames this backtester as flexible but harder to use than simpler data-source backtesters. These are setup instructions, not evidence of strategy performance; data quality, execution assumptions, and other backtest limitations are not evaluated.

Key ideas

  • The Pandas backtester accepts user-supplied data in a required dataframe format with timestamps and OHLCV fields.
  • Each dataset must be linked to an Asset and Data object with a timestep matching minute or daily data.
  • The tool supports several asset classes and uses America/New_York as its default timezone.
  • Backtest dates can be supplied directly or through environment variables.
  • Options expiration is modeled with physical settlement for equity and ETF options, cash settlement for index options, and expiration for out-of-the-money contracts.

Tags

Full text
# Do something here


.. _backtesting.pandas:

Pandas (CSV or other data)
===================================

.. meta::
   :description: NOTE: Please ensure you have installed the latest lumibot version using pip install lumibot --upgrade before proceeding as there have been some major changes to the.

**NOTE: Please ensure you have installed the latest lumibot version using ``pip install lumibot --upgrade`` before proceeding as there have been some major changes to the backtesting module in the latest version.**

**For most situations, you will want to use the Polygon backtester or the Yahoo backtester instead, they are much easier to use and get started with. The Pandas backtester is intended for advanced users who have their own data and want to use it with Lumibot.**

Pandas backtester is named after the python dataframe library because the user must provide a strictly formatted dataframe. You can use any csv, parquet, database data, etc that you wish, but Lumibot will only accept one format of dataframe.

Pandas backtester allows for intra-day and inter-day backtesting. Time frames for raw data are 1 minute and 1 day.

Additionally, with Pandas backtester, it is possible to backtest stocks, stock-like securities, futures contracts, crypto and FOREX.

Pandas backtester is the most flexible backtester in Lumibot, but it is also the most difficult to use. It is intended for advanced users who have their own data and want to use it with Lumibot.

Start by importing the Pandas backtester as follows:

.. code-block:: python

    from lumibot.backtesting import PandasDataBacktesting, BacktestingBroker

Next, create your Strategy class as you normally would. You can use any of the built-in indicators or create your own. You can also use any of the built-in order types or create your own.

.. code-block:: python

    from lumibot.strategies import Strategy

    class MyStrategy(Strategy):
        def on_trading_iteration(self):
            # Do something here

Lumibot will start trading at 0000 hrs for the first date and up to 2359 hrs for the last. This is considered to be in the default time zone of Lumibot unless changed. This is America/New York (aka: EST)

Pandas backtester will receive a dataframe in the following format:

.. code-block:: python

    Index: 
    name: datetime
    type: datetime64

    Columns: 
    names: ['open', 'high', 'low', 'close', 'volume']
    types: float

Your dataframe should look like this:

.. csv-table:: Example Dataframe
   :header: "datetime", "open", "high", "low", "close", "volume"

    2020-01-02 09:31:00,	3237.00,	3234.75,	3235.25,	3237.00,	16808
    2020-01-02 09:32:00,	3237.00,	3234.00,	3237.00,	3234.75,	10439
    2020-01-02 09:33:00,	3235.50,	3233.75,	3234.50,	3234.75,	8203
    ...,	...,	...,	...,	...,	...
    2020-04-22 15:56:00,	2800.75,	2796.25,	2800.75,	2796.25,	8272
    2020-04-22 15:57:00,	2796.50,	2794.00,	2796.25,	2794.00,	7440
    2020-04-22 15:58:00,	2794.75,	2793.00,	2794.25,	2793.25,	7569

Other formats for dataframes will not work.

You can download an example CSV using the yfinance library as follows:

.. code-block:: python

    import yfinance as yf

    # Download minute data for the last 5 days for AAPL
    data = yf.download("AAPL", period="5d", interval="1m")

    # Save the data to a CSV file
    data.to_csv("AAPL.csv")

The data objects will be collected in a dictionary called ``pandas_data`` using the asset as key and the data object as value. Subsequent assets + data can be added and then the dictionary can be passed into Lumibot for backtesting.

One of the important differences when using Pandas backtester is that you must use an ``Asset`` object for each data csv file loaded. You may not use a ``symbol`` as you might in Yahoo backtester.

For example, if you have a CSV file for AAPL, you must create an ``Asset`` object for AAPL and then pass that into the ``Data`` object.

.. code-block:: python

    from lumibot.entities import Asset

    asset = Asset(
        symbol="AAPL",
        asset_type=Asset.AssetType.STOCK,
    )

Next step will be to load the dataframe from csv.

.. code-block:: python

    import pandas as pd

    # The names of the columns are important. Also important that all dates in the 
    # dataframe are time aware before going into lumibot. 
    df = pd.read_csv("AAPL.csv")

Third we make a data object for the asset. The data object must have at least the asset object, the dataframe, and the timestep. The timestep can be either ``minute`` or ``day``. If you are using minute data, you must have a ``minute`` timestep. If you are using daily data, you must have a ``day`` timestep.

.. code-block:: python

    from lumibot.entities import Data

    data = Data(
        asset,
        df,
        timestep="minute",
    )

Next, we create or add to the dictionary that will be passed into Lumibot.

.. code-block:: python

    pandas_data = {
        asset: data
    }

Finally, we can pass the ``pandas_data`` dictionary into Lumibot and run the backtest.

.. code-block:: python

    # Run the backtesting
    trader = Trader(backtest=True)
    data_source = PandasDataBacktesting(
        pandas_data=pandas_data,
        datetime_start=backtesting_start,
        datetime_end=backtesting_end,
    )
    broker = BacktestingBroker(data_source)
    strat = MyStrategy(
        broker=broker,
        budget=100000,
    )
    trader.add_strategy(strat)
    trader.run_all()

In Summary
----------

Putting all of this together, and adding in budget and strategy information, the code would look like the following:

Getting the data would look something like this (using yfinance to download, but you can use any data source you wish):

.. code-block:: python

    import yfinance as yf

    # Download minute data for the last 5 days for AAPL
    data = yf.download("AAPL", period="5d", interval="1m")

    # Save the data to a CSV file
    data.to_csv("AAPL.csv")

Then, the strategy and backtesting code might look like this:

.. code-block:: python

    import pandas as pd
    from lumibot.backtesting import BacktestingBroker, PandasDataBacktesting
    from lumibot.entities import Asset, Data
    from lumibot.strategies import Strategy

    # A simple strategy that buys AAPL on the first day
    class MyStrategy(Strategy):
        def on_trading_iteration(self):
            if self.first_iteration:
                order = self.create_order("AAPL", 100, "buy")
                self.submit_order(order)

    # Read the data from the CSV file (in this example you must have a file named "AAPL.csv"
    # in a folder named "data" in the same directory as this script)
    df = pd.read_csv("AAPL.csv")
    asset = Asset(symbol="AAPL", asset_type=Asset.AssetType.STOCK)

    pandas_data = {
        asset: Data(asset, df, timestep="minute"),
    }

    backtesting_start = pandas_data[asset].datetime_start  # or datetime(2025, 1, 1)
    backtesting_end = pandas_data[asset].datetime_end      # or datetime(2025, 1, 31)

    # Run the backtest
    result = MyStrategy.run_backtest(
        PandasDataBacktesting,
        backtesting_start,
        backtesting_end,
        pandas_data=pandas_data,
    )

Options Expiration Settlement Policy
------------------------------------

Pandas backtesting follows broker-style defaults for options expiration:

- Equity/ETF options are physically settled at expiration.
  - Short in-the-money contracts are modeled as ``assigned``.
  - Long in-the-money contracts are modeled as ``exercised`` when account constraints allow delivery.
- Index options are cash settled at intrinsic value and exported as ``cash_settled``.
- Out-of-the-money contracts expire as ``expired``.

These statuses are written to the trade artifacts so downstream systems can distinguish
normal fills from expiration lifecycle events.

Optional: Environment Variables
-------------------------------
If you prefer not to specify `backtesting_start` and `backtesting_end` in code, you can set the following environment variables, and LumiBot will automatically detect them:

.. list-table::
   :header-rows: 1
   :widths: 20 60 20

   * - **Variable**
     - **Description**
     - **Example**
   * - IS_BACKTESTING
     - (Optional) Read only by startup code that checks it. It does not change a ``backtest()`` call into a broker run; see :doc:`strategy_run_modes`.
     - False
   * - BACKTESTING_START
     - (Optional) Start date (YYYY-MM-DD).
     - 2025-01-01
   * - BACKTESTING_END
     - (Optional) End date (YYYY-MM-DD).
     - 2025-01-31

Below is **the full code** that relies *entirely on environment variables*:

.. code-block:: python

    import pandas as pd
    from lumibot.backtesting import BacktestingBroker, PandasDataBacktesting
    from lumibot.entities import Asset, Data
    from lumibot.strategies import Strategy

    class MyStrategy(Strategy):
        def on_trading_iteration(self):
            if self.first_iteration:
                order = self.create_order("AAPL", 100, "buy")
                self.submit_order(order)

    if __name__ == "__main__":
        df = pd.read_csv("AAPL.csv")
        asset = Asset(symbol="AAPL", asset_type=Asset.AssetType.STOCK)
        pandas_data = {
            asset: Data(asset, df, timestep="minute"),
        }

        # We do not specify any backtesting_start or backtesting_end here.
        # LumiBot will look for them in environment variables (BACKTESTING_START, BACKTESTING_END).
        result = MyStrategy.run_backtest(
            PandasDataBacktesting,
            pandas_data=pandas_data
        )

Shown in full with attribution under the source's licence. Licence: GPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.