Common Lumibot Strategy Errors in Data, Options, and Order Handling
Summary
This guide catalogs implementation mistakes that can distort trading decisions or break a Lumibot strategy. It explains why backtests should use simulated time and completed candles, why persistent assets belong in strategy variables, and how to handle missing prices or Greeks without stopping unrelated logic. For crypto, it notes that the market schedule must reflect continuous trading, and it distinguishes historical bars from current prices and bid-ask quotes.
The options and order sections cover selecting listed expirations and delta strikes through helper tools, accounting for the contract multiplier, using quotes for illiquid options, and recognizing that positions update after order submission. It also discusses closing crypto futures, bracket order parameters, chart marker use, and avoiding hidden exceptions or blocking waits. A final diagnostic section says provider rate limits are retryable and recommends checking data-health details. The page is practical implementation guidance rather than a tested trading strategy; its recommendations are specific to Lumibot behavior and runtime versions.
Key ideas
- Use simulated timestamps and completed market data as evidence in backtests.
- Store strategy state in the framework's designated variables and preserve asset types during restoration.
- Check for missing prices and Greeks, and use quotes when last trades may be stale.
- Account for option contract multipliers and use available helpers to select expirations and strikes.
- Allow for asynchronous position updates after submitting orders and avoid blocking strategy iterations.
Tags
Full text
# common mistakes
Common Mistakes and How to Avoid Them
======================================
.. meta::
:description: This page documents the most common mistakes made when writing Lumibot strategies, along with the correct patterns to use instead.
This page documents the most common mistakes made when writing Lumibot strategies, along with the correct patterns to use instead.
Choosing an Intraday Order Duration
--------------------------------------------------------------------------------
``Strategy.create_order()`` defaults to ``time_in_force="gtc"``, while constructing
``Order`` directly defaults to ``"day"``. Specify the intended duration explicitly.
For example, an intraday market short can use:
.. code-block:: python
hedge = self.create_order(
Asset("SPY"), 100, Order.OrderSide.SELL_SHORT,
order_type=Order.OrderType.MARKET, time_in_force="day",
)
self.submit_order(hedge)
Schwab may reject GTC short-sale orders for hard-to-borrow securities. DAY does
not guarantee acceptance or a fill; other borrowing, account and market constraints
still apply. Inspect the broker's rejection reason before retrying. Preserve
intentional GTC orders instead of changing every order's duration globally.
Restoring Asset Variables After a Restart
--------------------------------------------------------------------------------
Keep instruments as ``Asset`` objects in ``self.vars``. New scheduled-file and
database backups preserve their type, including assets nested in lists,
dictionaries, or tuples. Restored objects can be passed directly to
``get_position()`` and ``add_ohlc()``.
Sets still restore as lists, with their asset values preserved.
Older backups may contain an untagged asset dictionary. Restoration reconstructs
it only when the same variable path already contains an ``Asset`` initialized by
the strategy. Saved contract details remain authoritative; initialization supplies
the expected type, not replacement quantities, strikes, or signals. Scheduled-file
restoration preserves the original bytes in a permission-restricted sibling
``.legacy-<sha256>.bak`` before applying the recovered variables.
Paths without an initialized instrument remain ordinary dictionaries because
the same shape can be strategy metadata. Migrate those explicitly with
``Asset.from_dict(value)`` after verifying the instrument path.
Do not apply this conversion to arbitrary dictionaries or discard other saved
strategy state. Backups written by the updated runtime should be restored by
the updated runtime; older versions do not understand the asset type tag.
For hosted scheduled bots, preserve the original remote state independently
before migration: a local sibling backup is not a durable cloud backup unless
the hosting system explicitly retains it.
FRED Data Errors
--------------------------------------------------------------------------------
Use official FRED series identifiers rather than market symbols. A failed series
can appear under ``errors`` even when other series succeed in the same snapshot.
HTTP and transport errors report a status or exception type without exposing the
API key in the request URL. Correct an invalid series; do not replace working
credentials because one series returns HTTP 400.
Critical Mistakes (Will Break Your Strategy)
--------------------------------------------
Using an Unfinished Crypto Candle as Historical Evidence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CCXT candles are timestamped at their opening time. At 09:00, the eventual
close of the 09:00–09:01 candle is not known. Default CCXT backtest history
and last-price queries therefore use completed candles only. This applies to
minute, hour and day bars, including AI research through those methods.
Do not add a negative history ``timeshift`` to make that future closing price
available to your strategy. The backtesting broker uses an execution-only
offset to simulate orders against the current candle; this does not make the
whole candle valid evidence for the preceding decision. When no candle has
closed yet, missing history is expected, not permission to invent a price.
Using datetime.now() Instead of self.get_datetime()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Backtest results will be completely wrong. Your strategy will think it's the current date instead of the simulated date.
.. code-block:: python
# WRONG
current_time = datetime.now()
target_date = datetime.today() + timedelta(days=30)
# CORRECT
current_time = self.get_datetime()
target_date = self.get_datetime() + timedelta(days=30)
Adding 'from __future__ import annotations'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Causes immediate crash during backtesting.
.. code-block:: python
# NEVER DO THIS - WILL CRASH YOUR STRATEGY
from __future__ import annotations
Simply remove this import. It's not needed and will break everything.
Assigning Attributes Directly on self
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Overrides Lumibot internals, causing crashes or unexpected behavior.
.. code-block:: python
# WRONG - collides with framework
def initialize(self):
self.name = "MyBot"
self.asset = Asset("SPY")
self.symbol = "SPY"
# CORRECT - use self.vars
def initialize(self):
self.vars.strategy_label = "MyBot"
self.vars.target_asset = Asset("SPY", asset_type=Asset.AssetType.STOCK)
self.vars.target_symbol = "SPY"
Forgetting set_market("24/7") for Crypto
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Crypto bot stops trading at 4pm EST every day.
.. code-block:: python
# WRONG - crypto bot stops at 4pm
def initialize(self):
self.sleeptime = "1M"
# CORRECT - crypto trades 24/7
def initialize(self):
self.set_market("24/7") # REQUIRED for crypto
self.sleeptime = "1M"
Data Mistakes
-------------
Not Checking if get_last_price() Returns None
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Crashes with NoneType error when data is unavailable.
.. code-block:: python
# WRONG - will crash on None
price = self.get_last_price(asset)
quantity = self.portfolio_value / price # Crashes if price is None
# CORRECT - always check for None
price = self.get_last_price(asset)
if price is None:
self.log_message(f"No price for {asset.symbol}", color="red")
return
quantity = self.portfolio_value / price
Using get_historical_prices() for Real-Time Data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Strategy uses stale data, missing current price movements.
.. code-block:: python
# WRONG - historical data can be 1 minute delayed
bars = self.get_historical_prices(asset, 1, "minute")
current_price = bars.df.iloc[-1]["close"]
# CORRECT - use get_last_price for real-time
current_price = self.get_last_price(asset)
# BEST - use get_quote for bid/ask
quote = self.get_quote(asset)
if quote and quote.bid and quote.ask:
mid_price = quote.mid_price
Returning Early When get_greeks() is None
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Strategy stops running for the entire iteration just because one option has no Greeks.
.. code-block:: python
# WRONG - blocks entire strategy
greeks = self.get_greeks(option_asset)
if greeks is None:
return # Strategy stops here!
# CORRECT - continue with other logic
greeks = self.get_greeks(option_asset)
if greeks is not None:
delta = greeks.get("delta")
# Use delta here
else:
self.log_message("Greeks unavailable, skipping delta check", color="yellow")
# Strategy continues with other logic...
Options Mistakes
----------------
Manually Selecting Option Expirations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Selected expiration may not have tradeable data during backtesting.
.. code-block:: python
# WRONG - expiration may not exist
target_expiry = self.get_datetime() + timedelta(days=30)
option = Asset("SPY", asset_type=Asset.AssetType.OPTION,
expiration=target_expiry.date(), strike=400, right="call")
# CORRECT - use OptionsHelper
chains = self.get_chains(underlying_asset)
target_expiry = self.get_datetime() + timedelta(days=30)
valid_expiry = self.options_helper.get_expiration_on_or_after_date(
target_expiry, chains, "call"
)
Brute-Forcing Deltas by Scanning Strikes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Backtests become very slow because calling ``get_greeks()`` repeatedly can trigger many option quote-history downloads.
.. code-block:: python
# WRONG - brute-force scan (slow)
strikes = chains.strikes(expiry, "PUT")
for strike in strikes:
option = Asset("SPY", asset_type=Asset.AssetType.OPTION, expiration=expiry, strike=strike, right="put")
greeks = self.get_greeks(option, underlying_price=underlying_price)
...
# CORRECT - use OptionsHelper (bounded probing + caching)
strike = self.options_helper.find_strike_for_delta(
underlying_asset=underlying_asset,
underlying_price=float(underlying_price),
target_delta=-0.20,
expiry=expiry,
right="put",
)
Forgetting Options are 100x Multiplied
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Position sizing is off by 100x.
.. code-block:: python
# WRONG - buys 100x too many contracts
option_price = 1.50 # $1.50 premium
contracts = 10000 / option_price # Wrong: 6666 contracts!
# CORRECT - account for multiplier
option_price = 1.50
actual_cost_per_contract = option_price * 100 # $150
contracts = int(10000 / actual_cost_per_contract) # Correct: 66 contracts
Using get_last_price() for Options Pricing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Stale or missing prices for illiquid options.
.. code-block:: python
# WRONG - last trade can be very stale for options
price = self.get_last_price(option_asset)
# CORRECT - use quote for bid/ask
quote = self.get_quote(option_asset)
if quote and quote.bid and quote.ask:
fair_price = quote.mid_price
else:
self.log_message("No valid quote for option", color="yellow")
Order Mistakes
--------------
Expecting Immediate Position Updates After submit_order()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Strategy logic based on outdated position data.
.. code-block:: python
# WRONG - position hasn't updated yet
self.submit_order(order)
position = self.get_position(asset) # Still shows old data!
# CORRECT - check on next iteration
self.submit_order(order)
# In the NEXT on_trading_iteration():
position = self.get_position(asset) # Now updated
Using submit_order() to Close Crypto Futures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Opens a new position instead of closing existing one.
.. code-block:: python
# WRONG - opens opposite position instead of closing
order = self.create_order(futures_asset, quantity, "sell")
self.submit_order(order)
# CORRECT - use close_position
self.close_position(futures_asset)
Using Deprecated take_profit_price/stop_loss_price
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** May cause order errors or unexpected behavior.
.. code-block:: python
# WRONG - deprecated parameters
order = self.create_order(asset, 100, "buy",
take_profit_price=110,
stop_loss_price=90)
# CORRECT - use secondary_ parameters
order = self.create_order(asset, 100, "buy",
order_class=Order.OrderClass.BRACKET,
secondary_limit_price=110, # Take profit
secondary_stop_price=90) # Stop loss
Visualization Mistakes
----------------------
Adding Markers Every Iteration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Chart crashes or becomes unusable due to thousands of markers.
.. code-block:: python
# WRONG - adds marker every iteration
def on_trading_iteration(self):
self.add_marker("Price", price, color="blue") # Chart explodes!
# CORRECT - markers only for significant events
def on_trading_iteration(self):
if signal_detected: # Only when something happens
self.add_marker("Buy Signal", price, color="green", asset=my_asset)
# Use add_line for continuous data
self.add_line("SMA_20", sma_value, color="blue", asset=my_asset)
Using 'text' Parameter in add_marker/add_line/add_ohlc
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** TypeError crash - there is no 'text' parameter.
.. code-block:: python
# WRONG - causes TypeError
self.add_marker("Signal", price, text="Buy now!")
# CORRECT - use detail_text for hover text
self.add_marker("Signal", price, detail_text="Buy signal triggered", asset=my_asset)
Forgetting to Pass asset Parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Indicators appear in separate subplot instead of overlaying price chart.
.. code-block:: python
# WRONG - indicator in separate subplot
self.add_line("SMA_20", sma_value, color="blue")
# CORRECT - overlays on asset's price chart
self.add_line("SMA_20", sma_value, color="blue", asset=spy_asset)
Code Organization Mistakes
--------------------------
Hardcoding API Keys
~~~~~~~~~~~~~~~~~~~
**Impact:** Security risk and deployment problems.
.. code-block:: python
# WRONG - hardcoded fallback
api_key = os.getenv('PERPLEXITY_API_KEY', 'your_api_key_here')
# CORRECT - None fallback
api_key = os.getenv('PERPLEXITY_API_KEY')
if api_key is None:
self.log_message("PERPLEXITY_API_KEY not set", color="red")
Setting Parameters in Multiple Places
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Confusing, parameters override each other unpredictably.
.. code-block:: python
# WRONG - parameters set in multiple places
class MyStrategy(Strategy):
parameters = {"symbol": "SPY"}
if __name__ == "__main__":
strategy = MyStrategy(parameters={"symbol": "AAPL"}) # Which one wins?
# CORRECT - one place only
class MyStrategy(Strategy):
parameters = {
"symbol": "SPY",
"period": 20
}
# Never override parameters elsewhere
Using try/except to Hide Errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Bugs are hidden, making debugging nearly impossible.
.. code-block:: python
# WRONG - hides real errors
try:
price = self.get_last_price(asset)
quantity = self.portfolio_value / price
except:
pass # What went wrong? No idea!
# CORRECT - explicit error handling
price = self.get_last_price(asset)
if price is None:
self.log_message(f"No price for {asset.symbol}", color="red")
return
quantity = self.portfolio_value / price
Using sleep() in Strategy
~~~~~~~~~~~~~~~~~~~~~~~~~
**Impact:** Blocks the entire bot, preventing important code from running.
.. code-block:: python
# WRONG - blocks everything
def on_trading_iteration(self):
self.submit_order(order)
time.sleep(5) # Bot frozen for 5 seconds!
# CORRECT - check conditions next iteration
def on_trading_iteration(self):
if not hasattr(self.vars, "order_time"):
self.submit_order(order)
self.vars.order_time = self.get_datetime()
return
elapsed = self.get_datetime() - self.vars.order_time
if elapsed > timedelta(seconds=5):
# Now do the next step
pass
IBKR history waits and data-health diagnostics
--------------------------------------------------------------------------------
A provider rate limit is a retryable wait, not evidence that an instrument has
no prices. When the downloader supplies structured rate-limit information,
LumiBot retains the request identity and exposes the provider wait in download
status. Buying more simultaneous quotes does not automatically remove
historical-data pacing restrictions.
Inspect the ``data_health`` field in backtest settings alongside the requested
window and provider error details. An incomplete diagnostic is not an automatic
backtest failure; a legitimate strategy can also produce no trades. Short daily
requests include the full required history and calendar padding. Longer
stock/index windows retain the five-year page cap and backward pagination.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.