Keltner Channel Grid-Strategie
Diese Strategie passt die Positionsgröße basierend auf dem relativen Abstand des Preises von einem gleitenden Durchschnitt innerhalb eines Keltner Channels an. Weicht der Kurs vom Mittelwert ab, wird die Position über ein Grid mit Marktorders neu ausbalanciert.
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>
/// Keltner Channel based grid rebalancing strategy.
/// </summary>
public class KeltnerChannelBasedGridStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _gridCoefficient;
private readonly StrategyParam<int> _numGrids;
private readonly StrategyParam<bool> _useExponential;
private readonly StrategyParam<int> _maxRebalances;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private int _rebalanceCount;
private int _barsSinceRebalance;
public int Length { get => _length.Value; set => _length.Value = value; }
public decimal GridCoefficient { get => _gridCoefficient.Value; set => _gridCoefficient.Value = value; }
public int NumGrids { get => _numGrids.Value; set => _numGrids.Value = value; }
public bool UseExponential { get => _useExponential.Value; set => _useExponential.Value = value; }
public int MaxRebalances { get => _maxRebalances.Value; set => _maxRebalances.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public KeltnerChannelBasedGridStrategy()
{
_length = Param(nameof(Length), 10)
.SetGreaterThanZero()
.SetDisplay("Length", "MA and ATR period", "Keltner")
.SetOptimize(5, 20, 5);
_gridCoefficient = Param(nameof(GridCoefficient), 1.33m)
.SetGreaterThanZero()
.SetDisplay("Grid coeff", "Multiplier for channel width", "Keltner")
.SetOptimize(1m, 2m, 0.1m);
_numGrids = Param(nameof(NumGrids), 12)
.SetGreaterThanZero()
.SetDisplay("Grids", "Number of grid levels", "Strategy")
.SetOptimize(5, 20, 1);
_useExponential = Param(nameof(UseExponential), true)
.SetDisplay("Use EMA", "Use EMA instead of SMA", "Keltner");
_maxRebalances = Param(nameof(MaxRebalances), 45)
.SetGreaterThanZero()
.SetDisplay("Max Rebalances", "Maximum rebalance orders per run", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 240)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Minimum bars between rebalances", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).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();
_rebalanceCount = 0;
_barsSinceRebalance = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rebalanceCount = 0;
_barsSinceRebalance = CooldownBars;
IIndicator ma = UseExponential ? new EMA { Length = Length } : new SMA { Length = Length };
var atr = new AverageTrueRange { Length = Length };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ma, atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ma);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal ma, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
_barsSinceRebalance++;
if (atrValue == 0)
return;
var bandWidth = atrValue * GridCoefficient;
var kcRate = (candle.ClosePrice - ma) / bandWidth;
var maxAmount = Portfolio?.CurrentValue / candle.ClosePrice ?? 0m;
var targetPosition = kcRate * maxAmount * -1m;
if (targetPosition > maxAmount)
targetPosition = maxAmount;
else if (targetPosition < -maxAmount)
targetPosition = -maxAmount;
var diff = Math.Abs(targetPosition - Position);
if (_rebalanceCount < MaxRebalances && _barsSinceRebalance >= CooldownBars && diff >= maxAmount / NumGrids)
{
if (targetPosition > Position)
BuyMarket(targetPosition - Position);
else if (targetPosition < Position)
SellMarket(Position - targetPosition);
_rebalanceCount++;
_barsSinceRebalance = 0;
}
}
}
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 ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class keltner_channel_based_grid_strategy(Strategy):
def __init__(self):
super(keltner_channel_based_grid_strategy, self).__init__()
self._length = self.Param("Length", 10) \
.SetGreaterThanZero() \
.SetDisplay("Length", "MA and ATR period", "Keltner")
self._grid_coeff = self.Param("GridCoefficient", 1.33) \
.SetGreaterThanZero() \
.SetDisplay("Grid coeff", "Multiplier for channel width", "Keltner")
self._num_grids = self.Param("NumGrids", 12) \
.SetGreaterThanZero() \
.SetDisplay("Grids", "Number of grid levels", "Strategy")
self._max_rebalances = self.Param("MaxRebalances", 45) \
.SetGreaterThanZero() \
.SetDisplay("Max Rebalances", "Maximum rebalance orders per run", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 240) \
.SetGreaterThanZero() \
.SetDisplay("Cooldown Bars", "Minimum bars between rebalances", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle type", "Type of candles", "General")
self._rebalance_count = 0
self._bars_since_rebalance = 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(keltner_channel_based_grid_strategy, self).OnReseted()
self._rebalance_count = 0
self._bars_since_rebalance = 0
def OnStarted2(self, time):
super(keltner_channel_based_grid_strategy, self).OnStarted2(time)
self._rebalance_count = 0
self._bars_since_rebalance = self._cooldown_bars.Value
ma = ExponentialMovingAverage()
ma.Length = self._length.Value
atr = AverageTrueRange()
atr.Length = self._length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ma, atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma)
self.DrawOwnTrades(area)
def OnProcess(self, candle, ma_val, atr_val):
if candle.State != CandleStates.Finished:
return
self._bars_since_rebalance += 1
ma_v = float(ma_val)
atr_v = float(atr_val)
if atr_v == 0.0:
return
close = float(candle.ClosePrice)
gc = float(self._grid_coeff.Value)
band_width = atr_v * gc
kc_rate = (close - ma_v) / band_width
target_pos = -kc_rate
if self._rebalance_count < self._max_rebalances.Value and self._bars_since_rebalance >= self._cooldown_bars.Value:
if target_pos > 0 and self.Position <= 0:
self.BuyMarket()
self._rebalance_count += 1
self._bars_since_rebalance = 0
elif target_pos < 0 and self.Position >= 0:
self.SellMarket()
self._rebalance_count += 1
self._bars_since_rebalance = 0
def CreateClone(self):
return keltner_channel_based_grid_strategy()