Ver no GitHub

Dual MA Trend Confirmation Strategy

Overview

The Dual MA Trend Confirmation Strategy replicates the original MetaTrader expert that combines a slow exponential moving average (EMA) with a fast linear weighted moving average (LWMA). The system waits for both moving averages to align in the same direction and uses the previous candle close as additional confirmation before entering a position. The idea is to participate only in strong momentum swings when the slow trend filter and the fast confirmation filter simultaneously slope upward or downward.

The StockSharp implementation processes only fully finished candles, tracks the slope of each moving average over the last three bars, and automatically manages protective orders via the built-in StartProtection mechanism. The strategy is instrument-agnostic: it can operate on any security and timeframe that provide candles and supports the concept of “points” via the instrument price step.

Indicators

  • Slow EMA – Default period 57. Represents the dominant trend direction. The strategy requires the EMA to increase (or decrease) for two consecutive candles before trading.
  • Fast LWMA – Default period 3. Acts as a momentum confirmation filter. Its slope must agree with the slow EMA, reinforcing that momentum supports the trend.

Parameters

Parameter Default Description
SlowMaLength 57 Period of the slow EMA trend filter.
FastMaLength 3 Period of the fast LWMA confirmation filter.
StopLossPoints 100 Protective stop distance expressed in instrument points (multiplied by Security.PriceStep).
TakeProfitPoints 100 Take-profit distance expressed in instrument points (multiplied by Security.PriceStep).
CandleType 15-minute time frame Candle data type used for all calculations.

All parameters are exposed as StrategyParam<T> values so they can be modified at runtime or optimized through StockSharp’s optimization tools.

Trading Rules

Long Setup

  1. Slow EMA is rising: current value > previous value > value two candles ago.
  2. Fast LWMA is rising: current value > previous value > value two candles ago.
  3. Previous candle close is above the previous slow EMA value.
  4. Current slow EMA value is above the current fast LWMA value.
  5. Current position is flat or short.
  6. When all conditions are met, the strategy sends a market buy order for Volume + |Position| to flip into a long position.

Short Setup

  1. Slow EMA is falling: current value < previous value < value two candles ago.
  2. Fast LWMA is falling: current value < previous value < value two candles ago.
  3. Previous candle close is below the previous slow EMA value.
  4. Current slow EMA value is below the current fast LWMA value.
  5. Current position is flat or long.
  6. When all conditions are met, the strategy sends a market sell order for Volume + |Position| to flip into a short position.

Protective Logic

  • StartProtection converts StopLossPoints and TakeProfitPoints into absolute price offsets by multiplying them with Security.PriceStep. Stop-loss and take-profit orders are issued as market exits so the engine can close the position even if limit orders are not supported.
  • When the opposite signal appears, the strategy immediately reverses the position regardless of the protective orders.

Implementation Details

  • Only finished candles are processed, emulating the new-bar check from the original MQL version.
  • The strategy keeps the last two moving average values and the previous close price in private fields to avoid indicator history lookups.
  • IsFormedAndOnlineAndAllowTrading() ensures trading occurs only when all data streams are active and trading is permitted.
  • Trade direction logs (LogInfo) provide transparency for debugging and live monitoring.
  • Chart rendering (if available) draws candles and both moving averages for quick visual validation.

Usage Notes

  • Choose Volume according to the instrument lot size. The strategy always sends market orders sized Volume + |Position| to reverse efficiently.
  • When running on instruments without a defined PriceStep, the code falls back to a value of 1. Adjust parameters accordingly if tick size differs.
  • Optimization can focus on the moving average periods and protective distances to adapt the strategy to different markets.
  • Combine with additional filters (volatility, session times, etc.) if required. The modular structure makes it easy to extend.

Suggested Optimization Ranges

  • SlowMaLength: 20 – 120 with step 5–10.
  • FastMaLength: 2 – 10 with step 1.
  • StopLossPoints / TakeProfitPoints: 50 – 200 depending on instrument volatility.

These ranges closely mirror the original expert settings while providing flexibility for other instruments.

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++;
	}
}