在 GitHub 上查看

Trail SL Manager 策略

概述

Trail SL Manager 是对 MetaTrader trailSL 脚本的迁移版本。 策略本身不会开仓,而是接管已有持仓,根据行情进展自动调整保护性止损。 它先在价格达到指定利润时将止损推至保本价,再按设定的步长逐级跟踪,从而持续锁定利润。

工作流程

  1. 订阅指定的 K 线类型,仅在 K 线收盘后处理数据。
  2. 读取当前持仓方向与平均建仓价,换算出已经获得的点数利润。
  3. 当利润达到 BreakEvenTriggerPoints 时,将止损移动到建仓价,并可额外加入 BreakEvenOffsetPoints 点的缓冲。
  4. 若允许提前启动或已经完成保本,策略每当价格再移动 TrailStepPoints 点时,就把止损再推进 TrailOffsetPoints 点;一旦行情回撤触发该价格,即以市价平仓。

所有计算均使用与原脚本相同的点数逻辑,方便从 MetaTrader 迁移到 StockSharp 的交易者继续保持熟悉的手感。

参数

名称 说明 默认值
EnableBreakEven 是否启用保本移动。 true
BreakEvenTriggerPoints 启动保本所需的盈利点数。 20
BreakEvenOffsetPoints 保本时在建仓价基础上额外锁定的点数。 10
EnableTrailing 是否启用跟踪止损。 true
TrailAfterBreakEven 若为 true,仅在完成保本后才开始跟踪。 true
TrailStartPoints 开始跟踪前需要达到的最小盈利点数。 40
TrailStepPoints 两次重新计算止损之间的盈利增量。 10
TrailOffsetPoints 每次推进止损所增加的点数。 10
InitialStopPoints 新建仓位时的初始保护止损距离。 200
CandleType 用于监控行情的 K 线类型。 1 Minute

使用方法

  1. 将策略加载到已经由其他策略或人工下单产生持仓的环境中。
  2. 按交易品种波动和经纪商限制配置各个点数阈值。
  3. 启动策略,使其在每根 K 线收盘后检查是否需要移动止损。
  4. 通过图表绘制观察策略执行情况,并根据需要与真实止损单配合使用。

提示: 策略在内部模拟跟踪止损,并在触发价被突破时发送市价单平仓。如需在交易所侧保留硬止损,请结合自身业务流程另行设置。

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>
/// Utility strategy that mirrors the trailSL MetaTrader script.
/// It manages open positions by moving the protective stop to break even and trailing it as price advances.
/// The strategy does not generate its own entry signals.
/// </summary>
public class TrailSlManagerStrategy : Strategy
{
	private readonly StrategyParam<bool> _enableBreakEven;
	private readonly StrategyParam<int> _breakEvenTriggerPoints;
	private readonly StrategyParam<int> _breakEvenOffsetPoints;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<bool> _trailAfterBreakEven;
	private readonly StrategyParam<int> _trailStartPoints;
	private readonly StrategyParam<int> _trailStepPoints;
	private readonly StrategyParam<int> _trailOffsetPoints;
	private readonly StrategyParam<int> _initialStopPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _priceStep;
	private decimal _longStop;
	private decimal _shortStop;
	private bool _longBreakEvenActive;
	private bool _shortBreakEvenActive;
	private decimal _lastEntryPrice;
	private SimpleMovingAverage _smaFast;
	private SimpleMovingAverage _smaSlow;
	private int _cooldown;
	private int _lastSignal;

	/// <summary>
	/// Enables automatic break-even adjustment.
	/// </summary>
	public bool EnableBreakEven
	{
		get => _enableBreakEven.Value;
		set => _enableBreakEven.Value = value;
	}

	/// <summary>
	/// Required profit in points before break-even is activated.
	/// </summary>
	public int BreakEvenTriggerPoints
	{
		get => _breakEvenTriggerPoints.Value;
		set => _breakEvenTriggerPoints.Value = value;
	}

	/// <summary>
	/// Additional points added to the break-even price once triggered.
	/// </summary>
	public int BreakEvenOffsetPoints
	{
		get => _breakEvenOffsetPoints.Value;
		set => _breakEvenOffsetPoints.Value = value;
	}

	/// <summary>
	/// Enables trailing stop management.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Trailing starts only after a successful break-even move.
	/// </summary>
	public bool TrailAfterBreakEven
	{
		get => _trailAfterBreakEven.Value;
		set => _trailAfterBreakEven.Value = value;
	}

	/// <summary>
	/// Minimum profit in points before trailing begins.
	/// </summary>
	public int TrailStartPoints
	{
		get => _trailStartPoints.Value;
		set => _trailStartPoints.Value = value;
	}

	/// <summary>
	/// Distance in points between trailing recalculations.
	/// </summary>
	public int TrailStepPoints
	{
		get => _trailStepPoints.Value;
		set => _trailStepPoints.Value = value;
	}

