ExpXmaRangeBands-Strategie
Diese Strategie repliziert die Logik des MetaTrader-Beispiels „Exp_XMA_Range_Bands" mit der StockSharp High-Level API. Sie verwendet einen Keltner-Kanal, um dynamische Unterstützung und Widerstand basierend auf einem gleitenden Durchschnitt und dem Average True Range zu definieren. Trades werden ausgelöst, wenn der Preis nach einem Ausbruch wieder in den Kanal eintritt.
Funktionsweise
- Aufbau eines Keltner-Kanals mit:
- EMA-Periode
MaLength - ATR-Periode
RangeLength - ATR-Multiplikator
Deviation
- EMA-Periode
- Wenn eine Kerze oberhalb des vorherigen oberen Bandes schließt, wird eine bestehende Short-Position geschlossen. Schließt die nächste Kerze wieder innerhalb des Kanals (Schluss ≤ aktuelles oberes Band), wird eine Long-Position eröffnet.
- Wenn eine Kerze unterhalb des vorherigen unteren Bandes schließt, wird eine bestehende Long-Position geschlossen. Schließt die nächste Kerze wieder innerhalb des Kanals (Schluss ≥ aktuelles unteres Band), wird eine Short-Position eröffnet.
- Stop-Loss- und Take-Profit-Niveaus werden in Punkten ausgedrückt und nach Positionseröffnung angewendet.
Parameter
MaLength– EMA-Periode für die Kanalmitte.RangeLength– ATR-Periode für die Kanalbreite.Deviation– Multiplikator, der auf den ATR zur Bandberechnung angewendet wird.StopLoss– Stop-Loss in Punkten (wird überSecurity.PriceStepin Preis umgerechnet).TakeProfit– Take-Profit in Punkten (wird überSecurity.PriceStepin Preis umgerechnet).CandleType– Kerzen-Serie für Berechnungen.
Indikatoren
- KeltnerChannels (EMA + ATR)
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>
/// Strategy converted from the MetaTrader example "Exp_XMA_Range_Bands".
/// Uses an EMA and ATR to build dynamic bands and trades when price re-enters the channel.
/// </summary>
public class ExpXmaRangeBandsStrategy : Strategy
{
private static readonly TimeSpan _signalCooldown = TimeSpan.FromHours(8);
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _rangeLength;
private readonly StrategyParam<decimal> _deviation;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevUpper;
private decimal _prevLower;
private decimal _prevClose;
private decimal _entryPrice;
private bool _isFirst = true;
private DateTime _lastSignalTime;
/// <summary>
/// EMA period for the channel center.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// ATR period used for channel width.
/// </summary>
public int RangeLength
{
get => _rangeLength.Value;
set => _rangeLength.Value = value;
}
/// <summary>
/// ATR multiplier for band width.
/// </summary>
public decimal Deviation
{
get => _deviation.Value;
set => _deviation.Value = value;
}
/// <summary>
/// Stop loss in points.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take profit in points.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpXmaRangeBandsStrategy"/>.
/// </summary>
public ExpXmaRangeBandsStrategy()
{
_maLength = Param(nameof(MaLength), 100)
.SetGreaterThanZero()
.SetDisplay("MA Length", "EMA period for channel center", "Indicator")
.SetOptimize(50, 200, 10);
_rangeLength = Param(nameof(RangeLength), 20)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period for channel width", "Indicator")
.SetOptimize(10, 60, 5);
_deviation = Param(nameof(Deviation), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviation", "ATR multiplier for channel width", "Indicator")
.SetOptimize(1m, 5m, 0.5m);
_stopLoss = Param(nameof(StopLoss), 1000m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop loss in points", "Risk")
.SetOptimize(500m, 2000m, 500m);
_takeProfit = Param(nameof(TakeProfit), 2000m)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take profit in points", "Risk")
.SetOptimize(1000m, 4000m, 500m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).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();
_prevUpper = 0;
_prevLower = 0;
_prevClose = 0;
_entryPrice = 0;
_isFirst = true;
_lastSignalTime = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var keltner = new KeltnerChannels(
new KeltnerChannelMiddle { Length = MaLength },
new AverageTrueRange { Length = RangeLength },
new KeltnerChannelBand { Length = MaLength },
new KeltnerChannelBand { Length = MaLength })
{
Multiplier = Deviation,
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(keltner, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, keltner);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue keltnerValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (keltnerValue is not IKeltnerChannelsValue keltnerTyped ||
keltnerTyped.Upper is not decimal upper ||
keltnerTyped.Lower is not decimal lower)
return;
if (_isFirst)
{
_prevUpper = upper;
_prevLower = lower;
_prevClose = candle.ClosePrice;
_isFirst = false;
return;
}
if (_prevClose > _prevUpper)
{
if (candle.ClosePrice <= upper && Position <= 0 && CanTrade(candle))
{
var volume = Position < 0 ? Volume + Math.Abs(Position) : Volume;
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_lastSignalTime = candle.CloseTime;
}
}
else if (_prevClose < _prevLower)
{
if (candle.ClosePrice >= lower && Position >= 0 && CanTrade(candle))
{
var volume = Position > 0 ? Volume + Position : Volume;
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_lastSignalTime = candle.CloseTime;
}
}
var step = Security.PriceStep ?? 1m;
var sl = step * StopLoss;
var tp = step * TakeProfit;
if (Position > 0)
{
if (candle.ClosePrice <= _entryPrice - sl || candle.ClosePrice >= _entryPrice + tp)
{
SellMarket(Position);
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (candle.ClosePrice >= _entryPrice + sl || candle.ClosePrice <= _entryPrice - tp)
{
BuyMarket(Math.Abs(Position));
_entryPrice = 0;
}
}
_prevUpper = upper;
_prevLower = lower;
_prevClose = candle.ClosePrice;
}
private bool CanTrade(ICandleMessage candle)
=> _lastSignalTime == default || candle.CloseTime >= _lastSignalTime + _signalCooldown;
}
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, DateTime
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import KeltnerChannels, KeltnerChannelMiddle, KeltnerChannelBand, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class exp_xma_range_bands_strategy(Strategy):
def __init__(self):
super(exp_xma_range_bands_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 100) \
.SetDisplay("MA Length", "EMA period for channel center", "Indicator")
self._range_length = self.Param("RangeLength", 20) \
.SetDisplay("ATR Length", "ATR period for channel width", "Indicator")
self._deviation = self.Param("Deviation", 2.0) \
.SetDisplay("Deviation", "ATR multiplier for channel width", "Indicator")
self._stop_loss = self.Param("StopLoss", 1000.0) \
.SetDisplay("Stop Loss", "Stop loss in points", "Risk")
self._take_profit = self.Param("TakeProfit", 2000.0) \
.SetDisplay("Take Profit", "Take profit in points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_upper = 0.0
self._prev_lower = 0.0
self._prev_close = 0.0
self._entry_price = 0.0
self._is_first = True
self._last_signal_time = DateTime.MinValue
self._signal_cooldown = TimeSpan.FromHours(8)
@property
def MaLength(self):
return self._ma_length.Value
@MaLength.setter
def MaLength(self, value):
self._ma_length.Value = value
@property
def RangeLength(self):
return self._range_length.Value
@RangeLength.setter
def RangeLength(self, value):
self._range_length.Value = value
@property
def Deviation(self):
return self._deviation.Value
@Deviation.setter
def Deviation(self, value):
self._deviation.Value = value
@property
def StopLoss(self):
return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value):
self._stop_loss.Value = value
@property
def TakeProfit(self):
return self._take_profit.Value
@TakeProfit.setter
def TakeProfit(self, value):
self._take_profit.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _can_trade(self, candle):
return (self._last_signal_time == DateTime.MinValue
or candle.CloseTime >= self._last_signal_time + self._signal_cooldown)
def OnStarted2(self, time):
super(exp_xma_range_bands_strategy, self).OnStarted2(time)
mid = KeltnerChannelMiddle()
mid.Length = self.MaLength
atr = AverageTrueRange()
atr.Length = self.RangeLength
upper_band = KeltnerChannelBand()
upper_band.Length = self.MaLength
lower_band = KeltnerChannelBand()
lower_band.Length = self.MaLength
keltner = KeltnerChannels(mid, atr, upper_band, lower_band)
keltner.Multiplier = self.Deviation
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.BindEx(keltner, self.ProcessCandle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, keltner)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, keltner_value):
if candle.State != CandleStates.Finished:
return
upper_raw = keltner_value.Upper
lower_raw = keltner_value.Lower
if upper_raw is None or lower_raw is None:
return
upper = float(upper_raw)
lower = float(lower_raw)
close = float(candle.ClosePrice)
if self._is_first:
self._prev_upper = upper
self._prev_lower = lower
self._prev_close = close
self._is_first = False
return
if self._prev_close > self._prev_upper:
if close <= upper and self.Position <= 0 and self._can_trade(candle):
volume = self.Volume + abs(self.Position) if self.Position < 0 else self.Volume
self.BuyMarket(volume)
self._entry_price = close
self._last_signal_time = candle.CloseTime
elif self._prev_close < self._prev_lower:
if close >= lower and self.Position >= 0 and self._can_trade(candle):
volume = self.Volume + self.Position if self.Position > 0 else self.Volume
self.SellMarket(volume)
self._entry_price = close
self._last_signal_time = candle.CloseTime
step_raw = self.Security.PriceStep
step = float(step_raw) if step_raw is not None else 1.0
sl = step * float(self.StopLoss)
tp = step * float(self.TakeProfit)
if self.Position > 0:
if close <= self._entry_price - sl or close >= self._entry_price + tp:
self.SellMarket(self.Position)
self._entry_price = 0.0
elif self.Position < 0:
if close >= self._entry_price + sl or close <= self._entry_price - tp:
self.BuyMarket(abs(self.Position))
self._entry_price = 0.0
self._prev_upper = upper
self._prev_lower = lower
self._prev_close = close
def OnReseted(self):
super(exp_xma_range_bands_strategy, self).OnReseted()
self._prev_upper = 0.0
self._prev_lower = 0.0
self._prev_close = 0.0
self._entry_price = 0.0
self._is_first = True
self._last_signal_time = DateTime.MinValue
def CreateClone(self):
return exp_xma_range_bands_strategy()