Ver no GitHub

Trend Me Leave Me Strategy

Overview

The Trend Me Leave Me strategy is a direct port of the classic MQL5 expert advisor by Yury Reshetov. It patiently waits for periods of quiet price action, joins the prevailing direction indicated by the Parabolic SAR, and alternates the trade direction after profitable exits. When a trade is stopped out, the strategy will attempt the same direction again, recreating the original "trend me, leave me" behaviour. This C# implementation uses the StockSharp high-level API and keeps the full decision flow of the source system while exposing every numeric input as a configurable parameter.

Core Ideas

Calm-market filter

  • The Average Directional Index (ADX) with AdxPeriod length measures directional strength.
  • Only when the ADX moving average drops below AdxQuietLevel does the strategy allow new entries, mimicking the EA's focus on low-volatility pullbacks.

SAR alignment for timing

  • Parabolic SAR points act as the directional guide. A long signal requires the candle close to print above the SAR dot, whereas a short signal requires a close below the dot.
  • The SAR parameters SarStep and SarMax match the acceleration settings from the MQL version and may be optimised if needed.

Direction scheduler

  • A TradeDirections flag represents the original cmd variable. It starts in the buy state.
  • After a take-profit exit the flag flips to the opposite side, inviting a reversal trade.
  • After a stop-loss (or breakeven) exit the flag remains on the same side so that the next opportunity retries the previous direction.

Trade Management

  • StopLossPips and TakeProfitPips define fixed distances from the average fill price. Setting either parameter to 0 disables the corresponding protection.
  • BreakevenPips moves the stop to the entry price once the market travels in favour by the specified pip distance. If price later returns to the entry level the trade is closed for roughly zero profit, which keeps the next signal on the same side.
  • The stop/take logic is evaluated on every completed candle using both the high and low to approximate intrabar hits, preserving the tick-by-tick behaviour of the EA as closely as possible in a bar-driven environment.

Position Sizing

  • Order volume is controlled by the base Strategy.Volume property. The sample keeps the risk model simple and does not include the fixed-risk money management object from the MQL script. Adjust Volume or override the strategy if more advanced sizing is required.

Parameters

Parameter Description Default
StopLossPips Distance in pips between the entry price and the protective stop. 50
TakeProfitPips Distance in pips between the entry price and the target. 180
BreakevenPips Move the stop to entry after this many pips of favourable movement. 5
AdxPeriod Smoothing period for the ADX filter. 14
AdxQuietLevel Maximum ADX reading that still qualifies as a quiet market. 20
SarStep Parabolic SAR acceleration step. 0.02
SarMax Parabolic SAR maximum acceleration factor. 0.2
CandleType Time frame used for calculations. 1h candles

Implementation Notes

  • Pip calculations follow the EA's digit adjustment: if the security uses 3 or 5 decimal places the price step is multiplied by 10 to convert the broker tick size into a standard pip.
  • Indicator bindings rely on the StockSharp high-level API, and all trading actions use BuyMarket/SellMarket to stay in line with the S# conventions.
  • No Python translation is included yet. The PY/ directory is intentionally absent as requested.
  • Attach the strategy to any symbol supported by StockSharp. Set the Volume before starting the strategy and adjust parameters to match the instrument's volatility.
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;
	}
}