Kaufman Trend Strategy
The Kaufman Trend Strategy uses a Kalman filter to estimate price and momentum. Trend strength is derived from the filter's velocity component and normalized over a recent window. Entries occur when strong trend conditions align with price being above or below the filtered value. Stops are based on recent swings plus ATR and profits are taken in stages as momentum weakens.
Details
- Entry Criteria: trend strength threshold with price above/below the filtered value.
- Long/Short: Both directions.
- Exit Criteria: staged take profits and trend weakening or stop hit.
- Stops: yes, swing low/high minus/plus ATR.
- Default Values:
TakeProfit1Percent = 50TakeProfit2Percent = 25TakeProfit3Percent = 25SwingLookback = 10AtrPeriod = 14TrendStrengthEntry = 60TrendStrengthExit = 40CandleType = TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Kalman
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (15m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Kaufman Trend strategy using Kalman filter for trend detection.
/// </summary>
public class KaufmanTrendStrategy : Strategy
{
private readonly StrategyParam<int> _trendStrengthEntry;
private readonly StrategyParam<int> _trendStrengthExit;
private readonly StrategyParam<decimal> _processNoise;
private readonly StrategyParam<decimal> _measurementNoise;
private readonly StrategyParam<int> _oscBufferLength;
private readonly StrategyParam<int> _maxEntries;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _filteredSrc;
private decimal _oscillator;
private decimal _p00 = 1m;
private decimal _p01;
private decimal _p10;
private decimal _p11 = 1m;
private decimal _oscAbsAverage;
private int _warmupCount;
private int _entriesExecuted;
private int _barsSinceSignal;
public int TrendStrengthEntry
{
get => _trendStrengthEntry.Value;
set => _trendStrengthEntry.Value = value;
}
public int TrendStrengthExit
{
get => _trendStrengthExit.Value;
set => _trendStrengthExit.Value = value;
}
public decimal ProcessNoise
{
get => _processNoise.Value;
set => _processNoise.Value = value;
}
public decimal MeasurementNoise
{
get => _measurementNoise.Value;
set => _measurementNoise.Value = value;
}
public int OscBufferLength
{
get => _oscBufferLength.Value;
set => _oscBufferLength.Value = value;
}
public int MaxEntries
{
get => _maxEntries.Value;
set => _maxEntries.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public KaufmanTrendStrategy()
{
_trendStrengthEntry = Param(nameof(TrendStrengthEntry), 80)
.SetDisplay("Trend Strength Entry", "Entry threshold.", "Trend");
_trendStrengthExit = Param(nameof(TrendStrengthExit), 20)
.SetDisplay("Trend Strength Exit", "Exit threshold.", "Trend");
_processNoise = Param(nameof(ProcessNoise), 0.01m)
.SetDisplay("Process Noise", "Kalman process noise.", "Kalman");
_measurementNoise = Param(nameof(MeasurementNoise), 500m)
.SetDisplay("Measurement Noise", "Observation noise.", "Kalman");
_oscBufferLength = Param(nameof(OscBufferLength), 10)
.SetDisplay("Oscillator Buffer", "Bars for normalization.", "Trend");
_maxEntries = Param(nameof(MaxEntries), 45)
.SetDisplay("Max Entries", "Maximum entries per run.", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 300)
.SetDisplay("Cooldown Bars", "Minimum bars between entries.", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_filteredSrc = 0m;
_oscillator = 0m;
_p00 = 1m;
_p01 = 0m;
_p10 = 0m;
_p11 = 1m;
_oscAbsAverage = 0m;
_warmupCount = 0;
_entriesExecuted = 0;
_barsSinceSignal = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_filteredSrc = 0m;
_oscillator = 0m;
_p00 = 1m;
_p01 = 0m;
_p10 = 0m;
_p11 = 1m;
_oscAbsAverage = 0m;
_warmupCount = 0;
_entriesExecuted = 0;
_barsSinceSignal = CooldownBars;
var atr = new AverageTrueRange { Length = 14 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
_barsSinceSignal++;
UpdateKalman(candle.ClosePrice);
var absOsc = Math.Abs(_oscillator);
if (_warmupCount == 0)
{
_oscAbsAverage = absOsc;
}
else
{
var alpha = 2m / (OscBufferLength + 1m);
_oscAbsAverage += (absOsc - _oscAbsAverage) * alpha;
}
_warmupCount++;
var trendStrength = _oscAbsAverage > 0m ? _oscillator / _oscAbsAverage * 100m : 0m;
if (_warmupCount < OscBufferLength)
return;
var priceAboveMa = candle.ClosePrice > _filteredSrc;
var priceBelowMa = candle.ClosePrice < _filteredSrc;
var trendStrongLong = trendStrength >= TrendStrengthEntry;
var trendStrongShort = trendStrength <= -TrendStrengthEntry;
var trendWeakLong = trendStrength < TrendStrengthExit;
var trendWeakShort = trendStrength > -TrendStrengthExit;
// Exit logic
if (Position > 0 && trendWeakLong)
{
SellMarket(Math.Abs(Position));
_barsSinceSignal = 0;
}
else if (Position < 0 && trendWeakShort)
{
BuyMarket(Math.Abs(Position));
_barsSinceSignal = 0;
}
// Entry logic
if (Position == 0 && _entriesExecuted < MaxEntries && _barsSinceSignal >= CooldownBars)
{
if (trendStrongLong && priceAboveMa)
{
BuyMarket();
_entriesExecuted++;
_barsSinceSignal = 0;
}
else if (trendStrongShort && priceBelowMa)
{
SellMarket();
_entriesExecuted++;
_barsSinceSignal = 0;
}
}
}
private void UpdateKalman(decimal price)
{
if (_filteredSrc == 0m)
{
_filteredSrc = price;
return;
}
_filteredSrc += _oscillator;
var p00p = _p00 + _p01 + _p10 + _p11 + ProcessNoise;
var p01p = _p01 + _p11;
var p10p = _p10 + _p11;
var p11p = _p11 + ProcessNoise;
var s = p00p + MeasurementNoise;
if (s == 0m) return;
var k0 = p00p / s;
var k1 = p10p / s;
var innovation = price - _filteredSrc;
_filteredSrc += k0 * innovation;
_oscillator += k1 * innovation;
_p00 = (1 - k0) * p00p;
_p01 = (1 - k0) * p01p;
_p10 = p10p - k1 * p00p;
_p11 = p11p - k1 * p01p;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class kaufman_trend_strategy(Strategy):
def __init__(self):
super(kaufman_trend_strategy, self).__init__()
self._trend_strength_entry = self.Param("TrendStrengthEntry", 80) \
.SetDisplay("Trend Strength Entry", "Entry threshold", "Trend")
self._trend_strength_exit = self.Param("TrendStrengthExit", 20) \
.SetDisplay("Trend Strength Exit", "Exit threshold", "Trend")
self._process_noise = self.Param("ProcessNoise", 0.01) \
.SetDisplay("Process Noise", "Kalman process noise", "Kalman")
self._measurement_noise = self.Param("MeasurementNoise", 500.0) \
.SetDisplay("Measurement Noise", "Observation noise", "Kalman")
self._osc_buffer_length = self.Param("OscBufferLength", 10) \
.SetDisplay("Oscillator Buffer", "Bars for normalization", "Trend")
self._max_entries = self.Param("MaxEntries", 45) \
.SetDisplay("Max Entries", "Maximum entries per run", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 300) \
.SetDisplay("Cooldown Bars", "Minimum bars between entries", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._filtered_src = 0.0
self._oscillator = 0.0
self._p00 = 1.0
self._p01 = 0.0
self._p10 = 0.0
self._p11 = 1.0
self._osc_abs_average = 0.0
self._warmup_count = 0
self._entries_executed = 0
self._bars_since_signal = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(kaufman_trend_strategy, self).OnReseted()
self._filtered_src = 0.0
self._oscillator = 0.0
self._p00 = 1.0
self._p01 = 0.0
self._p10 = 0.0
self._p11 = 1.0
self._osc_abs_average = 0.0
self._warmup_count = 0
self._entries_executed = 0
self._bars_since_signal = 0
def OnStarted2(self, time):
super(kaufman_trend_strategy, self).OnStarted2(time)
self._entries_executed = 0
self._bars_since_signal = self._cooldown_bars.Value
atr = AverageTrueRange()
atr.Length = 14
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _update_kalman(self, price):
pn = float(self._process_noise.Value)
mn = float(self._measurement_noise.Value)
if self._filtered_src == 0.0:
self._filtered_src = price
return
self._filtered_src += self._oscillator
p00p = self._p00 + self._p01 + self._p10 + self._p11 + pn
p01p = self._p01 + self._p11
p10p = self._p10 + self._p11
p11p = self._p11 + pn
s = p00p + mn
if s == 0.0:
return
k0 = p00p / s
k1 = p10p / s
innovation = price - self._filtered_src
self._filtered_src += k0 * innovation
self._oscillator += k1 * innovation
self._p00 = (1.0 - k0) * p00p
self._p01 = (1.0 - k0) * p01p
self._p10 = p10p - k1 * p00p
self._p11 = p11p - k1 * p01p
def OnProcess(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
self._bars_since_signal += 1
close = float(candle.ClosePrice)
self._update_kalman(close)
abs_osc = abs(self._oscillator)
osc_buf = self._osc_buffer_length.Value
if self._warmup_count == 0:
self._osc_abs_average = abs_osc
else:
alpha = 2.0 / (osc_buf + 1.0)
self._osc_abs_average += (abs_osc - self._osc_abs_average) * alpha
self._warmup_count += 1
if self._osc_abs_average > 0.0:
trend_strength = self._oscillator / self._osc_abs_average * 100.0
else:
trend_strength = 0.0
if self._warmup_count < osc_buf:
return
entry_th = float(self._trend_strength_entry.Value)
exit_th = float(self._trend_strength_exit.Value)
price_above = close > self._filtered_src
price_below = close < self._filtered_src
strong_long = trend_strength >= entry_th
strong_short = trend_strength <= -entry_th
weak_long = trend_strength < exit_th
weak_short = trend_strength > -exit_th
if self.Position > 0 and weak_long:
self.SellMarket()
self._bars_since_signal = 0
elif self.Position < 0 and weak_short:
self.BuyMarket()
self._bars_since_signal = 0
if self.Position == 0 and self._entries_executed < self._max_entries.Value and self._bars_since_signal >= self._cooldown_bars.Value:
if strong_long and price_above:
self.BuyMarket()
self._entries_executed += 1
self._bars_since_signal = 0
elif strong_short and price_below:
self.SellMarket()
self._entries_executed += 1
self._bars_since_signal = 0
def CreateClone(self):
return kaufman_trend_strategy()