← Back to Home
Can Kalman Filters Improve Your Trading Signals

Can Kalman Filters Improve Your Trading Signals

Kalman filters offer an advanced technique for signal processing, often used to extract underlying states, like trend or velocity, from noisy data. Applying this to financial markets allows us to estimate price movements potentially more adaptively than standard indicators.

This article details a backtrader strategy using a Kalman filter (via a custom KalmanFilterIndicator) to estimate price velocity. The strategy enters trades based on the sign of this estimated velocity and relies exclusively on a trailing stop-loss for exits.

Strategy Logic Overview:

  1. Filtering: A KalmanFilterIndicator estimates the underlying price and its velocity based on closing prices.
  2. Entry Signal: Enter long if the estimated velocity turns positive (> 0). Enter short if the velocity turns negative (< 0). Entries only happen when flat.
  3. Exit Signal: A percentage-based trailing stop-loss (trail_percent) manages exits. Once a position is open, the Kalman velocity sign is ignored for exiting.

The Supporting Indicator: KalmanFilterIndicator

(The code for KalmanFilterIndicator as you provided it is assumed here. It calculates kf_price and kf_velocity and is set to plot on the main chart panel using plotinfo = dict(subplot=False)).

The Strategy Class: KalmanFilterTrendWithTrail

This class orchestrates the trading logic using the indicator’s output.

1. Parameters (params)

These allow configuration of the filter and the trailing stop.

# --- Inside KalmanFilterTrendWithTrail class ---
    params = (
        # Parameters passed to the Kalman Filter Indicator
        ('process_noise', 1e-5),    # Filter parameter: Assumed noise in the price model (Q)
        ('measurement_noise', 1e-1),# Filter parameter: Assumed noise in the price data (R)

        # Strategy-specific parameter
        ('trail_percent', 0.05),    # Trailing stop loss percentage (e.g., 0.05 = 5%)
        ('printlog', True),         # Enable logging output
    )

2. Initialization (__init__)

Sets up the strategy by creating the indicator instance.

# --- Inside KalmanFilterTrendWithTrail class ---
    def __init__(self):
        # Instantiate the Kalman Filter Indicator, passing relevant parameters
        self.kf = KalmanFilterIndicator(
            process_noise=self.p.process_noise,
            measurement_noise=self.p.measurement_noise
        )

        # Create convenient references to the indicator's output lines
        self.kf_price = self.kf.lines.kf_price
        self.kf_velocity = self.kf.lines.kf_velocity

        # Initialize order trackers
        self.order = None # Tracks pending entry orders
        self.stop_order = None # Tracks pending stop orders

        if self.params.printlog:
            # Log the parameters being used
            print(f"Strategy Parameters: Process Noise={self.params.process_noise}, "
                  f"Measurement Noise={self.params.measurement_noise}, "
                  f"Trail Percent={self.params.trail_percent * 100:.2f}%")

3. Entry Logic (next)

The next method contains the core logic executed on each bar. For entries, it checks the position status and the Kalman velocity sign.

# --- Inside KalmanFilterTrendWithTrail class ---
    def next(self):
        # If an entry order is pending, do nothing
        if self.order:
            return

        # Get the estimated velocity from the indicator
        # Need to check length because indicator might need warmup
        if len(self.kf_velocity) == 0:
             return # Indicator not ready yet

        estimated_velocity = self.kf_velocity[0]
        current_position_size = self.position.size
        current_close = self.data.close[0] # For logging

        # --- Trading Logic ---
        # Only evaluate entries if FLAT
        if current_position_size == 0:
            if self.stop_order: # Safety check - cancel any stray stop orders if flat
                self.log("Warning: Position flat but stop order exists. Cancelling.", doprint=True)
                self.cancel(self.stop_order)
                self.stop_order = None

            # --- Entry Signal Check ---
            if estimated_velocity > 0:
                 # Positive velocity -> Go Long
                 self.log(f'BUY CREATE (KF Vel > 0), Close={current_close:.2f}, KF Vel={estimated_velocity:.4f}', doprint=True)
                 self.order = self.buy() # Place buy order and track it
            elif estimated_velocity < 0:
                 # Negative velocity -> Go Short
                 self.log(f'SELL CREATE (KF Vel < 0 - Short Entry), Close={current_close:.2f}, KF Vel={estimated_velocity:.4f}', doprint=True)
                 self.order = self.sell() # Place sell order and track it
        else:
             # If already in a position, do nothing here.
             # The trailing stop placed via notify_order handles the exit.
             pass

This logic is straightforward: if flat, buy on positive velocity, sell on negative velocity. If already in a position, it relies entirely on the trailing stop.

4. Exit Logic (notify_order)

Exits are handled by placing a StopTrail order immediately after an entry order is successfully filled. This logic resides within the notify_order method.

# --- Inside KalmanFilterTrendWithTrail class ---
    def notify_order(self, order):
        # (Initial checks for Submitted/Accepted status omitted for brevity)
        ...
        if order.status == order.Completed:
            # Check if it's the ENTRY order we were waiting for
            if self.order and order.ref == self.order.ref:
                entry_type = "BUY" if order.isbuy() else "SELL"
                exit_func = self.sell if order.isbuy() else self.buy # Determine exit order type

                # Log entry execution (code omitted for brevity)
                ...

                # Place the TRAILING STOP order if trail_percent is valid
                if self.p.trail_percent and self.p.trail_percent > 0.0:
                    self.stop_order = exit_func(exectype=bt.Order.StopTrail,
                                                trailpercent=self.p.trail_percent)
                    self.log(f'Trailing Stop Placed for {entry_type} order ref {self.stop_order.ref} at {self.p.trail_percent * 100:.2f}% trail', doprint=True)
                else:
                     self.log(f'No Trailing Stop Placed (trail_percent={self.p.trail_percent})', doprint=True)

                self.order = None # Reset entry order tracker

            # Check if it's the STOP order that completed
            elif self.stop_order and order.ref == self.stop_order.ref:
                # Log stop execution (code omitted for brevity)
                ...
                self.stop_order = None # Reset stop order tracker
                self.order = None # Reset entry tracker too
        # Handle Failed orders (code omitted for brevity)
        ...

This ensures that as soon as an entry trade is confirmed, the trailing stop is activated to manage the exit.

Running the Backtest

The __main__ block in your provided code sets up cerebro, fetches data (BTC-USD, 2021-2023), configures the broker/sizer/analyzers, and runs the strategy with specific parameters (process_noise=0.001, measurement_noise=0.5, trail_percent=0.02). It then prints performance metrics and attempts to plot the results, including the Kalman Filter price overlayed on the main chart.

Pasted image 20250424185531.png

Tuning and Considerations

Conclusion

This backtrader strategy demonstrates using a Kalman filter’s velocity estimate for trend direction signals, combined with a trailing stop for risk management. While conceptually interesting, its practical effectiveness hinges critically on careful parameter tuning and understanding its limitations, particularly the sensitivity to noise when using only the velocity sign for entries.