	/// <summary>
	/// Stop loss increment applied on each trailing step (points).
	/// </summary>
	public int TrailOffsetPoints
	{
		get => _trailOffsetPoints.Value;
		set => _trailOffsetPoints.Value = value;
	}

	/// <summary>
	/// Initial protective stop distance measured in points.
	/// </summary>
	public int InitialStopPoints
	{
		get => _initialStopPoints.Value;
		set => _initialStopPoints.Value = value;
	}

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

	public TrailSlManagerStrategy()
	{
		_enableBreakEven = Param(nameof(EnableBreakEven), true)
		.SetDisplay("Break Even", "Enable break-even stop adjustment", "Risk")
		;
		_breakEvenTriggerPoints = Param(nameof(BreakEvenTriggerPoints), 20)
		.SetDisplay("Break Even Trigger", "Points required before moving to break-even", "Risk")
		;
		_breakEvenOffsetPoints = Param(nameof(BreakEvenOffsetPoints), 10)
		.SetDisplay("Break Even Offset", "Extra points locked when break-even triggers", "Risk")
		;
		_enableTrailing = Param(nameof(EnableTrailing), true)
		.SetDisplay("Trailing", "Enable trailing stop management", "Risk")
		;
		_trailAfterBreakEven = Param(nameof(TrailAfterBreakEven), true)
		.SetDisplay("Trail After Break Even", "Start trailing only after break-even", "Risk")
		;
		_trailStartPoints = Param(nameof(TrailStartPoints), 40)
		.SetDisplay("Trail Start", "Points of profit before trailing is considered", "Risk")
		;
		_trailStepPoints = Param(nameof(TrailStepPoints), 10)
		.SetDisplay("Trail Step", "Price step that triggers a new trailing recalculation", "Risk")
		;
		_trailOffsetPoints = Param(nameof(TrailOffsetPoints), 10)
		.SetDisplay("Trail Offset", "Points added to the stop on every trailing step", "Risk")
		;
		_initialStopPoints = Param(nameof(InitialStopPoints), 200)
		.SetDisplay("Initial Stop", "Initial stop distance used before trailing", "Risk")
		;
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Candle subscription for monitoring", "Data");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetState();
		_priceStep = 0;
		_longStop = 0;
		_shortStop = 0;
		_longBreakEvenActive = false;
		_shortBreakEvenActive = false;
		_lastEntryPrice = 0;
		_smaFast = default;
		_smaSlow = default;
		_cooldown = 0;
		_lastSignal = 0;
	}

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

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

