View on GitHub

XOSignal Re-Open Strategy

This strategy reproduces the MetaTrader expert Exp_XOSignal_ReOpen inside StockSharp using the high-level API. It trades candlestick data of the selected symbol and timeframe with an XO-style breakout detector built on ATR(13). When an up arrow appears the algorithm closes shorts, optionally opens a long, and then adds to the position every time price progresses by a fixed number of ticks. Down arrows behave symmetrically for shorts. Hard stops and targets in ticks are applied to every layer of the pyramid.

Core Logic

  • The strategy computes an XO range channel whose bands expand by Range * PriceStep. Breakouts reset the bands and establish the current trend direction.
  • ATR(13) controls how far below/above the candle the virtual entry levels (arrows) are plotted: long arrows appear at Low - ATR * 3/8, short arrows at High + ATR * 3/8.
  • Only completed candles are processed. Signals can be delayed by SignalBar bars to mimic the original buffering logic.

Entry Rules

  • Long entry: when an up arrow is emitted, long entries are allowed (EnableBuyEntries = true), no short position is open, and the signal has not been executed yet. The trade volume equals Volume.
  • Long re-entry: while in a long position, every additional PriceStepTicks ticks in favour of the trade (based on candle close) triggers another buy until MaxPyramidingPositions layers are opened. Each re-entry updates the protective stop/target levels.
  • Short entry / re-entry: mirror logic of the long side using the down arrow.

Exit Rules

  • Signal-based exits: an up arrow closes every active short when EnableSellExits = true; a down arrow closes the long when EnableBuyExits = true.
  • Risk exits: every open layer carries the same stop loss and take profit distance defined in ticks (StopLossTicks, TakeProfitTicks). When price pierces the level within the current candle, the whole position is flattened.
  • Manual flattening: opposite entry signals also neutralise the previous direction before opening a new position.

Position Management

  • Position size is fixed by Volume for each order.
  • Stop loss and take profit are measured in security ticks. Setting them to zero disables the corresponding protection.
  • The pyramid counter resets to zero after any full exit so that the next signal starts from a fresh base position.

Parameters

Parameter Description Default
Volume Order size for each entry 1
StopLossTicks Stop distance in ticks, 0 disables 1000
TakeProfitTicks Take profit distance in ticks, 0 disables 2000
PriceStepTicks Minimum favourable move before adding to the position 300
MaxPyramidingPositions Maximum number of layered entries (including the first) 10
EnableBuyEntries / EnableSellEntries Allow opening long/short positions true
EnableBuyExits / EnableSellExits Allow closing long/short positions on opposite arrows true
CandleType Timeframe used for signals H4
Range XO box height in ticks 10
AppliedPrice Price source used in the XO detector Close
SignalBar Number of closed bars to delay signals 1

The strategy is designed for backtesting or live trading with instruments that provide a reliable price step. Adjust the tick-based distances to match the volatility of the selected market.

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>
/// XOSignal based breakout strategy with re-entry logic.
/// </summary>
public class XoSignalReOpenStrategy : Strategy
{
	/// <summary>
	/// Price source applied to the XO calculation.
	/// </summary>
	public enum AppliedPriceTypes
	{
		Close = 1,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simple,
		Quarter,
		TrendFollow0,
		TrendFollow1,
		Demark
	}

	private readonly StrategyParam<int> _atrPeriod;

	private readonly StrategyParam<int> _stopLossTicks;
	private readonly StrategyParam<int> _takeProfitTicks;
	private readonly StrategyParam<int> _priceStepTicks;
	private readonly StrategyParam<int> _maxPyramidingPositions;
	private readonly StrategyParam<bool> _enableBuyEntries;
	private readonly StrategyParam<bool> _enableSellEntries;
	private readonly StrategyParam<bool> _enableBuyExits;
	private readonly StrategyParam<bool> _enableSellExits;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _range;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<int> _signalBar;

	private readonly Queue<SignalInfo> _signalQueue = new();

	private decimal _hi;
	private decimal _lo;
	private int _kr;
	private int _no;
	private int _trend;
	private bool _initialized;
	private DateTimeOffset? _lastBuySignalTime;
	private DateTimeOffset? _lastSellSignalTime;
	private DateTimeOffset? _lastExecutedBuySignalTime;
	private DateTimeOffset? _lastExecutedSellSignalTime;
	private int _longOrderCount;
	private int _shortOrderCount;
	private decimal _lastLongEntryPrice;
	private decimal _lastShortEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;


