Ver en GitHub

Estrategia de Stop Trailing y Take

Descripción general

La Estrategia de Stop Trailing y Take es una adaptación directa de StockSharp del asesor experto de MetaTrader de MQL/19963. Se centra en la gestión activa de operaciones: una vez que una posición está abierta, la estrategia adjunta niveles iniciales de stop-loss y take-profit y luego sigue ambos niveles a medida que el precio se mueve. Los ajustes de trailing respetan tamaños mínimos de paso configurables, protección de break-even y la opción de evitar el trailing mientras una operación todavía está en pérdida.

La estrategia opera en un único instrumento usando velas terminadas. Cuando la estrategia está plana, abre una posición en la dirección del cuerpo de la vela más reciente (los cierres alcistas conducen a largos, los cierres bajistas conducen a cortos). Esto refleja el comportamiento de prueba original utilizado por el script MQL y proporciona un flujo continuo de posiciones para que el motor de trailing gestione.

Cómo funciona

  1. Suscribirse al tipo de vela configurado y procesar solo velas terminadas.
  2. Cuando no hay posición abierta, entrar largo en velas alcistas o corto en velas bajistas (respetando el filtro de tipo de posición).
  3. En una nueva posición, inicializar las distancias de stop-loss y take-profit usando InitialStopLossPoints/InitialTakeProfitPoints. Si son cero, se usan las distancias de trailing en su lugar.
  4. En cada cierre de vela, calcular los objetivos de trailing actualizados:
    • Los stops se acercan al precio solo después de que el mercado avanza por el paso de trailing.
    • Los take-profits se acercan cuando el precio retrocede al menos el paso de trailing.
    • La protección de break-even evita mover los niveles a una zona de pérdida cuando AllowTrailingLoss está deshabilitado.
  5. Cuando el precio cruza un stop trailing o nivel de take-profit, salir con orden de mercado y restablecer todos los niveles almacenados.

Lógica de trailing

Posiciones largas

  • El stop inicial se limita a al menos SpreadMultiplier * PriceStep de distancia de la entrada.
  • El take-profit inicial se posiciona al menos la misma distancia mínima por encima de la entrada.
  • El trailing stop sigue el precio de cierre a la baja por TrailingStopLossPoints mientras respeta el paso de trailing y el filtro de break-even opcional.
  • El trailing take-profit se ajusta cuando el precio retrocede, nunca moviéndose por debajo del nivel de break-even cuando el trailing en pérdidas está deshabilitado.

Posiciones cortas

  • El stop inicial se establece por encima de la entrada, no más cerca que la distancia del multiplicador de spread.
  • El take-profit inicial comienza por debajo de la entrada con la misma regla de distancia mínima.
  • El trailing stop baja cuando el precio cae, pero no se moverá más alto que el break-even a menos que se permita el trailing en pérdidas.
  • El trailing take-profit sube hacia el precio en los retrocesos, limitado al break-even cuando es necesario.

Parámetros

Parámetro Descripción
CandleType Agregación de velas usada para la evaluación de precios.
Volume Volumen de orden predeterminado para entradas y salidas.
PositionType Restringe el motor a gestionar posiciones largas, cortas o ambas.
InitialStopLossPoints Tamaño inicial del stop-loss en puntos de precio (usa la distancia de trailing si es cero).
InitialTakeProfitPoints Tamaño inicial del take-profit en puntos de precio (usa la distancia de trailing si es cero).
TrailingStopLossPoints Distancia entre el precio y el trailing stop.
TrailingTakeProfitPoints Distancia entre el precio y el trailing take-profit.
TrailingStepPoints Movimiento mínimo en puntos requerido antes de ajustar stops o objetivos.
AllowTrailingLoss Habilita el trailing mientras la operación todavía está por debajo del break-even.
BreakevenPoints Desplazamiento en puntos añadido al precio de entrada para formar la barrera de break-even.
SpreadMultiplier Multiplicador para la aproximación de distancia mínima del stop (simula el StopLevel de MQL).

Notas

  • Los stops y objetivos se ejecutan con órdenes de mercado cuando se activan, lo que mantiene la implementación simple y refleja las modificaciones originales de stop.
  • SpreadMultiplier aproxima el comportamiento de MQL donde los niveles de stop no pueden colocarse más cerca que el spread actual. Ajustar este valor para coincidir con el venue de ejecución.
  • La estrategia evita intencionalmente una versión de Python y se centra exclusivamente en la implementación C#, según lo solicitado.
  • Considere combinar el motor de trailing con su propio filtro de entrada deshabilitando las entradas integradas e inyectando órdenes externas si es necesario.
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;
	}
}