Ver no GitHub

Chandel Exit Re-Entry Strategy

This strategy ports the MetaTrader expert "Exp_ChandelExitSign_ReOpen" to the StockSharp high-level API. It trades breakouts using the Chandelier Exit bands and automatically re-opens positions when the trend continues. The system reacts to indicator signals computed on a configurable higher timeframe while managing risk with ATR-based stops and optional take-profit levels.

The core idea is to treat the Chandelier Exit as both a trend filter and a trailing barrier. When the down band crosses above the up band, a bullish impulse is detected; when the opposite happens, a bearish impulse appears. The strategy can work symmetrically on long and short sides, and every signal can be enabled or disabled individually through parameters. Once in position, price must advance by a number of price steps (PriceStepPoints) before an add-on order is allowed. The add-ons mimic the original expert advisor behaviour and are capped by MaxAdditions to prevent runaway position sizes.

Trading logic

  • Signal calculation
    • RangePeriod bars (offset by Shift) define the highest high and lowest low used by the Chandelier Exit bands.
    • AtrPeriod together with AtrMultiplier produce a volatility buffer that shifts the exit bands away from price.
    • SignalBar (default 1) delays execution so the strategy acts on the previous finished candle, replicating the MT5 implementation.
  • Entries
    • Long: triggered when the down band crosses above the up band (IsUpSignal). Requires EnableBuyEntries = true. If a short position exists, the strategy first tries to flatten it when EnableSellExits = true.
    • Short: triggered when the bands cross in the opposite direction (IsDownSignal) and EnableSellEntries = true. Existing longs are closed only if EnableBuyExits = true.
  • Exits
    • Long positions close on bearish signals when EnableBuyExits = true, or when protective stops/targets are hit.
    • Short positions close on bullish signals when EnableSellExits = true, or through protective levels.
    • The strategy also scans older indicator values when both entry and exit toggles are enabled to ensure a close signal is available even if the most recent candle produced only an entry.
  • Re-entry / scale-in
    • After each entry, the last fill price is stored. When price moves in favour by at least PriceStepPoints * PriceStep, an additional order of size Volume is sent, up to MaxAdditions times.
    • Every add-on resets the stop/take calculations to the latest fill so the protection stays close to the newest exposure.
  • Risk management
    • StopLossPoints and TakeProfitPoints express distances in price steps from the latest fill. Stops and targets are optional; set them to zero to disable.
    • All protective checks run on every finished candle. If price breaches a stop or target intrabar the position is closed at market.

Default parameters

Parameter Default Description
CandleType TimeSpan.FromHours(4).TimeFrame() Timeframe used for indicator calculations.
RangePeriod 15 Lookback window for the highest high / lowest low.
Shift 1 Number of recent bars skipped before computing the range.
AtrPeriod 14 ATR length for the volatility buffer.
AtrMultiplier 4 ATR multiplier applied to the buffer.
SignalBar 1 How many completed bars back to read the signal from.
PriceStepPoints 300 Minimal favourable move in price steps before adding to a trade.
MaxAdditions 10 Maximum number of add-on orders after the initial entry.
StopLossPoints 1000 Stop-loss distance in price steps.
TakeProfitPoints 2000 Take-profit distance in price steps.
EnableBuyEntries / EnableSellEntries true Allow opening long/short trades on signals.
EnableBuyExits / EnableSellExits true Allow closing long/short trades on opposite signals.

Practical notes

  • The strategy relies on Volume to define the base order size. Add-on trades reuse the same size. Adjust Volume or MaxAdditions to fit risk limits.
  • Because re-entries require a move expressed in price steps, ensure the security metadata (PriceStep) is configured correctly. Instruments with large point values may need different defaults.
  • SignalBar can be set to zero to act on the most recent completed candle, but the original expert used a one-bar delay to avoid acting on the candle that generated the signal.
  • Start the strategy on a symbol/portfolio combination that supports both long and short trades. Use the built-in parameter toggles to constrain it to one direction if needed.
  • Charting helpers (DrawCandles, DrawIndicator, DrawOwnTrades) activate automatically when a chart area is available, making it easier to visualise bands and fills.

Example workflow

  1. Wait for a bullish crossover: the down band breaks above the up band on the higher timeframe candle.
  2. If no position exists and long entries are enabled, place a market buy order of size Volume. Stops and targets are set relative to the fill price.
  3. If price rallies by at least PriceStepPoints * PriceStep, send an additional buy order (respecting MaxAdditions).
  4. Close the entire long when a bearish signal appears, when the stop-loss is hit, or when the take-profit is reached. The process mirrors for short trades.

