在 GitHub 上查看

跟踪止损与止盈策略

概述

Trailing Stop And Take StrategyMQL/19963 中 MetaTrader 专家顾问的 StockSharp 版本。策略专注于头寸管理:开仓后立即设置初始止损与止盈,然后在价格波动时动态跟踪两者。跟踪调整遵循可配置的最小步长、保本保护,并可选择禁止在亏损区间内移动。

策略基于单一标的的收盘 K 线运行。当没有持仓时,它会按照最近一根 K 线的实体方向开仓(阳线买入、阴线卖出),以复现原 MQL 脚本的测试行为,从而为跟踪模块提供持续的持仓样本。

工作流程

  1. 订阅配置的 K 线类型,仅处理收盘的 K 线。
  2. 空仓时根据 K 线方向开多或开空(遵循仓位类型过滤器)。
  3. 新持仓建立后,使用 InitialStopLossPoints/InitialTakeProfitPoints 设置初始止损与止盈;若为零,则改用对应的跟踪距离。
  4. 每根 K 线收盘时计算新的跟踪价格:
    • 价格朝有利方向移动超过跟踪步长后才推动止损靠近价格。
    • 价格向不利方向回调超过跟踪步长时才收紧止盈。
    • AllowTrailingLoss 关闭时,保本阈值阻止止损/止盈回到亏损区间。
  5. 当价格穿越任何跟踪止损或止盈时,使用市价单离场并重置所有记录的价格。

跟踪逻辑

多头

  • 初始止损至少要距离开仓价 SpreadMultiplier * PriceStep
  • 初始止盈同样至少高于开仓价这一最小距离。
  • 跟踪止损按 TrailingStopLossPoints 与收盘价保持距离,在满足步长与保本条件后上移。
  • 跟踪止盈在价格回调时下移,且在禁止亏损跟踪时不会低于保本线。

空头

  • 初始止损位于开仓价上方,距离不少于乘以点差倍数的最小距离。
  • 初始止盈位于开仓价下方,也遵循相同的距离规则。
  • 跟踪止损随价格下跌而下移,但若禁用亏损跟踪则不会高于保本线。
  • 跟踪止盈在价格反弹时上移,并在需要时限制在保本价位。

参数

参数 说明
CandleType 用于计算的 K 线周期。
Volume 开仓与平仓的默认数量。
PositionType 仅管理多头、空头或同时管理两者。
InitialStopLossPoints 初始止损距离(若为零则使用跟踪止损距离)。
InitialTakeProfitPoints 初始止盈距离(若为零则使用跟踪止盈距离)。
TrailingStopLossPoints 跟踪止损与价格之间的距离。
TrailingTakeProfitPoints 跟踪止盈与价格之间的距离。
TrailingStepPoints 更新止损或止盈所需的最小价格变动。
AllowTrailingLoss 是否允许在亏损区间内移动止损/止盈。
BreakevenPoints 用于计算保本价的点数偏移。
SpreadMultiplier 近似模拟 MQL StopLevel 的最小距离倍数。

说明

  • 触发止损或止盈时使用市价单离场,简单直观,同时保留原脚本修改仓位的风格。
  • SpreadMultiplier 近似 MQL 中的点差限制,可根据交易所或经纪商调整。
  • 根据要求,本策略仅提供 C# 实现,不包含 Python 版本。
  • 如需与自定义信号结合,可关闭内置入场逻辑并从外部注入订单,仅使用该跟踪模块。
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 manages trailing stop-loss and take-profit levels similar to the original MQL Expert Advisor.
/// </summary>
public class TrailingStopAndTakeStrategy : Strategy
{
	public enum TrailingPositionTypes
	{
		All,
		Long,
		Short,
	}

