在 GitHub 上查看

Stopreversal Trailing 策略

Stopreversal Trailing 策略复刻了 MT5 专家顾问 Exp_Stopreversal.mq5。它调用 Stopreversal 自定义指标,在所选的 K 线价格周围构建动态追踪止损线。当价格向上突破该追踪线时,被视为看涨反转,可选地平掉空头仓位并开多;向下突破则执行相反的看跌操作。为了与原始 EA 保持一致,可以通过参数将信号延后若干个已收盘的 K 线才执行。

细节

  • 入场逻辑:响应 Stopreversal 指标在价格穿越自适应追踪止损时产生的箭头信号。
  • 多空方向:同时支持多头与空头,并提供独立开仓开关。
  • 出场逻辑:反向 Stopreversal 信号可关闭当前仓位,同时可启用保护性的止损与止盈。
  • 止损/止盈:固定价格步长的止损、止盈,加上由指标触发的反转平仓。
  • 数据来源:任意时间框架;默认使用 4 小时 K 线,复现原始专家的多时间框架调用。
  • 信号延迟SignalBar 参数会将下单延迟指定数量的已完成 K 线(默认 1 根)。
  • 风险控制:启动时调用仓位保护服务,并可使用按价格步长设置的硬止损。
  • 指标参数Npips 控制价格与追踪线之间的距离;PriceMode 指定用于计算的价格类型。
  • 默认值
    • Volume = 1
    • StopLossSteps = 1000
    • TakeProfitSteps = 2000
    • BuyPositionOpen = true
    • SellPositionOpen = true
    • BuyPositionClose = true
    • SellPositionClose = true
    • Npips = 0.004
    • PriceMode = Close
    • SignalBar = 1

参数

参数 说明
CandleType 用于 Stopreversal 计算和交易的 K 线类型,默认是 4 小时。
Volume 新建仓位时发送的基础下单量。
StopLossSteps 止损距离(价格步长数量),0 表示关闭。
TakeProfitSteps 止盈距离(价格步长数量),0 表示关闭。
BuyPositionOpen 看涨信号出现时是否允许开多。
SellPositionOpen 看跌信号出现时是否允许开空。
BuyPositionClose 看跌信号出现时是否平掉已有多头。
SellPositionClose 看涨信号出现时是否平掉已有空头。
Npips 调整追踪止损距离的比例系数。
PriceMode 所使用的价格类型(收盘价、开盘价、最高价、最低价、中位价、典型价、加权价、简单均价、四价平均、趋势跟随或 Demark)。
SignalBar 在执行信号前需等待的已收盘 K 线数量,对应 MT5 中的同名参数。

