Auf GitHub ansehen

XOSignal Wiedereinstiegs-Strategie

Diese Strategie reproduziert den MetaTrader-Experten Exp_XOSignal_ReOpen innerhalb von StockSharp unter Verwendung der High-Level-API. Sie handelt Kerzendaten des ausgewählten Symbols und Zeitrahmens mit einem XO-Stil-Ausbruchsdetektor, der auf ATR(13) aufgebaut ist. Wenn ein Aufwärtspfeil erscheint, schließt der Algorithmus Shorts, öffnet optional einen Long, und fügt dann jedes Mal zur Position hinzu, wenn der Preis um eine feste Anzahl von Ticks fortschreitet. Abwärtspfeile verhalten sich symmetrisch für Shorts. Harte Stops und Targets in Ticks werden auf jede Schicht der Pyramide angewendet.

Kernlogik

  • Die Strategie berechnet einen XO-Bereichskanal, dessen Bänder sich um Range * PriceStep erweitern. Ausbrüche setzen die Bänder zurück und legen die aktuelle Trendrichtung fest.
  • ATR(13) steuert, wie weit unterhalb/oberhalb der Kerze die virtuellen Einstiegsniveaus (Pfeile) gezeichnet werden: Long-Pfeile erscheinen bei Low - ATR * 3/8, Short-Pfeile bei High + ATR * 3/8.
  • Nur abgeschlossene Kerzen werden verarbeitet. Signale können um SignalBar Bars verzögert werden, um die ursprüngliche Pufferlogik nachzuahmen.

Einstiegsregeln

  • Long-Einstieg: wenn ein Aufwärtspfeil ausgelöst wird, Long-Einstiege erlaubt sind (EnableBuyEntries = true), keine Short-Position offen ist und das Signal noch nicht ausgeführt wurde. Das Tradevolumen beträgt Volume.
  • Long-Wiedereinstieg: während einer Long-Position löst jede weitere PriceStepTicks Ticks zugunsten des Trades (basierend auf dem Kerzenschluss) eine weitere Kauforder aus, bis MaxPyramidingPositions Schichten geöffnet sind. Jeder Wiedereinstieg aktualisiert die Schutz-Stop-/Target-Niveaus.
  • Short-Einstieg/-Wiedereinstieg: Spiegellogik der Long-Seite unter Verwendung des Abwärtspfeils.

Ausstiegsregeln

  • Signalbasierte Ausstiege: Ein Aufwärtspfeil schließt jeden aktiven Short, wenn EnableSellExits = true; ein Abwärtspfeil schließt den Long, wenn EnableBuyExits = true.
  • Risikoausstiege: Jede offene Schicht trägt dieselbe Stop-Loss- und Take-Profit-Distanz, definiert in Ticks (StopLossTicks, TakeProfitTicks). Wenn der Preis das Niveau innerhalb der aktuellen Kerze durchbricht, wird die gesamte Position glattgestellt.
  • Manuelles Glätten: Entgegengesetzte Einstiegssignale neutralisieren auch die vorherige Richtung vor dem Öffnen einer neuen Position.

Positionsverwaltung

  • Die Positionsgröße ist durch Volume für jede Order fest.
  • Stop-Loss und Take-Profit werden in Sicherheits-Ticks gemessen. Sie auf null setzen deaktiviert den entsprechenden Schutz.
  • Der Pyramidenzähler setzt sich nach jedem vollständigen Ausstieg auf null zurück, damit das nächste Signal von einer frischen Basisposition beginnt.

Parameter

Parameter Beschreibung Standard
Volume Ordergröße für jeden Einstieg 1
StopLossTicks Stop-Abstand in Ticks, 0 deaktiviert 1000
TakeProfitTicks Take-Profit-Abstand in Ticks, 0 deaktiviert 2000
PriceStepTicks Minimale günstige Bewegung vor dem Aufstocken 300
MaxPyramidingPositions Maximale Anzahl gestapelter Einstiege (einschließlich des ersten) 10
EnableBuyEntries / EnableSellEntries Öffnen von Long/Short-Positionen erlauben true
EnableBuyExits / EnableSellExits Schließen von Long/Short-Positionen bei entgegengesetzten Pfeilen erlauben true
CandleType Zeitrahmen für Signale H4
Range XO-Box-Höhe in Ticks 10
AppliedPrice Im XO-Detektor verwendete Preisquelle Close
SignalBar Anzahl geschlossener Bars zur Signalverzögerung 1

Die Strategie ist für Backtesting oder Live-Trading mit Instrumenten konzipiert, die einen zuverlässigen Kursschritt bereitstellen. Die tick-basierten Abstände anpassen, um der Volatilität des ausgewählten Marktes zu entsprechen.

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