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:
KalmanFilterIndicator
estimates the underlying price and its velocity based on closing
prices.> 0
). Enter short if the velocity turns
negative (< 0
). Entries only happen when flat.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
( )
process_noise
& measurement_noise
:
Control the Kalman filter’s behavior. Finding good values requires
testing and optimization specific to the asset and timeframe.trail_percent
: Determines the percentage drawdown from
the peak price (for longs) or trough price (for shorts) that triggers
the stop-loss.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(
=self.p.process_noise,
process_noise=self.p.measurement_noise
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
= self.kf_velocity[0]
estimated_velocity = self.position.size
current_position_size = self.data.close[0] # For logging
current_close
# --- 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:
= "BUY" if order.isbuy() else "SELL"
entry_type = self.sell if order.isbuy() else self.buy # Determine exit order type
exit_func
# 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,
=self.p.trail_percent)
trailpercentself.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.
Tuning and Considerations
process_noise
,
measurement_noise
, and trail_percent
parameters. The values used (0.001
, 0.5
,
0.02
) are specific examples and likely require optimization
for different market conditions or assets.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.