Exp XMA Range Bands Strategy
This strategy replicates the logic of the MetaTrader sample "Exp_XMA_Range_Bands" using StockSharp high level API. It employs a Keltner Channel to define dynamic support and resistance based on a moving average and average true range. Trades are triggered when price re-enters the channel after moving outside.
How It Works
- Build a Keltner Channel using:
- EMA period
MaLength - ATR period
RangeLength - ATR multiplier
Deviation
- EMA period
- When a candle closes above the previous upper band, any short position is closed. If the next candle closes back inside the channel (close ≤ current upper band) a long position is opened.
- When a candle closes below the previous lower band, any long position is closed. If the next candle closes back inside (close ≥ current lower band) a short position is opened.
- Stop-loss and take-profit levels are expressed in points and applied once a position is entered.
Parameters
MaLength– EMA period for the channel center.RangeLength– ATR period used for channel width.Deviation– Multiplier applied to ATR to compute bands.StopLoss– Stop loss in points (converted to price bySecurity.PriceStep).TakeProfit– Take profit in points (converted to price bySecurity.PriceStep).CandleType– Candle series used for calculations.
Indicators
- 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()