The XD Range Switch strategy recreates the MetaTrader 5 expert advisor Exp_XD-RangeSwitch using the StockSharp high-level API. It relies on the custom XD-RangeSwitch channel indicator, which plots alternating upper and lower bands together with arrows whenever the dominant band flips. The strategy can either fade those arrows (counter-trend behaviour) or trade in the direction of the breakout depending on the TradeDirection parameter. Order sizing follows the base Strategy.Volume setting, while the original money-management formulas are replaced by StockSharp's position management helpers.
XD-RangeSwitch indicator recreation
The indicator tracks the last Peaks completed candles to determine the highest high and lowest low ranges.
A bullish channel (lower band) is printed when the current close is above the highest high of the previous Peaks bars. Its value equals the minimum low across the same window plus the current bar.
A bearish channel (upper band) is printed when the current close is below the lowest low of the previous Peaks bars. Its value equals the maximum high across the same window plus the current bar.
If neither breakout occurs, the previous channel values are propagated forward.
Whenever a channel reappears after being empty the strategy records an arrow signal at the channel price. This mirrors the behaviour of the MT5 buffers 2 and 3 used by the original expert.
Only fully finished candles are processed, ensuring consistent values across live and historical runs.
Trading logic
The strategy processes candles from the timeframe selected by CandleType and stores the reconstructed indicator buffers.
For every new candle it inspects the indicator value that is SignalBar candles old (the MT5 code uses the same shift when calling CopyBuffer).
Signal mapping depends on the TradeDirection option:
AgainstSignal replicates the default MT5 behaviour — bullish arrows trigger longs and also request to close short trades, bearish arrows trigger shorts and request to close longs.
WithSignal flips the interpretation, so bullish arrows are treated as exit points for longs and entry points for shorts, effectively trading in the same direction as the channel breakout.
Trend buffers without arrows are still respected as exit signals, matching the original SELL_Close and BUY_Close flags.
Closings always execute before openings, allowing the strategy to flatten an opposite position before entering in the new direction.
Orders are submitted with market execution helpers (BuyMarket/SellMarket). When a flip occurs while an opposite position is open the requested quantity is automatically increased to fully offset the exposure before establishing the new position.
Risk management
Optional stop-loss and take-profit logic is provided through the UseStopLoss/StopLossPoints and UseTakeProfit/TakeProfitPoints parameters.
The distances are measured in absolute price units, mirroring the "points" inputs in the MT5 script.
Stops and targets are evaluated on every finished candle using the candle's high/low to emulate intra-bar triggering.
If both a stop and a target are active the stop has priority — the position is closed once either level is reached.
Parameters
Name
Default
Description
CandleType
H4 candles
Timeframe used for the XD-RangeSwitch calculations.
Peaks
4
Number of peaks (lookback length) analysed by the indicator.
SignalBar
1
Number of completed bars back when reading indicator buffers.
TradeDirection
AgainstSignal
Choose between counter-trend or trend-following interpretation of the signals.
AllowBuyEntry / AllowSellEntry
true
Enable or disable opening new positions in the corresponding direction.
AllowBuyExit / AllowSellExit
true
Permit the strategy to close existing positions when the indicator requests it.
UseStopLoss / StopLossPoints
true / 1000
Activate stop-loss handling and define its distance in price units.
UseTakeProfit / TakeProfitPoints
true / 2000
Activate take-profit handling and define its distance in price units.
Notes
The high/low buffers are maintained internally inside the strategy instead of relying on StockSharp collections, staying faithful to the MT5 implementation while adhering to the conversion guidelines.
Signals are evaluated on finished candles only. If SignalBar is greater than zero, the order is placed on the next candle after the one that produced the signal, as in the MT5 expert.
The indicator values are kept in a short rolling history that extends just beyond the largest of Peaks and SignalBar, ensuring deterministic memory usage even during long simulations.
The default configuration mirrors the MT5 defaults: H4 candles, Peaks = 4, SignalBar = 1, counter-trend trading, and a 1,000/2,000 point risk envelope.
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>
/// XD-RangeSwitch strategy using Highest/Lowest channel breakouts.
/// Buys on breakout above channel high, sells on breakout below channel low.
/// Uses stop-loss and take-profit for risk management.
/// </summary>
public class XdRangeSwitchStrategy : Strategy
{
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private Highest _highest;
private Lowest _lowest;
private decimal _prevHigh;
private decimal _prevLow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Channel lookback period.
/// </summary>
public int ChannelPeriod
{
get => _channelPeriod.Value;
set => _channelPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public XdRangeSwitchStrategy()
{
_channelPeriod = Param(nameof(ChannelPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Channel Period", "Lookback for highest/lowest channel", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highest = null;
_lowest = null;
_prevHigh = 0;
_prevLow = 0;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highest = new Highest { Length = ChannelPeriod };
_lowest = new Lowest { Length = ChannelPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_highest, _lowest, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal highValue, decimal lowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_highest.IsFormed || !_lowest.IsFormed)
{
_prevHigh = highValue;
_prevLow = lowValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevHigh = highValue;
_prevLow = lowValue;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 30;
_prevHigh = highValue;
_prevLow = lowValue;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 30;
_prevHigh = highValue;
_prevLow = lowValue;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 30;
_prevHigh = highValue;
_prevLow = lowValue;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 30;
_prevHigh = highValue;
_prevLow = lowValue;
return;
}
}
// Breakout above previous channel high
if (close > _prevHigh && _prevHigh > 0 && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 30;
}
// Breakout below previous channel low
else if (close < _prevLow && _prevLow > 0 && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 30;
}
_prevHigh = highValue;
_prevLow = lowValue;
}
}
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 Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class xd_range_switch_strategy(Strategy):
def __init__(self):
super(xd_range_switch_strategy, self).__init__()
self._channel_period = self.Param("ChannelPeriod", 200) \
.SetDisplay("Channel Period", "Lookback for highest/lowest channel", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._highest = None
self._lowest = None
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def channel_period(self):
return self._channel_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(xd_range_switch_strategy, self).OnReseted()
self._highest = None
self._lowest = None
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(xd_range_switch_strategy, self).OnStarted2(time)
self._highest = Highest()
self._highest.Length = self.channel_period
self._lowest = Lowest()
self._lowest.Length = self.channel_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._highest, self._lowest, self._process_candle)
subscription.Start()
def _process_candle(self, candle, high_value, low_value):
if candle.State != CandleStates.Finished:
return
h_val = float(high_value)
l_val = float(low_value)
if not self._highest.IsFormed or not self._lowest.IsFormed:
self._prev_high = h_val
self._prev_low = l_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_high = h_val
self._prev_low = l_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 30
self._prev_high = h_val
self._prev_low = l_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 30
self._prev_high = h_val
self._prev_low = l_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 30
self._prev_high = h_val
self._prev_low = l_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 30
self._prev_high = h_val
self._prev_low = l_val
return
# Breakout above previous channel high
if close > self._prev_high and self._prev_high > 0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 30
# Breakout below previous channel low
elif close < self._prev_low and self._prev_low > 0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 30
self._prev_high = h_val
self._prev_low = l_val
def CreateClone(self):
return xd_range_switch_strategy()