在 GitHub 上查看

XOSignal Re-Open 策略

该策略在 StockSharp 中复刻 MetaTrader 专家顾问 Exp_XOSignal_ReOpen,并使用高层 API 实现。算法基于所选品种与周期的K线,通过 ATR(13) 构建的 XO 型突破指示器来生成交易信号。出现上箭头时平掉空单、可选择开多,并在价格每向有利方向移动固定 tick 数时加仓;下箭头对空头执行同样的逻辑。每一层加仓都会附带以 tick 为单位的固定止损与止盈。

核心逻辑

  • 构建宽度为 Range * PriceStep 的 XO 通道。突破通道将重置边界并确定当前趋势方向。
  • ATR(13) 决定箭头离当根K线的距离:多头箭头位于 Low - ATR * 3/8,空头箭头位于 High + ATR * 3/8
  • 仅处理收盘后的K线,可通过 SignalBar 参数将信号延后若干根。

入场规则

  • 开多:当出现上箭头、允许多头入场(EnableBuyEntries = true)、没有持仓或空头已平且该信号尚未执行时,以 Volume 为数量买入。
  • 多头加仓:持有多单期间,每当收盘价较最近加仓价上涨 PriceStepTicks 个 tick,就再买入一次,最多达到 MaxPyramidingPositions 层,并同步更新保护性止损/止盈。
  • 开空与加仓:与多头逻辑完全镜像。

出场规则

  • 信号出场:上箭头在 EnableSellExits = true 时平掉所有空单;下箭头在 EnableBuyExits = true 时平掉所有多单。
  • 风险控制:所有持仓层级共用 StopLossTicksTakeProfitTicks 指定的 tick 距离。当当根K线触及该水平时,头寸被全部平仓。
  • 方向切换:出现相反方向的入场信号时,会先平掉已有仓位再建立新方向。

仓位管理

  • 每笔订单的数量由 Volume 决定。
  • 止损与止盈以 tick 表示,设置为 0 可关闭该防护。
  • 当仓位全部出场后,加仓计数会重置,下一次信号重新从第一层开始。

参数

参数 含义 默认值
Volume 每次下单数量 1
StopLossTicks 止损 tick 距离(0 表示关闭) 1000
TakeProfitTicks 止盈 tick 距离(0 表示关闭) 2000
PriceStepTicks 有利方向移动多少 tick 后加仓 300
MaxPyramidingPositions 含首单在内的最大分层数量 10
EnableBuyEntries / EnableSellEntries 允许开多 / 开空 true
EnableBuyExits / EnableSellExits 允许在反向箭头时平多 / 平空 true
CandleType 计算使用的K线周期 H4
Range XO 盒子的高度(tick) 10
AppliedPrice XO 指标取值类型 Close
SignalBar 信号延后执行的已收盘K线数量 1

在实际使用前请根据交易品种的波动幅度调整 tick 型参数,并确认品种的最小报价步长设置正确。

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