Открыть на GitHub

Стратегия Trend Me Leave Me

Обзор

Trend Me Leave Me — это точная конверсия классического советника MQL5 Юрия Решетова. Стратегия терпеливо ждёт спокойные периоды рынка, входит в направлении Parabolic SAR и после прибыльной сделки меняет сторону. Если позиция закрывается по стопу, алгоритм снова попытает ту же сторону, полностью повторяя исходную логику "trend me, leave me". Реализация на C# использует высокоуровневый API StockSharp и предоставляет доступ ко всем числовым параметрам оригинала.

Ключевые идеи

Фильтр спокойного рынка

  • Индикатор Average Directional Index (ADX) с периодом AdxPeriod измеряет силу тренда.
  • Новые входы разрешены только тогда, когда скользящее среднее ADX опускается ниже AdxQuietLevel, что соответствует поиску низковолатильных откатов в исходном советнике.

Тайминг по Parabolic SAR

  • Точки Parabolic SAR задают направление. Лонг возможен, когда закрытие свечи выше точки SAR, шорт — когда закрытие ниже.
  • Параметры SarStep и SarMax повторяют настройки ускорения из MQL и при необходимости могут оптимизироваться.

Планировщик направления

  • Внутренний флаг TradeDirections заменяет переменную cmd. На старте он находится в состоянии buy.
  • После выхода по тейк-профиту флаг переключается на противоположную сторону и стратегия ищет разворотную сделку.
  • После стоп-лосса (или срабатывания безубыточности) флаг сохраняет прежнюю сторону, чтобы повторить попытку.

Управление позицией

  • StopLossPips и TakeProfitPips задают фиксированные расстояния от средней цены входа. Значение 0 отключает соответствующую защиту.
  • BreakevenPips переносит стоп в точку входа после движения в прибыль на заданное количество пунктов. Возврат цены к входу закрывает сделку около нуля и оставляет флаг направления без изменений.
  • Проверка стопов и целей выполняется по завершённой свече с учётом High/Low, что максимально приближает поведение к тиковому исполнению исходного эксперта.

Управление объёмом

  • Объём заявок определяется базовым свойством Strategy.Volume. В примере не используется объект CMoneyFixedRisk из MQL — при необходимости можно изменить Volume или расширить стратегию для более сложного манименеджмента.

Параметры

Параметр Описание Значение по умолчанию
StopLossPips Расстояние до защитного стопа в пунктах. 50
TakeProfitPips Расстояние до тейк-профита в пунктах. 180
BreakevenPips Перевод стопа в безубыток после указанного движения. 5
AdxPeriod Период сглаживания ADX. 14
AdxQuietLevel Максимальное значение ADX для входа. 20
SarStep Шаг ускорения Parabolic SAR. 0.02
SarMax Максимальное ускорение Parabolic SAR. 0.2
CandleType Таймфрейм расчёта. Свечи 1 час

Особенности реализации

  • Размер пункта вычисляется как шаг цены, умноженный на 10 для инструментов с 3 или 5 знаками после запятой — как и в MQL.
  • Индикаторы подключены через высокоуровневый API StockSharp, сделки выполняются рыночными заявками BuyMarket/SellMarket.
  • Python-версия намеренно отсутствует, папка PY/ не создавалась согласно задаче.
  • Перед запуском стратегии выберите инструмент, установите Volume и настройте параметры под волатильность конкретного рынка.
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;
	}
}