Ver no GitHub

Estratégia de Trailing Stop e Take

Visão geral

A Estratégia de Trailing Stop e Take é uma adaptação direta do StockSharp do consultor especialista MetaTrader de MQL/19963. Foca no gerenciamento ativo de operações: uma vez que uma posição está aberta, a estratégia anexa níveis iniciais de stop-loss e take-profit e então segue ambos os níveis conforme o preço se move. Os ajustes de trailing respeitam tamanhos mínimos de passo configuráveis, proteção de break-even e a opção de evitar o trailing enquanto uma operação ainda está em perda.

A estratégia opera em um único instrumento usando velas concluídas. Quando a estratégia está plana, abre uma posição na direção do corpo da vela mais recente (fechamentos de alta levam a posições compradas, fechamentos de baixa levam a posições vendidas). Isso reflete o comportamento de teste original usado pelo script MQL e fornece um fluxo contínuo de posições para o motor de trailing gerenciar.

Como funciona

  1. Subscrever ao tipo de vela configurado e processar apenas velas concluídas.
  2. Quando não há posição aberta, entrar comprado em velas de alta ou vendido em velas de baixa (respeitando o filtro de tipo de posição).
  3. Em uma nova posição, inicializar as distâncias de stop-loss e take-profit usando InitialStopLossPoints/InitialTakeProfitPoints. Se forem zero, as distâncias de trailing são usadas em seu lugar.
  4. Em cada fechamento de vela, calcular os alvos de trailing atualizados:
    • Os stops se aproximam do preço apenas depois que o mercado avança pelo passo de trailing.
    • Os take-profits se aproximam quando o preço recua pelo menos o passo de trailing.
    • A proteção de break-even evita mover os níveis para uma zona de perda quando AllowTrailingLoss está desabilitado.
  5. Quando o preço cruza um trailing stop ou nível de take-profit, sair com ordem de mercado e redefinir todos os níveis armazenados.

Lógica de trailing

Posições compradas

  • O stop inicial está limitado a pelo menos SpreadMultiplier * PriceStep de distância da entrada.
  • O take-profit inicial é posicionado pelo menos a mesma distância mínima acima da entrada.
  • O trailing stop segue o preço de fechamento para baixo em TrailingStopLossPoints respeitando o passo de trailing e o filtro de break-even opcional.
  • O trailing take-profit aperta quando o preço recua, nunca se movendo abaixo do nível de break-even quando o trailing em perdas está desabilitado.

Posições vendidas

  • O stop inicial é definido acima da entrada, não mais próximo que a distância do multiplicador de spread.
  • O take-profit inicial começa abaixo da entrada com a mesma regra de distância mínima.
  • O trailing stop cai quando o preço cai, mas não se moverá mais alto que o break-even a menos que o trailing de perda seja permitido.
  • O trailing take-profit sobe em direção ao preço nos recuos, limitado ao break-even quando necessário.

Parâmetros

Parâmetro Descrição
CandleType Agregação de velas usada para avaliação de preços.
Volume Volume de ordem padrão para entradas e saídas.
PositionType Restringe o motor a gerenciar posições compradas, vendidas ou ambas.
InitialStopLossPoints Tamanho inicial do stop-loss em pontos de preço (usa distância de trailing se zero).
InitialTakeProfitPoints Tamanho inicial do take-profit em pontos de preço (usa distância de trailing se zero).
TrailingStopLossPoints Distância entre o preço e o trailing stop.
TrailingTakeProfitPoints Distância entre o preço e o trailing take-profit.
TrailingStepPoints Movimento mínimo em pontos necessário antes de ajustar stops ou alvos.
AllowTrailingLoss Habilita o trailing enquanto a operação ainda está abaixo do break-even.
BreakevenPoints Deslocamento em pontos adicionado ao preço de entrada para formar a barreira de break-even.
SpreadMultiplier Multiplicador para a aproximação de distância mínima do stop (simula o StopLevel do MQL).

Notas

  • Os stops e alvos são executados com ordens de mercado quando acionados, o que mantém a implementação simples e reflete as modificações de stop originais.
  • SpreadMultiplier aproxima o comportamento do MQL onde os níveis de stop não podem ser colocados mais perto que o spread atual. Ajuste este valor para corresponder ao local de execução.
  • A estratégia evita intencionalmente uma versão Python e se concentra exclusivamente na implementação C#, conforme solicitado.
  • Considere combinar o motor de trailing com seu próprio filtro de entrada desabilitando as entradas integradas e injetando ordens externas se necessário.
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;
	}
}