Ver no GitHub

Estratégia Trend Me Leave Me

Visão geral

A estratégia Trend Me Leave Me é um port direto do clássico consultor especialista MQL5 de Yury Reshetov. Ela espera pacientemente por períodos de ação de preço tranquila, segue a direção predominante indicada pelo Parabolic SAR e alterna a direção da operação após saídas rentáveis. Quando uma operação é stopada, a estratégia tentará a mesma direção novamente, recriando o comportamento original "trend me, leave me". Esta implementação em C# usa a API de alto nível do StockSharp e mantém o fluxo de decisão completo do sistema fonte enquanto expõe cada entrada numérica como um parâmetro configurável.

Ideias principais

Filtro de mercado calmo

  • O Average Directional Index (ADX) com comprimento AdxPeriod mede a força direcional.
  • Apenas quando a média móvel do ADX cai abaixo de AdxQuietLevel a estratégia permite novas entradas, imitando o foco do EA em retrocessos de baixa volatilidade.

Alinhamento SAR para temporização

  • Os pontos do Parabolic SAR atuam como guia direcional. Um sinal comprado requer que o fechamento da vela esteja acima do ponto SAR, enquanto um sinal vendido requer um fechamento abaixo do ponto.
  • Os parâmetros SarStep e SarMax correspondem às configurações de aceleração da versão MQL e podem ser otimizados se necessário.

Agendador de direção

  • Um flag TradeDirections representa a variável original cmd. Começa no estado compra.
  • Após uma saída por take-profit o flag muda para o lado oposto, convidando uma operação de reversão.
  • Após uma saída por stop-loss (ou breakeven) o flag permanece no mesmo lado para que a próxima oportunidade retente a direção anterior.

Gestão de operações

  • StopLossPips e TakeProfitPips definem distâncias fixas a partir do preço médio de execução. Definir qualquer parâmetro como 0 desabilita a proteção correspondente.
  • BreakevenPips move o stop para o preço de entrada assim que o mercado se desloca a favor pela distância de pips especificada. Se o preço posteriormente retornar ao nível de entrada, a operação é fechada com lucro aproximadamente nulo, o que mantém o próximo sinal no mesmo lado.
  • A lógica de stop/take é avaliada em cada vela completa usando tanto a máxima quanto a mínima para aproximar os toques intrabarra, preservando o comportamento tick a tick do EA o mais fielmente possível em um ambiente baseado em barras.

Dimensionamento da posição

  • O volume da ordem é controlado pela propriedade base Strategy.Volume. O exemplo mantém o modelo de risco simples e não inclui o objeto de gerenciamento de dinheiro de risco fixo do script MQL. Ajuste Volume ou sobrescreva a estratégia se um dimensionamento mais avançado for necessário.

Parâmetros

Parâmetro Descrição Padrão
StopLossPips Distância em pips entre o preço de entrada e o stop de proteção. 50
TakeProfitPips Distância em pips entre o preço de entrada e o alvo. 180
BreakevenPips Mover o stop para a entrada após este número de pips de movimento favorável. 5
AdxPeriod Período de suavização para o filtro ADX. 14
AdxQuietLevel Leitura máxima de ADX que ainda qualifica como mercado calmo. 20
SarStep Passo de aceleração do Parabolic SAR. 0.02
SarMax Fator de aceleração máximo do Parabolic SAR. 0.2
CandleType Período usado para cálculos. Velas de 1h

Notas de implementação

  • Os cálculos de pips seguem o ajuste de dígitos do EA: se o instrumento usa 3 ou 5 casas decimais, o passo de preço é multiplicado por 10 para converter o tamanho do tick do broker em um pip padrão.
  • As ligações de indicadores dependem da API de alto nível do StockSharp, e todas as ações de trading usam BuyMarket/SellMarket para manter conformidade com as convenções do S#.
  • Ainda não há tradução para Python. O diretório PY/ está intencionalmente ausente conforme solicitado.
  • Anexe a estratégia a qualquer símbolo suportado pelo StockSharp. Defina Volume antes de iniciar a estratégia e ajuste os parâmetros para corresponder à volatilidade do 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;
	}
}