在 GitHub 上查看

Trailing Stop Manager 策略

概述

Trailing Stop Manager 策略 是对 MetaTrader 专家顾问 Trailing Sl.mq5 的 StockSharp 移植版本。原始 EA 并不会主动开仓, 而是监听具有指定 magic number 的既有持仓,当行情向有利方向推进时上调止损位。本 C# 实现利用 StockSharp 的高层 API 重现这一逻辑,为任何受 StockSharp 支持的标的提供透明的跟踪止损管理。

跟踪逻辑

  1. 订阅盘口,持续获取最新的买一与卖一报价。
  2. 判断当前净头寸的方向(多头或空头)。
  3. 使用相应的市场一档报价计算浮动盈亏(多头使用买价,空头使用卖价)。
  4. 当浮盈超过 TriggerPoints(通过 PriceStep 转换为价格单位)时启动跟踪模式。
  5. 在当前行情附近按照 TrailingPoints 的距离放置跟踪止损线。
  6. 仅在盈利扩大时向盈利方向移动止损,以持续锁定利润。
  7. 一旦一档报价触及计算出的止损线,立即发送对冲方向的市价单平掉整笔仓位。

订单与风控

  • 策略不会发送初始进场订单,只负责管理已经存在的头寸,无论该头寸来自手动交易还是其他策略。
  • 通过 BuyMarket/SellMarket 完成平仓,对应 MetaTrader 中的 PositionModify 操作。
  • 跟踪距离会根据标的的 PriceStep 自动换算,保留 EA 中以点为单位的配置体验。
  • 头寸平仓后,内部状态会被重置,新建头寸会重新按照触发条件开始管理。

参数

名称 类型 默认值 说明
TrailingPoints int 1000 当前价格与跟踪止损之间的距离,单位为价格步长。
TriggerPoints int 1500 启动跟踪所需的最小浮盈,单位为价格步长。

使用建议

  • 将策略附加到需要被管理的证券上后,它会立即开始跟踪当前的净头寸。
  • 请将策略的初始 Volume 设置为与现有仓位数量一致。StockSharp 采用净头寸模式,触发止损后会一次性平掉全部仓位。
  • 如果交易品种的价格步长较大,可适当上调 TrailingPointsTriggerPoints 以避免过早出场。
  • 策略的全部状态都保存在 StockSharp 内部,可与任何依赖 StockSharp 执行订单的人工或自动系统搭配使用。

与原版 MetaTrader EA 的差异

  • MetaTrader 根据 magic number 区分不同订单;StockSharp 采用单一净头寸模型,因此不再需要逐票筛选。
  • 原 EA 中的 SetlossTakeProfitLots 参数未被使用,本移植版本也不再提供,以突出跟踪止损的核心功能。
  • PositionModify 被替换为直接的市价平仓,这是 StockSharp 净值账户中最稳定的做法。
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>
/// Strategy that mirrors the MetaTrader "Trailing Sl" expert by managing trailing stops for existing positions.
/// Adds SMA crossover entries for backtesting.
/// </summary>
public class TrailingStopTriggerManagerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _trailingPoints;
	private readonly StrategyParam<int> _triggerPoints;

	private decimal _lastEntryPrice;
	private decimal? _activeStopPrice;
	private bool _trailingEnabled;
	private decimal _trailingDistance;
	private decimal _triggerDistance;
	private decimal _prevFast;
	private decimal _prevSlow;
	private bool _hasPrev;

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

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public int TrailingPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Profit distance that activates trailing stop.
	/// </summary>
	public int TriggerPoints
	{
		get => _triggerPoints.Value;
		set => _triggerPoints.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public TrailingStopTriggerManagerStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_trailingPoints = Param(nameof(TrailingPoints), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Points", "Trailing stop distance", "Trailing Management");

		_triggerPoints = Param(nameof(TriggerPoints), 1500)
			.SetGreaterThanZero()
			.SetDisplay("Trigger Points", "Profit to activate trailing", "Trailing Management");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_lastEntryPrice = 0m;
		_activeStopPrice = null;
		_trailingEnabled = false;
		_trailingDistance = 0m;
		_triggerDistance = 0m;
		_prevFast = 0m;
		_prevSlow = 0m;
		_hasPrev = false;
	}

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

		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m) step = 1m;
		_trailingDistance = step * TrailingPoints;
		_triggerDistance = step * TriggerPoints;

		var smaFast = new SimpleMovingAverage { Length = 10 };
		var smaSlow = new SimpleMovingAverage { Length = 30 };

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

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

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		_lastEntryPrice = trade.Trade.Price;
	}

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

		var price = candle.ClosePrice;

		// Trailing stop management
		if (Position > 0 && _lastEntryPrice > 0)
		{
			var profit = price - _lastEntryPrice;
			if (!_trailingEnabled && profit >= _triggerDistance)
			{
				_trailingEnabled = true;
				_activeStopPrice = price - _trailingDistance;
			}
			else if (_trailingEnabled)
			{
				var desiredStop = price - _trailingDistance;
				if (!_activeStopPrice.HasValue || desiredStop > _activeStopPrice.Value)
					_activeStopPrice = desiredStop;
			}

			if (_trailingEnabled && _activeStopPrice.HasValue && price <= _activeStopPrice.Value)
			{
				SellMarket();
				ResetTrailingState();
				return;
			}
		}
		else if (Position < 0 && _lastEntryPrice > 0)
		{
			var profit = _lastEntryPrice - price;
			if (!_trailingEnabled && profit >= _triggerDistance)
			{
				_trailingEnabled = true;
				_activeStopPrice = price + _trailingDistance;
			}
			else if (_trailingEnabled)
			{
				var desiredStop = price + _trailingDistance;
				if (!_activeStopPrice.HasValue || desiredStop < _activeStopPrice.Value)
					_activeStopPrice = desiredStop;
			}

			if (_trailingEnabled && _activeStopPrice.HasValue && price >= _activeStopPrice.Value)
			{
				BuyMarket();
				ResetTrailingState();
				return;
			}
		}

		// SMA crossover entries
		if (_hasPrev)
		{
			var crossUp = _prevFast <= _prevSlow && fast > slow;
			var crossDown = _prevFast >= _prevSlow && fast < slow;

			if (crossUp && Position <= 0)
			{
				if (Position < 0)
					BuyMarket();
				BuyMarket();
				ResetTrailingState();
			}
			else if (crossDown && Position >= 0)
			{
				if (Position > 0)
					SellMarket();
				SellMarket();
				ResetTrailingState();
			}
		}

		_prevFast = fast;
		_prevSlow = slow;
		_hasPrev = true;
	}

	private void ResetTrailingState()
	{
		_trailingEnabled = false;
		_activeStopPrice = null;
	}
}