Backtesting Stocks and Options with ThetaData Historical Data
Summary
The document explains how to use ThetaData as a historical data source for LumiBot backtests covering stocks and options, as well as other asset types. It supports minute and daily bars directly; hourly bars can be built from minute data. Downloaded data is cached locally, which can speed later runs, and different sources can be assigned to stock and option assets.
It also describes pricing details relevant to options: last-trade prices may be stale, while bid and ask quotes can support midpoint marks and quote-based fills. Where daily option history is missing, an intraday quote snapshot may provide a mark for daily-cadence backtests. Coverage checks for regular-session minute data account for the last session close. These data-handling features do not establish strategy profitability, and the document gives no comparative performance evidence; users still depend on provider coverage, credentials, and the assumptions of their fill and valuation models.
Key ideas
- ThetaData supplies historical stock and options data for LumiBot backtests at minute and daily intervals, with hourly bars aggregated from minute data.
- Locally cached downloads can reduce the time needed for subsequent backtests.
- Option last-trade prices may be stale, so bid and ask quotes can provide a more current midpoint mark.
- Intraday quote snapshots may fill gaps in daily option history for mark-to-market valuation.
- Backtest data coverage and results depend on provider availability and pricing assumptions.
Tags
Full text
# backtesting.thetadata
.. _backtesting.thetadata:
ThetaData Options and Stock Backtesting with LumiBot
====================================================
.. meta::
:description: Backtest options, stocks, indexes, forex, and crypto with ThetaData and LumiBot using minute, hourly, or daily historical data.
.. important::
**Sign up at** `ThetaData <https://www.thetadata.net/>`_. **Use the promo code ``BotSpot10`` at checkout for 10% off the first order—ThetaData tracks the code so they can credit BotSpot for the referral.**
ThetaData backtester allows for flexible and robust backtesting. It uses the thetadata API to fetch pricing data for stocks, options, forex, and cryptocurrencies. This backtester simplifies the process of getting pricing data; simply use the thetadata DataSource and it will automatically fetch pricing data when you call `get_last_price()` or `get_historical_prices()`.
Supported timesteps:
- ``"minute"`` and ``"day"`` are supported directly.
- ``"hour"`` (and multi-hour like ``"4h"``) is supported when minute bars are available; LumiBot aggregates minute bars into hourly bars for you.
Use this data source with the :doc:`AI iron condor <agents_example_iron_condor_ai_trading_bot>`
options tutorial or the :doc:`AI opening range breakout
<agents_example_opening_range_breakout_ai_trading_bot>` intraday stock tutorial.
As of this writing, ThetaData provides historical data for free. If you pay for an API you can get many years of data and the backtesting will download data much faster because it won't be rate limited.
This backtesting method caches the data on your computer making it faster for subsequent backtests. So even if it takes a bit of time the first time, the following backtests will be much faster.
Session close coverage (index/stock minute bars)
------------------------------------------------
For some assets, minute-bar feeds are **regular-session (RTH) bounded** (for example, U.S. indexes such as SPX typically have bars from ~09:30 to ~16:00 ET).
In those cases, LumiBot treats **coverage through the last trading session close at or before the requested end** as “complete” for backtest cache reuse (holiday/weekend/early-close safe). This prevents pathological behavior where a backtest end date represented as midnight (or UTC-midnight) would otherwise imply a requirement for bars through ``23:59`` even though the provider does not publish them.
Options pricing and mark-to-market (important)
----------------------------------------------
Options can be illiquid: they may not trade every day, so the **last traded price** can be stale. In live brokers, mark-to-market and many fill models rely on **NBBO quotes** (bid/ask) when available.
LumiBot follows that model:
- ``get_last_price()`` is trade-derived (last trade / bar close) and can be stale for options.
- ``get_quote()`` exposes bid/ask and allows LumiBot to derive a **mark** (mid) for option valuation and quote-based fills.
In some historical option series, ThetaData can have gaps in **EOD/day** option history even when **intraday quote history** exists. In daily-cadence backtests, LumiBot may fall back to an intraday quote snapshot to compute an option mark when day/EOD pricing is unavailable. This avoids “unpriceable” option positions and keeps daily option backtests broker-like.
To use this feature, you need to obtain an API key from thetadata, which is free and you can get in the Dashboard after you have created an account. You must then replace `username` and `password` with your own.
Start by importing the ThetaDataBacktesting, BacktestingBroker and other necessary classes:
.. code-block:: python
from datetime import datetime
from lumibot.backtesting import BacktestingBroker, ThetaDataBacktesting
from lumibot.strategies import Strategy
from lumibot.traders import Trader
Next, create a strategy class that inherits from the Strategy class. This class will be used to define the strategy that will be backtested. In this example, we will create a simple strategy that buys a stock on the first iteration and holds it until the end of the backtest. The strategy will be initialized with a symbol parameter that will be used to determine which stock to buy. The initialize method will be used to set the sleeptime to 1 day. The on_trading_iteration method will be used to buy the stock on the first iteration. The strategy will be run from 2025-01-01 to 2025-01-31.
.. code-block:: python
class MyStrategy(Strategy):
parameters = {
"symbol": "AAPL",
}
def initialize(self):
self.sleeptime = "1D"
def on_trading_iteration(self):
if self.first_iteration:
symbol = self.parameters["symbol"]
price = self.get_last_price(symbol)
qty = self.portfolio_value / price
order = self.create_order(symbol, quantity=qty, side="buy")
self.submit_order(order)
Set the start and end dates for the backtest:
.. code-block:: python
backtesting_start = datetime(2025, 1, 1)
backtesting_end = datetime(2025, 1, 31)
Finally, run the backtest:
.. code-block:: python
result = MyStrategy.run_backtest(
ThetaDataBacktesting,
backtesting_start,
backtesting_end,
benchmark_asset="SPY")
Alternatively, if you want to use polygon for stock data, and thetadata for option data,
you can pass a dictionary with both data sources as the first argument to `run_backtest`.
In this example, we are using thetadata for both stock and option data.
.. code-block:: python
result = MyStrategy.run_backtest(
{"STOCK":PolygonDataBacktesting,"OPTION":ThetaDataBacktesting},
backtesting_start,
backtesting_end,
benchmark_asset="SPY")
Here's the full code (with explicit dates):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Make sure to replace the `THETADATA_USERNAME` and `THETADATA_PASSWORD` (it's free)**
.. code-block:: python
from datetime import datetime
from lumibot.backtesting import BacktestingBroker, ThetaDataBacktesting
from lumibot.strategies import Strategy
from lumibot.traders import Trader
class MyStrategy(Strategy):
parameters = {
"symbol": "AAPL",
}
def initialize(self):
self.sleeptime = "1D"
def on_trading_iteration(self):
if self.first_iteration:
symbol = self.parameters["symbol"]
price = self.get_last_price(symbol)
qty = self.portfolio_value / price
order = self.create_order(symbol, quantity=qty, side="buy")
self.submit_order(order)
if __name__ == "__main__":
backtesting_start = datetime(2025, 1, 1)
backtesting_end = datetime(2025, 1, 31)
result = MyStrategy.run_backtest(
ThetaDataBacktesting,
backtesting_start,
backtesting_end,
benchmark_asset="SPY"
)
.. important::
**You can get a username and password at** `thetadata.net <https://www.thetadata.net/>`_. **Please use the full link to give us credit for the sale (https://www.thetadata.net), it helps support this project. You can use the coupon code 'LUMI' for 10% off.**
Optional: Environment Variables
-------------------------------
Instead of specifying `backtesting_start` and `backtesting_end` in code, you can set these environment variables (along with `IS_BACKTESTING`). LumiBot will detect them automatically:
.. 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
from lumibot.backtesting import BacktestingBroker, ThetaDataBacktesting
from lumibot.strategies import Strategy
from lumibot.traders import Trader
class MyStrategy(Strategy):
parameters = {
"symbol": "AAPL",
}
def initialize(self):
self.sleeptime = "1D"
def on_trading_iteration(self):
if self.first_iteration:
symbol = self.parameters["symbol"]
price = self.get_last_price(symbol)
qty = self.portfolio_value / price
order = self.create_order(symbol, quantity=qty, side="buy")
self.submit_order(order)
if __name__ == "__main__":
# No start/end in code. We rely on environment variables for backtesting dates.
result = MyStrategy.run_backtest(
ThetaDataBacktesting,
benchmark_asset="SPY"
)
In summary, the ThetaData backtester is a powerful tool for fetching pricing data for backtesting various strategies. With its capability to cache data for faster subsequent backtesting and its easy integration with thetadata API, it is a versatile choice for any backtesting needs.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.