Kaufmanトレンド戦略
Kaufmanトレンド戦略はカルマンフィルターを使用して価格とモメンタムを推定します。トレンド強度はフィルターの速度成分から導出され、直近のウィンドウ内で正規化されます。強いトレンド条件がフィルター値の上下いずれかと一致するときにエントリーします。ストップは直近のスイング高値・安値にATRを加減したものに基づき、モメンタムが弱まるにつれて段階的に利益確定します。
詳細
- エントリー条件: トレンド強度のしきい値とフィルター値に対する価格の位置。
- ロング/ショート: 両方向。
- エグジット条件: 段階的な利益確定とトレンド弱化またはストップ到達。
- ストップ: あり、スイング安値/高値 ± ATR。
- デフォルト値:
TakeProfit1Percent = 50TakeProfit2Percent = 25TakeProfit3Percent = 25SwingLookback = 10AtrPeriod = 14TrendStrengthEntry = 60TrendStrengthExit = 40CandleType = TimeSpan.FromMinutes(15).TimeFrame()
- フィルター:
- カテゴリ: トレンドフォロー
- 方向: 両方
- インジケーター: Kalman
- ストップ: はい
- 複雑さ: 中級
- 時間軸: イントラデイ (15m)
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
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.
/// A two-state Kalman filter estimates price and its velocity. Trend strength is the velocity divided by the largest absolute
/// velocity of the last OscBufferLength candles, in percent. A long opens when strength reaches TrendStrengthEntry with the close above
/// the filtered price and a short in the mirror case, reversing an opposite position. The stop sits at the SwingLookback low (high)
/// minus (plus) ATR. While in profit, TakeProfit1Percent of the entry volume closes when strength falls below TrendStrengthEntry and
/// TakeProfit2Percent when it falls below the midpoint of the two thresholds; the rest closes below TrendStrengthExit.
/// </summary>
public class KaufmanTrendStrategy : Strategy
{
private readonly StrategyParam<decimal> _takeProfit1Percent;
private readonly StrategyParam<decimal> _takeProfit2Percent;
private readonly StrategyParam<decimal> _takeProfit3Percent;
private readonly StrategyParam<int> _swingLookback;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _trendStrengthEntry;
private readonly StrategyParam<decimal> _trendStrengthExit;
private readonly StrategyParam<decimal> _processNoise;
private readonly StrategyParam<decimal> _measurementNoise;
private readonly StrategyParam<int> _oscBufferLength;
private readonly StrategyParam<DataType> _candleType;
private Highest _swingHigh;
private Lowest _swingLow;
private Highest _oscMax;
private decimal? _filtered;
private decimal _velocity;
private decimal _p00;
private decimal _p01;
private decimal _p10;
private decimal _p11;
private decimal? _stopPrice;
private decimal _entryPrice;
private decimal _entryVolume;
private int _stage;
/// <summary>
/// Percent of the entry volume closed at the first stage.
/// </summary>
public decimal TakeProfit1Percent
{
get => _takeProfit1Percent.Value;
set => _takeProfit1Percent.Value = value;
}
/// <summary>
/// Percent of the entry volume closed at the second stage.
/// </summary>
public decimal TakeProfit2Percent
{
get => _takeProfit2Percent.Value;
set => _takeProfit2Percent.Value = value;
}
/// <summary>
/// Percent of the entry volume closed at the final stage.
/// </summary>
public decimal TakeProfit3Percent
{
get => _takeProfit3Percent.Value;
set => _takeProfit3Percent.Value = value;
}
/// <summary>
/// Candles the swing high/low spans.
/// </summary>
public int SwingLookback
{
get => _swingLookback.Value;
set => _swingLookback.Value = value;
}
/// <summary>
/// ATR period.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Trend strength required for entries.
/// </summary>
public decimal TrendStrengthEntry
{
get => _trendStrengthEntry.Value;
set => _trendStrengthEntry.Value = value;
}
/// <summary>
/// Trend strength below which the position closes.
/// </summary>
public decimal TrendStrengthExit
{
get => _trendStrengthExit.Value;
set => _trendStrengthExit.Value = value;
}
/// <summary>
/// Kalman process noise.
/// </summary>
public decimal ProcessNoise
{
get => _processNoise.Value;
set => _processNoise.Value = value;
}
/// <summary>
/// Kalman measurement noise.
/// </summary>
public decimal MeasurementNoise
{
get => _measurementNoise.Value;
set => _measurementNoise.Value = value;
}
/// <summary>
/// Candles the trend strength normalization spans.
/// </summary>
public int OscBufferLength
{
get => _oscBufferLength.Value;
set => _oscBufferLength.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="KaufmanTrendStrategy"/>.
/// </summary>
public KaufmanTrendStrategy()
{
_takeProfit1Percent = Param(nameof(TakeProfit1Percent), 50m)
.SetNotNegative()
.SetDisplay("Take Profit 1 %", "Percent of the entry volume closed at the first stage", "Exits");
_takeProfit2Percent = Param(nameof(TakeProfit2Percent), 25m)
.SetNotNegative()
.SetDisplay("Take Profit 2 %", "Percent of the entry volume closed at the second stage", "Exits");
_takeProfit3Percent = Param(nameof(TakeProfit3Percent), 25m)
.SetNotNegative()
.SetDisplay("Take Profit 3 %", "Percent of the entry volume closed at the final stage", "Exits");
_swingLookback = Param(nameof(SwingLookback), 10)
.SetGreaterThanZero()
.SetDisplay("Swing Lookback", "Candles the swing high/low spans", "Risk");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR period", "Risk");
_trendStrengthEntry = Param(nameof(TrendStrengthEntry), 60m)
.SetDisplay("Trend Strength Entry", "Trend strength required for entries", "Trend");
_trendStrengthExit = Param(nameof(TrendStrengthExit), 40m)
.SetDisplay("Trend Strength Exit", "Trend strength below which the position closes", "Trend");
_processNoise = Param(nameof(ProcessNoise), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Process Noise", "Kalman process noise", "Kalman");
_measurementNoise = Param(nameof(MeasurementNoise), 500m)
.SetGreaterThanZero()
.SetDisplay("Measurement Noise", "Kalman measurement noise", "Kalman");
_oscBufferLength = Param(nameof(OscBufferLength), 10)
.SetGreaterThanZero()
.SetDisplay("Oscillator Buffer", "Candles the trend strength normalization spans", "Trend");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_filtered = null;
_velocity = 0m;
_p00 = 1m;
_p01 = 0m;
_p10 = 0m;
_p11 = 1m;
_stopPrice = null;
_entryPrice = 0m;
_entryVolume = 0m;
_stage = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var atr = new AverageTrueRange { Length = AtrPeriod };
_swingHigh = new Highest { Length = SwingLookback };
_swingLow = new Lowest { Length = SwingLookback };
_oscMax = new Highest { Length = OscBufferLength };
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 atr)
{
if (candle.State != CandleStates.Finished)
return;
var time = candle.OpenTime;
var close = candle.ClosePrice;
UpdateKalman(close);
var maxValue = _oscMax.Process(new DecimalIndicatorValue(_oscMax, Math.Abs(_velocity), time) { IsFinal = true });
var highValue = _swingHigh.Process(new DecimalIndicatorValue(_swingHigh, candle.HighPrice, time) { IsFinal = true });
var lowValue = _swingLow.Process(new DecimalIndicatorValue(_swingLow, candle.LowPrice, time) { IsFinal = true });
if (!_oscMax.IsFormed || !_swingHigh.IsFormed || !_swingLow.IsFormed || _filtered is not decimal filtered)
return;
var maxAbs = maxValue.GetValue<decimal>();
var strength = maxAbs > 0m ? _velocity / maxAbs * 100m : 0m;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var midLevel = (TrendStrengthEntry + TrendStrengthExit) / 2m;
if (Position > 0)
{
if (_stopPrice is decimal stop && candle.LowPrice <= stop || strength < TrendStrengthExit)
{
SellMarket(Position);
_stopPrice = null;
return;
}
if (close > _entryPrice && TryTakePartial(strength < TrendStrengthEntry, strength < midLevel, false))
return;
}
else if (Position < 0)
{
if (_stopPrice is decimal stop && candle.HighPrice >= stop || strength > -TrendStrengthExit)
{
BuyMarket(-Position);
_stopPrice = null;
return;
}
if (close < _entryPrice && TryTakePartial(strength > -TrendStrengthEntry, strength > -midLevel, true))
return;
}
if (strength >= TrendStrengthEntry && close > filtered && Position <= 0)
{
var stop = lowValue.GetValue<decimal>() - atr;
if (stop >= close)
return;
BuyMarket(Volume + Math.Abs(Position));
OnEntered(close, stop);
}
else if (strength <= -TrendStrengthEntry && close < filtered && Position >= 0)
{
var stop = highValue.GetValue<decimal>() + atr;
if (stop <= close)
return;
SellMarket(Volume + Math.Abs(Position));
OnEntered(close, stop);
}
}
private void OnEntered(decimal price, decimal stop)
{
_entryPrice = price;
_entryVolume = Volume;
_stopPrice = stop;
_stage = 0;
}
private bool TryTakePartial(bool firstStage, bool secondStage, bool isShort)
{
decimal percent;
if (_stage == 0 && firstStage)
percent = TakeProfit1Percent;
else if (_stage == 1 && secondStage)
percent = TakeProfit2Percent;
else
return false;
_stage++;
var volume = Math.Min(Math.Abs(Position), _entryVolume * percent / 100m);
if (volume <= 0m)
return false;
if (isShort)
BuyMarket(volume);
else
SellMarket(volume);
return true;
}
private void UpdateKalman(decimal price)
{
if (_filtered is not decimal filtered)
{
_filtered = price;
return;
}
// Constant-velocity model: predict, then correct with the new close.
var predicted = filtered + _velocity;
var p00 = _p00 + _p01 + _p10 + _p11 + ProcessNoise;
var p01 = _p01 + _p11;
var p10 = _p10 + _p11;
var p11 = _p11 + ProcessNoise;
var s = p00 + MeasurementNoise;
var k0 = p00 / s;
var k1 = p10 / s;
var innovation = price - predicted;
_filtered = predicted + k0 * innovation;
_velocity += k1 * innovation;
_p00 = (1 - k0) * p00;
_p01 = (1 - k0) * p01;
_p10 = p10 - k1 * p00;
_p11 = p11 - k1 * p01;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange, Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class kaufman_trend_strategy(Strategy):
"""
Kaufman Trend strategy.
A two-state Kalman filter estimates price and its velocity. Trend strength is the velocity divided by the largest absolute
velocity of the last OscBufferLength candles, in percent. A long opens when strength reaches TrendStrengthEntry with the close above
the filtered price and a short in the mirror case, reversing an opposite position. The stop sits at the SwingLookback low (high)
minus (plus) ATR. While in profit, TakeProfit1Percent of the entry volume closes when strength falls below TrendStrengthEntry and
TakeProfit2Percent when it falls below the midpoint of the two thresholds; the rest closes below TrendStrengthExit.
"""
def __init__(self):
super(kaufman_trend_strategy, self).__init__()
self._take_profit1_percent = self.Param("TakeProfit1Percent", 50.0).SetNotNegative().SetDisplay("Take Profit 1 %", "Percent of the entry volume closed at the first stage", "Exits")
self._take_profit2_percent = self.Param("TakeProfit2Percent", 25.0).SetNotNegative().SetDisplay("Take Profit 2 %", "Percent of the entry volume closed at the second stage", "Exits")
self._take_profit3_percent = self.Param("TakeProfit3Percent", 25.0).SetNotNegative().SetDisplay("Take Profit 3 %", "Percent of the entry volume closed at the final stage", "Exits")
self._swing_lookback = self.Param("SwingLookback", 10).SetGreaterThanZero().SetDisplay("Swing Lookback", "Candles the swing high/low spans", "Risk")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "ATR period", "Risk")
self._trend_strength_entry = self.Param("TrendStrengthEntry", 60.0).SetDisplay("Trend Strength Entry", "Trend strength required for entries", "Trend")
self._trend_strength_exit = self.Param("TrendStrengthExit", 40.0).SetDisplay("Trend Strength Exit", "Trend strength below which the position closes", "Trend")
self._process_noise = self.Param("ProcessNoise", 0.01).SetGreaterThanZero().SetDisplay("Process Noise", "Kalman process noise", "Kalman")
self._measurement_noise = self.Param("MeasurementNoise", 500.0).SetGreaterThanZero().SetDisplay("Measurement Noise", "Kalman measurement noise", "Kalman")
self._osc_buffer_length = self.Param("OscBufferLength", 10).SetGreaterThanZero().SetDisplay("Oscillator Buffer", "Candles the trend strength normalization spans", "Trend")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._swing_high = None
self._swing_low = None
self._osc_max = None
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._filtered = None
self._velocity = 0.0
self._p00 = 1.0
self._p01 = 0.0
self._p10 = 0.0
self._p11 = 1.0
self._stop_price = None
self._entry_price = 0.0
self._entry_volume = 0.0
self._stage = 0
def OnReseted(self):
super(kaufman_trend_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(kaufman_trend_strategy, self).OnStarted2(time)
self._reset_state()
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
self._swing_high = Highest()
self._swing_high.Length = self._swing_lookback.Value
self._swing_low = Lowest()
self._swing_low.Length = self._swing_lookback.Value
self._osc_max = Highest()
self._osc_max.Length = self._osc_buffer_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
time = candle.OpenTime
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
atr = float(atr_value)
self._update_kalman(close)
max_abs = float(process_float(self._osc_max, abs(self._velocity), time, True))
swing_high = float(process_float(self._swing_high, high, time, True))
swing_low = float(process_float(self._swing_low, low, time, True))
if not self._osc_max.IsFormed or not self._swing_high.IsFormed or not self._swing_low.IsFormed or self._filtered is None:
return
strength = self._velocity / max_abs * 100.0 if max_abs > 0.0 else 0.0
if not self.IsFormedAndOnlineAndAllowTrading():
return
entry_level = float(self._trend_strength_entry.Value)
exit_level = float(self._trend_strength_exit.Value)
mid_level = (entry_level + exit_level) / 2.0
if self.Position > 0:
if (self._stop_price is not None and low <= self._stop_price) or strength < exit_level:
self.SellMarket(self.Position)
self._stop_price = None
return
if close > self._entry_price and self._try_take_partial(strength < entry_level, strength < mid_level, False):
return
elif self.Position < 0:
if (self._stop_price is not None and high >= self._stop_price) or strength > -exit_level:
self.BuyMarket(-self.Position)
self._stop_price = None
return
if close < self._entry_price and self._try_take_partial(strength > -entry_level, strength > -mid_level, True):
return
if strength >= entry_level and close > self._filtered and self.Position <= 0:
stop = swing_low - atr
if stop >= close:
return
self.BuyMarket(self.Volume + abs(self.Position))
self._on_entered(close, stop)
elif strength <= -entry_level and close < self._filtered and self.Position >= 0:
stop = swing_high + atr
if stop <= close:
return
self.SellMarket(self.Volume + abs(self.Position))
self._on_entered(close, stop)
def _on_entered(self, price, stop):
self._entry_price = price
self._entry_volume = float(self.Volume)
self._stop_price = stop
self._stage = 0
def _try_take_partial(self, first_stage, second_stage, is_short):
if self._stage == 0 and first_stage:
percent = float(self._take_profit1_percent.Value)
elif self._stage == 1 and second_stage:
percent = float(self._take_profit2_percent.Value)
else:
return False
self._stage += 1
volume = min(abs(float(self.Position)), self._entry_volume * percent / 100.0)
if volume <= 0.0:
return False
if is_short:
self.BuyMarket(Decimal(volume))
else:
self.SellMarket(Decimal(volume))
return True
def _update_kalman(self, price):
if self._filtered is None:
self._filtered = price
return
pn = float(self._process_noise.Value)
mn = float(self._measurement_noise.Value)
# Constant-velocity model: predict, then correct with the new close.
predicted = self._filtered + self._velocity
p00 = self._p00 + self._p01 + self._p10 + self._p11 + pn
p01 = self._p01 + self._p11
p10 = self._p10 + self._p11
p11 = self._p11 + pn
s = p00 + mn
k0 = p00 / s
k1 = p10 / s
innovation = price - predicted
self._filtered = predicted + k0 * innovation
self._velocity += k1 * innovation
self._p00 = (1.0 - k0) * p00
self._p01 = (1.0 - k0) * p01
self._p10 = p10 - k1 * p00
self._p11 = p11 - k1 * p01
def CreateClone(self):
return kaufman_trend_strategy()