This documentation mirrors the original MT5 strategy while embracing StockSharp conventions such as strategy parameters, high-level candle subscriptions, and explicit position management.

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 ChandelExitSign expert advisor with re-entry logic.
/// </summary>
public class ChandelExitReopenStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rangePeriod;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<decimal> _priceStepPoints;
	private readonly StrategyParam<int> _maxAdditions;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _enableBuyEntries;
	private readonly StrategyParam<bool> _enableSellEntries;
	private readonly StrategyParam<bool> _enableBuyExits;
	private readonly StrategyParam<bool> _enableSellExits;

	private readonly List<CandleInfo> _history = new();
	private readonly List<SignalInfo> _signals = new();

	private decimal? _previousUp;
	private decimal? _previousDown;
	private int _direction;

	private int _longAdditions;
	private int _shortAdditions;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortTakePrice;
	private DateTimeOffset? _lastLongAdditionTime;
	private DateTimeOffset? _lastShortAdditionTime;

	/// <summary>
	/// Initializes a new instance of <see cref="ChandelExitReopenStrategy"/>.
	/// </summary>
	public ChandelExitReopenStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for signals", "General");

		_rangePeriod = Param(nameof(RangePeriod), 15)
			.SetDisplay("Range Period", "Lookback for highest high and lowest low", "Indicator")
			.SetGreaterThanZero()
			;

		_shift = Param(nameof(Shift), 1)
			.SetDisplay("Shift", "Bars to skip from the most recent data", "Indicator")
			.SetNotNegative()
			;

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetDisplay("ATR Period", "ATR length for volatility filter", "Indicator")
			.SetGreaterThanZero()
			;

		_atrMultiplier = Param(nameof(AtrMultiplier), 4m)
			.SetDisplay("ATR Multiplier", "Multiplier applied to ATR", "Indicator")
			.SetGreaterThanZero()
			;

		_signalBar = Param(nameof(SignalBar), 1)
			.SetDisplay("Signal Bar", "How many bars back to read signals", "Trading")
			.SetNotNegative();

		_priceStepPoints = Param(nameof(PriceStepPoints), 1000m)
			.SetDisplay("Re-entry Distance", "Minimum favorable move in price steps before adding", "Position Management")
			.SetNotNegative()
			;

		_maxAdditions = Param(nameof(MaxAdditions), 1)
			.SetDisplay("Max Additions", "Maximum number of re-entries after the initial position", "Position Management")
			.SetNotNegative();

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetDisplay("Stop Loss Points", "Stop-loss distance in price steps", "Risk Management")
			.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
			.SetDisplay("Take Profit Points", "Take-profit distance in price steps", "Risk Management")
			.SetNotNegative();

		_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
			.SetDisplay("Enable Long Entries", "Allow opening long positions on up signals", "Trading");

		_enableSellEntries = Param(nameof(EnableSellEntries), true)
			.SetDisplay("Enable Short Entries", "Allow opening short positions on down signals", "Trading");

		_enableBuyExits = Param(nameof(EnableBuyExits), true)
			.SetDisplay("Enable Long Exits", "Allow closing long positions on down signals", "Trading");

		_enableSellExits = Param(nameof(EnableSellExits), true)
			.SetDisplay("Enable Short Exits", "Allow closing short positions on up signals", "Trading");
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Range length for the Chandelier exit bands.
	/// </summary>
	public int RangePeriod
	{
		get => _rangePeriod.Value;
		set => _rangePeriod.Value = value;
	}

	/// <summary>
	/// Number of the most recent bars skipped before measuring the range.
	/// </summary>
	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	/// <summary>
	/// ATR length used in the signal calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the ATR value.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// Offset of the signal bar relative to the latest finished candle.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Required move in price steps before another position add is allowed.
	/// </summary>
	public decimal PriceStepPoints
	{
		get => _priceStepPoints.Value;
		set => _priceStepPoints.Value = value;
	}

	/// <summary>
	/// Maximum number of additional entries after the first fill.
	/// </summary>
	public int MaxAdditions
	{
		get => _maxAdditions.Value;
		set => _maxAdditions.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Enables long entries generated by the up buffer.
	/// </summary>
	public bool EnableBuyEntries
	{
		get => _enableBuyEntries.Value;
		set => _enableBuyEntries.Value = value;
	}

	/// <summary>
	/// Enables short entries generated by the down buffer.
	/// </summary>
	public bool EnableSellEntries
	{
		get => _enableSellEntries.Value;
		set => _enableSellEntries.Value = value;
	}

	/// <summary>
	/// Enables long exits on down signals.
	/// </summary>
	public bool EnableBuyExits
	{
		get => _enableBuyExits.Value;
		set => _enableBuyExits.Value = value;
	}

	/// <summary>
	/// Enables short exits on up signals.
	/// </summary>
	public bool EnableSellExits
	{
		get => _enableSellExits.Value;
		set => _enableSellExits.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_history.Clear();
		_signals.Clear();

		_previousUp = null;
		_previousDown = null;
		_direction = 0;

		ResetLongState();
		ResetShortState();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);

		subscription
			.BindEx(atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, atr);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var atr = atrValue.IsFinal ? atrValue.ToDecimal() : 0m;
		var info = new CandleInfo(candle.OpenTime, candle.HighPrice, candle.LowPrice, candle.ClosePrice, atr);

		_history.Add(info);

		SignalInfo signal;
		if (atrValue.IsFinal)
		{
			signal = CalculateSignal(info);
		}
		else
		{
			signal = SignalInfo.Empty(info.Time);
		}

		_signals.Add(signal);
		TrimCache();

		if (!atrValue.IsFinal)
			return;

		if (_signals.Count <= SignalBar)
			return;

		var signals = _signals.ToArray();
		var targetIndex = signals.Length - 1 - SignalBar;
		if (targetIndex < 0)
			return;

		var targetSignal = signals[targetIndex];
		if (targetSignal is null)
			return;

		var buyOpen = targetSignal.IsUpSignal && EnableBuyEntries;
		var sellOpen = targetSignal.IsDownSignal && EnableSellEntries;
		var buyClose = targetSignal.IsDownSignal && EnableBuyExits;
		var sellClose = targetSignal.IsUpSignal && EnableSellExits;

		if (((EnableBuyEntries && EnableBuyExits) || (EnableSellEntries && EnableSellExits)) && !buyClose && !sellClose)
		{
			for (var idx = targetIndex - 1; idx >= 0; idx--)
			{
				var previousSignal = signals[idx];
				if (previousSignal is null)
					continue;

				if (!sellClose && EnableSellExits && previousSignal.IsUpSignal)
				{
					sellClose = true;
					break;
				}

				if (!buyClose && EnableBuyExits && previousSignal.IsDownSignal)
				{
					buyClose = true;
					break;
				}
			}
		}

		var step = Security.PriceStep ?? 1m;
		var priceStep = PriceStepPoints * step;

		var longClosed = false;
		var shortClosed = false;

		if (Position > 0m)
		{
			if (_longStopPrice is decimal sl && candle.LowPrice <= sl)
			{
				SellMarket();
				ResetLongState();
				longClosed = true;
				this.LogInfo($"Long stop triggered at {sl:0.########}");
			}
			else if (_longTakePrice is decimal tp && candle.HighPrice >= tp)
			{
				SellMarket();
				ResetLongState();
				longClosed = true;
				this.LogInfo($"Long take profit triggered at {tp:0.########}");
			}
		}

		if (Position < 0m)
		{
			if (_shortStopPrice is decimal sl && candle.HighPrice >= sl)
			{
				BuyMarket();
				ResetShortState();
				shortClosed = true;
				this.LogInfo($"Short stop triggered at {sl:0.########}");
			}
			else if (_shortTakePrice is decimal tp && candle.LowPrice <= tp)
			{
				BuyMarket();
				ResetShortState();
				shortClosed = true;
				this.LogInfo($"Short take profit triggered at {tp:0.########}");
			}
		}

		if (!longClosed && buyClose && Position > 0m)
		{
			SellMarket();
			ResetLongState();
			longClosed = true;
			this.LogInfo($"Long exit on down signal at {candle.ClosePrice:0.########}");
		}

		if (!shortClosed && sellClose && Position < 0m)
		{
			BuyMarket();
			ResetShortState();
			shortClosed = true;
			this.LogInfo($"Short exit on up signal at {candle.ClosePrice:0.########}");
		}

		if (!longClosed && Position > 0m && MaxAdditions > 0 && _longEntryPrice is decimal lastLongPrice && priceStep > 0m && _longAdditions < MaxAdditions)
		{
			if (candle.ClosePrice - lastLongPrice >= priceStep && _lastLongAdditionTime != candle.OpenTime)
			{
				if (Volume > 0m)
				{
					BuyMarket();
					_longAdditions++;
					_longEntryPrice = candle.ClosePrice;
					_lastLongAdditionTime = candle.OpenTime;
					UpdateLongProtection(candle.ClosePrice, step);
					this.LogInfo($"Added to long position at {candle.ClosePrice:0.########} (add #{_longAdditions})");
				}
			}
		}

		if (!shortClosed && Position < 0m && MaxAdditions > 0 && _shortEntryPrice is decimal lastShortPrice && priceStep > 0m && _shortAdditions < MaxAdditions)
		{
			if (lastShortPrice - candle.ClosePrice >= priceStep && _lastShortAdditionTime != candle.OpenTime)
			{
				if (Volume > 0m)
				{
					SellMarket();
					_shortAdditions++;
					_shortEntryPrice = candle.ClosePrice;
					_lastShortAdditionTime = candle.OpenTime;
					UpdateShortProtection(candle.ClosePrice, step);
					this.LogInfo($"Added to short position at {candle.ClosePrice:0.########} (add #{_shortAdditions})");
				}
			}
		}

		if (buyOpen && Position < 0m && !EnableSellExits)
		buyOpen = false;

		if (sellOpen && Position > 0m && !EnableBuyExits)
		sellOpen = false;

		if (buyOpen && Volume > 0m)
		{
			BuyMarket();
			ResetShortState();
			_longAdditions = 0;
			_longEntryPrice = candle.ClosePrice;
			_lastLongAdditionTime = candle.OpenTime;
			UpdateLongProtection(candle.ClosePrice, step);
			this.LogInfo($"Opened long position at {candle.ClosePrice:0.########}");
		}

		if (sellOpen && Volume > 0m)
		{
			SellMarket();
			ResetLongState();
			_shortAdditions = 0;
			_shortEntryPrice = candle.ClosePrice;
			_lastShortAdditionTime = candle.OpenTime;
			UpdateShortProtection(candle.ClosePrice, step);
			this.LogInfo($"Opened short position at {candle.ClosePrice:0.########}");
		}
	}

	private void TrimCache()
	{
		var maxItems = Math.Max(RangePeriod + Shift + 5, SignalBar + 5) + 50;
		if (_history.Count <= maxItems)
			return;

		var removeCount = _history.Count - maxItems;
		_history.RemoveRange(0, removeCount);
		_signals.RemoveRange(0, removeCount);
	}

	private SignalInfo CalculateSignal(CandleInfo current)
	{
		var history = _history.ToArray();
		var currentIndex = history.Length - 1;
		var range = RangePeriod;
		var shift = Shift;

		if (range <= 0 || currentIndex - shift < 0)
		return SignalInfo.Empty(current.Time);

		var windowEnd = currentIndex - shift;
		var windowStart = windowEnd - (range - 1);

		if (windowStart < 0 || windowEnd >= history.Length)
		return SignalInfo.Empty(current.Time);

		var highestHigh = decimal.MinValue;
		var lowestLow = decimal.MaxValue;

		for (var i = windowStart; i <= windowEnd; i++)
		{
			var item = history[i];
			if (item is null)
				continue;

			if (item.High > highestHigh)
			highestHigh = item.High;
			if (item.Low < lowestLow)
			lowestLow = item.Low;
		}

		if (highestHigh == decimal.MinValue || lowestLow == decimal.MaxValue)
			return SignalInfo.Empty(current.Time);

		var atr = current.Atr * AtrMultiplier;
		var upperBand = highestHigh - atr;
		var lowerBand = lowestLow + atr;

		decimal up;
		decimal down;

		if (_direction >= 0)
		{
			if (current.Close < upperBand)
			{
				_direction = -1;
				up = lowerBand;
				down = upperBand;
			}
			else
			{
				up = upperBand;
				down = lowerBand;
			}
		}
		else
		{
			if (current.Close > lowerBand)
			{
				_direction = 1;
				down = lowerBand;
				up = upperBand;
			}
			else
			{
				up = lowerBand;
				down = upperBand;
			}
		}

		var isUpSignal = false;
		var isDownSignal = false;

		if (_previousDown is decimal prevDn && _previousUp is decimal prevUp)
		{
			if (prevDn <= prevUp && down > up)
			isUpSignal = true;

			if (prevDn >= prevUp && down < up)
			isDownSignal = true;
		}

		_previousUp = up;
		_previousDown = down;

		return new SignalInfo(current.Time, isUpSignal, isDownSignal, up, down);
	}

	private void UpdateLongProtection(decimal entryPrice, decimal step)
	{
		_longStopPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * step : null;
		_longTakePrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * step : null;
	}

	private void UpdateShortProtection(decimal entryPrice, decimal step)
	{
		_shortStopPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * step : null;
		_shortTakePrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * step : null;
	}

	private void ResetLongState()
	{
		_longAdditions = 0;
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_lastLongAdditionTime = null;
	}

	private void ResetShortState()
	{
		_shortAdditions = 0;
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastShortAdditionTime = null;
	}

	private sealed record CandleInfo(DateTimeOffset Time, decimal High, decimal Low, decimal Close, decimal Atr);

	private sealed record SignalInfo(DateTimeOffset Time, bool IsUpSignal, bool IsDownSignal, decimal Up, decimal Down)
	{
		public static SignalInfo Empty(DateTimeOffset time) => new(time, false, false, 0m, 0m);
	}
}