	/// <summary>
	/// ATR lookback period used for volatility assessment.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Stop loss distance in ticks (0 disables it).
	/// </summary>
	public int StopLossTicks
	{
		get => _stopLossTicks.Value;
		set => _stopLossTicks.Value = value;
	}

	/// <summary>
	/// Take profit distance in ticks (0 disables it).
	/// </summary>
	public int TakeProfitTicks
	{
		get => _takeProfitTicks.Value;
		set => _takeProfitTicks.Value = value;
	}

	/// <summary>
	/// Additional entry trigger distance in ticks for re-entry.
	/// </summary>
	public int PriceStepTicks
	{
		get => _priceStepTicks.Value;
		set => _priceStepTicks.Value = value;
	}

	/// <summary>
	/// Maximum number of layered positions including the first one.
	/// </summary>
	public int MaxPyramidingPositions
	{
		get => _maxPyramidingPositions.Value;
		set => _maxPyramidingPositions.Value = value;
	}

	/// <summary>
	/// Enable opening long positions on signals.
	/// </summary>
	public bool EnableBuyEntries
	{
		get => _enableBuyEntries.Value;
		set => _enableBuyEntries.Value = value;
	}

	/// <summary>
	/// Enable opening short positions on signals.
	/// </summary>
	public bool EnableSellEntries
	{
		get => _enableSellEntries.Value;
		set => _enableSellEntries.Value = value;
	}

	/// <summary>
	/// Enable closing long positions on opposite signals.
	/// </summary>
	public bool EnableBuyExits
	{
		get => _enableBuyExits.Value;
		set => _enableBuyExits.Value = value;
	}

	/// <summary>
	/// Enable closing short positions on opposite signals.
	/// </summary>
	public bool EnableSellExits
	{
		get => _enableSellExits.Value;
		set => _enableSellExits.Value = value;
	}

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

	/// <summary>
	/// XO box range in ticks.
	/// </summary>
	public int Range
	{
		get => _range.Value;
		set => _range.Value = value;
	}

