Ver en GitHub

Estrategia Trend Me Leave Me

Descripción general

La estrategia Trend Me Leave Me es un port directo del clásico asesor experto MQL5 de Yury Reshetov. Espera pacientemente períodos de acción de precio tranquila, se une a la dirección predominante indicada por el Parabolic SAR y alterna la dirección de la operación después de salidas rentables. Cuando una operación es detenida, la estrategia intentará la misma dirección de nuevo, recreando el comportamiento original "trend me, leave me". Esta implementación en C# usa la API de alto nivel de StockSharp y conserva el flujo completo de decisiones del sistema fuente mientras expone cada entrada numérica como un parámetro configurable.

Ideas principales

Filtro de mercado tranquilo

  • El Average Directional Index (ADX) con longitud AdxPeriod mide la fuerza direccional.
  • Solo cuando la media móvil del ADX cae por debajo de AdxQuietLevel la estrategia permite nuevas entradas, imitando el enfoque del EA en retrocesos de baja volatilidad.

Alineación SAR para el timing

  • Los puntos del Parabolic SAR actúan como guía direccional. Una señal larga requiere que el cierre de la vela esté por encima del punto SAR, mientras que una señal corta requiere un cierre por debajo del punto.
  • Los parámetros SarStep y SarMax coinciden con la configuración de aceleración de la versión MQL y pueden optimizarse si es necesario.

Programador de dirección

  • Un flag TradeDirections representa la variable original cmd. Comienza en el estado compra.
  • Después de una salida por take-profit el flag cambia al lado opuesto, invitando a una operación de reversión.
  • Después de una salida por stop-loss (o break-even) el flag permanece en el mismo lado para que la próxima oportunidad reintente la dirección anterior.

Gestión de operaciones

  • StopLossPips y TakeProfitPips definen distancias fijas desde el precio promedio de ejecución. Establecer cualquier parámetro en 0 deshabilita la protección correspondiente.
  • BreakevenPips mueve el stop al precio de entrada una vez que el mercado se desplaza a favor por la distancia de pips especificada. Si el precio luego regresa al nivel de entrada, la operación se cierra con ganancias aproximadamente nulas, lo que mantiene la próxima señal en el mismo lado.
  • La lógica de stop/take se evalúa en cada vela completada usando tanto el máximo como el mínimo para aproximar los toques intrabarra, preservando el comportamiento tick a tick del EA lo más fielmente posible en un entorno basado en barras.

Dimensionamiento de la posición

  • El volumen de la orden es controlado por la propiedad base Strategy.Volume. El ejemplo mantiene el modelo de riesgo simple y no incluye el objeto de gestión de dinero de riesgo fijo del script MQL. Ajustar Volume o anular la estrategia si se requiere un dimensionamiento más avanzado.

Parámetros

Parámetro Descripción Por defecto
StopLossPips Distancia en pips entre el precio de entrada y el stop de protección. 50
TakeProfitPips Distancia en pips entre el precio de entrada y el objetivo. 180
BreakevenPips Mover el stop a la entrada después de este número de pips de movimiento favorable. 5
AdxPeriod Período de suavizado para el filtro ADX. 14
AdxQuietLevel Lectura máxima de ADX que aún califica como mercado tranquilo. 20
SarStep Paso de aceleración del Parabolic SAR. 0.02
SarMax Factor de aceleración máximo del Parabolic SAR. 0.2
CandleType Marco temporal usado para los cálculos. Velas de 1h

Notas de implementación

  • Los cálculos de pips siguen el ajuste de dígitos del EA: si el instrumento usa 3 o 5 decimales, el paso de precio se multiplica por 10 para convertir el tamaño del tick del broker en un pip estándar.
  • Los enlaces de indicadores se apoyan en la API de alto nivel de StockSharp, y todas las acciones de trading usan BuyMarket/SellMarket para mantenerse en línea con las convenciones de S#.
  • Aún no se incluye traducción a Python. El directorio PY/ está intencionalmente ausente según lo solicitado.
  • Adjunte la estrategia a cualquier símbolo compatible con StockSharp. Establezca Volume antes de iniciar la estrategia y ajuste los parámetros para coincidir con la volatilidad del instrumento.
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>
/// Trend Me Leave Me strategy converted from the original MQL5 version.
/// Waits for calm markets, trades with Parabolic SAR direction and flips after profitable exits.
/// </summary>
public class TrendMeLeaveMeStrategy : Strategy
{
	private enum TradeDirections
	{
		None,
		Buy,
		Sell
	}

	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _breakevenPips;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxQuietLevel;
	private readonly StrategyParam<decimal> _sarStep;
	private readonly StrategyParam<decimal> _sarMax;
	private readonly StrategyParam<DataType> _candleType;

