Skip to main content

Welcome to a Grasp lesson

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

Lesson 19

Building a Bear Call Spread Class

Recommended resources

Hello! Welcome back to our course on building a trading system.

In the previous lesson, we successfully implemented our first concrete strategy, the BullPutSpread class. We saw how to inherit from our abstract Strategy base class and encapsulate the entire logic for finding a bullish credit spread, from fetching options to pairing the final legs.

Today, we will leverage that foundation to build the mirror image of that strategy. The learning outcome is to implement a BearCallSpread strategy class inheriting from the abstract strategy base class. You'll see how the Strategy design pattern we've established makes adding new, related strategies incredibly efficient. We will follow a similar process, but adapt the logic for a bearish market view using call options.

1. Understanding the Bear Call Spread

Before we write any code, let's quickly review the Bear Call Spread. It's a credit spread strategy used when you have a neutral to bearish outlook on an underlying asset. The goal is for the asset's price to stay below the strike price of the call option you sell.

A bear call spread is constructed by:

  1. Selling a call option with a strike price (K1) that is out-of-the-money (above the current stock price). This generates a premium.
  2. Buying a call option with a higher strike price (K2) and the same expiration date. This serves as protection and defines the maximum loss.

The net result is a credit received, which is also the maximum potential profit.

Plot Options Payoff: Bull and Bear Spread Strategies

To solidify your understanding of the mechanics and payoff profile, please read the 'Bear Call Spread' section from the article 'Plot Options Payoff' on CodeArmo.

Focus on the description of the Bear Call Spread. Pay attention to how it's constructed (selling a lower strike call, buying a higher strike), the formulas for max profit and loss, and the accompanying payoff diagram. This will be the blueprint for our code's logic.

The payoff diagram from the article shows a limited profit and limited loss profile, which is exactly what we want to create with our code.

Call Bear Spread Delta vs Payoff
This graph illustrates the relationship between the underlying price, the spread's payoff (blue line), and its delta (red line) with 30 days to expiration. Notice how the delta is negative, confirming the bearish bias of the strategy—the position gains value as the underlying price falls.

2. Structuring the BearCallSpread Class

Just like with BullPutSpread, our new class will inherit from the abstract Strategy class. This means it must provide a concrete implementation for any abstract methods defined in the parent, like find_trade_opportunity.

The concept of creating a specialized subclass that inherits common functionality but adds its own specific behavior is fundamental to object-oriented programming.

Python OOP Tutorial 4: Inheritance - Creating Subclasses

As a quick refresher on how to customize a subclass in Python, this video from Corey Schafer provides a clear, practical example. It will reinforce the concepts of overriding attributes and using super().__init__.

Please watch the segment from 06:06 to 08:45. Focus on how the Developer subclass is given its own __init__ method to handle an extra attribute (prog_lang) while still leveraging the parent class's __init__ method via super().

Now, let's define the skeleton for our BearCallSpread class. The __init__ method will look very similar to the one for the BullPutSpread, taking a symbol and a dictionary of parameters. The key difference will be the default delta ranges, which are now for call options.

# In framework/strategies.py
from .base import Strategy # Assuming Strategy ABC is in base.py

class BearCallSpread(Strategy):
    """
    A concrete implementation of a Bear Call Spread strategy.
    """
    def __init__(self, symbol: str, params: dict):
        """
        Initializes the BearCallSpread strategy.

        Args:
            symbol (str): The underlying symbol for the strategy.
            params (dict): A dictionary of strategy-specific parameters.
        """
        super().__init__(name=f"BearCallSpread_{symbol}")
        self.symbol = symbol
        
        # Unpack parameters with default values
        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 call legs
        # For OTM calls, delta is positive and ranges from 0 to 1.
        self.criteria = params.get('criteria', {
            'short_call': {'delta': (0.15, 0.40)},  # Delta range for the call we sell
            'long_call':  {'delta': (0.05, 0.20)}   # Delta range for the call we buy
        })

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

Notice the new criteria for the deltas. For a bear call spread, we sell a call that is closer to the money (higher delta) and buy one that is further out-of-the-money (lower delta).


3. Implementing the Core Logic: find_trade_opportunity

This is where we implement the heart of the strategy. The workflow is almost identical to the BullPutSpread, which demonstrates the power of our framework's design. The main differences are:

  1. We fetch call options instead of puts.
  2. We use the delta criteria defined for calls.
  3. The pairing logic is reversed to satisfy the conditions of a bear call spread.

Here is the full implementation of the BearCallSpread class, including all the necessary helper methods adapted for this strategy. Pay close attention to the comments highlighting the key changes from the previous lesson.

# In framework/strategies.py (full class implementation)