	/// <summary>
	/// Applied price mode for XO calculations.
	/// </summary>
	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	/// <summary>
	/// Number of bars to delay signals.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="XoSignalReopenStrategy"/> class.
	/// </summary>
	public XoSignalReOpenStrategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 13)
		.SetGreaterThanZero()
		.SetDisplay("ATR Period", "ATR lookback used for volatility assessment", "Indicator")
		;


		_stopLossTicks = Param(nameof(StopLossTicks), 1000)
			.SetDisplay("Stop Loss", "Stop loss in ticks", "Risk")
			.SetNotNegative();

		_takeProfitTicks = Param(nameof(TakeProfitTicks), 2000)
			.SetDisplay("Take Profit", "Take profit in ticks", "Risk")
			.SetNotNegative();

		_priceStepTicks = Param(nameof(PriceStepTicks), 1000)
			.SetDisplay("Re-entry Step", "Ticks to add position", "Trading")
			.SetNotNegative();

		_maxPyramidingPositions = Param(nameof(MaxPyramidingPositions), 1)
			.SetDisplay("Max Layers", "Maximum layered entries", "Trading")
			.SetGreaterThanZero();

		_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
			.SetDisplay("Enable Long", "Allow long entries", "Permissions");

		_enableSellEntries = Param(nameof(EnableSellEntries), true)
			.SetDisplay("Enable Short", "Allow short entries", "Permissions");

		_enableBuyExits = Param(nameof(EnableBuyExits), true)
			.SetDisplay("Close Long", "Close long on short signal", "Permissions");

		_enableSellExits = Param(nameof(EnableSellExits), true)
			.SetDisplay("Close Short", "Close short on long signal", "Permissions");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Working timeframe", "General");

		_range = Param(nameof(Range), 10)
			.SetDisplay("Range", "XO box height in ticks", "Indicator")
			.SetGreaterThanZero();

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Close)
			.SetDisplay("Applied Price", "Price source", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetDisplay("Signal Shift", "Bars to delay signals", "Indicator")
			.SetNotNegative();
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_signalQueue.Clear();
		_hi = 0m;
		_lo = 0m;
		_kr = 0;
		_no = 0;
		_trend = 0;
		_initialized = false;
		_lastBuySignalTime = null;
		_lastSellSignalTime = null;
		_lastExecutedBuySignalTime = null;
		_lastExecutedSellSignalTime = null;
		_longOrderCount = 0;
		_shortOrderCount = 0;
		_lastLongEntryPrice = 0m;
		_lastShortEntryPrice = 0m;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}

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

		var atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(atr, ProcessCandle).Start();
	}

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

		if (atr <= 0m)
			return;

		var step = Security?.PriceStep ?? 1m;
		var rangeStep = Math.Max(1, Range) * step;
		var price = GetAppliedPrice(candle);

		if (!_initialized)
		{
			_hi = price;
			_lo = price;
			_initialized = true;
		}

		if (price > _hi + rangeStep)
		{
			_hi = price;
			_lo = _hi - rangeStep;
			_kr++;
			_no = 0;
		}
		else if (price < _lo - rangeStep)
		{
			_lo = price;
			_hi = _lo + rangeStep;
			_no++;
			_kr = 0;
		}

		var trend = _trend;
		if (_kr > 0)
			trend = 1;
		if (_no > 0)
			trend = -1;

		var buySignal = _trend < 0 && trend > 0;
		var sellSignal = _trend > 0 && trend < 0;
		_trend = trend;

		var closeTime = candle.OpenTime + (TimeSpan)CandleType.Arg;
		var buyTime = buySignal ? closeTime : (_lastBuySignalTime ?? closeTime);
		var sellTime = sellSignal ? closeTime : (_lastSellSignalTime ?? closeTime);
		var buyLevel = candle.LowPrice - atr * 3m / 8m;
		var sellLevel = candle.HighPrice + atr * 3m / 8m;

		var info = new SignalInfo(
			buySignal,
			sellSignal,
			sellSignal,
			buySignal,
			buyTime,
			sellTime,
			buyLevel,
			sellLevel,
			candle.ClosePrice);

		_signalQueue.Enqueue(info);

		if (_signalQueue.Count <= SignalBar)
			return;

		var activeSignal = _signalQueue.Dequeue();

		HandleStops(candle);
		ApplySignal(activeSignal, candle);
		HandleReentries(candle);
	}

	private void ApplySignal(SignalInfo signal, ICandleMessage candle)
	{
		if (signal.BuyEntry || signal.SellExit)
			_lastBuySignalTime = signal.BuySignalTime;

		if (signal.SellEntry || signal.BuyExit)
			_lastSellSignalTime = signal.SellSignalTime;

		if (signal.BuyExit && EnableBuyExits && Position > 0)
		{
			SellMarket();
			ResetLongState();
		}

		if (signal.SellExit && EnableSellExits && Position < 0)
		{
			BuyMarket();
			ResetShortState();
		}

		if (signal.BuyEntry && EnableBuyEntries)
		{
			if (_lastExecutedBuySignalTime != signal.BuySignalTime)
			{
				if (Position < 0)
				{
					BuyMarket();
					ResetShortState();
				}

				if (Position <= 0)
				{
					BuyMarket();
					_lastExecutedBuySignalTime = signal.BuySignalTime;
					_longOrderCount = 1;
					_shortOrderCount = 0;
					_lastLongEntryPrice = candle.ClosePrice;
					UpdateLongRiskLevels(candle.ClosePrice);
				}
			}
		}

		if (signal.SellEntry && EnableSellEntries)
		{
			if (_lastExecutedSellSignalTime != signal.SellSignalTime)
			{
				if (Position > 0)
				{
					SellMarket();
					ResetLongState();
				}

				if (Position >= 0)
				{
					SellMarket();
					_lastExecutedSellSignalTime = signal.SellSignalTime;
					_shortOrderCount = 1;
					_longOrderCount = 0;
					_lastShortEntryPrice = candle.ClosePrice;
					UpdateShortRiskLevels(candle.ClosePrice);
				}
			}
		}
	}

	private void HandleStops(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				ResetLongState();
			}
			else if (_longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else
		{
			_longStopPrice = null;
			_longTakePrice = null;
		}

		if (Position < 0)
		{
			if (_shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				ResetShortState();
			}
			else if (_shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
		else
		{
			_shortStopPrice = null;
			_shortTakePrice = null;
		}
	}

	private void HandleReentries(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 1m;
		var distance = PriceStepTicks * step;

		if (distance <= 0m)
			return;

		if (EnableBuyEntries && Position > 0 && _longOrderCount > 0 && _longOrderCount < MaxPyramidingPositions)
		{
			if (candle.ClosePrice >= _lastLongEntryPrice + distance)
			{
				BuyMarket();
				_longOrderCount++;
				_lastLongEntryPrice = candle.ClosePrice;
				UpdateLongRiskLevels(candle.ClosePrice);
			}
		}

		if (EnableSellEntries && Position < 0 && _shortOrderCount > 0 && _shortOrderCount < MaxPyramidingPositions)
		{
			if (candle.ClosePrice <= _lastShortEntryPrice - distance)
			{
				SellMarket();
				_shortOrderCount++;
				_lastShortEntryPrice = candle.ClosePrice;
				UpdateShortRiskLevels(candle.ClosePrice);
			}
		}
	}

	private void UpdateLongRiskLevels(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 1m;
		_longStopPrice = StopLossTicks > 0 ? entryPrice - StopLossTicks * step : null;
		_longTakePrice = TakeProfitTicks > 0 ? entryPrice + TakeProfitTicks * step : null;
	}

	private void UpdateShortRiskLevels(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 1m;
		_shortStopPrice = StopLossTicks > 0 ? entryPrice + StopLossTicks * step : null;
		_shortTakePrice = TakeProfitTicks > 0 ? entryPrice - TakeProfitTicks * step : null;
	}

	private void ResetLongState()
	{
		_longOrderCount = 0;
		_lastLongEntryPrice = 0m;
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortState()
	{
		_shortOrderCount = 0;
		_lastShortEntryPrice = 0m;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
			AppliedPriceTypes.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			AppliedPriceTypes.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
			AppliedPriceTypes.Quarter => (candle.OpenPrice + candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			AppliedPriceTypes.TrendFollow0 => candle.ClosePrice > candle.OpenPrice ? candle.HighPrice : candle.ClosePrice < candle.OpenPrice ? candle.LowPrice : candle.ClosePrice,
			AppliedPriceTypes.TrendFollow1 => candle.ClosePrice > candle.OpenPrice ? (candle.HighPrice + candle.ClosePrice) / 2m : candle.ClosePrice < candle.OpenPrice ? (candle.LowPrice + candle.ClosePrice) / 2m : candle.ClosePrice,
			AppliedPriceTypes.Demark => CalculateDemarkPrice(candle),
			_ => candle.ClosePrice,
		};
	}

	private decimal CalculateDemarkPrice(ICandleMessage candle)
	{
		var res = candle.HighPrice + candle.LowPrice + candle.ClosePrice;
		if (candle.ClosePrice < candle.OpenPrice)
			res = (res + candle.LowPrice) / 2m;
		else if (candle.ClosePrice > candle.OpenPrice)
			res = (res + candle.HighPrice) / 2m;
		else
			res = (res + candle.ClosePrice) / 2m;

		return ((res - candle.LowPrice) + (res - candle.HighPrice)) / 2m;
	}

	private readonly struct SignalInfo
	{
		public SignalInfo(bool buyEntry, bool sellEntry, bool buyExit, bool sellExit, DateTimeOffset buySignalTime, DateTimeOffset sellSignalTime, decimal buyLevel, decimal sellLevel, decimal closePrice)
		{
			BuyEntry = buyEntry;
			SellEntry = sellEntry;
			BuyExit = buyExit;
			SellExit = sellExit;
			BuySignalTime = buySignalTime;
			SellSignalTime = sellSignalTime;
			BuyLevel = buyLevel;
			SellLevel = sellLevel;
			ClosePrice = closePrice;
		}

		public bool BuyEntry { get; }
		public bool SellEntry { get; }
		public bool BuyExit { get; }
		public bool SellExit { get; }
		public DateTimeOffset BuySignalTime { get; }
		public DateTimeOffset SellSignalTime { get; }
		public decimal BuyLevel { get; }
		public decimal SellLevel { get; }
		public decimal ClosePrice { get; }
	}
}