Ver en GitHub

Adjustable Moving Average Strategy

This strategy recreates the MetaTrader "Adjustable Moving Average" expert advisor using StockSharp's high-level API. Two moving averages of the same type but different lengths monitor their distance. When the faster curve crosses the slower one by at least a configurable gap the strategy closes any opposite position and optionally opens a trade in the new direction. Additional session filters, protective exits and an optional trailing stop provide the same operational flexibility as the original robot.

Trading logic

  • Two moving averages (fast and slow) share the same calculation method. The faster period is automatically set to the smaller input, the slower period to the larger input.
  • A signal is produced only after both moving averages are fully formed and their absolute distance exceeds the MinGapPoints threshold converted into price units.
  • When the fast MA is above the slow MA by the required gap the internal signal state becomes bullish. A bearish state is recorded when the slow MA is above the fast MA.
  • A state flip closes any existing position if CloseOutsideSession is enabled or the current time is within the session window. New orders follow the selected Mode (buy only, sell only, or both) and use either a fixed lot or the auto-lot sizing rule.
  • Protective logic is checked on every finished candle:
    • Stop loss and take profit distances are measured in instrument points and evaluated against the candle range.
    • The trailing stop activates once price moves in favour of the position by at least TrailStopPoints points. The stop is tightened only when the session filter allows trailing or TrailOutsideSession is enabled. Once the stop is in place it remains active even outside trading hours.

Position sizing

  • With EnableAutoLot = false the strategy sends the FixedLot volume (after applying instrument step, minimum and maximum limits).
  • With EnableAutoLot = true the volume is approximated from the available portfolio value: (PortfolioValue / 10,000) * LotPer10kFreeMargin, rounded to one decimal lot. The computed volume is also aligned to the exchange constraints.

Parameters

Name Type / Default Description
CandleType TimeFrame = 5-minute candles Timeframe used for moving-average calculations.
FastPeriod int = 3 Short moving-average length. Must differ from SlowPeriod.
SlowPeriod int = 9 Long moving-average length. Must differ from FastPeriod.
MaMethod MovingAverageMethod = Exponential Moving-average algorithm (Simple, Exponential, Smoothed, Weighted).
MinGapPoints decimal = 3 Minimum distance between the fast and slow averages in instrument points. Converted using the instrument price step.
StopLossPoints decimal = 0 Protective stop distance in instrument points. Set to zero to disable.
TakeProfitPoints decimal = 0 Profit target distance in instrument points. Set to zero to disable.
TrailStopPoints decimal = 0 Trailing stop distance in instrument points. Set to zero to disable.
Mode EntryMode = Both Allowed direction for new trades (Both, BuyOnly, SellOnly).
SessionStart TimeSpan = 00:00 Session start time (platform clock).
SessionEnd TimeSpan = 23:59 Session end time (platform clock). Supports overnight sessions when SessionEnd < SessionStart.
CloseOutsideSession bool = true If true, opposite positions are closed even outside the session window.
TrailOutsideSession bool = true If true, the trailing stop keeps updating after the session closes.
FixedLot decimal = 0.1 Volume used when automatic sizing is disabled.
EnableAutoLot bool = false Enable volume estimation from portfolio value.
LotPer10kFreeMargin decimal = 1 Lots allocated per 10,000 units of portfolio value in auto-lot mode.
MaxSlippage int = 3 Retained for completeness; StockSharp market orders do not expose a direct slippage parameter.
TradeComment string = "AdjustableMovingAverageEA" Text included in log messages when trades are executed.

