Skip to content
All library documents

Selecting and Validating Options Contracts with OptionsHelper

Article Lumibot

Summary

This guide explains how LumiBot’s OptionsHelper supports options selection and order construction. It covers finding expirations on or after a target date, selecting strikes by target delta, validating quote quality, and assembling common multi-leg structures such as vertical spreads. Calls use positive target deltas and puts negative ones; the guide advises supplying the underlying price explicitly and reusing fetched option chains.

It presents these helpers as a way to reduce excessive data requests and avoid choosing contracts without usable market data. Suggested checks include evaluating bid and ask quality and requiring an actionable price before trading. Examples show caching a selected strike when the underlying price has changed little, and choosing condor short legs by delta while checking that wings exist. The guide also states that package pricing needs valid quotes for every leg and is not a fill guarantee. It documents implementation patterns, not strategy returns or proof of profitability.

Key ideas

  • Use expiration selection helpers to locate expiries with usable option data.
  • Select strikes by target delta instead of scanning large strike lists and repeatedly requesting Greeks.
  • Pass the underlying price explicitly and reuse option chains to limit redundant data work.
  • Evaluate quotes and require actionable prices before submitting trades.
  • Multi-leg price estimates require valid quotes for every leg and do not guarantee execution.

Tags

Full text
# options helper


OptionsHelper
=============

.. meta::
   :description: OptionsHelper is LumiBot's high-level helper for options selection (expirations, strikes, deltas) and multi-leg order building/execution.

``OptionsHelper`` is LumiBot's high-level helper for **options selection** (expirations, strikes, deltas) and **multi-leg order building/execution**.
For most options strategies, using ``OptionsHelper`` is both **more reliable** (avoids non-existent expiries/strikes during backtests) and **much faster** than brute-force approaches that scan large strike lists and call ``get_greeks()`` per strike.

Why use OptionsHelper?
----------------------

Options backtests are easy to accidentally make **slow** or **brittle**. ``OptionsHelper`` exists to push strategies toward the safe, high-performance path:

- **Reliability in backtests:** not every expiry/strike returned by a chain lookup is actually tradeable for the full backtest window. ``OptionsHelper`` methods validate that an option has usable market data.
- **Performance:** brute-force patterns (scan many strikes and call ``get_greeks()`` or quote history per strike) can trigger a large number of data requests and slow backtests dramatically.
- **Cleaner strategy code:** most common spreads can be built and executed with a few helper calls, without hand-assembling each leg.

If you are new to option data in LumiBot, also see:

- :doc:`strategy_methods.options` (low-level option methods on ``Strategy``)
- :doc:`backtesting.performance` (how to profile and reduce request fanout)
- :doc:`common_mistakes` (common options pitfalls)

Core conventions (important)
----------------------------

Rights and delta sign
~~~~~~~~~~~~~~~~~~~~~

``OptionsHelper`` uses the standard conventions:

- ``right`` is typically ``"call"`` or ``"put"`` (strings).
- ``target_delta`` is **positive** for calls and **negative** for puts. Example: 20Δ call = ``+0.20``; 20Δ put = ``-0.20``.

Always pass ``underlying_price`` (float)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Many selection helpers require ``underlying_price``. Always pass it explicitly as a ``float``:

- Prefer ``underlying_price=float(self.get_last_price(underlying_asset))`` (after checking for ``None``).
- Call ``OptionsHelper.find_strike_for_delta(...)`` with **keyword arguments**. This prevents subtle argument-order mistakes.

Call ``get_chains`` once, then reuse it
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``self.get_chains(...)`` can be expensive depending on data source. When you need chains (for example, to pick an expiry), fetch them once per iteration and reuse the result.

Performance & reliability tips
------------------------------

Avoid brute-force delta hunting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Do **not** do this:

- build a large strike list,
- loop over strikes, and
- call ``self.get_greeks()`` repeatedly to “hunt” a 20Δ strike.

That pattern is a common cause of slow runs because each greek/mark computation can trigger additional data work.

Instead, use:

