View on GitHub

Universal MA Cross V4 Strategy

Overview

The Universal MA Cross V4 Strategy is a high-level StockSharp port of the MetaTrader 4 expert advisor "Universal MACross EA v4". The algorithm follows the interaction between a configurable fast moving average and a slow moving average. It supports several moving average types, selectable price sources, an hourly trading window, and flexible position management including stop-and-reverse behaviour, protective targets and trailing stops. The strategy is designed for bar-based execution using the StockSharp high-level API with candle subscriptions.

Trading Logic

Indicator processing

  • Two moving averages are evaluated on every finished candle. Each moving average can use its own length, smoothing method (Simple, Exponential, Smoothed or Linear Weighted) and price source (close, open, high, low, median, typical or weighted).
  • The MinCrossDistancePoints filter requires the fast and slow averages to diverge by at least the specified number of price steps at the crossover bar. When ConfirmedOnEntry is enabled the divergence is validated on the previous completed candle, reproducing the "confirmed" mode from the original EA.
  • Setting ReverseCondition swaps bullish and bearish signals without changing the indicator configuration.

Entry rules

  1. A long entry occurs when the fast average crosses above the slow average by at least MinCrossDistancePoints. A short entry requires the opposite cross.
  2. When StopAndReverse is true, an opposite signal closes the active position before new entries are considered.
  3. OneEntryPerBar prevents multiple entries inside the same candle by tracking the timestamp of the most recent order.
  4. The order size is controlled by TradeVolume. StockSharp automatically applies this volume to the generated market orders.

Position management

  • Stop-loss and take-profit distances are defined in points through StopLossPoints and TakeProfitPoints. They are converted into absolute prices using the instrument price step. When PureSar is active all protective logic is disabled, just like the "Pure SAR" option in the MQL version.
  • Trailing stop management mirrors the MQL implementation: once price moves further than TrailingStopPoints from the entry level the stop is pulled behind the market by the same distance. Trailing stops are ignored when PureSar is enabled.
  • Protective levels are monitored on every closed candle. If the candle range violates the active stop or target the strategy closes the position by market order to maintain deterministic behaviour on historical data.

Session filter

  • The UseHourTrade flag restricts trading to the inclusive window between StartHour and EndHour (0–23). Session bounds wrap around midnight when the end hour is smaller than the start hour. Position management, including trailing stops, remains active outside the session, but no new entries are allowed.

Parameters

Parameter Description
FastMaPeriod, SlowMaPeriod Lengths of the fast and slow moving averages.
FastMaType, SlowMaType Moving average methods: Simple, Exponential, Smoothed or Linear Weighted.
FastPriceType, SlowPriceType Price sources fed into each moving average.
StopLossPoints, TakeProfitPoints Protective distances in price steps. Set to 0 to disable.
TrailingStopPoints Trailing stop distance in price steps. Set to 0 to disable trailing.
MinCrossDistancePoints Minimum separation between the averages required to validate a cross.
ReverseCondition Swap bullish and bearish rules without changing indicators.
ConfirmedOnEntry Validate signals on the previous closed bar. Disable for immediate confirmation.
OneEntryPerBar Allow at most one new position per candle.
StopAndReverse Close and reverse the current position when the opposite signal appears.
PureSar Disable stop-loss, take-profit and trailing stop logic.
UseHourTrade, StartHour, EndHour Session filter that restricts entries to a specific hour range.
TradeVolume Order volume used by BuyMarket and SellMarket.
CandleType Candle series subscribed for indicator calculations.

