Ver en GitHub

Estrategia de Tendencia Forzada

Descripción general

  • Conversión del asesor experto MetaTrader 5 Exp_ForceTrend.mq5 ubicado en MQL/18817.
  • Usa el oscilador ForceTrend propietario para detectar transiciones entre momentum alcista y bajista.
  • Implementa la lógica con la API de alto nivel de StockSharp, basándose en suscripciones de velas e indicadores integrados en lugar del acceso directo a series.

Indicador ForceTrend

  • El indicador mira atrás Length velas y mide la distancia entre el máximo más alto y el mínimo más bajo dentro de esa ventana.
  • El precio medio de la vela actual se normaliza dentro de ese rango y se suaviza dos veces:
    • La primera etapa produce un valor force intermedio con coeficientes 0.66 y 0.67.
    • La segunda etapa aplica una transformación logarítmica combinada con suavizado de vida media para obtener el valor final de ForceTrend.
  • Los valores por encima de cero se tratan como alcistas (originalmente renderizados en azul) y los valores por debajo de cero son bajistas (renderizados en magenta).

Parámetros

  • Length – tamaño de la ventana de lookback de ForceTrend; debe permanecer positivo.
  • SignalBar – cuántas velas finalizadas se desplaza la señal. 0 reacciona a la barra cerrada más reciente, 1 imita la configuración MT5 por defecto esperando una barra extra, y valores mayores retrasan más la ejecución.
  • EnableLongEntry – si está deshabilitado, la estrategia no abrirá posiciones largas en transiciones alcistas.
  • EnableShortEntry – si está deshabilitado, la estrategia no abrirá posiciones cortas en transiciones bajistas.
  • EnableLongExit – controla si las señales alcistas pueden cerrar posiciones cortas existentes.
  • EnableShortExit – controla si las señales bajistas pueden cerrar posiciones largas existentes.
  • CandleType – marco temporal de las velas usadas para los cálculos del indicador.

Reglas de trading

  1. La salida de ForceTrend se convierte en una dirección discreta (+1, 0, -1).
  2. Las direcciones se almacenan en un historial de longitud fija para que la estrategia pueda comparar la barra en el offset SignalBar con la barra inmediatamente anterior.
  3. Una señal alcista (direction > 0) activa:
    • Cerrar cualquier posición corta abierta si EnableShortExit es true.
    • Abrir o revertir a una posición larga (orden de mercado de tamaño Volume + |Position|) cuando la dirección anterior no era alcista y EnableLongEntry es true.
  4. Una señal bajista (direction < 0) activa las acciones simétricas para posiciones largas cuando EnableLongExit/EnableShortEntry están habilitados.
  5. Las lecturas neutras de ForceTrend heredan la última dirección conocida para que el sistema no oscile entre estados planos.
  6. Las órdenes se envían solo cuando la estrategia está completamente formada, en línea y el trading está permitido por el runtime de StockSharp.

Notas de implementación

  • Las velas se reciben a través de SubscribeCandles(CandleType); el procesamiento del indicador se realiza en el callback ProcessCandle.
  • Los precios más altos y más bajos se obtienen mediante los indicadores Highest y Lowest de StockSharp, asegurando que no se requiera gestión manual de buffers ni operaciones LINQ.
  • El historial de dirección se almacena en un pequeño array fijo dimensionado según SignalBar para reproducir el comportamiento MT5 original sin recrear colecciones para cada tick.
  • Las reversiones de posición usan una sola orden de mercado con volumen igual a la suma de la exposición deseada y la posición absoluta actual, emulando los helpers BuyPositionOpen/SellPositionOpen de la versión MQL.
  • Los parámetros de gestión monetaria del asesor experto (dimensionamiento de lotes, stop-loss y take-profit en puntos) se omiten intencionalmente; la estrategia StockSharp depende del Volume configurado por el usuario y módulos de protección externos opcionales.
  • Los interruptores booleanos reflejan las entradas MT5 (BuyPosOpen, SellPosOpen, BuyPosClose, SellPosClose).

Consejos de uso

  • Configurar la propiedad Volume antes de iniciar la estrategia para controlar el tamaño de la orden.
  • Elegir un tipo de vela que coincida con el marco temporal usado durante las pruebas en MT5 (por defecto son velas de cuatro horas).
  • Combinar con componentes de riesgo/protección de StockSharp si se requiere automatización de stop-loss o take-profit.

Archivos

  • Implementación de la estrategia: CS/ForceTrendStrategy.cs
  • Archivos MQL originales: MQL/18817/mql5/Experts/Exp_ForceTrend.mq5 y 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];
	}
}