	private readonly StrategyParam<decimal> _epsilon;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<TrailingPositionTypes> _positionType;
	private readonly StrategyParam<decimal> _initialStopLossPoints;
	private readonly StrategyParam<decimal> _initialTakeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopLossPoints;
	private readonly StrategyParam<decimal> _trailingTakeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<bool> _allowTrailingLoss;
	private readonly StrategyParam<decimal> _breakevenPoints;
	private readonly StrategyParam<int> _spreadMultiplier;

	private decimal _priceStep;
	private decimal _previousPosition;

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

	/// <summary>
	/// Initializes a new instance of the <see cref="TrailingStopAndTakeStrategy"/>.
	/// </summary>
	public TrailingStopAndTakeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle aggregation used for trailing decisions", "General");


		_positionType = Param(nameof(PositionType), TrailingPositionTypes.All)
			.SetDisplay("Position Filter", "Positions managed by the trailing engine", "Trading");

		_initialStopLossPoints = Param(nameof(InitialStopLossPoints), 400m)
			.SetRange(0m, 10000m)
			.SetDisplay("Initial Stop", "Initial stop-loss size in price points", "Risk")
			;

		_initialTakeProfitPoints = Param(nameof(InitialTakeProfitPoints), 400m)
			.SetRange(0m, 10000m)
			.SetDisplay("Initial Take", "Initial take-profit size in price points", "Risk")
			;

		_trailingStopLossPoints = Param(nameof(TrailingStopLossPoints), 200m)
			.SetRange(0m, 10000m)
			.SetDisplay("Trailing Stop", "Trailing stop distance in price points", "Risk")
			;