Conversion Notes

  • Price-based distances are expressed in MetaTrader points. The helper GetPriceOffset converts those values into StockSharp prices using the security price step or decimal precision. This keeps the strategy behaviour aligned with the original EA regardless of instrument.
  • Trailing stops are managed internally because StockSharp high-level strategies operate on finished candles. This deterministic approach ensures that backtests using candles reproduce the intended MT4 trailing logic.
  • No Python port is included, matching the conversion request. Only the C# implementation and multilingual documentation are provided in this package.
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>
/// Port of the "Universal MACross EA v4" MetaTrader expert advisor.
/// The strategy trades the crossover between configurable fast and slow moving averages
/// with optional session filters, stop-and-reverse behaviour and trailing stop management.
/// </summary>
public class UniversalMaCrossV4Strategy : Strategy
{
	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<MovingAverageMethods> _fastMaType;
	private readonly StrategyParam<MovingAverageMethods> _slowMaType;
	private readonly StrategyParam<AppliedPrices> _fastPriceType;
	private readonly StrategyParam<AppliedPrices> _slowPriceType;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _minCrossDistancePoints;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<bool> _confirmedOnEntry;
	private readonly StrategyParam<bool> _oneEntryPerBar;
	private readonly StrategyParam<bool> _stopAndReverse;
	private readonly StrategyParam<bool> _pureSar;
	private readonly StrategyParam<bool> _useHourTrade;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _volume;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _fastMa;
	private IIndicator _slowMa;

	private decimal? _fastPrev;
	private decimal? _fastPrevPrev;
	private decimal? _slowPrev;
	private decimal? _slowPrevPrev;

	private DateTimeOffset? _lastEntryBar;
	private TradeDirections _lastTrade = TradeDirections.None;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Method applied to the fast moving average.
	/// </summary>
	public MovingAverageMethods FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	/// <summary>
	/// Method applied to the slow moving average.
	/// </summary>
	public MovingAverageMethods SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	/// <summary>
	/// Price source for the fast moving average.
	/// </summary>
	public AppliedPrices FastPriceType
	{
		get => _fastPriceType.Value;
		set => _fastPriceType.Value = value;
	}

	/// <summary>
	/// Price source for the slow moving average.
	/// </summary>
	public AppliedPrices SlowPriceType
	{
		get => _slowPriceType.Value;
		set => _slowPriceType.Value = value;
	}

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

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

	/// <summary>
	/// Trailing stop distance expressed in points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum distance between moving averages to validate a crossover.
	/// </summary>
	public decimal MinCrossDistancePoints
	{
		get => _minCrossDistancePoints.Value;
		set => _minCrossDistancePoints.Value = value;
	}

	/// <summary>
	/// Swap bullish and bearish signals when set to <c>true</c>.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// Require the crossover to be confirmed on the previous closed bar.
	/// </summary>
	public bool ConfirmedOnEntry
	{
		get => _confirmedOnEntry.Value;
		set => _confirmedOnEntry.Value = value;
	}

	/// <summary>
	/// Allow only one new position per candle.
	/// </summary>
	public bool OneEntryPerBar
	{
		get => _oneEntryPerBar.Value;
		set => _oneEntryPerBar.Value = value;
	}

	/// <summary>
	/// Close and reverse the active position when the opposite signal appears.
	/// </summary>
	public bool StopAndReverse
	{
		get => _stopAndReverse.Value;
		set => _stopAndReverse.Value = value;
	}

	/// <summary>
	/// Disable stop-loss, take-profit and trailing stop logic.
	/// </summary>
	public bool PureSar
	{
		get => _pureSar.Value;
		set => _pureSar.Value = value;
	}

	/// <summary>
	/// Enable the hour-based trading session filter.
	/// </summary>
	public bool UseHourTrade
	{
		get => _useHourTrade.Value;
		set => _useHourTrade.Value = value;
	}

	/// <summary>
	/// Start hour of the trading window (0-23).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// End hour of the trading window (0-23).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Order volume applied to each market order.
	/// </summary>
	public decimal TradeVolume
	{
		get => _volume.Value;
		set => _volume.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="UniversalMaCrossV4Strategy"/> class.
	/// </summary>
	public UniversalMaCrossV4Strategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Length of the fast moving average", "Indicators")
			
			.SetOptimize(5, 40, 1);

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Length of the slow moving average", "Indicators")
			
			.SetOptimize(30, 200, 5);

		_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Fast MA Method", "Smoothing method applied to the fast moving average", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Slow MA Method", "Smoothing method applied to the slow moving average", "Indicators");

