Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Strike Price Selection: Delta & Spread Width

Recommended resources

Hello and welcome back to our course on building a professional trading system in Python.

In our last two lessons, we successfully implemented the BullPutSpread and BearCallSpread classes. We used a filtering method based on separate delta ranges for the short and long legs to identify potential trades. While functional, this approach can be imprecise. Today, we will elevate our strategy logic to align more closely with how professional traders think about risk and probability.

Our learning outcome for this lesson is to develop logic within each class to select appropriate strike prices based on delta and spread width. Instead of defining two separate delta ranges, we will target a specific delta for the short leg (our primary source of premium and risk) and then select the long leg based on a fixed dollar-width for the spread. This gives us much finer control over the trade's characteristics.

1. The "Why": Selecting Strikes Like a Trader

Before diving into code, let's understand the rationale behind this new approach. For credit spread traders, the two most important factors when entering a trade are:

  1. Probability of Profit (PoP): What are the chances the trade will be profitable at expiration?
  2. Risk vs. Reward: How much am I risking to make a certain amount of premium?

Delta is a fantastic proxy for the probability of an option expiring in-the-money. The spread width, in turn, directly defines the maximum risk of the position. Therefore, by specifying a target delta for our short strike and a fixed spread width, we are directly controlling the trade's PoP and risk profile.

To explore this concept in more detail, let's watch a couple of videos that break down the art and science of strike selection.

Best Delta for Put Spreads (Avoid This Common Mistake)

First, this video from Options Trading IQ provides a concise breakdown of how to think about delta in terms of risk appetite. It categorizes delta ranges into 'conservative', 'balanced', and 'aggressive' plays.

Please watch the sections from the beginning to 02:02 and from 05:08 to 05:55. Focus on how delta is used as a proxy for probability and how different delta levels (10-20, 30, 40) correspond to different trading styles and risk tolerances.

The Ultimate Credit Spread Strike Price Selection Blueprint

This next video, from Options With Davis, builds on this by explaining how spread width is determined by your risk tolerance. It clearly connects the concept of 'risk per trade' to the dollar width of the spread.

Now, watch the section from 07:46 to 13:26. Pay close attention to the trade-offs discussed between choosing a high-win-rate (low delta) spread versus a higher-premium (high delta) spread. Also, note how the video explains that a wider spread increases both the premium collected and the maximum risk.

Key Concepts from the Videos:

  • Delta as Probability: A put option with a delta of -0.30 has a roughly 30% chance of expiring in-the-money. This means a credit spread where you sell that put has an approximate 70% probability of expiring worthless (your desired outcome).
  • Spread Width as Risk: The width of the spread (e.g., $5, $10) directly determines the maximum loss on the trade, calculated as (Spread Width * 100) - Net Credit Received. A wider spread increases your maximum potential loss but also typically allows you to collect more premium.
  • The Sweet Spot: Many traders consider a short-leg delta of around 20-30 to be a good balance between a high probability of profit and receiving a meaningful amount of premium.

Now, let's translate this refined understanding into our Python code.


2. The "How": Refactoring Our Strategy Logic

We'll refactor our BullPutSpread and BearCallSpread classes to use this new, more precise selection logic. This involves two main changes:

  1. Updating the __init__ method to accept new parameters.
  2. Rewriting the core pairing logic to search for a spread based on the short-leg delta and a fixed width.

Updating the __init__ Method

Let's modify the BullPutSpread class first. We will replace the criteria dictionary with more explicit parameters: target_delta, delta_tolerance, and spread_width.

# In framework/strategies.py (updated __init__ for BullPutSpread)

class BullPutSpread(Strategy):
    """
    A concrete implementation of a Bull Put Spread strategy.
    Selects a spread based on short-leg delta and a fixed width.
    """
    def __init__(self, symbol: str, params: dict):
        super().__init__(name=f"BullPutSpread_{symbol}")
        self.symbol = symbol
        
        # General parameters
        self.min_dte = params.get('min_dte', 21)
        self.max_dte = params.get('max_dte', 60)
        self.oi_threshold = params.get('oi_threshold', 50)
        
        # --- NEW: Parameters for precise strike selection ---
        # For a bull put spread, delta is negative.
        self.target_delta = params.get('target_delta', -0.30)
        self.delta_tolerance = params.get('delta_tolerance', 0.05)
        self.spread_width = params.get('spread_width', 5) # e.g., $5 wide

    # ... rest of the class methods will be refactored ...

This new structure is much more intuitive. We are explicitly telling the strategy: "Find me a bull put spread where the short put has a delta of about -0.30, and the spread is $5 wide."

Refactoring the Selection Logic