Notes

  • The original MetaTrader version applied stop loss, take profit and trailing stops via order modifications. The StockSharp port emulates the behaviour by evaluating candle ranges and sending opposing market orders.
  • Portfolio value is used as an approximation of free margin because MetaTrader's AccountFreeMargin() is not available in StockSharp.
  • When the instrument lacks a valid PriceStep, point-based calculations (gap, stops, trailing) remain inactive.
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>
/// Moving average crossover strategy with adjustable gap, session control, and optional trailing stop.
/// </summary>
public class AdjustableMovingAverageStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<MovingAverageMethods> _maMethod;
	private readonly StrategyParam<decimal> _minGapPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingPoints;
	private readonly StrategyParam<EntryModes> _entryMode;
	private readonly StrategyParam<TimeSpan> _sessionStart;
	private readonly StrategyParam<TimeSpan> _sessionEnd;
	private readonly StrategyParam<bool> _closeOutsideSession;
	private readonly StrategyParam<bool> _trailOutsideSession;
	private readonly StrategyParam<decimal> _fixedLot;
	private readonly StrategyParam<bool> _enableAutoLot;
	private readonly StrategyParam<decimal> _lotPer10k;
	private readonly StrategyParam<int> _maxSlippage;
	private readonly StrategyParam<string> _tradeComment;

	private DecimalLengthIndicator _fastMa;
	private DecimalLengthIndicator _slowMa;
	private decimal _pointValue;
	private decimal _minGapThreshold;
	private int _previousSignal;
	private bool _hasInitialSignal;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public AdjustableMovingAverageStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle timeframe", "Timeframe used to build moving averages", "General")
			;

		_fastPeriod = Param(nameof(FastPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast period", "Short moving average length", "Moving averages")
			
			.SetOptimize(2, 30, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Slow period", "Long moving average length", "Moving averages")
			
			.SetOptimize(3, 60, 1);

		_maMethod = Param(nameof(MaMethod), MovingAverageMethods.Exponential)
			.SetDisplay("MA method", "Moving average calculation method", "Moving averages")
			;

		_minGapPoints = Param(nameof(MinGapPoints), 3m)
			.SetNotNegative()
			.SetDisplay("Minimum gap (points)", "Required distance between fast and slow MAs before signalling", "Trading")
			
			.SetOptimize(0m, 20m, 1m);

		_stopLossPoints = Param(nameof(StopLossPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Stop loss (points)", "Protective stop distance in price points", "Risk management");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Take profit (points)", "Profit target distance in price points", "Risk management");

		_trailingPoints = Param(nameof(TrailStopPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Trailing stop (points)", "Trailing stop distance in price points", "Risk management");

		_entryMode = Param(nameof(Mode), EntryModes.Both)
			.SetDisplay("Entry mode", "Allowed trade direction", "Trading");

		_sessionStart = Param(nameof(SessionStart), TimeSpan.Zero)
			.SetDisplay("Session start", "Trading session start time (platform time)", "Session");

		_sessionEnd = Param(nameof(SessionEnd), new TimeSpan(23, 59, 0))
			.SetDisplay("Session end", "Trading session end time (platform time)", "Session");

		_closeOutsideSession = Param(nameof(CloseOutsideSession), true)
			.SetDisplay("Close outside session", "Allow closing positions when the session filter is inactive", "Session");

		_trailOutsideSession = Param(nameof(TrailOutsideSession), true)
			.SetDisplay("Trail outside session", "Continue trailing even when trading session is closed", "Session");

		_fixedLot = Param(nameof(FixedLot), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Fixed lot", "Volume used when auto lot sizing is disabled", "Money management");

		_enableAutoLot = Param(nameof(EnableAutoLot), false)
			.SetDisplay("Enable auto lot", "Approximate AccountFreeMargin based sizing", "Money management");

		_lotPer10k = Param(nameof(LotPer10kFreeMargin), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Lots per 10k", "Lots per 10,000 of account value when auto lot is enabled", "Money management");

		_maxSlippage = Param(nameof(MaxSlippage), 3)
			.SetNotNegative()
			.SetDisplay("Max slippage", "Placeholder parameter retained from the MQL version", "Trading");

		_tradeComment = Param(nameof(TradeComment), "AdjustableMovingAverageEA")
			.SetDisplay("Trade comment", "Tag applied to diagnostic messages", "General");
	}

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

	/// <summary>
	/// Fast moving average length.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average length.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MovingAverageMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Minimum distance between fast and slow moving averages in instrument points.
	/// </summary>
	public decimal MinGapPoints
	{
		get => _minGapPoints.Value;
		set => _minGapPoints.Value = value;
	}

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

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

	/// <summary>
	/// Trailing stop distance in instrument points.
	/// </summary>
	public decimal TrailStopPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Allowed trade direction.
	/// </summary>
	public EntryModes Mode
	{
		get => _entryMode.Value;
		set => _entryMode.Value = value;
	}

	/// <summary>
	/// Session start time in platform time zone.
	/// </summary>
	public TimeSpan SessionStart
	{
		get => _sessionStart.Value;
		set => _sessionStart.Value = value;
	}

	/// <summary>
	/// Session end time in platform time zone.
	/// </summary>
	public TimeSpan SessionEnd
	{
		get => _sessionEnd.Value;
		set => _sessionEnd.Value = value;
	}

	/// <summary>
	/// Close positions even when the session filter is inactive.
	/// </summary>
	public bool CloseOutsideSession
	{
		get => _closeOutsideSession.Value;
		set => _closeOutsideSession.Value = value;
	}

	/// <summary>
	/// Continue updating the trailing stop outside the session window.
	/// </summary>
	public bool TrailOutsideSession
	{
		get => _trailOutsideSession.Value;
		set => _trailOutsideSession.Value = value;
	}

	/// <summary>
	/// Fixed order volume used when auto lot sizing is disabled.
	/// </summary>
	public decimal FixedLot
	{
		get => _fixedLot.Value;
		set => _fixedLot.Value = value;
	}

	/// <summary>
	/// Toggle automatic lot sizing based on approximate free margin.
	/// </summary>
	public bool EnableAutoLot
	{
		get => _enableAutoLot.Value;
		set => _enableAutoLot.Value = value;
	}

	/// <summary>
	/// Lots allocated per 10,000 units of portfolio value.
	/// </summary>
	public decimal LotPer10kFreeMargin
	{
		get => _lotPer10k.Value;
		set => _lotPer10k.Value = value;
	}

	/// <summary>
	/// Placeholder for the original slippage tolerance.
	/// </summary>
	public int MaxSlippage
	{
		get => _maxSlippage.Value;
		set => _maxSlippage.Value = value;
	}

	/// <summary>
	/// Comment attached to log messages when orders are placed.
	/// </summary>
	public string TradeComment
	{
		get => _tradeComment.Value;
		set => _tradeComment.Value = value;
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_pointValue = 0m;
		_minGapThreshold = 0m;
		_previousSignal = 0;
		_hasInitialSignal = false;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

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

		var fastLength = Math.Min(FastPeriod, SlowPeriod);
		var slowLength = Math.Max(FastPeriod, SlowPeriod);

		if (fastLength == slowLength)
		{
			LogWarning("Fast and slow periods must differ.");
			Stop();
			return;
		}

		_fastMa = CreateMovingAverage(MaMethod, fastLength);
		_slowMa = CreateMovingAverage(MaMethod, slowLength);

		_pointValue = CalculatePointValue();
		_minGapThreshold = MinGapPoints * _pointValue;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastMa, _slowMa, ProcessCandle)
			.Start();
	}

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

		var inSession = InSession(candle.OpenTime);
		var allowTrading = inSession && true;

		UpdateTrailing(candle, inSession || TrailOutsideSession);
		HandleProtectiveExits(candle);

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

		if (!_fastMa.IsFormed || !_slowMa.IsFormed)
			return;

		var gapUp = fast - slow;
		var gapDown = slow - fast;

		if (!_hasInitialSignal)
		{
			if (gapUp >= _minGapThreshold)
			{
				_previousSignal = 1;
				_hasInitialSignal = true;
			}
			else if (gapDown >= _minGapThreshold)
			{
				_previousSignal = -1;
				_hasInitialSignal = true;
			}
			return;
		}

		if (_previousSignal > 0)
		{
			if (gapDown >= _minGapThreshold)
			{
				if (CloseOutsideSession || inSession)
					CloseCurrentPosition();

				if (allowTrading && Mode != EntryModes.BuyOnly)
				{
					OpenShort(candle.ClosePrice);
				}

				_previousSignal = -1;
				ResetTrailing();
			}
		}
		else if (_previousSignal < 0)
		{
			if (gapUp >= _minGapThreshold)
			{
				if (CloseOutsideSession || inSession)
					CloseCurrentPosition();

				if (allowTrading && Mode != EntryModes.SellOnly)
				{
					OpenLong(candle.ClosePrice);
				}

				_previousSignal = 1;
				ResetTrailing();
			}
		}
	}

	private void OpenLong(decimal price)
	{
		var volume = CalculateOrderVolume(price);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		LogInfo($"{TradeComment}: opened long, volume={volume:0.###}");
	}

	private void OpenShort(decimal price)
	{
		var volume = CalculateOrderVolume(price);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		LogInfo($"{TradeComment}: opened short, volume={volume:0.###}");
	}

	private void CloseCurrentPosition()
	{
		if (Position > 0m)
		{
			SellMarket(Position);
			LogInfo($"{TradeComment}: closed existing long");
		}
		else if (Position < 0m)
		{
			BuyMarket(-Position);
			LogInfo($"{TradeComment}: closed existing short");
		}
	}

	private void UpdateTrailing(ICandleMessage candle, bool allowUpdate)
	{
		if (TrailStopPoints <= 0m || _pointValue <= 0m)
			return;

		var distance = TrailStopPoints * _pointValue;

		if (Position > 0m)
		{
			if (allowUpdate)
			{
				var move = candle.ClosePrice - 0m;
				if (move >= distance)
				{
					var newStop = candle.ClosePrice - distance;
					if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value)
						_longTrailingStop = newStop;
				}
			}

			if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
			{
				SellMarket(Position);
				LogInfo($"{TradeComment}: trailing stop hit (long)");
				ResetTrailing();
			}
		}
		else if (Position < 0m)
		{
			var absPosition = -Position;

			if (allowUpdate)
			{
				var move = 0m - candle.ClosePrice;
				if (move >= distance)
				{
					var newStop = candle.ClosePrice + distance;
					if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value)
						_shortTrailingStop = newStop;
				}
			}

			if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
			{
				BuyMarket(absPosition);
				LogInfo($"{TradeComment}: trailing stop hit (short)");
				ResetTrailing();
			}
		}
		else
		{
			ResetTrailing();
		}
	}

	private void HandleProtectiveExits(ICandleMessage candle)
	{
		if (_pointValue <= 0m)
			return;

		if (Position > 0m)
		{
			var stop = StopLossPoints > 0m ? 0m - StopLossPoints * _pointValue : (decimal?)null;
			var target = TakeProfitPoints > 0m ? 0m + TakeProfitPoints * _pointValue : (decimal?)null;

			if (stop.HasValue && candle.LowPrice <= stop.Value)
			{
				SellMarket(Position);
				LogInfo($"{TradeComment}: stop-loss hit (long)");
				ResetTrailing();
				return;
			}

			if (target.HasValue && candle.HighPrice >= target.Value)
			{
				SellMarket(Position);
				LogInfo($"{TradeComment}: take-profit hit (long)");
				ResetTrailing();
			}
		}
		else if (Position < 0m)
		{
			var absPosition = -Position;
			var stop = StopLossPoints > 0m ? 0m + StopLossPoints * _pointValue : (decimal?)null;
			var target = TakeProfitPoints > 0m ? 0m - TakeProfitPoints * _pointValue : (decimal?)null;

			if (stop.HasValue && candle.HighPrice >= stop.Value)
			{
				BuyMarket(absPosition);
				LogInfo($"{TradeComment}: stop-loss hit (short)");
				ResetTrailing();
				return;
			}

			if (target.HasValue && candle.LowPrice <= target.Value)
			{
				BuyMarket(absPosition);
				LogInfo($"{TradeComment}: take-profit hit (short)");
				ResetTrailing();
			}
		}
		else
		{
			ResetTrailing();
		}
	}

	private decimal CalculateOrderVolume(decimal price)
	{
		var desired = FixedLot;

		if (EnableAutoLot)
		{
			var equity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue;
			if (equity is decimal value && value > 0m && price > 0m)
			{
				var lots = Math.Round((value / 10000m) * LotPer10kFreeMargin, 1, MidpointRounding.AwayFromZero);
				if (lots > 0m)
					desired = lots;
			}
		}

		var adjusted = AdjustVolume(desired);
		return adjusted > 0m ? adjusted : 0m;
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security == null)
			return volume;

		var step = security.VolumeStep ?? 1m;
		if (step > 0m)
		{
			var steps = Math.Max(1m, Math.Round(volume / step, 0, MidpointRounding.AwayFromZero));
			volume = steps * step;
		}

		var minVolume = security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = security.MaxVolume ?? decimal.MaxValue;
		if (volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private bool InSession(DateTimeOffset time)
	{
		var start = SessionStart;
		var end = SessionEnd;
		var current = time.TimeOfDay;

		if (end < start)
		{
			return current >= start || current <= end;
		}

		return current >= start && current <= end;
	}

	private decimal CalculatePointValue()
	{
		var step = Security?.PriceStep ?? Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;

		var point = step;
		if (point == 0.00001m || point == 0.001m)
			point *= 10m;

		return point;
	}

	private DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
	{
		DecimalLengthIndicator indicator = method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = length },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = length },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = length },
			_ => new ExponentialMovingAverage { Length = length }
		};

		return indicator;
	}

	private void ResetTrailing()
	{
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Weighted
	}

	/// <summary>
	/// Directional filter for new positions.
	/// </summary>
	public enum EntryModes
	{
		/// <summary>
		/// Allow both long and short entries.
		/// </summary>
		Both,

		/// <summary>
		/// Allow only long entries.
		/// </summary>
		BuyOnly,

		/// <summary>
		/// Allow only short entries.
		/// </summary>
		SellOnly
	}
}