		_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
			.SetDisplay("Fast MA Price", "Price source injected into the fast moving average", "Indicators");

		_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
			.SetDisplay("Slow MA Price", "Price source injected into the slow moving average", "Indicators");

		_stopLossPoints = Param(nameof(StopLossPoints), 100m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Stop-loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Take-profit distance in price steps", "Risk");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 40m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps", "Risk");

		_minCrossDistancePoints = Param(nameof(MinCrossDistancePoints), 0m)
			.SetNotNegative()
			.SetDisplay("Min Cross Distance (points)", "Minimum separation between the moving averages", "Filters");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signals", "Swap bullish and bearish conditions", "General");

		_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
			.SetDisplay("Confirmed On Entry", "Validate signals on the previous closed bar", "General");

		_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
			.SetDisplay("One Entry Per Bar", "Allow at most one entry per candle", "General");

		_stopAndReverse = Param(nameof(StopAndReverse), true)
			.SetDisplay("Stop And Reverse", "Close and reverse when the opposite signal appears", "Risk");

		_pureSar = Param(nameof(PureSar), false)
			.SetDisplay("Pure SAR", "Disable protective stops and trailing", "Risk");

		_useHourTrade = Param(nameof(UseHourTrade), false)
			.SetDisplay("Use Hour Filter", "Restrict trading to a specific session", "Session");

		_startHour = Param(nameof(StartHour), 10)
			.SetDisplay("Start Hour", "Trading window start hour", "Session");

		_endHour = Param(nameof(EndHour), 11)
			.SetDisplay("End Hour", "Trading window end hour", "Session");

