Открыть на GitHub

Стратегия двойного подтверждения тренда скользящими средними

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

Стратегия двойного подтверждения тренда повторяет оригинального советника MetaTrader, который сочетает медленную экспоненциальную скользящую среднюю (EMA) и быструю линейно-взвешенную среднюю (LWMA). Сделки открываются только тогда, когда обе скользящие средние последовательно наклонены в одну сторону, а закрытие предыдущей свечи подтверждает направление движения. Такой подход позволяет входить в наиболее сильные импульсные участки тренда.

Реализация на StockSharp обрабатывает исключительно закрытые свечи, хранит наклон обеих средних за последние три бара и автоматически управляет защитными приказами через StartProtection. Стратегия не зависит от конкретного инструмента — требуется лишь наличие свечей и определённого шага цены.

Индикаторы

  • Медленная EMA (по умолчанию 57) — фильтрует глобальный тренд и должна расти/падать две свечи подряд.
  • Быстрая LWMA (по умолчанию 3) — подтверждает импульс. Её направление должно совпадать с направлением медленной EMA.

Параметры

Параметр Значение по умолчанию Описание
SlowMaLength 57 Период медленной EMA.
FastMaLength 3 Период быстрой LWMA.
StopLossPoints 100 Размер стоп-лосса в пунктах инструмента (переводится в цену через Security.PriceStep).
TakeProfitPoints 100 Размер тейк-профита в пунктах инструмента (переводится в цену через Security.PriceStep).
CandleType 15 минут Тип свечей, используемый в расчётах.

Все параметры описаны через StrategyParam<T>, поэтому их можно менять на лету или использовать в оптимизации.

Правила входа и выхода

Покупка

  1. EMA растёт: текущее значение > предыдущее > значение два бара назад.
  2. LWMA растёт: текущее значение > предыдущее > значение два бара назад.
  3. Закрытие предыдущей свечи выше значения EMA на предыдущем баре.
  4. Текущая EMA выше текущей LWMA.
  5. Текущая позиция отсутствует или короткая.
  6. При выполнении всех условий отправляется рыночная заявка на покупку объёмом Volume + |Position| для переворота в длинную позицию.

Продажа

  1. EMA падает: текущее значение < предыдущее < значение два бара назад.
  2. LWMA падает: текущее значение < предыдущее < значение два бара назад.
  3. Закрытие предыдущей свечи ниже значения EMA на предыдущем баре.
  4. Текущая EMA ниже текущей LWMA.
  5. Текущая позиция отсутствует или длинная.
  6. При выполнении условий отправляется рыночная заявка на продажу объёмом Volume + |Position| для переворота в короткую позицию.

Защитные механизмы

  • StartProtection преобразует значения стоп-лосса и тейк-профита из пунктов в абсолютную цену, умножая их на Security.PriceStep, и выставляет защитные приказы рыночным способом.
  • При появлении обратного сигнала стратегия сразу разворачивает позицию, независимо от состояния защитных заявок.

Реализация

  • В расчётах участвуют только свечи со статусом CandleStates.Finished, что эквивалентно проверке нового бара в MQL.
  • Внутренние поля хранят последние два значения индикаторов и предыдущее закрытие, поэтому не требуется обращаться к истории индикаторов.
  • Метод IsFormedAndOnlineAndAllowTrading() гарантирует, что торговля ведётся только при наличии всех данных и разрешённом состоянии стратегии.
  • Записи LogInfo подробно фиксируют события входа, что упрощает отладку и мониторинг.
  • При наличии графической панели рисуются свечи и обе скользящие средние для визуальной проверки.

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

  • Настройте Volume в соответствии с лотностью инструмента — стратегия всегда отправляет ордера объёмом Volume + |Position|.
  • Если у инструмента не задан PriceStep, код использует значение 1. В этом случае параметры стопов и профитов стоит подстроить вручную.
  • Наиболее полезные направления оптимизации: периоды скользящих средних и расстояния защитных приказов.
  • При необходимости можно добавить дополнительные фильтры (волатильность, торговые сессии и т. п.) — архитектура стратегии легко расширяется.

Рекомендуемые диапазоны оптимизации

  • SlowMaLength: 20 – 120 (шаг 5–10).
  • FastMaLength: 2 – 10 (шаг 1).
  • StopLossPoints / TakeProfitPoints: 50 – 200 (в зависимости от волатильности).

