Открыть на GitHub

Стратегия Force Trend

Общее описание

  • Конвертация советника MetaTrader 5 Exp_ForceTrend.mq5 из каталога MQL/18817.
  • Использует осциллятор ForceTrend для определения смены доминирующего направления.
  • Логика реализована через высокоуровневый API StockSharp: подписка на свечи, встроенные индикаторы и обработчик Bind вместо прямого доступа к буферам.

Индикатор ForceTrend

  • Рассматривает Length последних свечей и вычисляет диапазон между наибольшим максимумом и наименьшим минимумом.
  • Средняя цена текущей свечи нормализуется внутри диапазона и дважды сглаживается:
    • На первом этапе вычисляется промежуточное значение force с коэффициентами 0.66 и 0.67.
    • На втором этапе применяется логарифмическое преобразование и полураспадное сглаживание, что воспроизводит буфер Ind оригинального индикатора.
  • Значения выше нуля трактуются как бычьи (в МТ5 подсвечены синим), значения ниже нуля — как медвежьи (подсветка магентой).

Параметры

  • Length — длина окна ForceTrend; должна быть строго положительной.
  • SignalBar — сколько закрытых свечей пропустить перед генерацией сигнала. 0 реагирует на последнюю закрытую свечу, 1 повторяет дефолт МТ5 (ожидание одной дополнительной свечи), большие значения сильнее задерживают реакцию.
  • EnableLongEntry — разрешает открытие длинных позиций при переходе индикатора в бычье состояние.
  • EnableShortEntry — разрешает открытие коротких позиций при переходе в медвежье состояние.
  • EnableLongExit — разрешает закрывать длинные позиции при медвежьих сигналах.
  • EnableShortExit — разрешает закрывать короткие позиции при бычьих сигналах.
  • CandleType — таймфрейм свечей, на которых рассчитывается ForceTrend.

Правила торговли

  1. Значение ForceTrend преобразуется в дискретное направление (+1, 0, -1).
  2. История направлений хранится в массиве фиксированной длины, чтобы сравнивать свечу на смещении SignalBar с предыдущей.
  3. Бычий сигнал (direction > 0) вызывает:
    • Закрытие коротких позиций при включённом EnableShortExit (объёмом |Position|).
    • Открытие или разворот в лонг (рыночный ордер объёмом Volume + |Position|), если предыдущая свеча не была бычьей и EnableLongEntry = true.
  4. Медвежий сигнал (direction < 0) выполняет симметричные действия для длинных позиций в зависимости от EnableLongExit и EnableShortEntry.
  5. Нейтральные значения индикатора наследуют последнее известное направление, чтобы избежать дребезга сигналов.
  6. Приказы отправляются только после полной инициализации стратегии и разрешения торговли (IsFormedAndOnlineAndAllowTrading).

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

  • Используются подписки на свечи через SubscribeCandles(CandleType) и обработчик ProcessCandle.
  • Экстремумы вычисляются индикаторами Highest и Lowest, что исключает создание собственных коллекций и ручную обработку серий.
  • Для хранения направлений создан массив фиксированного размера (минимум две ячейки), чтобы повторить механику MT5 без динамических очередей на каждом тике.
  • Развороты позиций выполняются одним рыночным ордером, объём которого складывается из требуемого объёма и абсолютного текущего положения, аналогично функциям BuyPositionOpen/SellPositionOpen из MQL-версии.
  • Параметры управления капиталом советника (MM, стоп-лосс/тейк-профит в пунктах, допуски) не переносятся: в StockSharp размер задаётся через Volume, а защиту можно подключить отдельными компонентами.
  • Булевы флаги полностью соответствуют входным параметрам MT5 (BuyPosOpen, SellPosOpen, BuyPosClose, SellPosClose).

Рекомендации по использованию

  • Перед запуском задайте свойство Volume, определяющее торговый объём.
  • Подберите таймфрейм CandleType, соответствующий тестам в MT5 (по умолчанию — четырёхчасовые свечи).
  • Для стоп-лоссов и тейк-профитов подключайте механизмы защиты StockSharp (StartProtection или внешние модули).

Файлы

  • Реализация стратегии: CS/ForceTrendStrategy.cs
  • Оригинальные файлы MQL: MQL/18817/mql5/Experts/Exp_ForceTrend.mq5, 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];
	}
}