		_volume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume for each market entry", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle subscription used by the strategy", "General");
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_fastPrev = null;
		_fastPrevPrev = null;
		_slowPrev = null;
		_slowPrevPrev = null;
		_lastEntryBar = null;
		_lastTrade = TradeDirections.None;
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;

		Volume = TradeVolume;
	}

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

		_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
		_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

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

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		ManageExistingPosition(candle);

		if (_fastMa is null || _slowMa is null)
			return;

		var fastPrice = GetPrice(candle, FastPriceType);
		var slowPrice = GetPrice(candle, SlowPriceType);

		var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
		if (fastResult.IsEmpty) return;
		var fastValue = fastResult.GetValue<decimal>();
		var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });
		if (slowResult.IsEmpty) return;
		var slowValue = slowResult.GetValue<decimal>();

		var prevFast = _fastPrev;
		var prevSlow = _slowPrev;
		var prevFastPrev = _fastPrevPrev;
		var prevSlowPrev = _slowPrevPrev;

		_fastPrevPrev = prevFast;
		_slowPrevPrev = prevSlow;
		_fastPrev = fastValue;
		_slowPrev = slowValue;

		

		var minDistance = GetPriceOffset(MinCrossDistancePoints);

		var crossUp = false;
		var crossDown = false;

		if (ConfirmedOnEntry)
		{
			// Confirm signals using the previous completed bar (shift 2 -> 1 in MQL terms).
			if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
			{
				var diff = prevFast.Value - prevSlow.Value;
				crossUp = prevFastPrev.Value < prevSlowPrev.Value && prevFast.Value > prevSlow.Value && diff >= minDistance;
				crossDown = prevFastPrev.Value > prevSlowPrev.Value && prevFast.Value < prevSlow.Value && -diff >= minDistance;
			}
		}
		else
		{
			// Validate crossovers on the current finished bar.
			if (prevFast.HasValue && prevSlow.HasValue)
			{
				var diff = fastValue - slowValue;
				crossUp = prevFast.Value < prevSlow.Value && fastValue > slowValue && diff >= minDistance;
				crossDown = prevFast.Value > prevSlow.Value && fastValue < slowValue && -diff >= minDistance;
			}
		}

		bool buySignal;
		bool sellSignal;

		if (!ReverseCondition)
		{
			buySignal = crossUp;
			sellSignal = crossDown;
		}
		else
		{
			buySignal = crossDown;
			sellSignal = crossUp;
		}

		if (!IsWithinTradingHours(candle))
			return;

		if (StopAndReverse && Position != 0)
		{
			var reverseToShort = _lastTrade == TradeDirections.Long && sellSignal;
			var reverseToLong = _lastTrade == TradeDirections.Short && buySignal;

			if (reverseToLong || reverseToShort)
			{
				ClosePosition();
				ResetProtection();
				_lastTrade = TradeDirections.None;
			}
		}

		if (Position != 0)
			return;

		if (OneEntryPerBar && _lastEntryBar == candle.OpenTime)
			return;

		if (buySignal)
		{
			BuyMarket(TradeVolume);
			SetProtectionLevels(candle.ClosePrice, true);
			_lastTrade = TradeDirections.Long;
			_lastEntryBar = candle.OpenTime;
		}
		else if (sellSignal)
		{
			SellMarket(TradeVolume);
			SetProtectionLevels(candle.ClosePrice, false);
			_lastTrade = TradeDirections.Short;
			_lastEntryBar = candle.OpenTime;
		}
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetProtection();
			return;
		}

		UpdateTrailingStop(candle);

		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				ClosePosition();
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				ClosePosition();
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				ClosePosition();
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				ClosePosition();
				ResetProtection();
			}
		}
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (PureSar || TrailingStopPoints <= 0m || !_entryPrice.HasValue)
			return;

		var trailingDistance = GetPriceOffset(TrailingStopPoints);
		if (trailingDistance <= 0m)
			return;

		if (Position > 0)
		{
			var move = candle.ClosePrice - _entryPrice.Value;
			if (move > trailingDistance)
			{
				var candidate = candle.ClosePrice - trailingDistance;
				if (!_stopPrice.HasValue || candidate > _stopPrice.Value)
				{
					_stopPrice = candidate;
				}
			}
		}
		else if (Position < 0)
		{
			var move = _entryPrice.Value - candle.ClosePrice;
			if (move > trailingDistance)
			{
				var candidate = candle.ClosePrice + trailingDistance;
				if (!_stopPrice.HasValue || candidate < _stopPrice.Value)
				{
					_stopPrice = candidate;
				}
			}
		}
	}

	private bool IsWithinTradingHours(ICandleMessage candle)
	{
		if (!UseHourTrade)
			return true;

		var hour = candle.OpenTime.Hour;
		var start = StartHour;
		var end = EndHour;

		if (start <= end)
			return hour >= start && hour <= end;

		return hour >= start || hour <= end;
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
	{
		return method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MovingAverageMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new SimpleMovingAverage { Length = period }
		};
	}

	private static decimal GetPrice(ICandleMessage candle, AppliedPrices priceType)
	{
		return priceType switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private void SetProtectionLevels(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;

		if (PureSar)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		var stopDistance = GetPriceOffset(StopLossPoints);
		var takeDistance = GetPriceOffset(TakeProfitPoints);

		_stopPrice = stopDistance > 0m ? (isLong ? entryPrice - stopDistance : entryPrice + stopDistance) : null;
		_takeProfitPrice = takeDistance > 0m ? (isLong ? entryPrice + takeDistance : entryPrice - takeDistance) : null;
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private decimal GetPriceOffset(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			return points * step;

		var decimals = Security?.Decimals;
		if (decimals.HasValue && decimals.Value > 0)
		{
			decimal scale = 1m;
			for (var i = 0; i < decimals.Value; i++)
				scale /= 10m;

			return points * scale;
		}

		return points;
	}

	private void ClosePosition()
	{
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();
	}

	private enum TradeDirections
	{
		None,
		Long,
		Short
	}
}