Ver no GitHub

Estratégia de Tendência Forçada

Visão geral

  • Conversão do consultor especializado MetaTrader 5 Exp_ForceTrend.mq5 localizado em MQL/18817.
  • Usa o oscilador ForceTrend proprietário para detectar transições entre momentum de alta e de baixa.
  • Implementa a lógica com a API de alto nível do StockSharp, dependendo de assinaturas de candles e indicadores integrados em vez de acesso direto a séries.

Indicador ForceTrend

  • O indicador olha para trás Length candles e mede a distância entre a máxima mais alta e a mínima mais baixa dentro dessa janela.
  • O preço médio do candle atual é normalizado dentro desse intervalo e suavizado duas vezes:
    • O primeiro estágio produz um valor force intermediário com coeficientes 0.66 e 0.67.
    • O segundo estágio aplica uma transformação logarítmica combinada com suavização de meia-vida para obter o valor final de ForceTrend.
  • Valores acima de zero são tratados como de alta (originalmente renderizados em azul) e valores abaixo de zero são de baixa (renderizados em magenta).

Parâmetros

  • Length – tamanho da janela de lookback do ForceTrend; deve permanecer positivo.
  • SignalBar – quantos candles concluídos o sinal é deslocado. 0 reage à barra fechada mais recente, 1 imita a configuração padrão do MT5 esperando uma barra extra, e valores maiores atrasam ainda mais a execução.
  • EnableLongEntry – se desabilitado, a estratégia não abrirá posições compradas em transições de alta.
  • EnableShortEntry – se desabilitado, a estratégia não abrirá posições vendidas em transições de baixa.
  • EnableLongExit – controla se sinais de alta podem fechar posições vendidas existentes.
  • EnableShortExit – controla se sinais de baixa podem fechar posições compradas existentes.
  • CandleType – período dos candles usados para cálculos do indicador.

Regras de negociação

  1. A saída do ForceTrend é convertida em uma direção discreta (+1, 0, -1).
  2. As direções são armazenadas em um histórico de comprimento fixo para que a estratégia possa comparar a barra no offset SignalBar com a barra imediatamente anterior.
  3. Um sinal de alta (direction > 0) aciona:
    • Fechar qualquer posição vendida aberta se EnableShortExit for true.
    • Abrir ou reverter para uma posição comprada (ordem a mercado de tamanho Volume + |Position|) quando a direção anterior não era de alta e EnableLongEntry é true.
  4. Um sinal de baixa (direction < 0) aciona as ações simétricas para posições compradas quando EnableLongExit/EnableShortEntry estão habilitados.
  5. Leituras neutras do ForceTrend herdam a última direção conhecida para que o sistema não oscile entre estados neutros.
  6. As ordens são enviadas apenas quando a estratégia está completamente formada, online e a negociação é permitida pelo runtime do StockSharp.

Notas de implementação

  • Os candles são recebidos através de SubscribeCandles(CandleType); o processamento do indicador é realizado no callback ProcessCandle.
  • Os preços mais altos e mais baixos são obtidos via indicadores Highest e Lowest do StockSharp, garantindo que não seja necessário gerenciamento manual de buffers nem operações LINQ.
  • O histórico de direção é armazenado em um pequeno array fixo dimensionado de acordo com SignalBar para reproduzir o comportamento MT5 original sem recriar coleções para cada tick.
  • As reversões de posição usam uma única ordem a mercado com volume igual à soma da exposição desejada e a posição absoluta atual, emulando os helpers BuyPositionOpen/SellPositionOpen da versão MQL.
  • Os parâmetros de gestão monetária do consultor especializado (dimensionamento de lotes, stop-loss e take-profit em pontos) são omitidos intencionalmente; a estratégia StockSharp depende do Volume configurado pelo usuário e módulos de proteção externos opcionais.
  • Os interruptores booleanos espelham as entradas MT5 (BuyPosOpen, SellPosOpen, BuyPosClose, SellPosClose).

Dicas de uso

  • Configurar a propriedade Volume antes de iniciar a estratégia para controlar o tamanho da ordem.
  • Escolher um tipo de candle que corresponda ao período usado durante os testes no MT5 (padrão: candles de quatro horas).
  • Combinar com componentes de risco/proteção do StockSharp se a automação de stop-loss ou take-profit for necessária.

Arquivos

  • Implementação da estratégia: CS/ForceTrendStrategy.cs
  • Arquivos MQL originais: MQL/18817/mql5/Experts/Exp_ForceTrend.mq5 e MQL/18817/mql5/Indicators/ForceTrend.mq5
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Force Trend strategy that mirrors the original MT5 expert advisor logic.
/// It reacts to ForceTrend indicator color changes to switch between long and short positions.
/// </summary>
public class ForceTrendStrategy : Strategy
{
	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _enableLongEntry;
	private readonly StrategyParam<bool> _enableShortEntry;
	private readonly StrategyParam<bool> _enableLongExit;
	private readonly StrategyParam<bool> _enableShortExit;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest = null!;
	private Lowest _lowest = null!;
	private decimal _previousForceValue;
	private decimal _previousIndicatorValue;
	private int?[] _directionHistory = Array.Empty<int?>();
	private int _historyCount;
	private int? _lastKnownDirection;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public ForceTrendStrategy()
	{
		_length = Param(nameof(Length), 13)
			.SetDisplay("Length", "ForceTrend lookback length", "Indicator")
			.SetGreaterThanZero()
			;

		_signalBar = Param(nameof(SignalBar), 1)
			.SetDisplay("Signal Bar", "Number of finished bars to shift the signal", "Trading")
			;

		_enableLongEntry = Param(nameof(EnableLongEntry), true)
			.SetDisplay("Enable Long Entry", "Allow opening long positions", "Trading")
			;

		_enableShortEntry = Param(nameof(EnableShortEntry), true)
			.SetDisplay("Enable Short Entry", "Allow opening short positions", "Trading")
			;

		_enableLongExit = Param(nameof(EnableLongExit), true)
			.SetDisplay("Enable Long Exit", "Allow closing long positions", "Trading")
			;

		_enableShortExit = Param(nameof(EnableShortExit), true)
			.SetDisplay("Enable Short Exit", "Allow closing short positions", "Trading")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for ForceTrend calculations", "General");
	}

