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:
- Decoupling: The
BullPutSpreadlogic is completely separate from theVolatilitySignalGeneratorlogic. We can change the signal logic (e.g., build a new generator based on moving averages) without touching the strategy code at all. - Flexibility: We could give one
BullPutSpreadinstance 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:
- Update the
__init__method of our concrete strategy classes to accept an instance of aSignalGenerator. - 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:
- Set up the configuration.
- Create an instance of our
VolatilitySignalGenerator. - Create an instance of
BullPutSpread, passing the signal generator into its constructor. - Call
find_and_enter_position(). This will trigger the_check_entry_signal()method, which in turn callsgenerate_signals()on the volatility generator. Theyfinancelibrary 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
SignalGeneratorandStrategyclasses using composition (a "has-a" relationship). - The
Strategyclass now delegates the decision of when to trade to itsSignalGeneratorinstance. - 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.