Skip to main content

Welcome to a Grasp lesson

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

Lesson 29

Signal Generator Integration

Recommended resources

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

In our last lesson, we developed a concrete VolatilitySignalGenerator class. This component successfully encapsulates the logic of comparing implied and historical volatility to produce a raw 'ENTRY' or 'NO_ENTRY' signal. However, this signal currently exists in isolation.

Today, we will bridge that gap. The learning outcome is to integrate the signal generator to produce 'entry' or 'no_entry' signals for the strategy classes. We will modify our BullPutSpread and BearCallSpread strategy classes so they can hold and use a signal generator. This crucial step makes our strategies "smarter" by allowing them to decide when to start looking for a trade, based on external market conditions.

1. Architectural Design: Connecting the Components

So far, we have a Strategy class (which knows how to construct a spread) and a SignalGenerator class (which knows when to trade). The most effective way to connect them is through composition, where a Strategy object has a SignalGenerator object.

This follows the Strategy Design Pattern, which you've seen before. Just as we use this pattern to make the entire trading strategy interchangeable, we are also using it here to make the signal generation logic interchangeable. A BullPutSpread strategy doesn't need to know the complex details of IV vs. HV; it only needs to ask its signal generator, "Is it a good time to enter?"

This design provides two major benefits:

  1. Decoupling: The BullPutSpread logic is completely separate from the VolatilitySignalGenerator logic. We can change the signal logic (e.g., build a new generator based on moving averages) without touching the strategy code at all.
  2. Flexibility: We could give one BullPutSpread instance a volatility-based signal generator and another instance (for a different underlying) a momentum-based generator, all within the same framework.

To implement this, we'll perform two main steps:

  1. Update the __init__ method of our concrete strategy classes to accept an instance of a SignalGenerator.
  2. Implement a method within the strategy class, let's call it _check_entry_signal(), that calls the generator and interprets its signal.

2. Modifying the Strategy Classes for Integration

Let's apply this design to the code we've already written. We'll focus on the BullPutSpread class, but the changes for BearCallSpread are identical.

First, recall the abstract SignalGenerator from two lessons ago and the VolatilitySignalGenerator from the last lesson. We don't need to change them. They already provide the interface we need (generate_signals()).

Our work will be in the files containing our strategy definitions. I'll assume you have a framework/strategies.py file based on our work in Module 5.

Step 1: Update the __init__ Method

We need to modify the constructor of our BullPutSpread class to accept and store the signal generator.

Step 2: Implement the Signal Check Method

Next, we'll add a new method that uses the stored generator. This method will be responsible for calling generate_signals() and returning a simple boolean: True for an entry signal, False otherwise.

Here is the updated BullPutSpread class. The same changes should be applied to your BearCallSpread class.

# In framework/strategies.py (or your equivalent file)

# Assuming these imports exist from previous lessons
from framework.interfaces import Strategy, SignalGenerator
from framework.analytics import calculate_spread_greeks, calculate_breakeven_points # etc.

class BullPutSpread(Strategy):
    """
    Implements the Bull Put Spread strategy.
    
    Inherits from the abstract Strategy class and implements the logic for
    selecting strikes and managing the position.
    """
    # The 'ticker', 'trade_config', and 'data_handler' are from previous lessons
    def __init__(self, ticker: str, trade_config: dict, data_handler, signal_generator: SignalGenerator):
        """
        Initializes the BullPutSpread strategy.

        Args:
            ticker (str): The underlying asset's ticker symbol.
            trade_config (dict): Configuration for the trade (e.g., delta, width).
            data_handler: An object to fetch market data.
            signal_generator (SignalGenerator): An instance of a signal generator class.
        """
        super().__init__(ticker, trade_config, data_handler)
        self.signal_generator = signal_generator # <-- NEW: Store the generator

    def _check_entry_signal(self) -> bool:
        """
        Checks for an entry signal from the assigned signal generator.

        This method encapsulates the call to the signal generator, making the
        main strategy logic cleaner.

        Returns:
            bool: True if an 'ENTRY' signal is received, False otherwise.
        """
        # <-- NEW: The core integration logic
        signal = self.signal_generator.generate_signals()
        print(f"Signal for {self.ticker}: {signal}")
        
        if signal == 'ENTRY':
            return True
        return False

    def find_and_enter_position(self):
        """
        Checks for an entry signal and, if present, finds the best
        strikes and prepares to enter the position.
        """
        # <-- MODIFIED: Use the new signal check method
        if not self._check_entry_signal():
            print(f"No entry signal for {self.ticker}. Waiting.")
            return

        print(f"Entry signal received for {self.ticker}. Proceeding to find strikes...")
        
        # ... existing logic from Module 5 to select strikes would go here ...
        # For example:
        # 1. Fetch options chain using self.data_handler
        # 2. Filter for desired expiration (DTE)
        # 3. Find short put strike based on self.trade_config['delta_target']
        # 4. Find long put strike based on self.trade_config['spread_width']
        # 5. Create order object (which we will cover in the next module)
        
        pass

    # ... other methods like manage_position, check_exit_conditions, etc.

