Auf GitHub ansehen

Force-Trend-Strategie

Überblick

  • Konvertierung des MetaTrader-5-Expertenberaters Exp_ForceTrend.mq5 in MQL/18817.
  • Verwendet den proprietären ForceTrend-Oszillator, um Übergänge zwischen bullischem und bärischem Momentum zu erkennen.
  • Implementiert die Logik mit der High-Level-API von StockSharp unter Verwendung von Kerzenabonnements und integrierten Indikatoren statt direktem Serienzugriff.

ForceTrend-Indikator

  • Der Indikator schaut Length Kerzen zurück und misst den Abstand zwischen dem höchsten Hoch und dem tiefsten Tief innerhalb dieses Fensters.
  • Der Mittelpreis der aktuellen Kerze wird innerhalb dieser Spanne normalisiert und zweimal geglättet:
    • Die erste Stufe erzeugt einen intermediären force-Wert mit den Koeffizienten 0.66 und 0.67.
    • Die zweite Stufe wendet eine logarithmische Transformation kombiniert mit Halbwertszeit-Glättung an, um den endgültigen ForceTrend-Wert zu erhalten.
  • Werte über null werden als bullisch behandelt (ursprünglich blau dargestellt) und Werte unter null sind bärisch (magenta dargestellt).

Parameter

  • Length – Größe des ForceTrend-Lookback-Fensters; muss positiv bleiben.
  • SignalBar – wie viele abgeschlossene Kerzen das Signal verschoben wird. 0 reagiert auf die neueste geschlossene Kerze, 1 ahmt die MT5-Standardeinstellung nach, indem eine extra Kerze abgewartet wird, und höhere Werte verzögern die Ausführung noch mehr.
  • EnableLongEntry – wenn deaktiviert, öffnet die Strategie keine Long-Positionen bei bullischen Übergängen.
  • EnableShortEntry – wenn deaktiviert, öffnet die Strategie keine Short-Positionen bei bärischen Übergängen.
  • EnableLongExit – steuert, ob bullische Signale vorhandene Short-Positionen schließen dürfen.
  • EnableShortExit – steuert, ob bärische Signale vorhandene Long-Positionen schließen dürfen.
  • CandleType – Zeitrahmen der für Indikatorberechnungen verwendeten Kerzen.

Handelsregeln

  1. Die ForceTrend-Ausgabe wird in eine diskrete Richtung (+1, 0, -1) konvertiert.
  2. Richtungen werden in einem Verlauf fester Länge gespeichert, damit die Strategie die Kerze beim SignalBar-Offset mit der unmittelbar vorherigen Kerze vergleichen kann.
  3. Ein bullisches Signal (direction > 0) löst aus:
    • Schließen offener Short-Positionen, wenn EnableShortExit true ist.
    • Eröffnen oder Umkehren in eine Long-Position (Marktorder in Größe Volume + |Position|), wenn die vorherige Richtung nicht bullisch war und EnableLongEntry true ist.
  4. Ein bärisches Signal (direction < 0) löst die symmetrischen Aktionen für Long-Positionen aus, wenn EnableLongExit/EnableShortEntry aktiviert sind.
  5. Neutrale ForceTrend-Lesungen erben die zuletzt bekannte Richtung, damit das System nicht zwischen Flat-Zuständen oszilliert.
  6. Orders werden nur gesendet, wenn die Strategie vollständig geformt, online und der Handel durch die StockSharp-Runtime erlaubt ist.

Implementierungshinweise

  • Kerzen werden über SubscribeCandles(CandleType) empfangen; die Indikatorverarbeitung erfolgt im ProcessCandle-Callback.
  • Höchst- und Tiefstkurse werden über StockSharp-Indikatoren Highest und Lowest ermittelt, sodass keine manuelle Pufferverwaltung oder LINQ-Operationen erforderlich sind.
  • Der Richtungsverlauf wird in einem kleinen festen Array gespeichert, das entsprechend SignalBar dimensioniert ist, um das ursprüngliche MT5-Verhalten ohne Neuerstellung von Collections für jeden Tick zu reproduzieren.
  • Positionsumkehrungen verwenden eine einzelne Marktorder mit einem Volumen, das der Summe aus dem gewünschten Exposure und der absoluten aktuellen Position entspricht, und ahmt die BuyPositionOpen/SellPositionOpen-Helfer der MQL-Version nach.
  • Geldverwaltungsparameter des Expertenberaters (Lot-Dimensionierung, Stop-Loss und Take-Profit in Punkten) werden bewusst weggelassen; die StockSharp-Strategie verlässt sich auf das vom Benutzer konfigurierte Volume und optionale externe Schutzmodule.
  • Die booleschen Schalter spiegeln die MT5-Eingaben wider (BuyPosOpen, SellPosOpen, BuyPosClose, SellPosClose).

Verwendungshinweise

  • Die Volume-Eigenschaft vor dem Start der Strategie konfigurieren, um die Ordergröße zu steuern.
  • Einen Kerzentyp wählen, der dem bei MT5-Tests verwendeten Zeitrahmen entspricht (Standard: 4-Stunden-Kerzen).
  • Mit StockSharp-Risiko-/Schutzkomponenten kombinieren, wenn Stop-Loss- oder Take-Profit-Automatisierung erforderlich ist.

Dateien

  • Strategieimplementierung: CS/ForceTrendStrategy.cs
  • Ursprüngliche MQL-Dateien: MQL/18817/mql5/Experts/Exp_ForceTrend.mq5 und 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];
	}
}