class BearCallSpread(Strategy):
    """
    A concrete implementation of a Bear Call Spread 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)
        self.strike_range_pct = params.get('strike_range_pct', 0.10) # Wider range for calls
        self.criteria = params.get('criteria', {
            'short_call': {'delta': (0.15, 0.40)},
            'long_call':  {'delta': (0.05, 0.20)}
        })

    def find_trade_opportunity(self, data_handler, analytics_engine):
        """
        Orchestrates the workflow to find a bear call spread.
        """
        # 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 CALL options within DTE and strike range
        # --- CHANGE: Fetch 'call' options ---
        call_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='call' # Key change here
        )

        # 3. Process options to find candidates
        short_call_candidates = []
        long_call_candidates = []

        for option_contract in call_options:
            if not self._validate_liquidity(option_contract, data_handler):
                continue
            
            option_with_analytics = analytics_engine.get_analytics(option_contract)
            if option_with_analytics is None:
                continue

            if self._check_candidate_conditions(option_with_analytics, 'short_call'):
                short_call_candidates.append(option_with_analytics)
            
            if self._check_candidate_conditions(option_with_analytics, 'long_call'):
                long_call_candidates.append(option_with_analytics)

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

        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

    # --- Helper Methods ---

    def _validate_liquidity(self, option_contract, data_handler) -> bool:
        """Checks if the option meets the open interest threshold."""
        open_interest = 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']
        
        return delta_range[0] <= delta <= delta_range[1]

    def _pair_candidates(self, short_calls: list, long_calls: list, underlying_price: float) -> dict or None:
        """
        Pairs short and long call candidates to form a valid spread.
        
        --- CHANGE: Logic for a Bear Call Spread ---
        We require:
        - Same expiration date.
        - underlying_price < short_call_strike < long_call_strike
        """
        for sc in short_calls:
            for lc in long_calls:
                # Must have same expiration
                if sc['expiration_date'] != lc['expiration_date']:
                    continue
                
                # Check strike price condition for a bear call spread
                sc_strike = sc['strike_price']
                lc_strike = lc['strike_price']
                if (underlying_price < sc_strike < lc_strike):
                    # Found a valid pair
                    return {'short_leg': sc, 'long_leg': lc}
        
        return None
Test your understanding!

In our _pair_candidates method, we are looking for the first valid pair we can find and returning it immediately. This is efficient, but may not be optimal.

Imagine you want to find the bear call spread that offers the widest possible "cushion"—that is, the greatest distance between the underlying price and the short call strike (short_call_strike - underlying_price).

How would you modify the _pair_candidates method to find the spread that maximizes this cushion, instead of just returning the first one it finds?

Show answer

Instead of returning immediately, you would need to iterate through all possible valid pairs and keep track of the one with the best "cushion" found so far.

Here's how the logic would change:

  1. Initialize a best_spread variable to None and a max_cushion variable to 0 (or a very small number).
  2. Iterate through all short_calls and long_calls with nested loops, just as before.
  3. Inside the loops, check for the validity of the pair (same expiration and underlying_price < sc_strike < lc_strike).
  4. If a valid pair is found, calculate its cushion: cushion = sc_strike - underlying_price.
  5. Compare this cushion to max_cushion. If it's greater, update max_cushion = cushion and set best_spread to the current pair ({'short_leg': sc, 'long_leg': lc}).
  6. After the loops complete, return best_spread.

This change transforms the method from a simple "find-first" search to an optimization search, allowing the strategy to be more selective based on a specific criterion.


Conclusion

Excellent work! We have now successfully implemented the BearCallSpread class. By building upon the structure from the previous lesson, you've seen firsthand how a well-designed framework allows for rapid extension. We now have two distinct, interchangeable strategies ready to be deployed by our system.

Key Takeaways:

  • The BearCallSpread class was created by inheriting from the abstract Strategy class, just like its bullish counterpart.
  • We adapted the __init__ method with parameters and delta criteria specific to a bearish call spread.
  • The core logic in find_trade_opportunity was easily adapted by changing the option type to 'call' and modifying the pairing logic to underlying_price < short_call_strike < long_call_strike.
  • This process reinforces the power of the Strategy design pattern: the "context" (our main event loop) can use either BullPutSpread or BearCallSpread without knowing the internal details of how they work, as long as they both provide a find_trade_opportunity method.

Preview of the Next Lesson:
In this lesson and the last, we've used delta ranges to filter for potential legs. In our next lesson, we will develop logic within each class to select appropriate strike prices based on delta and spread width. This will refine our selection process, allowing us to specify not just the delta of the short leg, but also the exact desired width of the spread (e.g., a $5 wide spread), making our strategy's risk profile even more precise.