Skip to main content

Welcome to a Grasp lesson

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

Lesson 18

Implementing a Bull Put Spread Strategy Class

Recommended resources

Hello! Welcome to the next step in building our trading framework.

In our last lesson, we built the functions to calculate the static risk profile of a credit spread: its maximum profit, maximum loss, and breakeven point. These metrics provide a fixed boundary for the risk/reward of any trade we enter.

Today, we will build the first "brain" of our system. The learning outcome is to implement a BullPutSpread strategy class inheriting from the abstract strategy base class. This is a major step: we are moving from creating isolated tools and analytics to defining an autonomous strategy component that can make decisions. We'll leverage the Strategy design pattern we established in Module 1 to create a concrete, plug-and-play implementation for a bull put spread.

Recap: The Strategy Design Pattern and ABCs

As a quick refresher, our framework's architecture is based on the Strategy design pattern. This allows us to define a family of algorithms (our trading strategies), encapsulate each one, and make them interchangeable.

The foundation for this is an Abstract Base Class (ABC) in Python, which we'll call Strategy. We likely defined this in Module 1. It acts as a contract, ensuring that any concrete strategy we create, like BullPutSpread, adheres to a common interface.

Abstract Base Class Inheritance Diagram
This diagram illustrates the core concept. An abstract 'Image' class defines an interface (`load_image`, `save_image`) that concrete classes like 'Bitmap' and 'Jpeg' must implement. We will do the same with an abstract 'Strategy' class and our concrete 'BullPutSpread' class.

To reinforce the Python syntax for this, you can review how to create an abstract class and have a concrete class inherit from it.

Python Interfaces and Abstract Base Class (ABC): A Must-Know for Advanced Programmers

This video, 'Python Interfaces and Abstract Base Class (ABC)', provides a concise and practical demonstration of using Python's abc module to define and implement an interface. It's a great refresher on the mechanics.

Watch the segments from 05:54 to 09:43. Pay attention to: How ABC and @abstractmethod are imported and used. How a concrete class (Dog) inherits from an abstract one (Animal). How a class (TalkingDog) can implement an interface (Talking).


1. Defining the BullPutSpread Class Structure

First, let's define the skeleton of our BullPutSpread class. It will inherit from the abstract Strategy class and we'll start by defining its constructor.

A strategy class needs to be initialized with parameters that define its behavior. For a bull put spread, what information is critical for finding a trade?

  • The underlying symbol.
  • Criteria for filtering options: days-to-expiration (DTE), delta ranges, liquidity (open interest).
  • Risk parameters: how much capital to allocate.

The following article provides a detailed, practical guide to building a bull put spread strategy. We will use it as our primary reference for the logic inside our class.

The Bull Put Spread Explained (and How to Trade in Python)

Please read the section 'Pick Your Strategy Inputs' from the article 'The Bull Put Spread Explained' by Alpaca. This section outlines the exact parameters we need for our strategy.

Read 'Step 2: Pick Your Strategy Inputs: Risk Management and Position Sizing'. Focus on the parameters defined in the code block, such as underlying_symbol, STRIKE_RANGE, BUY_POWER_LIMIT, OI_THRESHOLD, expiration date ranges, and the criteria dictionary for the short and long puts.

Drawing from that, we can now structure our BullPutSpread class. For now, we will focus on the logic for finding a trade, which we'll place in a method called find_trade_opportunity. This method will be our implementation of the abstract method required by our Strategy base class.

Here is the initial structure:

# In framework/strategies.py
from abc import ABC, abstractmethod

# A reminder of our abstract base class from Module 1
class Strategy(ABC):
    """
    Abstract base class for all trading strategies.
    """
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def find_trade_opportunity(self, data_handler, analytics_engine):
        """
        Scans for and identifies a potential trade.
        """
        pass