- ``OptionsHelper.find_strike_for_delta(...)`` (bounded probing + caching)
- strategy-level caching: if you retry entry intraday, cache the selected expiry/strike(s) in ``self.vars`` and only recompute when the underlying moved materially.

Validate market data before trading
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For live trading (and for realistic backtests), it is common to encounter:

- missing bid/ask,
- very wide spreads,
- stale last trade prints.

Use ``OptionsHelper.evaluate_option_market(...)`` and gate trading on ``OptionsHelper.has_actionable_price(...)``.

Quickstart (delta-based strike)
--------------------------------

.. code-block:: python

   from datetime import timedelta
   from lumibot.entities import Asset
   from lumibot.components.options_helper import OptionsHelper

   def initialize(self):
       self.options_helper = OptionsHelper(self)

	   def pick_20_delta_put(self):
	       underlying = Asset("SPY", asset_type=Asset.AssetType.STOCK)
	       chains = self.get_chains(underlying)
	       if not chains:
           self.log_message("No option chains available", color="yellow")
           return None

       expiry = self.options_helper.get_expiration_on_or_after_date(
           self.get_datetime() + timedelta(days=0),
           chains,
           "put",
           underlying_asset=underlying,
       )
       if expiry is None:
           self.log_message("No valid expiry found", color="yellow")
           return None

       underlying_price = self.get_last_price(underlying)
       if underlying_price is None:
           self.log_message("Underlying price unavailable", color="yellow")
           return None

	       strike = self.options_helper.find_strike_for_delta(
	           underlying_asset=underlying,
	           underlying_price=float(underlying_price),
	           target_delta=-0.20,
	           expiry=expiry,
	           right="put",
	       )
	       return strike

Common workflows
----------------

1) Find a tradeable expiry near a target date
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``OptionsHelper.get_expiration_on_or_after_date(...)`` chooses an expiry on/after a target date and can validate that the expiry has tradeable data (when given ``underlying_asset``).

.. code-block:: python

   from datetime import timedelta
   from lumibot.entities import Asset

   underlying = Asset("SPY", asset_type=Asset.AssetType.STOCK)
   chains = self.get_chains(underlying)
   if not chains:
       return

   target_dt = self.get_datetime() + timedelta(days=7)
   expiry = self.options_helper.get_expiration_on_or_after_date(
       target_dt,
       chains,
       "call",
       underlying_asset=underlying,
   )

2) Pick a strike by delta (fast path)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Use ``OptionsHelper.find_strike_for_delta(...)`` instead of scanning strikes. Always pass keyword args.

.. code-block:: python

   underlying_price = self.get_last_price(underlying)
   if underlying_price is None:
       return

   put_strike = self.options_helper.find_strike_for_delta(
       underlying_asset=underlying,
       underlying_price=float(underlying_price),
       target_delta=-0.20,
       expiry=expiry,
       right="put",
   )

3) Validate liquidity / data quality before placing orders
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``evaluate_option_market`` returns an ``OptionMarketEvaluation`` with prices and flags. Only trade when prices are actionable.

.. code-block:: python

   option = Asset(
       underlying.symbol,
       asset_type=Asset.AssetType.OPTION,
       expiration=expiry,
       strike=put_strike,
       right="put",
       underlying_asset=underlying,
   )
   evaluation = self.options_helper.evaluate_option_market(option, max_spread_pct=0.25)
   self.log_message(f"Option evaluation: {evaluation}", color="blue")

   if not self.options_helper.has_actionable_price(evaluation):
       self.log_message(f"Skipping trade: {evaluation.data_quality_flags}", color="yellow")
       return

4) Build multi-leg orders (spreads/condors) without hand-coding legs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Most common spreads have both:

- a ``build_*`` helper that returns orders (so you can configure order type, risk logic, etc.), and
- an ``execute_*`` helper that builds + submits for you.

Example: put credit spread (sell higher strike, buy lower strike).