The old logic searched for two independent lists of candidates and then tried to pair them. The new logic is more sequential and efficient:

  1. Find a suitable short leg that meets our delta criteria.
  2. For that short leg, calculate the required strike for the long leg.
  3. Search for an option that matches the required long-leg strike and has the same expiration.

To implement this efficiently, we can first organize the option chain by expiration date and then, for each expiration, by strike price. This allows for very fast lookups.

Here is the refactored implementation of find_trade_opportunity and its new helper method for the BullPutSpread class.

Backtesting 0DTE Options with Python: The Full Guide - Alpaca

The following code is inspired by the structured approach shown in the Alpaca article 'Backtesting 0DTE Options with Python'. While our logic differs in its specifics, the idea of systematically iterating through the chain, calculating metrics, and applying validation rules is a powerful pattern.

Briefly review the function find_short_and_long_puts. Notice how it iterates chronologically, calculates delta for each option, and then validates the spread width. We will adopt a similar, structured approach but tailor it to our target-delta and fixed-width requirement.

Here's the new implementation for BullPutSpread.

# In framework/strategies.py (full refactored BullPutSpread class)
# Make sure to have `from collections import defaultdict` at the top of the file.

class BullPutSpread(Strategy):
    def __init__(self, symbol: str, params: dict):
        super().__init__(name=f"BullPutSpread_{symbol}")
        self.symbol = symbol
        self.min_dte = params.get('min_dte', 21)
        self.max_dte = params.get('max_dte', 60)
        self.oi_threshold = params.get('oi_threshold', 50)
        self.target_delta = params.get('target_delta', -0.30)
        self.delta_tolerance = params.get('delta_tolerance', 0.05)
        self.spread_width = params.get('spread_width', 5)

    def find_trade_opportunity(self, data_handler, analytics_engine):
        # 1. Get all available PUT options
        put_options = data_handler.get_option_chain(
            underlying_symbol=self.symbol,
            min_dte=self.min_dte,
            max_dte=self.max_dte,
            option_type='put'
        )

        # 2. Add analytics and filter for liquidity
        options_with_analytics = []
        for option in put_options:
            if data_handler.get_open_interest(option) > self.oi_threshold:
                enriched_option = analytics_engine.get_analytics(option)
                if enriched_option:
                    options_with_analytics.append(enriched_option)
        
        # 3. Find a valid spread
        selected_spread = self._find_spread(options_with_analytics)

        if selected_spread:
            print(f"Found valid Bull Put Spread for {self.symbol}:")
            print(f"  Short Put: {selected_spread['short_leg']['symbol']}")
            print(f"  Long Put:  {selected_spread['long_leg']['symbol']}")
            return selected_spread
        
        print(f"No valid Bull Put Spread found for {self.symbol}")
        return None

    def _find_spread(self, options_list: list) -> dict or None:
        """
        Finds a valid spread based on target delta and width.
        """
        # Group options by expiration date for efficient processing
        options_by_exp = defaultdict(list)
        for opt in options_list:
            options_by_exp[opt['expiration_date']].append(opt)
        
        # Define the delta range for the short put
        min_delta = self.target_delta - self.delta_tolerance
        max_delta = self.target_delta + self.delta_tolerance

        # Iterate through each expiration date, sorted to be deterministic
        for exp_date in sorted(options_by_exp.keys()):
            expiration_group = options_by_exp[exp_date]
            
            # Create a quick lookup map of strike to option for this expiration
            strikes_map = {opt['strike_price']: opt for opt in expiration_group}

            # Sort by strike descending to start further OTM
            sorted_options = sorted(expiration_group, key=lambda x: x['strike_price'], reverse=True)

            # 1. Find a potential short put
            for short_candidate in sorted_options:
                delta = short_candidate['greeks']['delta']
                if min_delta <= delta <= max_delta:
                    # 2. Found a potential short leg. Now find the long leg.
                    short_strike = short_candidate['strike_price']
                    long_strike_needed = short_strike - self.spread_width
                    
                    # 3. Look for the long leg in our strike map
                    if long_strike_needed in strikes_map:
                        long_candidate = strikes_map[long_strike_needed]
                        # Found a valid pair!
                        return {'short_leg': short_candidate, 'long_leg': long_candidate}
                        
        return None # No valid spread found across all expirations

Notice how much more direct this logic is. The _find_spread helper method first organizes all options by expiration. Then, for each expiration, it finds a short put that meets our delta criteria and performs a quick lookup to see if the corresponding long put (defined by the spread_width) exists. This is both efficient and robust.

Test your understanding!

Now that we have the refactored logic for BullPutSpread, how would you adapt the _find_spread method for the BearCallSpread class?