		_smaFast = new SimpleMovingAverage { Length = 10 };
		_smaSlow = new SimpleMovingAverage { Length = 30 };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_smaFast, _smaSlow, ProcessCandleWithIndicators).Start();

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

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

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

		if (trade.Order.Side == Sides.Buy && Position > 0)
		{
			_longStop = InitialStopPoints > 0 ? _lastEntryPrice - InitialStopPoints * _priceStep : 0m;
			_longBreakEvenActive = false;
		}
		else if (trade.Order.Side == Sides.Sell && Position < 0)
		{
			_shortStop = InitialStopPoints > 0 ? _lastEntryPrice + InitialStopPoints * _priceStep : 0m;
			_shortBreakEvenActive = false;
		}
	}

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

		if (_cooldown > 0)
		{
			_cooldown--;
		}
		else
		{
			var signal = fast > slow ? 1 : fast < slow ? -1 : 0;

			if (signal == 1 && _lastSignal != 1 && Position == 0)
			{
				BuyMarket();
				_lastEntryPrice = candle.ClosePrice;
				_lastSignal = 1;
				_cooldown = 20;
			}
			else if (signal == -1 && _lastSignal != -1 && Position == 0)
			{
				SellMarket();
				_lastEntryPrice = candle.ClosePrice;
				_lastSignal = -1;
				_cooldown = 20;
			}
		}

		ManageLongPosition(candle);
		ManageShortPosition(candle);
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
		if (Position <= 0)
		{
			_longStop = 0m;
			_longBreakEvenActive = false;
			return;
		}

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

		var currentPrice = candle.ClosePrice;
		var profitPoints = (currentPrice - entryPrice) / _priceStep;

		if (EnableBreakEven && !_longBreakEvenActive && profitPoints >= BreakEvenTriggerPoints && BreakEvenTriggerPoints > 0)
		{
			var newStop = BreakEvenOffsetPoints > 0
				? entryPrice + BreakEvenOffsetPoints * _priceStep
				: entryPrice;

			if (newStop < currentPrice)
			{
				_longStop = Math.Max(_longStop, newStop);
				_longBreakEvenActive = true;
			}
		}

		if (!EnableTrailing || TrailOffsetPoints <= 0 || TrailStepPoints <= 0)
			return;

		var requireBreakEven = TrailAfterBreakEven && EnableBreakEven;
		if (requireBreakEven && !_longBreakEvenActive)
			return;

		var baseStop = requireBreakEven
			? (_longStop > 0m ? _longStop : (BreakEvenOffsetPoints > 0 ? entryPrice + BreakEvenOffsetPoints * _priceStep : entryPrice))
			: (InitialStopPoints > 0 ? entryPrice - InitialStopPoints * _priceStep : (_longStop > 0m ? _longStop : 0m));

		if (baseStop <= 0m)
			return;

		if (!requireBreakEven && profitPoints < TrailStartPoints)
			return;

		if (requireBreakEven)
		{
			var baseDistance = (currentPrice - baseStop) / _priceStep;
			if (baseDistance < TrailStartPoints)
				return;
		}

		var startPrice = requireBreakEven
			? baseStop + (TrailStartPoints - TrailStepPoints) * _priceStep
			: entryPrice + (TrailStartPoints - TrailStepPoints) * _priceStep;

		var stepDistance = TrailStepPoints * _priceStep;
		if (stepDistance <= 0m)
			return;

		var openSteps = (currentPrice - startPrice) / stepDistance;
		if (openSteps <= 0m)
			return;

		var stepOpenPrice = (int)Math.Floor(openSteps);
		var currentStopSteps = _longStop > baseStop
			? (int)Math.Floor((_longStop - baseStop) / (TrailOffsetPoints * _priceStep))
			: 0;

		if (stepOpenPrice <= currentStopSteps)
			return;

		var proposedStop = baseStop + stepOpenPrice * TrailOffsetPoints * _priceStep;
		var maxStop = candle.LowPrice - _priceStep;
		if (proposedStop >= maxStop)
			proposedStop = maxStop;

		if (proposedStop > _longStop && proposedStop < currentPrice)
			_longStop = proposedStop;

		if (_longStop > 0m && candle.LowPrice <= _longStop)
			SellMarket(Position);
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
		if (Position >= 0)
		{
			_shortStop = 0m;
			_shortBreakEvenActive = false;
			return;
		}

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

		var currentPrice = candle.ClosePrice;
		var profitPoints = (entryPrice - currentPrice) / _priceStep;

		if (EnableBreakEven && !_shortBreakEvenActive && profitPoints >= BreakEvenTriggerPoints && BreakEvenTriggerPoints > 0)
		{
			var newStop = BreakEvenOffsetPoints > 0
				? entryPrice - BreakEvenOffsetPoints * _priceStep
				: entryPrice;

			if (newStop > currentPrice)
			{
				_shortStop = _shortStop == 0m ? newStop : Math.Min(_shortStop, newStop);
				_shortBreakEvenActive = true;
			}
		}

		if (!EnableTrailing || TrailOffsetPoints <= 0 || TrailStepPoints <= 0)
			return;

		var requireBreakEven = TrailAfterBreakEven && EnableBreakEven;
		if (requireBreakEven && !_shortBreakEvenActive)
			return;

		var baseStop = requireBreakEven
			? (_shortStop > 0m ? _shortStop : (BreakEvenOffsetPoints > 0 ? entryPrice - BreakEvenOffsetPoints * _priceStep : entryPrice))
			: (InitialStopPoints > 0 ? entryPrice + InitialStopPoints * _priceStep : (_shortStop > 0m ? _shortStop : 0m));

		if (baseStop <= 0m)
			return;

		if (!requireBreakEven && profitPoints < TrailStartPoints)
			return;

		if (requireBreakEven)
		{
			var baseDistance = (baseStop - currentPrice) / _priceStep;
			if (baseDistance < TrailStartPoints)
				return;
		}

		var startPrice = requireBreakEven
			? baseStop - (TrailStartPoints - TrailStepPoints) * _priceStep
			: entryPrice - (TrailStartPoints - TrailStepPoints) * _priceStep;

		var stepDistance = TrailStepPoints * _priceStep;
		if (stepDistance <= 0m)
			return;

		var openSteps = (startPrice - currentPrice) / stepDistance;
		if (openSteps <= 0m)
			return;

		var stepOpenPrice = (int)Math.Floor(openSteps);
		var currentStopSteps = _shortStop > 0m
			? (int)Math.Floor((baseStop - _shortStop) / (TrailOffsetPoints * _priceStep))
			: 0;

		if (stepOpenPrice <= currentStopSteps)
			return;

		var proposedStop = baseStop - stepOpenPrice * TrailOffsetPoints * _priceStep;
		var minStop = candle.HighPrice + _priceStep;
		if (proposedStop <= minStop)
			proposedStop = minStop;

		if ((_shortStop == 0m || proposedStop < _shortStop) && proposedStop > currentPrice)
			_shortStop = proposedStop;

		if (_shortStop > 0m && candle.HighPrice >= _shortStop)
			BuyMarket(Math.Abs(Position));
	}

	private void ResetState()
	{
		_longStop = 0m;
		_shortStop = 0m;
		_longBreakEvenActive = false;
		_shortBreakEvenActive = false;
	}
}