Skip to main content

Welcome to a Grasp lesson

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

Lesson 21

Entry Condition Definition

Recommended resources

Hello and welcome back to our journey of building a professional trading system in Python.

In our last lesson, we significantly refined our strategy classes. We moved from a broad, range-based filtering method to a precise, trader-focused approach for selecting strikes using a target_delta and a fixed spread_width. This gave us direct control over the probability of profit and the maximum risk of our trades.

Today, we'll complete our entry logic. We've built the capability to find the what (the specific options for a spread), and now we'll define the when. This lesson addresses the learning outcome: Define the entry conditions for the spread based on signals from the signal generation component. We will integrate a signal that acts as a trigger, telling our strategy when it's time to act, and add final validation filters to ensure we only consider high-quality trade opportunities.

1. The Entry Trigger: Responding to Market Signals

In a complete trading system, one component is responsible for analyzing the market and generating a directional view. This is our SignalGenerator (which we'll build in Module 7). It might use technical analysis, volatility metrics, or other indicators to produce a simple signal like 'BULLISH', 'BEARISH', or 'NEUTRAL'.

Our strategy classes don't need to know how the signal is generated; they only need to know how to react to it. A BullPutSpread strategy should only act on a 'BULLISH' signal, while a BearCallSpread should only act on a 'BEARISH' signal.

To understand what a real-world signal might look like, let's watch a brief clip.

How to Trade Bull Put Spreads (Short Put Verticals) | Official thinkorswim® Web Tutorial

This video from Charles Schwab discusses how traders identify entry points for a bull put spread. The example shown uses technical analysis—a stock breaking through a resistance level—as the signal to enter a trade.

Please watch the segment from 08:37 to 09:30. Notice how the trader first establishes a bullish outlook based on a chart pattern (the 'signal') before even looking at the option chain. Our system will formalize this by passing an explicit signal to our strategy.

This video illustrates the core concept: the decision to trade is separate from the selection of the specific contracts. Our SignalGenerator will handle the "chart pattern analysis," and our strategy will handle the "option chain" part, but only if the signal is appropriate.


2. Integrating Signals into Strategy Logic

Let's implement this separation of concerns. We'll modify our find_trade_opportunity method to accept a signal. The method will now check this signal before proceeding to find a spread.

First, let's define a simple way to represent our signals. We'll use a standard Python Enum for clarity and type safety. You can add this to a new file, say framework/enums.py.

# In framework/enums.py

from enum import Enum

class Signal(Enum):
    BULLISH = "BULLISH"
    BEARISH = "BEARISH"
    NEUTRAL = "NEUTRAL"

Now, let's update the BullPutSpread strategy in framework/strategies.py. We will import Signal and modify the find_trade_opportunity method.

# In framework/strategies.py

# ... other imports
from .enums import Signal  # Add this import

# ...

class BullPutSpread(Strategy):
    # ... __init__ method remains the same ...

    def find_trade_opportunity(self, signal: Signal, data_handler, analytics_engine):
        # 1. Check for the appropriate signal
        if signal != Signal.BULLISH:
            # print(f"Signal is {signal}. BullPutSpread requires a BULLISH signal. No action taken.")
            return None

        # 2. 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'
        )

        # ... the rest of the method from the previous lesson remains the same...

This simple if statement is powerful. It ensures our bullish strategy only runs when the market view is bullish, preventing it from incorrectly entering trades in a bearish or neutral market.

Test your understanding!

What single line of code would you add to the BearCallSpread class's find_trade_opportunity method to integrate the signal check correctly?

Show answer

You would add the following line at the beginning of the method, after updating its signature to accept signal: Signal:

if signal != Signal.BEARISH:
    return None

This ensures the bearish strategy only executes when it receives a BEARISH signal.


3. Entry Filters: Ensuring Trade Quality

A valid signal gets us to the starting line, but we shouldn't enter a trade just because the signal is right. Professional traders use additional filters to weed out low-quality setups. We've already implemented several filters:

  • Days to Expiration (DTE)
  • Open Interest (Liquidity)
  • Short Leg Delta (Probability of Profit)
  • Spread Width (Risk)

A very common and crucial filter is the minimum credit received. A credit spread is a bet that time will pass and volatility may decrease, earning you the premium. If the premium is too small, the risk isn't worth taking.

Let's read about how experienced traders think about the premium they collect versus the risk they take.

The Credit Put Spread

This article from DataDrivenOptions.com provides excellent insight into the author's preferred parameters for trading credit put spreads. It explains the rationale behind choosing specific delta values and the expected premium.

Read the sections 'The Basics' and 'Why pick these delta values?'. Focus on the connection the author makes between delta, implied volatility, and the premium collected (e.g., 'collect somewhere between 12 to 18% of the width of the spread'). This highlights why setting a minimum credit is a vital entry condition.

