View on GitHub

Double MA Crossover Breakout Strategy

Overview

This strategy reproduces the "DoubleMA Crossover" MetaTrader expert adviser inside the StockSharp framework. The logic monitors a fast and a slow simple moving average, waits for a directional crossover, and then requires a breakout confirmation before entering the market. The algorithm manages only one position at a time and includes optional trailing stop behaviour that mimics the original three trailing modes.

How It Works

  1. Signal detection – Two simple moving averages (defaults: 2 and 5) are calculated on the selected candle series. A bullish crossover occurs when the fast average crosses above the slow one and vice versa for a bearish crossover.
  2. Breakout confirmation – After a crossover the strategy stores a breakout level defined in price steps (BreakoutPips). A position is opened only when price reaches that level on a subsequent candle, replicating the stop order behaviour from the MQL version.
  3. Position management – Only a single position is allowed. While a trade is active the strategy monitors stop-loss, take-profit, and the configured trailing stop type. The internal trackers emulate broker-side execution to keep behaviour deterministic in backtests.
  4. Session filter – Trading can be restricted to a specific time window (StartHour..StopHour). The strategy still manages open trades outside the window but does not create new breakout levels when the filter blocks trading.
  5. Trailing stops – Three trailing modes are supported: immediate trailing with the initial stop distance, trailing after a custom distance, and the three-level logic with breakeven shifts just like the original EA.

Parameters

Parameter Description
FastMaPeriod, SlowMaPeriod Periods of the fast and slow simple moving averages.
BreakoutPips Distance in price steps added to the signal candle close to define the breakout trigger.
StopLossPips, TakeProfitPips Protective stop and optional take profit in price steps. Set take profit to zero to disable it.
UseTrailingStop Enables trailing stop management.
TrailingMode Trailing type: Type1 uses the original stop distance, Type2 waits for a custom distance (TrailingStopPips), Type3 uses the three MQL levels.
TrailingStopPips Distance for Type2 trailing.
Level1TriggerPips, Level1OffsetPips First trigger level and offset for Type3 trailing (moves stop to breakeven by default).
Level2TriggerPips, Level2OffsetPips Second trigger level and offset for Type3 trailing.
Level3TriggerPips, Level3OffsetPips Third trigger level and offset for Type3 trailing (converts to a classical trailing stop).
UseTimeLimit, StartHour, StopHour Enables the trading session filter and defines the inclusive hour range.
CandleType Candle series used for signal calculations.
TradeVolume Order volume expressed in lots.

Trailing Stop Modes

  • Type1 – Moves the stop using the original stop-loss distance once price advances by that amount.
  • Type2 – Waits until price moves by TrailingStopPips before trailing, then locks profit at that distance.
  • Type3 – Uses three levels: the first two shift the stop by the defined offsets, and the third converts to a continuous trailing stop using the current close and Level3OffsetPips.

Usage Tips

  • Align BreakoutPips with the instrument tick size to maintain the same behaviour as the MetaTrader expert adviser.
  • Review the session filter to match your trading hours; the default allows entries between 11:00 and 16:00 local time.
  • Disable the time filter (UseTimeLimit = false) for 24/7 instruments.
  • When testing trailing type 3, ensure the offset values are not larger than their corresponding trigger levels; otherwise the stop may remain behind the entry price.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Double moving average crossover with breakout confirmation and trailing protection.
/// </summary>
public class DoubleMaCrossoverStrategy : Strategy
{
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _breakoutPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<bool> _useTrailingStop;
	private readonly StrategyParam<TrailingTypes> _trailingMode;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _level1TriggerPips;
	private readonly StrategyParam<int> _level1OffsetPips;
	private readonly StrategyParam<int> _level2TriggerPips;
	private readonly StrategyParam<int> _level2OffsetPips;
	private readonly StrategyParam<int> _level3TriggerPips;
	private readonly StrategyParam<int> _level3OffsetPips;
	private readonly StrategyParam<bool> _useTimeLimit;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private decimal _prevFast;
	private decimal _prevSlow;
	private bool _hasPrev;

	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;