# Our new concrete strategy class
class BullPutSpread(Strategy):
    """
    A concrete implementation of a Bull Put Spread strategy.
    """
    def __init__(self, symbol: str, params: dict):
        """
        Initializes the BullPutSpread strategy.

        Args:
            symbol (str): The underlying symbol for the strategy.
            params (dict): A dictionary of strategy-specific parameters.
        """
        super().__init__(name=f"BullPutSpread_{symbol}")
        self.symbol = symbol
        
        # Unpack parameters with default values for safety
        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.strike_range_pct = params.get('strike_range_pct', 0.06)
        
        # Criteria for selecting the short and long put legs
        self.criteria = params.get('criteria', {
            'short_put': ((-0.60, -0.20)), # Delta range
            'long_put': ((-0.35, -0.10))   # Delta range
        })
        
        # We will also need access to other framework components
        self.data_handler = None
        self.analytics_engine = None

    def find_trade_opportunity(self, data_handler, analytics_engine):
        """
        Finds a suitable Bull Put Spread trade based on market data and criteria.
        
        This method will contain the core logic for selecting the two put options.
        """
        self.data_handler = data_handler
        self.analytics_engine = analytics_engine
        
        # ... Core logic to find the spread will go here ...
        
        return None # Placeholder

2. Implementing the Core Logic: Finding the Spread

Now for the main task: filling in the find_trade_opportunity method. The goal is to orchestrate a sequence of steps:

  1. Fetch all available put options for our symbol within our DTE and strike range.
  2. Filter these options based on liquidity (open interest).
  3. Calculate their Greeks (especially delta).
  4. Filter again to create two lists: potential short puts and potential long puts that match our delta criteria.
  5. Find a pair (one short, one long) with the same expiration that forms a valid bull put spread.

The Alpaca article provides an excellent set of functions that accomplish exactly this. We will adapt this procedural code into methods for our BullPutSpread class.

Bull Put Spread Profit/Loss Diagram
This diagram shows the payoff we are aiming for. Our code needs to find a short put and a long put that create this risk/reward profile.

The Bull Put Spread Explained (and How to Trade in Python)

Please study the code in 'Step 4' of the Alpaca article. This section provides the functions that form the blueprint for our strategy's core logic.

Carefully read through 'Step 4: Set up for a Bull Put Spread (Find two puts)'. You don't need to memorize every line, but understand the purpose of each function: get_options: Fetches contracts from the broker. validate_sufficient_OI: Checks liquidity. calculate_option_metrics: Fetches market data and Greeks (our analytics engine will handle this). build_option_dict and check_candidate_option_conditions: Filter candidates based on criteria. pair_put_candidates: Finds a valid short/long pair. find_options_for_bull_put_spread: The main function that orchestrates the entire process. This is the logic we will place inside our find_trade_opportunity method.


3. Integrating the Logic into Our Class

Let's refactor the logic from the article into our BullPutSpread class. The functions from the article will become private helper methods (prefixed with an underscore _), and the main orchestrator function becomes our public find_trade_opportunity method.

Notice how we replace function arguments with class attributes (e.g., self.symbol, self.criteria). This is a key benefit of the object-oriented approach.

# In framework/strategies.py (continued)

