在 GitHub 上查看

Breakeven Trailing Stop Tick 策略

概览

  • 根据 MetaTrader 专家顾问 e_Breakeven_v4 改写的基于 tick 的追踪止损管理器。
  • 当价格远离开仓价时,自动上移或下移虚拟止损,从而把仓位锁定在保本并继续跟随趋势。
  • 价格触及追踪水平时以市价平掉全部仓位,复现原始 EA 的“保本 + 步长”逻辑。
  • 内置可选的演示模式,在回测时随机开仓,便于观察追踪效果而无需额外信号源。

工作流程

  1. 订阅逐笔成交数据(DataType.Ticks),模拟 MQL5 中的 OnTick 回调。
  2. 只要持仓盈利并超过 TrailingStop + TrailingStep(单位:点),就把追踪止损移动到更接近当前价格的位置。
  3. 多头:若价格上涨超过阈值,则把止损放在 当前价格 - TrailingStop
  4. 空头:若价格下跌超过阈值,则把止损放在 当前价格 + TrailingStop
  5. 一旦最新成交价触碰追踪水平,立即按市价平仓并重置状态。
  6. 当标的报价精度为 3 或 5 位小数时,将最小价位变动乘以 10,把 point 转换成 pip,与 MQL5 的处理保持一致。
  7. 开启演示模式时,策略在空仓状态收到新的 tick 后会根据随机结果开多或开空,仓位大小取自 Volume

参数

名称 说明 默认值 备注
TrailingStopPips 当前价格与追踪止损之间的 pip 距离。 10 设为 0 可完全关闭追踪功能。
TrailingStepPips 每次继续移动止损前需要的额外 pip 距离。 1 当启用追踪时必须大于 0,与原 EA 的校验一致。
EnableDemoEntries 是否在测试时随机开仓。 false 设为 true 时,在空仓的 tick 上抛硬币决定方向。

仓位管理

  • 默认不主动开仓,仅对外部或人工仓位执行追踪;若开启演示模式,则使用随机信号。
  • 多、空两种方向都采用相同的追踪规则,可处理任意手数。
  • 使用虚拟止损,通过市价平仓来执行,不依赖券商的真实止损订单。
  • 可作为其他策略的“保护层”,只负责管理止损,不干预开仓逻辑。

使用提示

  • 推荐在提供逐笔成交数据的市场中使用,以便追踪能即时响应。
  • 使用演示模式时,请确保 Volume 与期望的下单手数一致。
  • pip 转换假设标的是外汇类品种(3 或 5 位小数),其他品种可根据需要调整默认参数。
  • 触发条件采用首个穿越止损的 tick,与 MQL5 版本的即时修改并平仓行为相符。

与原始 MQL5 EA 的差异

  • StockSharp 策略通过虚拟止损和市价平仓来实现保护,不直接修改经纪商侧的止损订单。
  • MetaTrader 测试器中的随机开仓被改造成可配置的 EnableDemoEntries 参数。
  • 点值转换通过 Security.PriceStep 与小数位统计实现,而非使用 Symbol().Digits()
  • 所有代码注释与日志均改为英文,满足仓库的统一要求。
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>
/// Trailing stop manager that moves stops to breakeven and beyond once price advances.
/// Designed to trail any manually opened position using pip based distances.
/// </summary>
public class BreakevenTrailingStopTickStrategy : Strategy
{
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<bool> _enableDemoEntries;

	private readonly StrategyParam<DataType> _candleType;
	private decimal _pointValue;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private bool _exitOrderPending;
	private decimal _entryPrice;
	private DateTimeOffset? _lastDemoEntryTime;

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing step in pips before the stop is moved again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Enable random demo entries to showcase the trailing behaviour in testing.
	/// </summary>
	public bool EnableDemoEntries
	{
		get => _enableDemoEntries.Value;
		set => _enableDemoEntries.Value = value;
	}