筛选信息

  • 类别:顺势反转
  • 方向:双向
  • 指标:Stopreversal(基于 ATR 的追踪止损)
  • 止损:固定止损与止盈,可选
  • 时间框架:可配置(默认 H4)
  • 季节性:无
  • 神经网络:无
  • 背离:无
  • 复杂度:中等(自定义追踪逻辑)
  • 风险级别:可通过止损距离和追踪参数调节
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>
/// Stopreversal indicator based trailing stop strategy.
/// </summary>
public class StopreversalTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stopLossSteps;
	private readonly StrategyParam<int> _takeProfitSteps;
	private readonly StrategyParam<bool> _buyPositionOpen;
	private readonly StrategyParam<bool> _sellPositionOpen;
	private readonly StrategyParam<bool> _buyPositionClose;
	private readonly StrategyParam<bool> _sellPositionClose;
	private readonly StrategyParam<decimal> _npips;
	private readonly StrategyParam<AppliedPriceModes> _priceMode;
	private readonly StrategyParam<int> _signalBar;

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

	private AverageTrueRange _atr = null!;
	private decimal? _previousStopLevel;
	private decimal? _previousPrice;

	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

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

		_atrPeriod = Param(nameof(AtrPeriod), 15)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR lookback for trailing stop", "Indicator");

		_stopLossSteps = Param(nameof(StopLossSteps), 10)
		.SetNotNegative()
		.SetDisplay("Stop Loss Steps", "Stop loss distance in price steps", "Risk")
		;

		_takeProfitSteps = Param(nameof(TakeProfitSteps), 20)
		.SetNotNegative()
		.SetDisplay("Take Profit Steps", "Take profit distance in price steps", "Risk")
		;

		_buyPositionOpen = Param(nameof(BuyPositionOpen), true)
		.SetDisplay("Open Long", "Allow opening long positions", "Trading");

		_sellPositionOpen = Param(nameof(SellPositionOpen), true)
		.SetDisplay("Open Short", "Allow opening short positions", "Trading");

		_buyPositionClose = Param(nameof(BuyPositionClose), true)
		.SetDisplay("Close Long", "Close long positions on sell signals", "Trading");

		_sellPositionClose = Param(nameof(SellPositionClose), true)
		.SetDisplay("Close Short", "Close short positions on buy signals", "Trading");

		_npips = Param(nameof(Npips), 0.004m)
		.SetGreaterThanZero()
		.SetDisplay("Trailing Offset", "Fractional offset applied to the stop line", "Indicator")
		;

		_priceMode = Param(nameof(PriceMode), AppliedPriceModes.Close)
		.SetDisplay("Applied Price", "Price source used by the trailing stop", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetNotNegative()
		.SetDisplay("Signal Bar", "Bar delay before acting on a signal", "Indicator")
		;
	}

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

	/// <summary>
	/// ATR period used for the trailing stop calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in price steps.
	/// </summary>
	public int StopLossSteps
	{
		get => _stopLossSteps.Value;
		set => _stopLossSteps.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in price steps.
	/// </summary>
	public int TakeProfitSteps
	{
		get => _takeProfitSteps.Value;
		set => _takeProfitSteps.Value = value;
	}

	/// <summary>
	/// Enable long entries.
	/// </summary>
	public bool BuyPositionOpen
	{
		get => _buyPositionOpen.Value;
		set => _buyPositionOpen.Value = value;
	}

	/// <summary>
	/// Enable short entries.
	/// </summary>
	public bool SellPositionOpen
	{
		get => _sellPositionOpen.Value;
		set => _sellPositionOpen.Value = value;
	}

	/// <summary>
	/// Close long positions on short signals.
	/// </summary>
	public bool BuyPositionClose
	{
		get => _buyPositionClose.Value;
		set => _buyPositionClose.Value = value;
	}

	/// <summary>
	/// Close short positions on long signals.
	/// </summary>
	public bool SellPositionClose
	{
		get => _sellPositionClose.Value;
		set => _sellPositionClose.Value = value;
	}

	/// <summary>
	/// Fractional offset used by the trailing stop calculation.
	/// </summary>
	public decimal Npips
	{
		get => _npips.Value;
		set => _npips.Value = value;
	}

	/// <summary>
	/// Price source used when computing the trailing level.
	/// </summary>
	public AppliedPriceModes PriceMode
	{
		get => _priceMode.Value;
		set => _priceMode.Value = value;
	}

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_signals.Clear();
		_previousStopLevel = null;
		_previousPrice = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
	}

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

		_atr = new AverageTrueRange
		{
			Length = AtrPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_atr, ProcessCandle)
		.Start();

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

		// no protection
	}

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

		UpdateStops(candle);

		var price = GetAppliedPrice(candle);
		var prevStop = _previousStopLevel ?? price * (1m - Npips);
		var prevPrice = _previousPrice ?? price;
		var hasPrev = _previousStopLevel.HasValue && _previousPrice.HasValue;

		var stop = CalculateStop(price, prevPrice, prevStop);

		var buySignal = hasPrev && price > stop && prevPrice < prevStop && prevStop != 0m;
		var sellSignal = hasPrev && price < stop && prevPrice > prevStop && prevStop != 0m;

		_previousPrice = price;
		_previousStopLevel = stop;

		_signals.Add(new SignalInfo
		{
			BuySignal = buySignal,
			SellSignal = sellSignal,
			ClosePrice = candle.ClosePrice,
			Time = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime
		});

		TrimSignals();

		if (_signals.Count <= SignalBar)
		return;

		var index = _signals.Count - 1 - SignalBar;
		if (index < 0)
		return;

		var signal = _signals[index];
		var allowTrading = _atr.IsFormed;

		ExecuteSignal(signal, allowTrading);
	}

	private void ExecuteSignal(SignalInfo signal, bool allowTrading)
	{
		if (SellPositionClose && signal.BuySignal && Position < 0)
		{
			BuyMarket();
			ResetShortStops();
		}

		if (BuyPositionClose && signal.SellSignal && Position > 0)
		{
			SellMarket();
			ResetLongStops();
		}

		if (!allowTrading || Position != 0)
		return;

		if (BuyPositionOpen && signal.BuySignal)
		{
			if (Volume > 0)
			{
				BuyMarket();
				ResetShortStops();
				SetLongStops(signal.ClosePrice);
			}
		}
		else if (SellPositionOpen && signal.SellSignal)
		{
			if (Volume > 0)
			{
				SellMarket();
				ResetLongStops();
				SetShortStops(signal.ClosePrice);
			}
		}
	}

	private void UpdateStops(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal longStop && candle.LowPrice <= longStop)
			{
				SellMarket();
				ResetLongStops();
				return;
			}

			if (_longTake is decimal longTake && candle.HighPrice >= longTake)
			{
				SellMarket();
				ResetLongStops();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal shortStop && candle.HighPrice >= shortStop)
			{
				BuyMarket();
				ResetShortStops();
				return;
			}

			if (_shortTake is decimal shortTake && candle.LowPrice <= shortTake)
			{
				BuyMarket();
				ResetShortStops();
			}
		}
	}

	private void TrimSignals()
	{
		var max = Math.Max(SignalBar + 5, 10);
		while (_signals.Count > max)
		{
			try { _signals.RemoveAt(0); } catch { break; }
		}
	}

	private decimal CalculateStop(decimal price, decimal prevPrice, decimal prevStop)
	{
		var offset = Npips;

		if (price == prevStop)
		return prevStop;

		if (prevPrice < prevStop && price < prevStop)
		return Math.Min(prevStop, price * (1m + offset));

		if (prevPrice > prevStop && price > prevStop)
		return Math.Max(prevStop, price * (1m - offset));

		return price > prevStop
		? price * (1m - offset)
		: price * (1m + offset);
	}

	private void SetLongStops(decimal basePrice)
	{
		var step = GetEffectiveStep();

		_longStop = StopLossSteps > 0 ? basePrice - step * StopLossSteps : null;
		_longTake = TakeProfitSteps > 0 ? basePrice + step * TakeProfitSteps : null;
	}

	private void SetShortStops(decimal basePrice)
	{
		var step = GetEffectiveStep();

		_shortStop = StopLossSteps > 0 ? basePrice + step * StopLossSteps : null;
		_shortTake = TakeProfitSteps > 0 ? basePrice - step * TakeProfitSteps : null;
	}

	private void ResetLongStops()
	{
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortStops()
	{
		_shortStop = null;
		_shortTake = null;
	}

	private decimal GetEffectiveStep()
	{
		var step = Security?.PriceStep;
		if (step is decimal s && s > 0)
		return s;

		return 0.0001m;
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		var open = candle.OpenPrice;
		var close = candle.ClosePrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		return PriceMode switch
		{
			AppliedPriceModes.Close => close,
			AppliedPriceModes.Open => open,
			AppliedPriceModes.High => high,
			AppliedPriceModes.Low => low,
			AppliedPriceModes.Median => (high + low) / 2m,
			AppliedPriceModes.Typical => (close + high + low) / 3m,
			AppliedPriceModes.Weighted => (2m * close + high + low) / 4m,
			AppliedPriceModes.Simple => (open + close) / 2m,
			AppliedPriceModes.Quarter => (open + close + high + low) / 4m,
			AppliedPriceModes.TrendFollow0 => close > open ? high : close < open ? low : close,
			AppliedPriceModes.TrendFollow1 => close > open ? (high + close) / 2m : close < open ? (low + close) / 2m : close,
			AppliedPriceModes.Demark => CalculateDemarkPrice(open, high, low, close),
			_ => close
		};
	}

	private static decimal CalculateDemarkPrice(decimal open, decimal high, decimal low, decimal close)
	{
		var result = high + low + close;

		if (close < open)
		result = (result + low) / 2m;
		else if (close > open)
		result = (result + high) / 2m;
		else
		result = (result + close) / 2m;

		return ((result - low) + (result - high)) / 2m;
	}

	private sealed class SignalInfo
	{
		public bool BuySignal { get; init; }
		public bool SellSignal { get; init; }
		public decimal ClosePrice { get; init; }
		public DateTimeOffset Time { get; init; }
	}

	/// <summary>
	/// Available price calculation modes.
	/// </summary>
	public enum AppliedPriceModes
	{
		/// <summary>
		/// Closing price.
		/// </summary>
		Close,

		/// <summary>
		/// Opening price.
		/// </summary>
		Open,

		/// <summary>
		/// Highest price.
		/// </summary>
		High,

		/// <summary>
		/// Lowest price.
		/// </summary>
		Low,

		/// <summary>
		/// Median price (high + low) / 2.
		/// </summary>
		Median,

		/// <summary>
		/// Typical price (close + high + low) / 3.
		/// </summary>
		Typical,

		/// <summary>
		/// Weighted close price (2 * close + high + low) / 4.
		/// </summary>
		Weighted,

		/// <summary>
		/// Simple average of open and close.
		/// </summary>
		Simple,

		/// <summary>
		/// Average of open, close, high and low.
		/// </summary>
		Quarter,

		/// <summary>
		/// Trend follow price variant 0.
		/// </summary>
		TrendFollow0,

		/// <summary>
		/// Trend follow price variant 1.
		/// </summary>
		TrendFollow1,

		/// <summary>
		/// Demark price formula.
		/// </summary>
		Demark
	}
}