.. code-block:: python

   orders = self.options_helper.build_put_vertical_spread_orders(
       underlying_asset=underlying,
       expiry=expiry,
       upper_strike=put_strike,           # short
       lower_strike=put_strike - 5.0,     # long wing
       quantity=1,
   )
   if not orders:
       return

   limit_price = self.options_helper.calculate_multileg_limit_price(orders, limit_type="mid")
   self.log_message(f"Mid price estimate: {limit_price}", color="blue")

   # Submit however your strategy prefers (market/limit/smart limit, etc.)
   self.submit_order(orders)

Examples (copy/paste patterns)
------------------------------

Sell a 20Δ put once per day (with caching)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This pattern avoids recomputing strike selection every iteration when nothing material changed.

.. code-block:: python

   def initialize(self):
       from lumibot.components.options_helper import OptionsHelper
       self.options_helper = OptionsHelper(self)
       self.vars.cached_put_strike = None
       self.vars.cached_put_expiry = None
       self.vars.cached_put_day = None
       self.vars.cached_spot = None

   def _get_cached_20d_put_strike(self, underlying):
       from datetime import timedelta

       now = self.get_datetime()
       spot = self.get_last_price(underlying)
       if spot is None:
           return None, None

       today = now.date()
       if (
           self.vars.cached_put_day == today
           and self.vars.cached_put_strike is not None
           and self.vars.cached_put_expiry is not None
           and self.vars.cached_spot is not None
           and abs(float(spot) - float(self.vars.cached_spot)) / float(self.vars.cached_spot) < 0.005  # 0.5%
       ):
           return self.vars.cached_put_expiry, float(self.vars.cached_put_strike)

       chains = self.get_chains(underlying)
       if not chains:
           return None, None

       expiry = self.options_helper.get_expiration_on_or_after_date(
           now + timedelta(days=0),
           chains,
           "put",
           underlying_asset=underlying,
       )
       if expiry is None:
           return None, None

       strike = self.options_helper.find_strike_for_delta(
           underlying_asset=underlying,
           underlying_price=float(spot),
           target_delta=-0.20,
           expiry=expiry,
           right="put",
       )
       if strike is None:
           return None, None

       self.vars.cached_put_day = today
       self.vars.cached_put_expiry = expiry
       self.vars.cached_put_strike = float(strike)
       self.vars.cached_spot = float(spot)
       return expiry, float(strike)

Iron condor: pick short legs by delta, validate wings exist
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For condors, a common pattern is:

1) select short call + short put by delta,
2) choose wing strikes at a fixed distance, and
3) ensure wing strikes are real strikes for the chosen expiry (for example, by checking chains strikes).

If you are building a condor strategy, prefer:

- ``find_strike_for_delta`` for the short legs, and
- ``build_*_vertical_spread_orders`` to build each spread leg.

Troubleshooting
---------------

``get_chains(...)`` returns ``None`` / empty
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Not all assets have options, and data availability depends on your data source.
- Always guard against ``None`` and log what asset/when you queried.

``get_greeks(...)`` returns ``None``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This often means the option does not have usable price/quote inputs at that time (illiquid, missing quote history, etc.).

- Always handle ``None`` and continue strategy execution.
- Prefer ``evaluate_option_market`` to understand what price inputs are missing and why.

``find_strike_for_delta(...)`` returns ``None``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Common causes:

- no valid expiry was found (start by validating expiry selection),
- the chain does not include the expected strike neighborhood,
- missing option marks for candidate strikes (data gap / illiquid contracts).

In these cases, log and skip for the iteration/day rather than crashing.

API Reference
-------------

.. currentmodule:: lumibot.components.options_helper

.. autoclass:: OptionMarketEvaluation
   :members:

.. autoclass:: OptionsHelper
   :members:
   :member-order: bysource
Package quote integrity
-----------------------

``calculate_multileg_limit_price`` requires valid bid/ask quotes for every
option leg. Missing, failed, nonfinite, negative or crossed quotes return
``None`` for the entire package, never a partial-leg price. The supported
styles are ``best``, ``mid`` and ``fastest``; other styles raise ``ValueError``.
The result remains a signed per-unit price, not a fill guarantee.

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.