	/// <summary>
/// Initializes a new instance of <see cref="BreakevenTrailingStopTickStrategy"/>.
/// </summary>
public BreakevenTrailingStopTickStrategy()
	{
		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Trailing")
			
			.SetOptimize(5m, 30m, 5m);

		_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
			.SetNotNegative()
			.SetDisplay("Trailing Step", "Additional pips required before stop moves again", "Trailing")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_enableDemoEntries = Param(nameof(EnableDemoEntries), true)
			.SetDisplay("Enable Demo Entries", "Automatically open random trades in testing", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for candles", "General");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_pointValue = 0m;
		_longStopPrice = null;
		_shortStopPrice = null;
		_exitOrderPending = false;
		_lastDemoEntryTime = null;
		_entryPrice = 0;
	}

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

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
			throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		_pointValue = CalculateAdjustedPoint();

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

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

		var price = candle.ClosePrice;

		if (EnableDemoEntries)
			TryCreateDemoEntry(candle, price);

		if (Position == 0)
		{
			ResetTrailingState();
			return;
		}

		if (TrailingStopPips <= 0m || _pointValue <= 0m)
			return;

		if (Position > 0)
			UpdateLongTrailing(price);
		else if (Position < 0)
			UpdateShortTrailing(price);
	}

	private void TryCreateDemoEntry(ICandleMessage candle, decimal price)
	{
		if (Position != 0 || _exitOrderPending)
			return;

		var serverTime = candle.CloseTime;
		if (_lastDemoEntryTime.HasValue && (serverTime - _lastDemoEntryTime.Value).TotalMinutes < 30)
			return;

		var volume = Volume;
		if (volume <= 0m)
			return;

		if (Random.Shared.NextDouble() < 0.5)
		{
			BuyMarket(volume);
			_entryPrice = price;
		}
		else
		{
			SellMarket(volume);
			_entryPrice = price;
		}

		_lastDemoEntryTime = serverTime;
	}

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

		var stopOffset = TrailingStopPips * _pointValue;
		var stepOffset = TrailingStepPips * _pointValue;
		if (stopOffset <= 0m)
			return;

		var activationOffset = stopOffset + stepOffset;
		if (currentPrice - entryPrice <= activationOffset)
			return;

		var threshold = currentPrice - activationOffset;
		if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
		{
			var newStop = currentPrice - stopOffset;
			if (newStop > 0m)
			{
				_longStopPrice = newStop;
				// log($"Long trailing stop moved to {newStop}.");
			}
		}

		if (_longStopPrice.HasValue && currentPrice <= _longStopPrice.Value)
			ExitLongPosition();
	}

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

		var stopOffset = TrailingStopPips * _pointValue;
		var stepOffset = TrailingStepPips * _pointValue;
		if (stopOffset <= 0m)
			return;

		var activationOffset = stopOffset + stepOffset;
		if (entryPrice - currentPrice <= activationOffset)
			return;

		var threshold = currentPrice + activationOffset;
		if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold)
		{
			var newStop = currentPrice + stopOffset;
			_shortStopPrice = newStop;
			// log($"Short trailing stop moved to {newStop}.");
		}

		if (_shortStopPrice.HasValue && currentPrice >= _shortStopPrice.Value)
			ExitShortPosition();
	}

	private void ExitLongPosition()
	{
		if (_exitOrderPending)
			return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_exitOrderPending = true;
		// log("Long position closed by trailing stop.");
	}

	private void ExitShortPosition()
	{
		if (_exitOrderPending)
			return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_exitOrderPending = true;
		// log("Short position closed by trailing stop.");
	}


	private void ResetTrailingState()
	{
		_longStopPrice = null;
		_shortStopPrice = null;
		_exitOrderPending = false;
		_entryPrice = 0m;
	}

	private decimal CalculateAdjustedPoint()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		return decimals is 3 or 5 ? step * 10m : step;
	}

	private static int CountDecimals(decimal value)
	{
		value = Math.Abs(value);
		var decimals = 0;

		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}

		return decimals;
	}
}