	/// <summary>
	/// ForceTrend lookback length.
	/// </summary>
	public int Length
	{
		get => _length.Value;
		set => _length.Value = value;
	}

	/// <summary>
	/// Number of finished candles used to shift the trade signal.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Enable opening long positions when the ForceTrend becomes bullish.
	/// </summary>
	public bool EnableLongEntry
	{
		get => _enableLongEntry.Value;
		set => _enableLongEntry.Value = value;
	}

	/// <summary>
	/// Enable opening short positions when the ForceTrend becomes bearish.
	/// </summary>
	public bool EnableShortEntry
	{
		get => _enableShortEntry.Value;
		set => _enableShortEntry.Value = value;
	}

	/// <summary>
	/// Enable closing long positions on bearish ForceTrend signals.
	/// </summary>
	public bool EnableLongExit
	{
		get => _enableLongExit.Value;
		set => _enableLongExit.Value = value;
	}

	/// <summary>
	/// Enable closing short positions on bullish ForceTrend signals.
	/// </summary>
	public bool EnableShortExit
	{
		get => _enableShortExit.Value;
		set => _enableShortExit.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_previousForceValue = 0m;
		_previousIndicatorValue = 0m;
		_directionHistory = Array.Empty<int?>();
		_historyCount = 0;
		_lastKnownDirection = null;
	}

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

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

		_previousForceValue = 0m;
		_previousIndicatorValue = 0m;
		_historyCount = 0;
		_lastKnownDirection = null;
		_directionHistory = new int?[Math.Max(SignalBar + 2, 2)];

		_highest = new Highest { Length = Length };
		_lowest = new Lowest { Length = Length };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var highestValue = _highest.Process(new CandleIndicatorValue(_highest, candle)).ToDecimal();
		var lowestValue = _lowest.Process(new CandleIndicatorValue(_lowest, candle)).ToDecimal();

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		var range = highestValue - lowestValue;
		decimal forceValue;

		if (range != 0m)
		{
			var average = (candle.HighPrice + candle.LowPrice) / 2m;
			var normalized = (average - lowestValue) / range - 0.5m;
			forceValue = 0.66m * normalized + 0.67m * _previousForceValue;
		}
		else
		{
			forceValue = 0.67m * _previousForceValue - 0.33m;
		}

		forceValue = Math.Clamp(forceValue, -0.999m, 0.999m);

		decimal indicatorValue;
		var denominator = 1m - forceValue;

		if (denominator != 0m)
		{
			var ratio = (forceValue + 1m) / denominator;
			indicatorValue = (decimal)(Math.Log((double)ratio) / 2.0) + _previousIndicatorValue / 2m;
		}
		else
		{
			indicatorValue = _previousIndicatorValue / 2m + 0.5m;
		}

		_previousForceValue = forceValue;
		_previousIndicatorValue = indicatorValue;

		var direction = indicatorValue > 0m ? 1 : indicatorValue < 0m ? -1 : _lastKnownDirection ?? 0;
		if (direction != 0)
			_lastKnownDirection = direction;

		AddDirection(direction);

		var currentDirection = GetDirection(SignalBar);
		if (currentDirection is null)
			return;

		var previousDirection = GetDirection(SignalBar + 1);
		var bullish = currentDirection.Value > 0;
		var bearish = currentDirection.Value < 0;
		var bullishFlip = bullish && previousDirection.HasValue && previousDirection.Value <= 0;
		var bearishFlip = bearish && previousDirection.HasValue && previousDirection.Value >= 0;

		// indicators processed manually, no BindEx

		if (bullish)
		{
			var volumeToBuy = 0m;

			if (EnableShortExit && Position < 0m)
				volumeToBuy += Math.Abs(Position);

			if (EnableLongEntry && bullishFlip && Position <= 0m)
				volumeToBuy += Volume;

			if (volumeToBuy > 0m)
				BuyMarket();
		}
		else if (bearish)
		{
			var volumeToSell = 0m;

			if (EnableLongExit && Position > 0m)
				volumeToSell += Math.Abs(Position);

			if (EnableShortEntry && bearishFlip && Position >= 0m)
				volumeToSell += 1m;

			if (volumeToSell > 0m)
				SellMarket();
		}
	}

	private void AddDirection(int direction)
	{
		if (_historyCount < _directionHistory.Length)
		{
			_directionHistory[_historyCount] = direction;
			_historyCount++;
		}
		else
		{
			for (var i = 1; i < _directionHistory.Length; i++)
				_directionHistory[i - 1] = _directionHistory[i];

			_directionHistory[^1] = direction;
		}
	}

	private int? GetDirection(int offset)
	{
		if (offset < 0)
			return null;

		var index = _historyCount - 1 - offset;
		if (index < 0)
			return null;

		return _directionHistory[index];
	}
}