As the article explains, collecting enough premium is essential. A trade might have a high probability of success, but if the premium is negligible, a single loss can wipe out many wins. We will add a min_credit filter to our strategy.


4. Implementing the Minimum Credit Filter

Our final entry logic will look like this:

  1. Check the signal (trigger).
  2. Find a potential spread using our delta/width logic.
  3. Validate the found spread against final filters, like min_credit.

Let's implement this.

First, add min_credit to the __init__ method of both strategy classes. We'll set a default of $0.25 per share, or $25 per contract.

# In framework/strategies.py (update for both BullPutSpread and BearCallSpread)

class BullPutSpread(Strategy):
    def __init__(self, symbol: str, params: dict):
        # ... other parameters ...
        self.spread_width = params.get('spread_width', 5)
        # --- NEW PARAMETER ---
        self.min_credit = params.get('min_credit', 0.25)

Next, let's create a new private helper method, _validate_spread, to check our final entry conditions. The credit for a spread is the premium received from the short leg minus the premium paid for the long leg.

Finally, we'll modify find_trade_opportunity to call this validation method. Here is the complete, final version of the BullPutSpread class incorporating all our entry logic.

# In framework/strategies.py

from collections import defaultdict
from .enums import Signal

class BullPutSpread(Strategy):
    """
    A concrete implementation of a Bull Put Spread strategy.
    Entry Condition: BULLISH signal.
    Filters: DTE, OI, target delta, spread width, minimum credit.
    """
    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)
        self.min_credit = params.get('min_credit', 0.25)

    def find_trade_opportunity(self, signal: Signal, data_handler, analytics_engine):
        # 1. TRIGGER: Check for the appropriate signal
        if signal != Signal.BULLISH:
            return None

        # 2. FILTERS (Part 1): Find a spread based on core criteria
        put_options = data_handler.get_option_chain(
            underlying_symbol=self.symbol,
            min_dte=self.min_dte,
            max_dte=self.max_dte,
            option_type='put'
        )

        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)
        
        potential_spread = self._find_spread(options_with_analytics)

        # 3. VALIDATION: Check the potential spread against final filters
        if potential_spread and self._validate_spread(potential_spread):
            print(f"Found and validated Bull Put Spread for {self.symbol}:")
            print(f"  Short Put: {potential_spread['short_leg']['symbol']}")
            print(f"  Long Put:  {potential_spread['long_leg']['symbol']}")
            return potential_spread
        
        # print(f"No valid Bull Put Spread found for {self.symbol}")
        return None

    def _find_spread(self, options_list: list) -> dict or None:
        # This method is unchanged from the previous lesson
        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}
            sorted_options = sorted(expiration_group, key=lambda x: x['strike_price'], reverse=True)

            for short_candidate in sorted_options:
                delta = short_candidate['greeks']['delta']
                if min_delta <= delta <= max_delta:
                    short_strike = short_candidate['strike_price']
                    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
    
    def _validate_spread(self, spread: dict) -> bool:
        """
        Performs final validation checks on a potential spread.
        """
        short_leg = spread['short_leg']
        long_leg = spread['long_leg']

        # Calculate net credit (using the mid-price of bid/ask)
        # Note: Prices are per share.
        short_price = (short_leg['quote']['bid_price'] + short_leg['quote']['ask_price']) / 2
        long_price = (long_leg['quote']['bid_price'] + long_leg['quote']['ask_price']) / 2
        credit = short_price - long_price

        # Check for minimum credit
        if credit < self.min_credit:
            print(f"Spread failed validation: Credit of {credit:.2f} is less than min_credit of {self.min_credit}")
            return False
            
        print(f"Spread validated: Credit received is {credit:.2f}")
        return True

Remember to apply the same logic to your BearCallSpread class: add the min_credit parameter, update find_trade_opportunity to call the validation method, and implement _validate_spread. The credit calculation remains the same (short_price - long_price).


Conclusion

Congratulations! You have now fully defined the entry conditions for your strategies. This is a significant milestone. Our system can now intelligently decide not only what trade to take, but also when to take it, based on a combination of a market signal and a series of rigorous quality filters.

Key Takeaways:

  • A robust entry condition consists of a trigger (the market signal) and a set of filters (DTE, OI, delta, spread width, min_credit).
  • We've decoupled signal generation from strategy logic by making the strategy react to a simple Signal enum.
  • Adding a min_credit filter is a practical way to avoid trades that don't offer sufficient reward for the risk taken.
  • Our find_trade_opportunity method now encapsulates the complete entry decision process: check signal -> find spread based on primary filters -> validate spread with secondary filters.

Preview of the Next Lesson:
With our entry logic now complete, the next critical question is: "When do we get out?" In the next lesson, we will begin building our position management logic by implementing an exit strategy based on achieving a specific percentage of the maximum profit. This will be our first step in systematically locking in gains.