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
- Slow EMA is rising: current value > previous value > value two candles ago.
- Fast LWMA is rising: current value > previous value > value two candles ago.
- Previous candle close is above the previous slow EMA value.
- Current slow EMA value is above the current fast LWMA value.
- Current position is flat or short.
- When all conditions are met, the strategy sends a market buy order for
Volume + |Position| to flip into a long position.
Short Setup
- Slow EMA is falling: current value < previous value < value two candles ago.
- Fast LWMA is falling: current value < previous value < value two candles ago.
- Previous candle close is below the previous slow EMA value.
- Current slow EMA value is below the current fast LWMA value.
- Current position is flat or long.
- 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++;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
class dual_ma_trend_confirmation_strategy(Strategy):
def __init__(self):
super(dual_ma_trend_confirmation_strategy, self).__init__()
self._slow_ma_length = self.Param("SlowMaLength", 57)
self._fast_ma_length = self.Param("FastMaLength", 3)
self._stop_loss_points = self.Param("StopLossPoints", 100.0)
self._take_profit_points = self.Param("TakeProfitPoints", 100.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
@property
def SlowMaLength(self):
return self._slow_ma_length.Value
@SlowMaLength.setter
def SlowMaLength(self, value):
self._slow_ma_length.Value = value
@property
def FastMaLength(self):
return self._fast_ma_length.Value
@FastMaLength.setter
def FastMaLength(self, value):
self._fast_ma_length.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(dual_ma_trend_confirmation_strategy, self).OnStarted2(time)
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowMaLength
self._fast_lwma = WeightedMovingAverage()
self._fast_lwma.Length = self.FastMaLength
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
self.StartProtection(
Unit(float(self.TakeProfitPoints) * step, UnitTypes.Absolute),
Unit(float(self.StopLossPoints) * step, UnitTypes.Absolute),
False, None, None, True)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._slow_ema, self._fast_lwma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, slow_value, fast_value):
if candle.State != CandleStates.Finished:
return
sv = float(slow_value)
fv = float(fast_value)
close = float(candle.ClosePrice)
if not self._slow_ema.IsFormed or not self._fast_lwma.IsFormed:
self._update_history(sv, fv, close)
return
if self._history_count < 2:
self._update_history(sv, fv, close)
return
slow_rising = sv > self._slow_previous and self._slow_previous > self._slow_previous2
fast_rising = fv > self._fast_previous and self._fast_previous > self._fast_previous2
slow_falling = sv < self._slow_previous and self._slow_previous < self._slow_previous2
fast_falling = fv < self._fast_previous and self._fast_previous < self._fast_previous2
price_above_slow = self._previous_close > self._slow_previous
price_below_slow = self._previous_close < self._slow_previous
slow_above_fast = sv > fv
slow_below_fast = sv < fv
if slow_rising and fast_rising and price_above_slow and slow_above_fast and self.Position <= 0:
self.BuyMarket()
elif slow_falling and fast_falling and price_below_slow and slow_below_fast and self.Position >= 0:
self.SellMarket()
self._update_history(sv, fv, close)
def _update_history(self, slow_value, fast_value, close):
self._slow_previous2 = self._slow_previous
self._slow_previous = slow_value
self._fast_previous2 = self._fast_previous
self._fast_previous = fast_value
self._previous_close = close
if self._history_count < 2:
self._history_count += 1
def OnReseted(self):
super(dual_ma_trend_confirmation_strategy, self).OnReseted()
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
def CreateClone(self):
return dual_ma_trend_confirmation_strategy()