	private AverageDirectionalIndex _adx = null!;
	private ParabolicSar _sar = null!;

	private TradeDirections _nextDirection = TradeDirections.Buy;
	private bool _breakevenActivated;
	private decimal _pipSize;
	private int _positionDirection;
	private bool _exitOrderPending;
	private decimal _entryPrice;

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Breakeven trigger distance expressed in pips.
	/// </summary>
	public int BreakevenPips
	{
		get => _breakevenPips.Value;
		set => _breakevenPips.Value = value;
	}

	/// <summary>
	/// ADX averaging period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set
		{
			_adxPeriod.Value = value;
			if (_adx != null)
				_adx.Length = value;
		}
	}

	/// <summary>
	/// ADX level that defines when the market is calm enough to enter.
	/// </summary>
	public decimal AdxQuietLevel
	{
		get => _adxQuietLevel.Value;
		set => _adxQuietLevel.Value = value;
	}

	/// <summary>
	/// Parabolic SAR acceleration step.
	/// </summary>
	public decimal SarStep
	{
		get => _sarStep.Value;
		set
		{
			_sarStep.Value = value;
			if (_sar != null)
				_sar.AccelerationStep = value;
		}
	}

	/// <summary>
	/// Maximum Parabolic SAR acceleration factor.
	/// </summary>
	public decimal SarMax
	{
		get => _sarMax.Value;
		set
		{
			_sarMax.Value = value;
			if (_sar != null)
				_sar.AccelerationMax = value;
		}
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="TrendMeLeaveMeStrategy"/> class.
	/// </summary>
	public TrendMeLeaveMeStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 180)
			.SetDisplay("Take Profit (pips)", "Take profit distance", "Risk");

		_breakevenPips = Param(nameof(BreakevenPips), 5)
			.SetDisplay("Breakeven (pips)", "Distance before moving stop to entry", "Risk");

		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Smoothing period for ADX", "Indicators");

		_adxQuietLevel = Param(nameof(AdxQuietLevel), 20m)
			.SetGreaterThanZero()
			.SetDisplay("ADX Quiet Level", "Maximum ADX value to allow entries", "Indicators");

		_sarStep = Param(nameof(SarStep), 0.02m)
			.SetGreaterThanZero()
			.SetDisplay("SAR Step", "Acceleration step for Parabolic SAR", "Indicators");

		_sarMax = Param(nameof(SarMax), 0.2m)
			.SetGreaterThanZero()
			.SetDisplay("SAR Max", "Maximum acceleration for Parabolic SAR", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");
	}

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

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

		_nextDirection = TradeDirections.Buy;
		_breakevenActivated = false;
		_pipSize = 0m;
		_positionDirection = 0;
		_exitOrderPending = false;
		_entryPrice = 0m;
	}

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

		// Pre-calculate pip size respecting fractional pricing conventions.
		_pipSize = CalculatePipSize();

		// Prepare indicators used for filtering and timing.
		_adx = new AverageDirectionalIndex
		{
			Length = AdxPeriod
		};

		_sar = new ParabolicSar
		{
			AccelerationStep = SarStep,
			AccelerationMax = SarMax
		};

		// Subscribe to candle stream and process indicators manually.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandleManual)
			.Start();

		// Draw everything on a chart if UI is attached.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _sar);
			DrawIndicator(area, _adx);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandleManual(ICandleMessage candle)
	{
		// Process only completed candles to stay close to bar-close logic from the EA.
		if (candle.State != CandleStates.Finished)
			return;

		// Process indicators manually to avoid BindEx crash.
		var adxValue = _adx.Process(candle);
		var sarValue = _sar.Process(candle);

		if (!_adx.IsFormed || !_sar.IsFormed)
			return;

		if (!adxValue.IsFinal || !sarValue.IsFinal)
			return;

		if (_pipSize <= 0m)
			_pipSize = CalculatePipSize();

		// Make sure we do not send new commands until exit orders are filled.
		if (_exitOrderPending)
		{
			if (Position == 0)
			{
				_exitOrderPending = false;
				_positionDirection = 0;
				_breakevenActivated = false;
			}
			else
			{
				return;
			}
		}

		if (Position != 0)
		{
			var currentDirection = Position > 0 ? 1 : -1;
			if (_positionDirection != currentDirection)
			{
				_positionDirection = currentDirection;
				_breakevenActivated = false;
			}

			// Manage protective logic for the active trade.
			ManageOpenPosition(candle);
			if (_exitOrderPending || Position != 0)
				return;
		}
		else
		{
			_positionDirection = 0;
			_breakevenActivated = false;
		}

		if (adxValue is not AverageDirectionalIndexValue adxData)
			return;
		if (adxData.MovingAverage is not decimal adx)
			return;

		var sar = sarValue.ToDecimal();
		var close = candle.ClosePrice;
		var quietMarket = adx < AdxQuietLevel;

		// Follow original cmd logic: buy after losses or initialization, sell after profits.
		if ((_nextDirection == TradeDirections.Buy || _nextDirection == TradeDirections.None) && quietMarket && close > sar)
		{
			_breakevenActivated = false;
			BuyMarket(Volume + Math.Abs(Position));
			_positionDirection = 1;
		}
		else if (_nextDirection == TradeDirections.Sell && quietMarket && close < sar)
		{
			_breakevenActivated = false;
			SellMarket(Volume + Math.Abs(Position));
			_positionDirection = -1;
		}
	}

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

		var direction = _positionDirection;
		var pip = _pipSize <= 0m ? 1m : _pipSize;

		if (direction > 0)
		{
			var stopPrice = StopLossPips > 0 ? entryPrice - StopLossPips * pip : decimal.MinValue;
			var takePrice = TakeProfitPips > 0 ? entryPrice + TakeProfitPips * pip : decimal.MaxValue;

			// Activate the breakeven flag once price moves far enough in favor.
			if (!_breakevenActivated && BreakevenPips > 0)
			{
				var trigger = entryPrice + BreakevenPips * pip;
				if (candle.HighPrice >= trigger)
					_breakevenActivated = true;
			}

			var stopTriggered = (StopLossPips > 0 && candle.LowPrice <= stopPrice) || (_breakevenActivated && candle.LowPrice <= entryPrice);
			var takeTriggered = TakeProfitPips > 0 && candle.HighPrice >= takePrice;

			// Exit long positions on either stop or target, mirroring the EA logic.
			if (stopTriggered || takeTriggered)
			{
				SellMarket(Position);
				_exitOrderPending = true;
				UpdateNextDirection(takeTriggered && !stopTriggered, direction);
			}
		}
		else if (direction < 0)
		{
			var stopPrice = StopLossPips > 0 ? entryPrice + StopLossPips * pip : decimal.MaxValue;
			var takePrice = TakeProfitPips > 0 ? entryPrice - TakeProfitPips * pip : decimal.MinValue;

			// Activate the breakeven flag once the short trade gains enough.
			if (!_breakevenActivated && BreakevenPips > 0)
			{
				var trigger = entryPrice - BreakevenPips * pip;
				if (candle.LowPrice <= trigger)
					_breakevenActivated = true;
			}

			var stopTriggered = (StopLossPips > 0 && candle.HighPrice >= stopPrice) || (_breakevenActivated && candle.HighPrice >= entryPrice);
			var takeTriggered = TakeProfitPips > 0 && candle.LowPrice <= takePrice;

			// Exit short trades and adjust the direction scheduler.
			if (stopTriggered || takeTriggered)
			{
				BuyMarket(Math.Abs(Position));
				_exitOrderPending = true;
				UpdateNextDirection(takeTriggered && !stopTriggered, direction);
			}
		}
	}

	private void UpdateNextDirection(bool wasProfit, int direction)
	{
		if (direction > 0)
			_nextDirection = wasProfit ? TradeDirections.Sell : TradeDirections.Buy;
		else if (direction < 0)
			_nextDirection = wasProfit ? TradeDirections.Buy : TradeDirections.Sell;
	}

	private decimal CalculatePipSize()
	{
		var security = Security;
		if (security == null)
			return 1m;

		var step = security.PriceStep ?? 1m;
		if (step <= 0m)
			return 1m;

		var decimals = GetDecimalPlaces(step);
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	/// <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;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var bits = decimal.GetBits(value);
		var scale = (bits[3] >> 16) & 0x7F;
		return scale;
	}
}