With these changes, the BullPutSpread class is now fully integrated with our signal generation system. The find_and_enter_position method now correctly acts as a gatekeeper, first checking the signal before doing the more intensive work of analyzing the options chain to find strikes.

Test your understanding!

Suppose you've created a new signal generator, RSI_SignalGenerator, that also adheres to the SignalGenerator abstract class and returns 'ENTRY' or 'NO_ENTRY'. You want to run a BullPutSpread on the QQQ ETF using this new RSI-based signal.

How would you instantiate the BullPutSpread strategy to use this new signal generator? You do not need to write the RSI_SignalGenerator code, just the line that creates the strategy instance.

Show answer

You would first instantiate your new signal generator and then pass that object into the BullPutSpread constructor. The strategy class itself requires no changes.

# Assuming data_handler and config are already defined
# and RSI_SignalGenerator is imported

# 1. Instantiate the new signal generator
rsi_gen = RSI_SignalGenerator(ticker='QQQ', rsi_period=14, entry_threshold=30)

# 2. Instantiate the strategy, passing in the new generator
qqq_bull_put = BullPutSpread(
    ticker='QQQ', 
    trade_config=config, 
    data_handler=data_handler,
    signal_generator=rsi_gen  # <-- Pass the new generator instance here
)

# Now, when you call qqq_bull_put.find_and_enter_position(), it will use the RSI signal.

This demonstrates the power of our pluggable design.


3. Demonstrating the Full Workflow

Let's create a simple script to see all the pieces working together. This script simulates what our main application loop will eventually do: instantiate the components and trigger the strategy logic.

Create a test file, e.g., run_integration_test.py, in the root of your project.

# In run_integration_test.py

from framework.signal_generators import VolatilitySignalGenerator
from framework.strategies import BullPutSpread

# A mock data handler for this example. In the real system, this would
# be our IBKR connection or a historical data provider.
class MockDataHandler:
    pass

def main():
    """
    Main function to demonstrate the integration of signal generator and strategy.
    """
    # 1. Configuration
    TICKER = 'SPY'
    TRADE_CONFIG = {
        'delta_target': 0.16,
        'spread_width': 10,
        'dte': 45
    }
    
    # 2. Instantiate Components
    # In a real system, these would come from our DataHandler module.
    # For now, we are using a placeholder.
    mock_data_handler = MockDataHandler()
    
    # Instantiate the signal generator from our previous lesson
    vol_signal_gen = VolatilitySignalGenerator(
        ticker=TICKER, 
        hv_window=21, 
        iv_threshold=1.1
    )
    
    # Instantiate the strategy, injecting the signal generator
    bull_put_strategy = BullPutSpread(
        ticker=TICKER,
        trade_config=TRADE_CONFIG,
        data_handler=mock_data_handler,
        signal_generator=vol_signal_gen
    )
    
    # 3. Run the Strategy Logic
    print(f"--- Running trade logic for {TICKER} ---")
    # This single call now triggers the entire signal->entry workflow
    bull_put_strategy.find_and_enter_position()
    print("--- Trade logic check complete ---")


if __name__ == '__main__':
    main()

When you run this script, the main function will:

  1. Set up the configuration.
  2. Create an instance of our VolatilitySignalGenerator.
  3. Create an instance of BullPutSpread, passing the signal generator into its constructor.
  4. Call find_and_enter_position(). This will trigger the _check_entry_signal() method, which in turn calls generate_signals() on the volatility generator. The yfinance library will be used to fetch live data, and you will see the output of the IV/HV comparison, followed by whether the strategy proceeds or waits.

This simple test elegantly validates that our components are correctly wired together.


Conclusion

Excellent work. You have now successfully integrated the signal generation and strategy components of our trading framework. This is a significant milestone that moves us from having isolated parts to a cohesive system that can make an intelligent decision about when to act.

Key Takeaways:

  • We connected the SignalGenerator and Strategy classes using composition (a "has-a" relationship).
  • The Strategy class now delegates the decision of when to trade to its SignalGenerator instance.
  • This approach, built on our abstract base classes, creates a highly flexible, decoupled architecture. You can now develop new signal generation ideas without ever touching the core strategy logic for selecting strikes.

Preview of the Next Lesson:
Our strategy can now decide that it wants to enter a trade. The next question is, what does that trade look like in a format the broker can understand? In the next module, we will begin building our ExecutionHandler. The very first step, and the topic of our next lesson, will be to design an Order object to represent market and limit orders for single and multi-leg strategies. This will be the data structure that carries the complete details of our desired trade—from the specific legs to the price—from the strategy to the execution system.