Hello! Welcome to the first lesson of our seventh module, "Signal Generation Interface."
In the previous modules, we have built a robust framework for managing a credit spread position from entry to exit. We have logic to select strikes, open a position, take profits, stop losses, and even perform complex adjustments like rolling. However, all this machinery currently lies dormant, waiting for one critical piece of information: a signal telling it when to act.
This lesson addresses that need. Our learning outcome is to design an abstract SignalGenerator class to create a pluggable interface for different entry signals. We will apply a fundamental software design principle—the Strategy Pattern—to ensure our signal generation logic is modular, flexible, and easy to extend without rewriting core parts of our system.
1. The Case for a Pluggable Design
Imagine we build our first entry signal based on a moving average crossover. We code this logic directly into our main strategy class. It works. A month later, we want to test a new signal based on volatility. To do this, we'd have to go back into our strategy class and add if/else statements or completely replace the existing logic. This approach is brittle, error-prone, and doesn't scale.
A much better design is to treat the signal generation algorithm as a "pluggable" component. Our main framework will know that it needs a SignalGenerator object, but it won't care how that object works internally. This allows us to swap different signal generation strategies—one based on moving averages, another on RSI, and a third on volatility—without changing any other part of the code.
This concept is formalized in the Strategy Design Pattern. To see it in action, let's watch a video that explains the core problem and the classic object-oriented solution.
The Strategy Pattern: Write BETTER PYTHON CODE Part 3
The video 'The Strategy Pattern' by ArjanCodes clearly demonstrates why long if-else blocks are a code smell and how to refactor them into a clean, extensible structure using an abstract class and concrete strategy classes.
Please watch from 01:27 to 07:03. First, you'll see a 'before' state with messy conditional logic. Then, you'll see how an abstract base class is introduced to define a common interface, and concrete classes are created for each distinct strategy. This is exactly the pattern we will implement.
The key takeaway is that by defining a common interface (the abstract TicketOrderingStrategy), the main CustomerSupport class can work with any ordering strategy without knowing its specific implementation details. We will now apply this exact principle to our SignalGenerator.
2. Designing the SignalGenerator Abstract Base Class
In Python, we create abstract interfaces using the abc (Abstract Base Classes) module. An ABC defines a "contract" that any concrete implementation must follow. It can't be used on its own, but it guarantees that any subclass will have the methods we need.
The core components are:
- Inheriting from
ABC. - Using the
@abstractmethoddecorator to mark methods that subclasses must implement.