		_trailingTakeProfitPoints = Param(nameof(TrailingTakeProfitPoints), 200m)
			.SetRange(0m, 10000m)
			.SetDisplay("Trailing Take", "Trailing take-profit distance in price points", "Risk")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 10m)
			.SetRange(0m, 1000m)
			.SetDisplay("Trailing Step", "Minimum movement required before adjusting targets", "Risk");

		_epsilon = Param(nameof(Epsilon), 0.0000001m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Epsilon", "Minimum trailing step size", "Risk");

		_allowTrailingLoss = Param(nameof(AllowTrailingLoss), false)
			.SetDisplay("Trail In Loss", "Allow trailing while position is not yet profitable", "Risk");

		_breakevenPoints = Param(nameof(BreakevenPoints), 6m)
			.SetRange(0m, 1000m)
			.SetDisplay("Breakeven Points", "Profit offset used for breakeven protection", "Risk");

		_spreadMultiplier = Param(nameof(SpreadMultiplier), 2)
			.SetRange(1, 20)
			.SetDisplay("Spread Multiplier", "Multiplier applied to minimal stop distance", "Execution");
	}

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


	/// <summary>
	/// Position filter managed by the trailing logic.
	/// </summary>
	public TrailingPositionTypes PositionType
	{
		get => _positionType.Value;
		set => _positionType.Value = value;
	}

	/// <summary>
	/// Initial stop-loss size expressed in price points.
	/// </summary>
	public decimal InitialStopLossPoints
	{
		get => _initialStopLossPoints.Value;
		set => _initialStopLossPoints.Value = value;
	}

	/// <summary>
	/// Initial take-profit size expressed in price points.
	/// </summary>
	public decimal InitialTakeProfitPoints
	{
		get => _initialTakeProfitPoints.Value;
		set => _initialTakeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price points.
	/// </summary>
	public decimal TrailingStopLossPoints
	{
		get => _trailingStopLossPoints.Value;
		set => _trailingStopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing take-profit distance expressed in price points.
	/// </summary>
	public decimal TrailingTakeProfitPoints
	{
		get => _trailingTakeProfitPoints.Value;
		set => _trailingTakeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum movement required before stops or targets are updated.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}
	/// <summary>
	/// Minimum trailing step size used as a floor.
	/// </summary>
	public decimal Epsilon
	{
		get => _epsilon.Value;
		set => _epsilon.Value = value;
	}


	/// <summary>
	/// Enables trailing adjustments while the position remains in the loss zone.
	/// </summary>
	public bool AllowTrailingLoss
	{
		get => _allowTrailingLoss.Value;
		set => _allowTrailingLoss.Value = value;
	}

	/// <summary>
	/// Profit offset in points used to define the breakeven level.
	/// </summary>
	public decimal BreakevenPoints
	{
		get => _breakevenPoints.Value;
		set => _breakevenPoints.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the minimal stop distance approximation.
	/// </summary>
	public int SpreadMultiplier
	{
		get => _spreadMultiplier.Value;
		set => _spreadMultiplier.Value = value;
	}

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

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

		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

		_previousPosition = 0m;
		ResetLevels();

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_priceStep = 0m;
		_previousPosition = 0m;
		_entryPrice = 0m;
		ResetLevels();
	}

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

		// Handle long positions first.
		if (Position > 0m)
		{
			if (PositionType == TrailingPositionTypes.Short)
			{
				ResetLongLevels();
			}
			else
			{
				if (_previousPosition <= 0m)
					ResetShortLevels();

				EnsureLongInitialized();
				UpdateLongTrailing(candle);
				ManageLongExits(candle);
			}
		}
		else if (Position < 0m)
		{
			if (PositionType == TrailingPositionTypes.Long)
			{
				ResetShortLevels();
			}
			else
			{
				if (_previousPosition >= 0m)
					ResetLongLevels();

				EnsureShortInitialized();
				UpdateShortTrailing(candle);
				ManageShortExits(candle);
			}
		}
		else
		{
			ResetLevels();
		}

		// Try to open a new position once flat.
		TryEnter(candle);

		_previousPosition = Position;
	}

	private void TryEnter(ICandleMessage candle)
	{
		if (Position != 0m || Volume <= 0m)
			return;

		// Simple directional entry mirroring the tester behavior from the MQL script.
		if (PositionType == TrailingPositionTypes.Long)
		{
			if (candle.ClosePrice > candle.OpenPrice)
				BuyMarket(Volume);
		}
		else if (PositionType == TrailingPositionTypes.Short)
		{
			if (candle.ClosePrice < candle.OpenPrice)
				SellMarket(Volume);
		}
		else
		{
			if (candle.ClosePrice > candle.OpenPrice)
				BuyMarket(Volume);
			else if (candle.ClosePrice < candle.OpenPrice)
				SellMarket(Volume);
		}
	}

	private void EnsureLongInitialized()
	{
		if (Position <= 0m)
			return;

		var entryPrice = _entryPrice;
		if (entryPrice <= 0m)
			return;

		var minDistance = GetMinStopDistance();

		if (_longStop == null)
		{
			var points = InitialStopLossPoints > 0m
				? InitialStopLossPoints
				: TrailingStopLossPoints > 0m ? TrailingStopLossPoints : 0m;

			if (points > 0m)
			{
				var candidate = entryPrice - points * _priceStep;
				var minAllowed = entryPrice - minDistance;
				_longStop = Math.Min(candidate, minAllowed);
			}
		}

		if (_longTake == null)
		{
			var points = InitialTakeProfitPoints > 0m
				? InitialTakeProfitPoints
				: TrailingTakeProfitPoints > 0m ? TrailingTakeProfitPoints : 0m;

			if (points > 0m)
			{
				var candidate = entryPrice + points * _priceStep;
				var minAllowed = entryPrice + minDistance;
				_longTake = Math.Max(candidate, minAllowed);
			}
		}
	}

	private void EnsureShortInitialized()
	{
		if (Position >= 0m)
			return;

		var entryPrice = _entryPrice;
		if (entryPrice <= 0m)
			return;

		var minDistance = GetMinStopDistance();

		if (_shortStop == null)
		{
			var points = InitialStopLossPoints > 0m
				? InitialStopLossPoints
				: TrailingStopLossPoints > 0m ? TrailingStopLossPoints : 0m;

			if (points > 0m)
			{
				var candidate = entryPrice + points * _priceStep;
				var minAllowed = entryPrice + minDistance;
				_shortStop = Math.Max(candidate, minAllowed);
			}
		}

		if (_shortTake == null)
		{
			var points = InitialTakeProfitPoints > 0m
				? InitialTakeProfitPoints
				: TrailingTakeProfitPoints > 0m ? TrailingTakeProfitPoints : 0m;

			if (points > 0m)
			{
				var candidate = entryPrice - points * _priceStep;
				var minAllowed = entryPrice - minDistance;
				_shortTake = Math.Min(candidate, minAllowed);
			}
		}
	}

	private void UpdateLongTrailing(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice <= 0m)
			return;

		var breakeven = entryPrice + BreakevenPoints * _priceStep;
		var trailingStep = Math.Max(TrailingStepPoints * _priceStep, Epsilon);
		var minDistance = GetMinStopDistance();

		if (TrailingStopLossPoints > 0m)
		{
			var candidate = candle.ClosePrice - TrailingStopLossPoints * _priceStep;
			var minAllowed = candle.ClosePrice - minDistance;
			var newStop = Math.Min(candidate, minAllowed);

			if (!AllowTrailingLoss && newStop < breakeven)
			{
				// Skip moving the stop into the loss area when disabled.
			}
			else if (_longStop == null || newStop > _longStop.Value + trailingStep)
			{
				_longStop = newStop;
			}
		}

		if (TrailingTakeProfitPoints > 0m)
		{
			var candidate = candle.ClosePrice + TrailingTakeProfitPoints * _priceStep;
			var minAllowed = candle.ClosePrice + minDistance;
			var newTake = Math.Max(candidate, minAllowed);

			if (!AllowTrailingLoss && newTake < breakeven)
				newTake = breakeven;

			if (_longTake == null || newTake < _longTake.Value - trailingStep)
				_longTake = newTake;
		}
	}

	private void UpdateShortTrailing(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice <= 0m)
			return;

		var breakeven = entryPrice - BreakevenPoints * _priceStep;
		var trailingStep = Math.Max(TrailingStepPoints * _priceStep, Epsilon);
		var minDistance = GetMinStopDistance();

		if (TrailingStopLossPoints > 0m)
		{
			var candidate = candle.ClosePrice + TrailingStopLossPoints * _priceStep;
			var minAllowed = candle.ClosePrice + minDistance;
			var newStop = Math.Max(candidate, minAllowed);

			if (!AllowTrailingLoss && newStop > breakeven)
			{
				// Skip moving the stop into the loss area when disabled.
			}
			else if (_shortStop == null || newStop < _shortStop.Value - trailingStep)
			{
				_shortStop = newStop;
			}
		}

		if (TrailingTakeProfitPoints > 0m)
		{
			var candidate = candle.ClosePrice - TrailingTakeProfitPoints * _priceStep;
			var minAllowed = candle.ClosePrice - minDistance;
			var newTake = Math.Min(candidate, minAllowed);

			if (!AllowTrailingLoss && newTake > breakeven)
				newTake = breakeven;

			if (_shortTake == null || newTake > _shortTake.Value + trailingStep)
				_shortTake = newTake;
		}
	}

	private void ManageLongExits(ICandleMessage candle)
	{
		if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
		{
			SellMarket(Position);
			ResetLongLevels();
			return;
		}

		if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
		{
			SellMarket(Position);
			ResetLongLevels();
		}
	}

	private void ManageShortExits(ICandleMessage candle)
	{
		if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetShortLevels();
			return;
		}

		if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetShortLevels();
		}
	}

	private decimal GetMinStopDistance()
	{
		var multiplier = SpreadMultiplier < 1 ? 1 : SpreadMultiplier;
		return _priceStep * multiplier;
	}

	private void ResetLevels()
	{
		ResetLongLevels();
		ResetShortLevels();
	}

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

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

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0)
			_entryPrice = 0m;
	}
}