Ver no GitHub

Estratégia XOSignal Re-Open

Esta estratégia reproduz o especialista do MetaTrader Exp_XOSignal_ReOpen dentro do StockSharp usando a API de alto nível. Opera com dados de velas do símbolo e período selecionados com um detector de rompimento estilo XO construído sobre ATR(13). Quando uma seta para cima aparece, o algoritmo fecha os vendidos, opcionalmente abre um comprado, e então adiciona à posição cada vez que o preço progride um número fixo de ticks. Setas para baixo se comportam simetricamente para vendidos. Stops duros e alvos em ticks são aplicados a cada camada da pirâmide.

Lógica central

  • A estratégia calcula um canal de intervalo XO cujas bandas se expandem em Range * PriceStep. Os rompimentos reiniciam as bandas e estabelecem a direção de tendência atual.
  • ATR(13) controla quão abaixo/acima da vela os níveis de entrada virtual (setas) são traçados: setas longas aparecem em Low - ATR * 3/8, setas curtas em High + ATR * 3/8.
  • Apenas velas completas são processadas. Os sinais podem ser atrasados em SignalBar barras para imitar a lógica de buffering original.

Regras de entrada

  • Entrada comprada: quando uma seta para cima é emitida, entradas compradas são permitidas (EnableBuyEntries = true), nenhuma posição vendida está aberta, e o sinal ainda não foi executado. O volume da operação é igual a Volume.
  • Reentrada comprada: enquanto em uma posição comprada, cada PriceStepTicks ticks adicionais a favor da operação (com base no fechamento da vela) aciona outra compra até que MaxPyramidingPositions camadas sejam abertas. Cada reentrada atualiza os níveis de stop/alvo protetores.
  • Entrada/reentrada vendida: lógica espelho do lado comprado usando a seta para baixo.

Regras de saída

  • Saídas baseadas em sinal: uma seta para cima fecha cada vendido ativo quando EnableSellExits = true; uma seta para baixo fecha o comprado quando EnableBuyExits = true.
  • Saídas de risco: cada camada aberta carrega a mesma distância de stop-loss e take-profit definida em ticks (StopLossTicks, TakeProfitTicks). Quando o preço viola o nível dentro da vela atual, toda a posição é zerada.
  • Zerar manualmente: sinais de entrada opostos também neutralizam a direção anterior antes de abrir uma nova posição.

Gestão de posição

  • O tamanho da posição é fixo em Volume para cada ordem.
  • Stop-loss e take-profit são medidos em ticks do instrumento. Defini-los como zero desabilita a proteção correspondente.
  • O contador de pirâmide reinicia para zero após qualquer saída completa para que o próximo sinal comece a partir de uma posição base nova.

Parâmetros

Parâmetro Descrição Padrão
Volume Tamanho de ordem para cada entrada 1
StopLossTicks Distância de stop em ticks, 0 desabilita 1000
TakeProfitTicks Distância de take-profit em ticks, 0 desabilita 2000
PriceStepTicks Movimento favorável mínimo antes de adicionar à posição 300
MaxPyramidingPositions Número máximo de entradas em camadas (incluindo a primeira) 10
EnableBuyEntries / EnableSellEntries Permitir abrir posições compradas/vendidas true
EnableBuyExits / EnableSellExits Permitir fechar posições compradas/vendidas em setas opostas true
CandleType Período usado para sinais H4
Range Altura da caixa XO em ticks 10
AppliedPrice Fonte de preço usada no detector XO Close
SignalBar Número de barras fechadas para atrasar sinais 1

A estratégia é projetada para backtesting ou operativa ao vivo com instrumentos que fornecem um passo de preço confiável. Ajustar as distâncias baseadas em ticks para corresponder à volatilidade do mercado selecionado.

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; }
	}
}