Consider these two key differences for a Bear Call Spread:

  1. You are trading call options, where delta is positive (0 to 1).
  2. The long call is a protective leg at a higher strike price.

What specific lines in the _find_spread method would need to change?

Show answer

You would need to make three main changes:

  1. Target Delta: The target_delta for calls would be positive, e.g., 0.30. The min_delta and max_delta calculation would remain the same structurally.
  2. Sorting Order: To start further out-of-the-money for calls (which have higher strikes), you'd sort the options by strike price in ascending order: sorted(expiration_group, key=lambda x: x['strike_price']).
  3. Long Strike Calculation: The long leg of a bear call spread has a higher strike. The calculation would be: long_strike_needed = short_strike + self.spread_width.

Everything else—grouping by expiration, creating the strike map, and the overall search flow—would remain identical, showcasing the power of this reusable logic pattern.


3. The Complete Code for BearCallSpread

Here is the complete, refactored BearCallSpread class incorporating the changes we just discussed.

# In framework/strategies.py

class BearCallSpread(Strategy):
    def __init__(self, symbol: str, params: dict):
        super().__init__(name=f"BearCallSpread_{symbol}")
        self.symbol = symbol
        self.min_dte = params.get('min_dte', 21)
        self.max_dte = params.get('max_dte', 60)
        self.oi_threshold = params.get('oi_threshold', 50)
        # For a bear call spread, delta is positive.
        self.target_delta = params.get('target_delta', 0.30) 
        self.delta_tolerance = params.get('delta_tolerance', 0.05)
        self.spread_width = params.get('spread_width', 5)

    def find_trade_opportunity(self, data_handler, analytics_engine):
        call_options = data_handler.get_option_chain(
            underlying_symbol=self.symbol,
            min_dte=self.min_dte,
            max_dte=self.max_dte,
            option_type='call'
        )

        options_with_analytics = []
        for option in call_options:
            if data_handler.get_open_interest(option) > self.oi_threshold:
                enriched_option = analytics_engine.get_analytics(option)
                if enriched_option:
                    options_with_analytics.append(enriched_option)
        
        selected_spread = self._find_spread(options_with_analytics)

        if selected_spread:
            print(f"Found valid Bear Call Spread for {self.symbol}:")
            print(f"  Short Call: {selected_spread['short_leg']['symbol']}")
            print(f"  Long Call:  {selected_spread['long_leg']['symbol']}")
            return selected_spread
        
        print(f"No valid Bear Call Spread found for {self.symbol}")
        return None

    def _find_spread(self, options_list: list) -> dict or None:
        options_by_exp = defaultdict(list)
        for opt in options_list:
            options_by_exp[opt['expiration_date']].append(opt)
        
        min_delta = self.target_delta - self.delta_tolerance
        max_delta = self.target_delta + self.delta_tolerance

        for exp_date in sorted(options_by_exp.keys()):
            expiration_group = options_by_exp[exp_date]
            strikes_map = {opt['strike_price']: opt for opt in expiration_group}

            # Sort by strike ascending for calls (further OTM = higher strike)
            sorted_options = sorted(expiration_group, key=lambda x: x['strike_price'])

            for short_candidate in sorted_options:
                delta = short_candidate['greeks']['delta']
                if min_delta <= delta <= max_delta:
                    short_strike = short_candidate['strike_price']
                    # For a bear call, the long strike is HIGHER
                    long_strike_needed = short_strike + self.spread_width
                    
                    if long_strike_needed in strikes_map:
                        long_candidate = strikes_map[long_strike_needed]
                        return {'short_leg': short_candidate, 'long_leg': long_candidate}
                        
        return None

Conclusion

Fantastic work today! We have significantly enhanced the intelligence and precision of our strategy classes. By moving from simple range-based filtering to a more deliberate selection based on a target delta and fixed spread width, our framework is now much more powerful and aligned with practical trading principles.

Key Takeaways:

  • We now select spreads by defining a target_delta for the short leg and a spread_width for the position, which gives us direct control over the probability of profit and the maximum risk.
  • The implementation logic was refactored to be more sequential and efficient: first, find a suitable short leg, then calculate the required long strike and look it up.
  • Grouping options by expiration and using a strike-price dictionary for lookups makes the search process highly efficient.
  • The core _find_spread logic pattern proved to be easily adaptable from the BullPutSpread to the BearCallSpread with minor, logical changes.

Preview of the Next Lesson:
Our entry logic is now sharp and precise. The next logical step is to define what happens after we enter a trade. In the upcoming lesson, we will begin implementing position management. Specifically, we will start with implementing exit logic based on achieving a certain percentage of maximum profit. This is a crucial component for systematically taking profits off the table.