Our SignalGenerator contract will be simple: it must have a single method, let's call it generate_signal, that takes in market data and returns a signal.
For a professional perspective on this design, let's look at a well-known open-source backtesting framework.
Event-Driven Backtesting with Python - Part IV
The article 'Event-Driven Backtesting with Python - Part IV' from QuantStart describes a very similar concept. It outlines an abstract Strategy class designed to receive market data and produce SignalEvent objects.
Read the initial section under the 'Strategy' heading and look at the Python code for the abstract Strategy class. Notice how it uses ABCMeta and @abstractmethod to define a calculate_signals method. This serves the same purpose as our generate_signal method.
Drawing from these examples, we can now define our own abstract class. We also need a standardized way to represent the signals themselves. Using an Enum is perfect for this, as it prevents errors from typos (e.g., 'buy' vs 'BUY') and makes the code more readable.
Here is the complete design for our pluggable interface. You can create a new file, framework/signals.py, for this code.
# In framework/signals.py
from abc import ABC, abstractmethod
from enum import Enum
import pandas as pd
class SignalType(Enum):
"""
Standardizes the types of signals that can be generated.
"""
ENTRY = "ENTRY" # Signal to look for an entry opportunity
NO_ENTRY = "NO_ENTRY" # Signal to stand down, conditions are not met
class SignalGenerator(ABC):
"""
Abstract base class for all signal generation strategies.
The goal of a SignalGenerator is to analyze market data and produce
a `SignalType` indicating whether the conditions are right to
enter a trade.
"""
@abstractmethod
def generate_signal(self, data: pd.DataFrame) -> SignalType:
"""
Provides the mechanism to calculate the trading signal.
Args:
data (pd.DataFrame): A DataFrame containing historical market data
for the underlying asset. It is expected to
have at least a 'Close' column.
Returns:
SignalType: The signal to be emitted (ENTRY or NO_ENTRY).
"""
raise NotImplementedError("Should implement generate_signal()")
With this in place, our main trading loop can be configured with any object that inherits from SignalGenerator and implements the generate_signal method. The loop simply calls this method, gets back a SignalType, and acts accordingly, completely decoupled from the specific logic that generated the signal.
3. Handling Strategy-Specific Parameters
A crucial aspect of this design is managing parameters. For example:
- A moving average crossover signal generator needs
short_windowandlong_windowparameters. - A volatility-based signal generator might need a
volatility_threshold.
These parameters are specific to each concrete strategy. Where should we configure them? A common mistake is to try and pass them through the generate_signal method, but this pollutes the clean interface we just designed.
There is a much cleaner solution, which is a best practice when implementing the Strategy pattern.
Solving A Common Issue With The Strategy Pattern // In Python
Let's return to ArjanCodes for 'Solving A Common Issue With The Strategy Pattern'. This video specifically addresses how to handle parameters for different strategies.
Watch the final solution presented from 10:43 to 14:31. The key insight is to pass strategy-specific parameters into the constructor (__init__) of the concrete strategy class itself. Pay attention to why this is the ideal place for this coupling to occur.
The principle is simple and powerful: parameters belong to the object that uses them. When we create an instance of a specific signal generator, we pass its required parameters to its constructor.
For example, a hypothetical MovingAverageCrossoverGenerator would look like this:
class MovingAverageCrossoverGenerator(SignalGenerator):
def __init__(self, short_window: int = 20, long_window: int = 50):
# Parameters are stored as instance attributes
self.short_window = short_window
self.long_window = long_window
print(f"Initialized MovingAverageCrossoverGenerator with windows: "
f"{self.short_window}/{self.long_window}")
def generate_signal(self, data: pd.DataFrame) -> SignalType:
# In a real implementation, we would use self.short_window and
# self.long_window to calculate the moving averages and generate a signal.
# For now, we just demonstrate the structure.
print("generate_signal called on MovingAverageCrossoverGenerator")
# ... logic would go here ...
return SignalType.NO_ENTRY # Placeholder
When our framework is initialized, the configuration file would specify which signal generator to use and what its parameters are. The main application would then create the instance like this:
signal_generator = MovingAverageCrossoverGenerator(short_window=10, long_window=30)
This signal_generator object is then passed to the part of the framework that needs it, which can call generate_signal() without ever knowing about short_window or long_window.
Test your understanding!
Imagine you are about to create a new signal generator called RSIThresholdGenerator. Its logic will be to generate an ENTRY signal when the Relative Strength Index (RSI) of the underlying asset falls below a certain "oversold" level.
Based on the design principles we've just discussed, sketch out the class definition for RSIThresholdGenerator. You don't need to implement the generate_signal logic; just create the class structure, its __init__ method with appropriate parameters, and the placeholder generate_signal method.
Show answer
Your sketch should look something like this. The key points are inheriting from SignalGenerator and accepting the strategy-specific parameters (rsi_period and oversold_threshold) in the __init__ method.
from framework.signals import SignalGenerator, SignalType
import pandas as pd
class RSIThresholdGenerator(SignalGenerator):
"""
Generates an entry signal when the RSI drops below a specified
oversold threshold.
"""
def __init__(self, rsi_period: int = 14, oversold_threshold: int = 30):
"""
Initializes the RSIThresholdGenerator.
Args:
rsi_period (int): The lookback period for the RSI calculation.
oversold_threshold (int): The RSI level below which an ENTRY
signal is generated.
"""
self.rsi_period = rsi_period
self.oversold_threshold = oversold_threshold
def generate_signal(self, data: pd.DataFrame) -> SignalType:
"""
Calculates RSI and compares it to the oversold threshold.
"""
# The actual RSI calculation and comparison logic would go here.
# For example:
# rsi = calculate_rsi(data['Close'], self.rsi_period)
# if rsi.iloc[-1] < self.oversold_threshold:
# return SignalType.ENTRY
# else:
# return SignalType.NO_ENTRY
raise NotImplementedError("RSI logic not yet implemented.")
Conclusion
In this lesson, you have laid a critical architectural foundation for our trading framework. By applying the Strategy Pattern, you've designed an interface that decouples the "what" (the need for a signal) from the "how" (the specific algorithm for generating that signal).
Key Takeaways:
- A pluggable interface for signal generation makes the system flexible and extensible.
- The Strategy Design Pattern is the ideal choice for this, separating algorithms into interchangeable classes.
- An Abstract Base Class (
SignalGenerator) defines the contract that all concrete signal generators must follow, ensuring a consistent interface (generate_signalmethod). - An Enum (
SignalType) standardizes the output, making the signals clear and reliable. - Strategy-specific parameters should be passed into the constructor (
__init__) of the concrete class, keeping the interface clean and the concerns separated.
Preview of the Next Lesson:
With our abstract SignalGenerator interface designed, our next step is to build our first concrete implementation. Your goal is to build a system for credit spreads, which are often sold when implied volatility is high. Therefore, a logical first signal is one based on volatility. In the next lesson, we will begin this process by implementing the first component needed for such a signal: calculating the historical volatility of the underlying asset from recent price data.