Такие диапазоны соответствуют исходной логике советника и подходят для адаптации под другие инструменты.

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>
/// Dual moving average trend confirmation strategy.
/// Uses a slow EMA and a fast LWMA to detect synchronized trends.
/// Enters long when both averages slope upward, price stays above the slow EMA, and the slow EMA is above the fast LWMA.
/// Enters short when both averages slope downward, price stays below the slow EMA, and the slow EMA is below the fast LWMA.
/// Built-in stop-loss and take-profit are defined in instrument points.
/// </summary>
public class DualMaTrendConfirmationStrategy : Strategy
{
	private readonly StrategyParam<int> _slowMaLength;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _previousClose;
	private decimal _slowPrevious;
	private decimal _slowPrevious2;
	private decimal _fastPrevious;
	private decimal _fastPrevious2;
	private int _historyCount;

	/// <summary>
	/// Slow EMA period length.
	/// </summary>
	public int SlowMaLength
	{
		get => _slowMaLength.Value;
		set => _slowMaLength.Value = value;
	}

	/// <summary>
	/// Fast LWMA period length.
	/// </summary>
	public int FastMaLength
	{
		get => _fastMaLength.Value;
		set => _fastMaLength.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in instrument points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in instrument points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="DualMaTrendConfirmationStrategy"/> class.
	/// </summary>
	public DualMaTrendConfirmationStrategy()
	{
		_slowMaLength = Param(nameof(SlowMaLength), 57)
			.SetDisplay("Slow EMA Length", "Period for the slow EMA trend filter", "Moving Averages")
			.SetRange(10, 200)
			;

		_fastMaLength = Param(nameof(FastMaLength), 3)
			.SetDisplay("Fast LWMA Length", "Period for the fast LWMA confirmation filter", "Moving Averages")
			.SetRange(1, 50)
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 100m)
			.SetDisplay("Stop Loss (points)", "Stop-loss distance measured in instrument points", "Risk Management")
			.SetRange(10m, 500m)
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 100m)
			.SetDisplay("Take Profit (points)", "Take-profit distance measured in instrument points", "Risk Management")
			.SetRange(10m, 500m)
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for moving average calculations", "General");
	}

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

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

		// Clear stored history so the next candle starts with a clean state.
		_previousClose = 0m;
		_slowPrevious = 0m;
		_slowPrevious2 = 0m;
		_fastPrevious = 0m;
		_fastPrevious2 = 0m;
		_historyCount = 0;
	}

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

		var slowEma = new ExponentialMovingAverage
		{
			Length = SlowMaLength
		};

		var fastLwma = new WeightedMovingAverage
		{
			Length = FastMaLength
		};

		var subscription = SubscribeCandles(CandleType);

		var step = Security.PriceStep ?? 1m;

		// Enable automatic stop-loss and take-profit management based on point offsets.
		StartProtection(
			takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
			stopLoss: new Unit(StopLossPoints * step, UnitTypes.Absolute),
			useMarketOrders: true);

		subscription
			.Bind(slowEma, fastLwma, (candle, slowValue, fastValue) => ProcessCandle(candle, slowValue, fastValue, slowEma, fastLwma))
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, slowEma);
			DrawIndicator(area, fastLwma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal slowValue, decimal fastValue, ExponentialMovingAverage slowEma, WeightedMovingAverage fastLwma)
	{
		// Work only with fully formed candles to avoid premature decisions.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure both indicators produced reliable values before trading logic.
		if (!slowEma.IsFormed || !fastLwma.IsFormed)
		{
			UpdateHistory(slowValue, fastValue, candle.ClosePrice);
			return;
		}

		// Accumulate at least two previous candles for slope calculations.
		if (_historyCount < 2)
		{
			UpdateHistory(slowValue, fastValue, candle.ClosePrice);
			return;
		}

		var slowRising = slowValue > _slowPrevious && _slowPrevious > _slowPrevious2;
		var fastRising = fastValue > _fastPrevious && _fastPrevious > _fastPrevious2;
		var slowFalling = slowValue < _slowPrevious && _slowPrevious < _slowPrevious2;
		var fastFalling = fastValue < _fastPrevious && _fastPrevious < _fastPrevious2;
		var priceAboveSlow = _previousClose > _slowPrevious;
		var priceBelowSlow = _previousClose < _slowPrevious;
		var slowAboveFast = slowValue > fastValue;
		var slowBelowFast = slowValue < fastValue;

		if (slowRising && fastRising && priceAboveSlow && slowAboveFast && Position <= 0)
		{
			BuyMarket();
		}
		else if (slowFalling && fastFalling && priceBelowSlow && slowBelowFast && Position >= 0)
		{
			SellMarket();
		}

		UpdateHistory(slowValue, fastValue, candle.ClosePrice);
	}

	private void UpdateHistory(decimal slowValue, decimal fastValue, decimal closePrice)
	{
		// Shift previous values so the last two candles are always available.
		_slowPrevious2 = _slowPrevious;
		_slowPrevious = slowValue;
		_fastPrevious2 = _fastPrevious;
		_fastPrevious = fastValue;
		_previousClose = closePrice;

		if (_historyCount < 2)
			_historyCount++;
	}
}