class BullPutSpread(Strategy):
    # __init__ method from before...
    
    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.strike_range_pct = params.get('strike_range_pct', 0.06)
        # Simplified criteria for this example
        self.criteria = params.get('criteria', {
            'short_put': {'delta': (-0.60, -0.20)},
            'long_put': {'delta': (-0.35, -0.10)}
        })

    def find_trade_opportunity(self, data_handler, analytics_engine):
        """
        Orchestrates the workflow to find a bull put spread.
        This is the public method called by the main event loop.
        """
        # 1. Get underlying price
        underlying_price = data_handler.get_latest_price(self.symbol)
        if underlying_price is None:
            print(f"Could not fetch price for {self.symbol}")
            return None

        # 2. Get all available put options within DTE and strike range
        put_options = data_handler.get_option_chain(
            underlying_symbol=self.symbol,
            min_dte=self.min_dte,
            max_dte=self.max_dte,
            strike_range_pct=self.strike_range_pct,
            option_type='put'
        )

        # 3. Process options to find candidates
        short_put_candidates = []
        long_put_candidates = []

        for option_contract in put_options:
            if not self._validate_liquidity(option_contract):
                continue
            
            # Fetch real-time quote and Greeks
            option_with_analytics = analytics_engine.get_analytics(option_contract)
            if option_with_analytics is None:
                continue

            # Check if it qualifies as a short or long leg
            if self._check_candidate_conditions(option_with_analytics, 'short_put'):
                short_put_candidates.append(option_with_analytics)
            
            if self._check_candidate_conditions(option_with_analytics, 'long_put'):
                long_put_candidates.append(option_with_analytics)

        # 4. Pair candidates to find a valid spread
        selected_spread = self._pair_candidates(short_put_candidates, long_put_candidates, underlying_price)

        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

    # --- Helper Methods (adapted from the Alpaca article) ---

    def _validate_liquidity(self, option_contract) -> bool:
        """Checks if the option meets the open interest threshold."""
        # This would use the data_handler to get open interest
        open_interest = self.data_handler.get_open_interest(option_contract)
        return open_interest is not None and open_interest > self.oi_threshold

    def _check_candidate_conditions(self, option_with_analytics: dict, leg_type: str) -> bool:
        """Check if a candidate option meets the filtering criteria for a given leg."""
        delta_range = self.criteria[leg_type]['delta']
        delta = option_with_analytics['greeks']['delta']
        
        if not (delta_range[0] <= delta <= delta_range[1]):
            return False
        
        # ... could add checks for IV, Theta, etc. here ...
        return True

    def _pair_candidates(self, short_puts: list, long_puts: list, underlying_price: float) -> dict or None:
        """
        Pairs short and long put candidates to form a valid spread.
        
        For a Bull Put Spread, we require:
        - Same expiration date.
        - long_put_strike < short_put_strike < underlying_price
        """
        for sp in short_puts:
            for lp in long_puts:
                # Must have same expiration
                if sp['expiration_date'] != lp['expiration_date']:
                    continue
                
                # Check strike price condition
                sp_strike = sp['strike_price']
                lp_strike = lp['strike_price']
                if (lp_strike < sp_strike < underlying_price):
                    # Found a valid pair
                    return {'short_leg': sp, 'long_leg': lp}
        
        return None
Test your understanding!

In the _pair_candidates helper method above, the condition for a bull put spread is defined as long_put_strike < short_put_strike < underlying_price. This ensures both legs are out-of-the-money.

The Alpaca article (ID=[LINK](https://alpaca.markets/learn/bull-put-spread), in the pair_put_candidates function) uses a slightly different condition: lp['strike_price'] <= underlying_price < sp['strike_price']. This allows the short put to be at-the-money.

How does this subtle difference in the logical condition change the risk profile and aggressiveness of the strategy?

Show answer

The condition long_put_strike < short_put_strike < underlying_price is more conservative. It requires the short put strike to be below the current underlying price, meaning the spread is opened fully out-of-the-money. This results in a lower premium (credit) but a higher probability of profit, as the stock has more room to fall before the position is challenged.

The condition lp['strike_price'] <= underlying_price < sp['strike_price'] is more aggressive. It allows the short put strike to be at or above the underlying price. This generates a higher initial credit (and thus higher max profit), but it also means the spread starts closer to being in-the-money, reducing the margin for error and lowering the probability of profit.

This highlights how a single line of code in the strategy's logic directly translates to its risk-reward characteristics. Our object-oriented design makes it easy to create different versions of the BullPutSpread class with these varying levels of aggressiveness.


Conclusion

Congratulations! You have now implemented the first complete, concrete strategy class in our framework. By inheriting from the abstract Strategy class and encapsulating the complex logic for finding a trade, we've created a modular and reusable component.

Key Takeaways:

  • We created the BullPutSpread class, which inherits from our abstract Strategy base class.
  • The class constructor (__init__) is used to configure the strategy with specific parameters like DTE, delta ranges, and liquidity thresholds.
  • We adapted procedural code from a practical example into class methods, demonstrating how to integrate external logic into an object-oriented framework.
  • The find_trade_opportunity method serves as the public interface for the strategy, orchestrating the process of finding a valid spread.

Preview of the Next Lesson:
With the BullPutSpread class complete, our next step is to implement its counterpart. In the next lesson, we will implement a BearCallSpread strategy class inheriting from the abstract strategy base class. The process will be very similar, which will reinforce the power and flexibility of the Strategy design pattern we have established.