在 GitHub 上查看

My TS15 移动均线跟踪止损

概述

该策略重现了 my_ts15.mq5 专家的核心功能:不负责建仓,只负责根据移动均线自动管理已有仓位的止损单。默认使用线性加权均线(LWMA),也可以选择其它平滑方式。核心流程如下:

  • 订阅完成的蜡烛并读取移动均线值。
  • 以均线和价格为基准计算两个候选的止损位置。
  • 只有当新的止损比当前止损至少改善 TrailStepPoints 个点时才会移动订单。
  • 可选的最大亏损控制会将止损限制在给定距离内,若价格突破该距离则立即平仓。

因此本策略通常与其他发出进场信号的模块一起使用,用于为已有仓位提供动态保护。

跟踪逻辑

  1. 通过 BindEx 订阅所选蜡烛并绑定移动均线指标,无需直接访问指标缓冲区。
  2. 在每根蜡烛收盘后保存指标结果,并取距当前 MaBarsTrail + MaShift 根蜡烛的位置作为参考。
  3. 利用合约的最小报价增量将所有“点”参数转换为绝对价格距离。
  4. 多头持仓时在以下两者中取较小者:均线减去均线偏移价格减去盈利偏移,随后再考虑亏损偏移与最大亏损限制。
  5. 空头持仓时在 均线加上均线偏移价格加上盈利偏移 中取较大者,并应用亏损偏移与最大亏损限制。
  6. 仅在改进幅度超过 TrailStepPoints(为零表示任意改进都接受)时更新止损订单。
  7. 若启用 EnforceMaxStopLoss 且价格突破 MaxStopLossPoints,立即关闭仓位。

移动均线使用 MaPrice 指定的蜡烛价格,默认等同于原 MQL 程序中采用的加权价格 PRICE_WEIGHTED

参数

参数 默认值 说明
MaPeriod 50 跟踪所用移动均线的长度。
MaShift 0 取值时向左平移的额外蜡烛数量。
MaMethod LinearWeighted 均线平滑方式(简单、指数、平滑、线性加权)。
MaPrice Weighted 输入到均线的蜡烛价格。
MaBarsTrail 1 当前蜡烛与参考均线值之间的完成蜡烛数量。
TrailBehindMaPoints 5 止损与均线之间保持的点数距离。
TrailBehindPricePoints 30 价格处于盈利区间时与价格保持的点数距离。
TrailBehindNegativePoints 60 价格处于亏损区间时与价格保持的点数距离。
TrailStepPoints 0 移动止损所需的最小改进点数,零表示任何改进都移动。
EnforceMaxStopLoss false 是否启用最大亏损控制。
MaxStopLossPoints 100 允许的最大亏损距离(点)。
ShowIndicator true 若界面可用,是否在图表上绘制均线及成交标记。
CandleType M1 用于计算的蜡烛数据类型。

所有点数参数均通过 Security.PriceStep 计算的点值转换为绝对价格。

转换说明

  • 原策略通过句柄手动刷新均线。现在借助 BindEx 自动处理指标,符合项目禁止直接访问缓冲区的要求。
  • MetaTrader 中使用 Bid/Ask 进行比较,而本实现基于 MaPrice 指定的蜡烛价格。这与原脚本一致,因为均线也使用同样的价格源。
  • PositionModify 被替换为“取消旧止损 + 注册新止损”的流程(多头使用 SellStop,空头使用 BuyStop),并保留上一止损价格以实现步进逻辑。
  • pre_init 功能通过 EnforceMaxStopLoss 实现:当价格突破最大亏损距离时立即平仓。
  • 策略不包含进场信号,需要与其他策略或手工交易协同使用。

使用建议

  1. 在与建仓模块相同的证券上运行本策略。
  2. 根据合约报价格式调整各个点数参数,外汇品种通常需要将点值与 pip 对齐。
  3. 若不希望频繁修改订单,可将 TrailStepPoints 设置为正值。
  4. 若已有其它风控系统管理最大亏损,可关闭 EnforceMaxStopLoss
  5. 调参阶段建议保持 ShowIndicator 打开,以便在图表上观察均线与止损的相互位置。
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// My TS15 strategy: WMA trend following with trailing stop management.
/// Enters on price crossing WMA, exits with trailing stop logic.
/// </summary>
public class MyTs15Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _trailMultiplier;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private decimal _entryPrice;
	private decimal _bestPrice;
	private bool _wasBullish;
	private bool _hasPrevSignal;
	private int _candlesSinceTrade;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public decimal TrailMultiplier { get => _trailMultiplier.Value; set => _trailMultiplier.Value = value; }
	public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }

	public MyTs15Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(120).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_maPeriod = Param(nameof(MaPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "WMA period", "Indicators");
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR period for trailing", "Indicators");
		_trailMultiplier = Param(nameof(TrailMultiplier), 3m)
			.SetDisplay("Trail Multiplier", "ATR multiplier for trailing stop", "Risk");
		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 12)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0m;
		_bestPrice = 0m;
		_wasBullish = false;
		_hasPrevSignal = false;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_entryPrice = 0;
		_bestPrice = 0;
		_hasPrevSignal = false;
		_candlesSinceTrade = SignalCooldownCandles;
		var wma = new WeightedMovingAverage { Length = MaPeriod };
		var atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(wma, atr, ProcessCandle).Start();
	}

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

		var close = candle.ClosePrice;
		var trailDist = atrValue * TrailMultiplier;
		var isBullish = close > wmaValue;

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		// Trailing stop check
		if (Position > 0)
		{
			if (close > _bestPrice) _bestPrice = close;
			if (_bestPrice - close > trailDist)
			{
				SellMarket();
				_entryPrice = 0;
				_bestPrice = 0;
				_candlesSinceTrade = 0;
				return;
			}
		}
		else if (Position < 0)
		{
			if (close < _bestPrice) _bestPrice = close;
			if (close - _bestPrice > trailDist)
			{
				BuyMarket();
				_entryPrice = 0;
				_bestPrice = 0;
				_candlesSinceTrade = 0;
				return;
			}
		}

		// Entry signals
		if (_hasPrevSignal && isBullish != _wasBullish && _candlesSinceTrade >= SignalCooldownCandles)
		{
			if (isBullish && Position <= 0)
			{
				BuyMarket();
				_entryPrice = close;
				_bestPrice = close;
				_candlesSinceTrade = 0;
			}
			else if (!isBullish && Position >= 0)
			{
				SellMarket();
				_entryPrice = close;
				_bestPrice = close;
				_candlesSinceTrade = 0;
			}
		}

		_wasBullish = isBullish;
		_hasPrevSignal = true;
	}
}