	private decimal? _entryPrice;
	private decimal? _currentStop;
	private decimal? _currentTakeProfit;
	private decimal _maxPriceSinceEntry;
	private decimal _minPriceSinceEntry;

	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	public int BreakoutPips
	{
		get => _breakoutPips.Value;
		set => _breakoutPips.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public bool UseTrailingStop
	{
		get => _useTrailingStop.Value;
		set => _useTrailingStop.Value = value;
	}

	public TrailingTypes TrailingMode
	{
		get => _trailingMode.Value;
		set => _trailingMode.Value = value;
	}

	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public int Level1TriggerPips
	{
		get => _level1TriggerPips.Value;
		set => _level1TriggerPips.Value = value;
	}

	public int Level1OffsetPips
	{
		get => _level1OffsetPips.Value;
		set => _level1OffsetPips.Value = value;
	}

	public int Level2TriggerPips
	{
		get => _level2TriggerPips.Value;
		set => _level2TriggerPips.Value = value;
	}

	public int Level2OffsetPips
	{
		get => _level2OffsetPips.Value;
		set => _level2OffsetPips.Value = value;
	}

	public int Level3TriggerPips
	{
		get => _level3TriggerPips.Value;
		set => _level3TriggerPips.Value = value;
	}

	public int Level3OffsetPips
	{
		get => _level3OffsetPips.Value;
		set => _level3OffsetPips.Value = value;
	}

	public bool UseTimeLimit
	{
		get => _useTimeLimit.Value;
		set => _useTimeLimit.Value = value;
	}

	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	public DoubleMaCrossoverStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 5)
		.SetDisplay("Fast MA Period", "Period for the fast moving average.", "General")
		;

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 15)
		.SetDisplay("Slow MA Period", "Period for the slow moving average.", "General")
		;

		_breakoutPips = Param(nameof(BreakoutPips), 15)
		.SetDisplay("Breakout Pips", "Distance in price steps added before submitting an entry.", "General")
		;

		_stopLossPips = Param(nameof(StopLossPips), 25)
		.SetDisplay("Stop Loss Pips", "Protective stop expressed in price steps.", "Risk")
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 0)
		.SetDisplay("Take Profit Pips", "Take profit distance expressed in price steps.", "Risk")
		;

		_useTrailingStop = Param(nameof(UseTrailingStop), false)
		.SetDisplay("Use Trailing", "Enable trailing stop management.", "Risk");

		_trailingMode = Param(nameof(TrailingMode), TrailingTypes.Type3)
		.SetDisplay("Trailing Type", "Trailing stop behaviour.", "Risk")
		;

		_trailingStopPips = Param(nameof(TrailingStopPips), 40)
		.SetDisplay("Trailing Stop Pips", "Trailing distance used by type 2 trailing.", "Risk")
		;

		_level1TriggerPips = Param(nameof(Level1TriggerPips), 20)
		.SetDisplay("Level 1 Trigger", "Profit in price steps required before the first trailing adjustment.", "Risk")
		;

		_level1OffsetPips = Param(nameof(Level1OffsetPips), 20)
		.SetDisplay("Level 1 Offset", "Offset in price steps applied after the first trigger.", "Risk")
		;

		_level2TriggerPips = Param(nameof(Level2TriggerPips), 30)
		.SetDisplay("Level 2 Trigger", "Profit in price steps required before the second trailing adjustment.", "Risk")
		;

		_level2OffsetPips = Param(nameof(Level2OffsetPips), 20)
		.SetDisplay("Level 2 Offset", "Offset in price steps applied after the second trigger.", "Risk")
		;

		_level3TriggerPips = Param(nameof(Level3TriggerPips), 50)
		.SetDisplay("Level 3 Trigger", "Profit in price steps required before the third trailing adjustment.", "Risk")
		;

		_level3OffsetPips = Param(nameof(Level3OffsetPips), 20)
		.SetDisplay("Level 3 Offset", "Offset in price steps applied after the third trigger.", "Risk")
		;

		_useTimeLimit = Param(nameof(UseTimeLimit), false)
		.SetDisplay("Use Time Limit", "Restrict the creation of new orders to a trading window.", "Schedule");

		_startHour = Param(nameof(StartHour), 11)
		.SetDisplay("Start Hour", "Hour when new setups become valid.", "Schedule")
		;

		_stopHour = Param(nameof(StopHour), 16)
		.SetDisplay("Stop Hour", "Hour after which no new setups are created.", "Schedule")
		;

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

		_tradeVolume = Param(nameof(TradeVolume), 1m)
		.SetDisplay("Volume", "Order volume in lots.", "Trading")
		.SetGreaterThanZero()
		;
	}

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

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

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

		// Reset internal buffers before processing market data.
		ResetState();
		Volume = TradeVolume;
		var fastMa = new SMA { Length = FastMaPeriod };
		var slowMa = new SMA { Length = SlowMaPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastMa, slowMa, ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastMa, decimal slowMa)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		if (!_hasPrev)
		{
			_prevFast = fastMa;
			_prevSlow = slowMa;
			_hasPrev = true;
			return;
		}

		// Identify crossovers based on the two moving averages.
		var crossUp = fastMa > slowMa && _prevFast <= _prevSlow;
		var crossDown = fastMa < slowMa && _prevFast >= _prevSlow;

		// Manage the current position before checking for fresh breakouts.
		ManageOpenPosition(candle, crossUp, crossDown);
		TriggerPendingEntries(candle);

		if (UseTimeLimit && !IsTradingTime(candle.OpenTime))
		{
			if (crossUp)
			{
				_pendingSellPrice = null;
			}

			if (crossDown)
			{
				_pendingBuyPrice = null;
			}

			_prevFast = fastMa;
			_prevSlow = slowMa;
			return;
		}

		if (crossDown)
		{
			_pendingBuyPrice = null;
		}

		if (crossUp)
		{
			_pendingSellPrice = null;
		}

		if (Position == 0)
		{
			var breakout = GetBreakoutDistance();

			if (crossUp)
			{
				_pendingBuyPrice = candle.ClosePrice + breakout;
			}
			else if (crossDown)
			{
				_pendingSellPrice = candle.ClosePrice - breakout;
			}
		}

		TriggerPendingEntries(candle);

		_prevFast = fastMa;
		_prevSlow = slowMa;
	}

	private void ManageOpenPosition(ICandleMessage candle, bool crossUp, bool crossDown)
	{
		if (Position == 0)
		{
			if (_entryPrice.HasValue)
			{
				// Clear trailing information once the position is flat.
				ResetPositionState();
			}

			return;
		}

		if (_entryPrice is null)
		{
			_entryPrice = candle.ClosePrice;
			_maxPriceSinceEntry = candle.ClosePrice;
			_minPriceSinceEntry = candle.ClosePrice;
		}

		UpdateExtremes(candle);
		UpdateTrailingStop(candle);

		if (CheckStopsAndTargets(candle))
		{
			return;
		}

		if (Position > 0 && crossDown)
		{
			ExitLong();
			return;
		}

		if (Position < 0 && crossUp)
		{
			ExitShort();
		}
	}

	private void UpdateExtremes(ICandleMessage candle)
	{
		var high = candle.HighPrice;
		var low = candle.LowPrice;
		_maxPriceSinceEntry = Math.Max(_maxPriceSinceEntry, high);
		_minPriceSinceEntry = Math.Min(_minPriceSinceEntry, low);
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (!UseTrailingStop || _entryPrice is null)
		{
			return;
		}

		var entryPrice = _entryPrice.Value;
		var closePrice = candle.ClosePrice;
		var step = GetPriceStep();

		switch (TrailingMode)
		{
			case TrailingTypes.Type1:
			{
				UpdateType1Trailing(entryPrice, closePrice, step);
				break;
			}
			case TrailingTypes.Type2:
			{
				UpdateType2Trailing(entryPrice, closePrice, step);
				break;
			}
			case TrailingTypes.Type3:
			{
				UpdateType3Trailing(entryPrice, closePrice, step);
				break;
			}
		}
	}

	private void UpdateType1Trailing(decimal entryPrice, decimal closePrice, decimal step)
	{
		var distance = step * Math.Abs(StopLossPips);
		if (distance == 0)
		{
			return;
		}

		if (Position > 0)
		{
			if (_maxPriceSinceEntry - entryPrice >= distance)
			{
				var candidate = closePrice - distance;
				UpdateStopForLong(candidate);
			}
		}
		else if (Position < 0)
		{
			if (entryPrice - _minPriceSinceEntry >= distance)
			{
				var candidate = closePrice + distance;
				UpdateStopForShort(candidate);
			}
		}
	}

	private void UpdateType2Trailing(decimal entryPrice, decimal closePrice, decimal step)
	{
		var distance = step * Math.Abs(TrailingStopPips);
		if (distance == 0)
		{
			return;
		}

		if (Position > 0)
		{
			if (_maxPriceSinceEntry - entryPrice >= distance)
			{
				var candidate = closePrice - distance;
				UpdateStopForLong(candidate);
			}
		}
		else if (Position < 0)
		{
			if (entryPrice - _minPriceSinceEntry >= distance)
			{
				var candidate = closePrice + distance;
				UpdateStopForShort(candidate);
			}
		}
	}

	private void UpdateType3Trailing(decimal entryPrice, decimal closePrice, decimal step)
	{
		var trigger1 = step * Math.Abs(Level1TriggerPips);
		if (trigger1 > 0)
		{
			if (Position > 0 && _maxPriceSinceEntry - entryPrice >= trigger1)
			{
				var candidate = entryPrice + trigger1 - step * Math.Abs(Level1OffsetPips);
				UpdateStopForLong(candidate);
			}
			else if (Position < 0 && entryPrice - _minPriceSinceEntry >= trigger1)
			{
				var candidate = entryPrice - trigger1 + step * Math.Abs(Level1OffsetPips);
				UpdateStopForShort(candidate);
			}
		}

		var trigger2 = step * Math.Abs(Level2TriggerPips);
		if (trigger2 > 0)
		{
			if (Position > 0 && _maxPriceSinceEntry - entryPrice >= trigger2)
			{
				var candidate = entryPrice + trigger2 - step * Math.Abs(Level2OffsetPips);
				UpdateStopForLong(candidate);
			}
			else if (Position < 0 && entryPrice - _minPriceSinceEntry >= trigger2)
			{
				var candidate = entryPrice - trigger2 + step * Math.Abs(Level2OffsetPips);
				UpdateStopForShort(candidate);
			}
		}

		var trigger3 = step * Math.Abs(Level3TriggerPips);
		if (trigger3 > 0)
		{
			if (Position > 0 && _maxPriceSinceEntry - entryPrice >= trigger3)
			{
				var candidate = closePrice - step * Math.Abs(Level3OffsetPips);
				UpdateStopForLong(candidate);
			}
			else if (Position < 0 && entryPrice - _minPriceSinceEntry >= trigger3)
			{
				var candidate = closePrice + step * Math.Abs(Level3OffsetPips);
				UpdateStopForShort(candidate);
			}
		}
	}

	private void UpdateStopForLong(decimal candidate)
	{
		if (!_currentStop.HasValue || candidate > _currentStop.Value)
		{
			_currentStop = candidate;
		}
	}

	private void UpdateStopForShort(decimal candidate)
	{
		if (!_currentStop.HasValue || candidate < _currentStop.Value)
		{
			_currentStop = candidate;
		}
	}

	private bool CheckStopsAndTargets(ICandleMessage candle)
	{
		// Simulate broker-side stop loss and take profit execution.
		if (Position > 0)
		{
			if (_currentTakeProfit.HasValue && candle.HighPrice >= _currentTakeProfit.Value)
			{
				ExitLong();
				return true;
			}

			if (_currentStop.HasValue && candle.LowPrice <= _currentStop.Value)
			{
				ExitLong();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_currentTakeProfit.HasValue && candle.LowPrice <= _currentTakeProfit.Value)
			{
				ExitShort();
				return true;
			}

			if (_currentStop.HasValue && candle.HighPrice >= _currentStop.Value)
			{
				ExitShort();
				return true;
			}
		}

		return false;
	}

	private void TriggerPendingEntries(ICandleMessage candle)
	{
		// Skip processing when a position already exists.
		if (Position != 0)
		{
			if (Position > 0)
			{
				_pendingBuyPrice = null;
			}
			else
			{
				_pendingSellPrice = null;
			}

			return;
		}

		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice)
		{
			// Breakout confirmed on the long side.
			EnterLong(buyPrice);
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
		else if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice)
		{
			// Breakout confirmed on the short side.
			EnterShort(sellPrice);
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void EnterLong(decimal price)
	{
		if (TradeVolume <= 0)
		{
			return;
		}

		Volume = TradeVolume;
		BuyMarket();

		// Initialize tracking variables for the new long trade.
		_entryPrice = price;
		_currentStop = StopLossPips > 0 ? price - GetPriceStep() * Math.Abs(StopLossPips) : null;
		_currentTakeProfit = TakeProfitPips > 0 ? price + GetPriceStep() * Math.Abs(TakeProfitPips) : null;
		_maxPriceSinceEntry = price;
		_minPriceSinceEntry = price;
	}

	private void EnterShort(decimal price)
	{
		if (TradeVolume <= 0)
		{
			return;
		}

		Volume = TradeVolume;
		SellMarket();

		// Initialize tracking variables for the new short trade.
		_entryPrice = price;
		_currentStop = StopLossPips > 0 ? price + GetPriceStep() * Math.Abs(StopLossPips) : null;
		_currentTakeProfit = TakeProfitPips > 0 ? price - GetPriceStep() * Math.Abs(TakeProfitPips) : null;
		_maxPriceSinceEntry = price;
		_minPriceSinceEntry = price;
	}

	private void ExitLong()
	{
		Volume = TradeVolume;
		SellMarket();
		ResetPositionState();
	}

	private void ExitShort()
	{
		Volume = TradeVolume;
		BuyMarket();
		ResetPositionState();
	}

	private bool IsTradingTime(DateTimeOffset time)
	{
		if (!UseTimeLimit)
		{
			return true;
		}

		var hour = time.Hour;

		if (StartHour <= StopHour)
		{
			return hour >= StartHour && hour <= StopHour;
		}

		return hour >= StartHour || hour <= StopHour;
	}

	private decimal GetBreakoutDistance()
	{
		return GetPriceStep() * Math.Abs(BreakoutPips);
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0 ? step : 1m;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_currentStop = null;
		_currentTakeProfit = null;
		_maxPriceSinceEntry = 0m;
		_minPriceSinceEntry = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
	}

	private void ResetState()
	{
		_prevFast = 0m;
		_prevSlow = 0m;
		_hasPrev = false;
		ResetPositionState();
	}

	public enum TrailingTypes
	{
		Type1,
		Type2,
		Type3
	}
}