Bollinger Kalman Filter
The Bollinger Kalman Filter strategy is built around Bollinger Kalman Filter.
Signals trigger when Bollinger confirms filtered entries on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like BollingerLength, BollingerDeviation. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
BollingerLength = 20BollingerDeviation = 2.0mKalmanQ = 0.01mKalmanR = 0.1mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Bollinger
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bollinger Bands with Kalman Filter Strategy.
/// Enters positions when price is at Bollinger extremes and confirmed by Kalman Filter trend direction.
/// </summary>
public class BollingerKalmanFilterStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerLength;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<decimal> _kalmanQ; // Process noise
private readonly StrategyParam<decimal> _kalmanR; // Measurement noise
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalCooldownBars;
private static readonly object _sync = new();
private decimal _upperBand;
private decimal _lowerBand;
private decimal _midBand;
private decimal _kalmanValue;
private decimal? _previousKalmanValue;
private int _cooldownRemaining;
/// <summary>
/// Bollinger Bands length.
/// </summary>
public int BollingerLength
{
get => _bollingerLength.Value;
set => _bollingerLength.Value = value;
}
/// <summary>
/// Bollinger Bands deviation.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Kalman Filter process noise.
/// </summary>
public decimal KalmanQ
{
get => _kalmanQ.Value;
set => _kalmanQ.Value = value;
}
/// <summary>
/// Kalman Filter measurement noise.
/// </summary>
public decimal KalmanR
{
get => _kalmanR.Value;
set => _kalmanR.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Closed candles to wait before taking the next signal.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Initialize strategy.
/// </summary>
public BollingerKalmanFilterStrategy()
{
_bollingerLength = Param(nameof(BollingerLength), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Length", "Length of the Bollinger Bands", "Bollinger Settings")
.SetOptimize(10, 30, 5);
_bollingerDeviation = Param(nameof(BollingerDeviation), 2.0m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands", "Bollinger Settings")
.SetOptimize(1.5m, 2.5m, 0.5m);
_kalmanQ = Param(nameof(KalmanQ), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Kalman Q", "Process noise for Kalman Filter", "Kalman Filter Settings")
.SetOptimize(0.001m, 0.1m, 0.01m);
_kalmanR = Param(nameof(KalmanR), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Kalman R", "Measurement noise for Kalman Filter", "Kalman Filter Settings")
.SetOptimize(0.01m, 1.0m, 0.1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 3)
.SetNotNegative()
.SetDisplay("Signal Cooldown", "Closed candles to wait before the next entry", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_upperBand = 0;
_lowerBand = 0;
_midBand = 0;
_kalmanValue = 0;
_previousKalmanValue = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var bollinger = new BollingerBands
{
Length = BollingerLength,
Width = BollingerDeviation
};
var kalmanFilter = new KalmanFilter
{
ProcessNoise = KalmanQ,
MeasurementNoise = KalmanR
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(candle => ProcessCandle(candle, bollinger, kalmanFilter))
.Start();
// Start position protection
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, bollinger);
DrawIndicator(area, kalmanFilter);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, BollingerBands bollinger, KalmanFilter kalmanFilter)
{
if (candle.State != CandleStates.Finished)
return;
lock (_sync)
{
var bollingerValue = bollinger.Process(new CandleIndicatorValue(bollinger, candle) { IsFinal = true });
var kalmanValue = kalmanFilter.Process(new DecimalIndicatorValue(kalmanFilter, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
if (!bollingerValue.IsFinal || !kalmanValue.IsFinal || !bollinger.IsFormed || !kalmanFilter.IsFormed)
return;
if (bollingerValue is not BollingerBandsValue bands ||
bands.UpBand is not decimal upperBand ||
bands.LowBand is not decimal lowerBand ||
bands.MovingAverage is not decimal midBand)
{
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var kalmanFilterValue = kalmanValue.ToDecimal();
var kalmanTrendUp = _previousKalmanValue is decimal previous && kalmanFilterValue > previous;
var kalmanTrendDown = _previousKalmanValue is decimal prior && kalmanFilterValue < prior;
_upperBand = upperBand;
_lowerBand = lowerBand;
_midBand = midBand;
_kalmanValue = kalmanFilterValue;
if (_cooldownRemaining == 0 && candle.LowPrice <= lowerBand && kalmanTrendUp && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && candle.HighPrice >= upperBand && kalmanTrendDown && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (Position > 0 && candle.ClosePrice >= midBand)
{
SellMarket(Position);
}
else if (Position < 0 && candle.ClosePrice <= midBand)
{
BuyMarket(-Position);
}
_previousKalmanValue = kalmanFilterValue;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import BollingerBands, KalmanFilter, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class bollinger_kalman_filter_strategy(Strategy):
"""
Bollinger Bands with Kalman Filter Strategy.
Enters positions when price is at Bollinger extremes and confirmed by Kalman Filter trend direction.
"""
def __init__(self):
super(bollinger_kalman_filter_strategy, self).__init__()
self._bollinger_length = self.Param("BollingerLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Length", "Length of the Bollinger Bands", "Bollinger Settings") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._bollinger_deviation = self.Param("BollingerDeviation", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands", "Bollinger Settings") \
.SetCanOptimize(True) \
.SetOptimize(1.5, 2.5, 0.5)
self._kalman_q = self.Param("KalmanQ", 0.01) \
.SetGreaterThanZero() \
.SetDisplay("Kalman Q", "Process noise for Kalman Filter", "Kalman Filter Settings") \
.SetCanOptimize(True) \
.SetOptimize(0.001, 0.1, 0.01)
self._kalman_r = self.Param("KalmanR", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Kalman R", "Measurement noise for Kalman Filter", "Kalman Filter Settings") \
.SetCanOptimize(True) \
.SetOptimize(0.01, 1.0, 0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 3) \
.SetNotNegative() \
.SetDisplay("Signal Cooldown", "Closed candles to wait before the next entry", "General")
self._upper_band = 0.0
self._lower_band = 0.0
self._mid_band = 0.0
self._kalman_value = 0.0
self._previous_kalman_value = None
self._cooldown_remaining = 0
self._bollinger = None
self._kalman_filter = None
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(bollinger_kalman_filter_strategy, self).OnReseted()
self._upper_band = 0.0
self._lower_band = 0.0
self._mid_band = 0.0
self._kalman_value = 0.0
self._previous_kalman_value = None
self._cooldown_remaining = 0
self._bollinger = None
self._kalman_filter = None
def OnStarted2(self, time):
super(bollinger_kalman_filter_strategy, self).OnStarted2(time)
self._bollinger = BollingerBands()
self._bollinger.Length = int(self._bollinger_length.Value)
self._bollinger.Width = Decimal(self._bollinger_deviation.Value)
self._kalman_filter = KalmanFilter()
self._kalman_filter.ProcessNoise = Decimal(self._kalman_q.Value)
self._kalman_filter.MeasurementNoise = Decimal(self._kalman_r.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.ProcessCandle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent)
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._bollinger)
self.DrawIndicator(area, self._kalman_filter)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
biv = CandleIndicatorValue(self._bollinger, candle)
biv.IsFinal = True
bollinger_val = self._bollinger.Process(biv)
kalman_val = process_float(self._kalman_filter, candle.ClosePrice, candle.OpenTime, True)
if not bollinger_val.IsFinal or not kalman_val.IsFinal or not self._bollinger.IsFormed or not self._kalman_filter.IsFormed:
return
if bollinger_val.UpBand is None or bollinger_val.LowBand is None or bollinger_val.MovingAverage is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
upper_band = float(bollinger_val.UpBand)
lower_band = float(bollinger_val.LowBand)
mid_band = float(bollinger_val.MovingAverage)
kalman_filter_value = float(kalman_val)
kalman_trend_up = self._previous_kalman_value is not None and kalman_filter_value > self._previous_kalman_value
kalman_trend_down = self._previous_kalman_value is not None and kalman_filter_value < self._previous_kalman_value
self._upper_band = upper_band
self._lower_band = lower_band
self._mid_band = mid_band
self._kalman_value = kalman_filter_value
cooldown_bars = int(self._signal_cooldown_bars.Value)
if self._cooldown_remaining == 0 and float(candle.LowPrice) <= lower_band and kalman_trend_up and self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
self._cooldown_remaining = cooldown_bars
elif self._cooldown_remaining == 0 and float(candle.HighPrice) >= upper_band and kalman_trend_down and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._cooldown_remaining = cooldown_bars
elif self.Position > 0 and float(candle.ClosePrice) >= mid_band:
self.SellMarket(self.Position)
elif self.Position < 0 and float(candle.ClosePrice) <= mid_band:
self.BuyMarket(Math.Abs(self.Position))
self._previous_kalman_value = kalman_filter_value
def CreateClone(self):
return bollinger_kalman_filter_strategy()