Ver en GitHub

VarMovAvg Strategy

Overview

The VarMovAvg strategy is a stop-and-reverse system converted from the MetaTrader 4 expert advisor VarMovAvg_v0011. It uses an adaptive Variable Moving Average (VMA) to measure trend direction and waits for a two-step pullback pattern (called Bar A and Bar B in the original EA) before reversing the position. While a position is active, a moving-average-based trailing stop protects profits and flips the trade when the opposite Bar A/Bar B sequence completes.

Trading Logic

  1. Adaptive VMA – The custom VariableMovingAverage indicator replicates the MT4 formula:
    • Efficiency ratio compares the current close with the close AmaPeriod bars ago and divides it by the accumulated absolute price movement.
    • The smoothing coefficient interpolates between the fast and slow periods and is raised to the SmoothingPower parameter just like the original G value.
  2. Signal Detection (Bar A / Bar B) – Two independent state machines track long and short setups:
    • Bar A: Price moves SignalPipsBarA (in pips) beyond the VMA in the potential trade direction.
    • Bar B: Price extends another SignalPipsBarB pips in the same direction, locking the extreme price.
    • Entry: When the close returns to the entry band defined by SignalPipsTrade ± EntryPipsDiff, the strategy enters (or reverses) using market orders.
  3. Trailing Stop and Reversal – While a position is open, a moving average computed on highs (for shorts) or lows (for longs) is shifted by StopMaShift bars and padded by StopPipsDiff.
    • If the candle pierces the stop level, the position is closed.
    • If the opposite Bar A/Bar B sequence triggers while a position exists, the strategy issues a single market order sized as |Position| + Volume to flip direction immediately, matching the EA behaviour.

Parameters

Parameter Description MT4 Source
AmaPeriod Lookback window used by the VMA. prm.vma.periodAMA
FastPeriod Fast smoothing factor inside the VMA. prm.vma.nfast
SlowPeriod Slow smoothing factor inside the VMA. prm.vma.nslow
SmoothingPower Exponent G applied to the adaptive coefficient. prm.vma.G
SignalPipsBarA Distance from the VMA required to accept Bar A. prm.sig.pipsBarA
SignalPipsBarB Additional distance required to accept Bar B. prm.sig.pipsBarB
SignalPipsTrade Offset from the Bar B extreme to the entry line. prm.sig.pipsTrade
EntryPipsDiff Accepted tolerance around the entry line. prm.entry.diff
StopPipsDiff Offset applied to the trailing stop moving average. prm.stop.diff
StopMaPeriod Period of the stop moving average. prm.mastop.period
StopMaShift Shift (bars) of the stop moving average. prm.mastop.shift
StopMaMethod Moving average method (MODE_SMA, EMA, SMMA, LWMA). prm.mastop.method
CandleType Working timeframe. Chart timeframe

Pip conversion – All pip distances are multiplied by Security.PriceStep when it is available. If the instrument does not have a configured step, the raw values are interpreted in price units, replicating the EA fallback.

Usage Notes

  • The strategy relies on SubscribeCandles and runs entirely on finished candles; the entry band logic mirrors the EA’s tick-by-tick checks using close prices.
  • Protective orders are modelled through market exits when the candle crosses the stop level, which matches the EA behaviour because stop orders were recalculated every tick.
  • The moving-average shift is implemented via a FIFO buffer, ensuring StopMaShift = 0 uses the latest value and positive shifts look back the requested number of bars.
  • After every trade (entry, reversal, or stop hit) both signal trackers reset to the neutral state to avoid duplicate orders, emulating the STATUS_TRADE reset logic in MetaTrader.

Quick Start

  1. Add the strategy to a StockSharp environment and assign an instrument with a valid PriceStep and tick size.
  2. Configure the timeframe through CandleType (the original expert was tested on intraday charts such as M5).
  3. Adjust the pip distances and trailing parameters to match the broker’s quote precision.
  4. Start the strategy; it will alternate between long and short positions whenever the Bar A/Bar B conditions are met.

Differences from the Original EA

  • The StockSharp version works on closed candles instead of tick-by-tick execution. The entry tolerance band keeps the trigger timing close to the MT4 behaviour.
  • Stop-loss handling is implemented by checking candle extremes rather than placing/modifying MT4 orders, because StockSharp strategies typically manage exits programmatically.
  • The VariableMovingAverage indicator is implemented directly in C# and exposes the smoothing power, eliminating the unused dK parameter that existed in the MQL source.
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>
/// Variable Moving Average (VarMovAvg) reversal strategy converted from the MetaTrader expert.
/// Tracks adaptive VMA swings and enters on the Bar A/Bar B breakout pattern.
/// </summary>
public class VarMovAvgStrategy : Strategy
{
	/// <summary>
	/// MetaTrader moving average methods supported by the stop calculation.
	/// </summary>
	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted
	}

	private readonly StrategyParam<int> _amaPeriod;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _smoothingPower;
	private readonly StrategyParam<decimal> _signalPipsBarA;
	private readonly StrategyParam<decimal> _signalPipsBarB;
	private readonly StrategyParam<decimal> _signalPipsTrade;
	private readonly StrategyParam<decimal> _entryPipsDiff;
	private readonly StrategyParam<decimal> _stopPipsDiff;
	private readonly StrategyParam<int> _stopMaPeriod;
	private readonly StrategyParam<int> _stopMaShift;
	private readonly StrategyParam<MovingAverageMethods> _stopMaMethod;
	private readonly StrategyParam<DataType> _candleType;

	private VariableMovingAverage _vma;
	private IIndicator _stopLowMa;
	private IIndicator _stopHighMa;
	private Queue<decimal> _lowMaValues;
	private Queue<decimal> _highMaValues;
	private SignalTracker _longSignal;
	private SignalTracker _shortSignal;

	/// <summary>
	/// VMA adaptive window length.
	/// </summary>
	public int AmaPeriod
	{
		get => _amaPeriod.Value;
		set => _amaPeriod.Value = value;
	}

	/// <summary>
	/// Fast smoothing period used inside the VMA efficiency ratio.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow smoothing period used inside the VMA efficiency ratio.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Power applied to the smoothing coefficient (MetaTrader parameter G).
	/// </summary>
	public decimal SmoothingPower
	{
		get => _smoothingPower.Value;
		set => _smoothingPower.Value = value;
	}

	/// <summary>
	/// Distance in pips required for Bar A confirmation.
	/// </summary>
	public decimal SignalPipsBarA
	{
		get => _signalPipsBarA.Value;
		set => _signalPipsBarA.Value = value;
	}

	/// <summary>
	/// Additional distance in pips required for Bar B confirmation.
	/// </summary>
	public decimal SignalPipsBarB
	{
		get => _signalPipsBarB.Value;
		set => _signalPipsBarB.Value = value;
	}

	/// <summary>
	/// Offset in pips between the Bar B extreme and the actual entry line.
	/// </summary>
	public decimal SignalPipsTrade
	{
		get => _signalPipsTrade.Value;
		set => _signalPipsTrade.Value = value;
	}

	/// <summary>
	/// Width in pips accepted when price touches the entry line.
	/// </summary>
	public decimal EntryPipsDiff
	{
		get => _entryPipsDiff.Value;
		set => _entryPipsDiff.Value = value;
	}

	/// <summary>
	/// Offset in pips applied to the trailing stop moving average.
	/// </summary>
	public decimal StopPipsDiff
	{
		get => _stopPipsDiff.Value;
		set => _stopPipsDiff.Value = value;
	}

	/// <summary>
	/// Period of the trailing stop moving average.
	/// </summary>
	public int StopMaPeriod
	{
		get => _stopMaPeriod.Value;
		set => _stopMaPeriod.Value = value;
	}

	/// <summary>
	/// Shift (in bars) applied to the trailing stop moving average.
	/// </summary>
	public int StopMaShift
	{
		get => _stopMaShift.Value;
		set => _stopMaShift.Value = value;
	}

	/// <summary>
	/// Moving average method used for trailing stop calculation.
	/// </summary>
	public MovingAverageMethods StopMaMethod
	{
		get => _stopMaMethod.Value;
		set => _stopMaMethod.Value = value;
	}

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

	/// <summary>
	/// Initializes the VarMovAvg strategy.
	/// </summary>
	public VarMovAvgStrategy()
	{
		_amaPeriod = Param(nameof(AmaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("VMA Length", "Adaptive moving average period", "Indicators")
			
			.SetOptimize(20, 120, 10);

		_fastPeriod = Param(nameof(FastPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast smoothing period for VMA", "Indicators")
			
			.SetOptimize(2, 15, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow smoothing period for VMA", "Indicators")
			
			.SetOptimize(15, 60, 5);

		_smoothingPower = Param(nameof(SmoothingPower), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Smoothing Power", "Exponent applied to the smoothing coefficient", "Indicators");

		_signalPipsBarA = Param(nameof(SignalPipsBarA), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Bar A Distance", "Pips distance below/above VMA for Bar A", "Signals");

		_signalPipsBarB = Param(nameof(SignalPipsBarB), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Bar B Distance", "Extra pips distance for Bar B confirmation", "Signals");

		_signalPipsTrade = Param(nameof(SignalPipsTrade), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Entry Offset", "Pips offset from Bar B extreme to entry", "Signals");

		_entryPipsDiff = Param(nameof(EntryPipsDiff), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Entry Band", "Accepted pips range around the entry price", "Signals");

		_stopPipsDiff = Param(nameof(StopPipsDiff), 34m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Offset", "Pips offset from the trailing moving average", "Risk");

		_stopMaPeriod = Param(nameof(StopMaPeriod), 52)
			.SetGreaterThanZero()
			.SetDisplay("Stop MA Period", "Period of the trailing moving average", "Risk");

		_stopMaShift = Param(nameof(StopMaShift), 0)
			.SetNotNegative()
			.SetDisplay("Stop MA Shift", "Bars shift applied to the stop moving average", "Risk");

		_stopMaMethod = Param(nameof(StopMaMethod), MovingAverageMethods.Exponential)
			.SetDisplay("Stop MA Method", "Moving average type used for stops", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Working candle timeframe", "General");
	}

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

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

		_vma = null;
		_stopLowMa = null;
		_stopHighMa = null;
		_lowMaValues = null;
		_highMaValues = null;
		_longSignal = null;
		_shortSignal = null;
	}

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

		StartProtection(null, null);

		_vma = new VariableMovingAverage
		{
			Length = AmaPeriod,
			FastPeriod = FastPeriod,
			SlowPeriod = SlowPeriod,
			SmoothingPower = SmoothingPower
		};

		_stopLowMa = CreateMovingAverage(StopMaMethod, StopMaPeriod);
		_stopHighMa = CreateMovingAverage(StopMaMethod, StopMaPeriod);

		_lowMaValues = new Queue<decimal>();
		_highMaValues = new Queue<decimal>();
		_longSignal = new SignalTracker(true);
		_shortSignal = new SignalTracker(false);

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

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

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

		if (_vma is null || _stopLowMa is null || _stopHighMa is null || _lowMaValues is null || _highMaValues is null || _longSignal is null || _shortSignal is null)
			return;

		var time = candle.CloseTime;
		var vmaResult = _vma.Process(new DecimalIndicatorValue(_vma, candle.ClosePrice, time) { IsFinal = true });
		if (vmaResult.IsEmpty) return;
		var vmaValue = vmaResult.GetValue<decimal>();
		var lowMaResult = _stopLowMa.Process(new DecimalIndicatorValue(_stopLowMa, candle.LowPrice, time) { IsFinal = true });
		if (lowMaResult.IsEmpty) return;
		var lowMaRaw = lowMaResult.GetValue<decimal>();
		var highMaResult = _stopHighMa.Process(new DecimalIndicatorValue(_stopHighMa, candle.HighPrice, time) { IsFinal = true });
		if (highMaResult.IsEmpty) return;
		var highMaRaw = highMaResult.GetValue<decimal>();

		var lowMa = GetShiftedValue(_lowMaValues, lowMaRaw, StopMaShift);
		var highMa = GetShiftedValue(_highMaValues, highMaRaw, StopMaShift);

		var barADistance = ToPriceDistance(SignalPipsBarA);
		var barBDistance = ToPriceDistance(SignalPipsBarB);
		var tradeOffset = ToPriceDistance(SignalPipsTrade);
		var entryBand = ToPriceDistance(EntryPipsDiff);
		var stopOffset = ToPriceDistance(StopPipsDiff);

		_longSignal.Update(candle, vmaValue, barADistance, barBDistance, tradeOffset);
		_shortSignal.Update(candle, vmaValue, barADistance, barBDistance, tradeOffset);

		if (Position == 0)
		{
			if (Volume > 0 && _longSignal.TryEnter(candle, entryBand))
			{
				BuyMarket();
				AfterEntry();
			}
			else if (Volume > 0 && _shortSignal.TryEnter(candle, entryBand))
			{
				SellMarket();
				AfterEntry();
			}
			return;
		}

		if (Position > 0)
		{
			if (Volume > 0 && _shortSignal.TryEnter(candle, entryBand))
			{
				var volumeToSell = Position + Volume;
				if (volumeToSell > 0)
					SellMarket(volumeToSell);
				AfterEntry();
				return;
			}

			var stopPrice = lowMa - stopOffset;
			if (stopPrice > 0m && candle.LowPrice <= stopPrice)
			{
				SellMarket();
				AfterExit();
			}
		}
		else
		{
			if (Volume > 0 && _longSignal.TryEnter(candle, entryBand))
			{
				var volumeToBuy = Math.Abs(Position) + Volume;
				if (volumeToBuy > 0)
					BuyMarket(volumeToBuy);
				AfterEntry();
				return;
			}

			var stopPrice = highMa + stopOffset;
			if (stopPrice > 0m && candle.HighPrice >= stopPrice)
			{
				BuyMarket();
				AfterExit();
			}
		}
	}

	private void AfterEntry()
	{
		_longSignal.Reset();
		_shortSignal.Reset();
	}

	private void AfterExit()
	{
		_longSignal.Reset();
		_shortSignal.Reset();
	}

	private decimal ToPriceDistance(decimal pips)
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? pips * step : pips;
	}

	private static decimal GetShiftedValue(Queue<decimal> buffer, decimal value, int shift)
	{
		buffer.Enqueue(value);

		var maxCount = Math.Max(1, shift + 1);
		while (buffer.Count > maxCount)
			buffer.Dequeue();

		var index = buffer.Count - 1 - Math.Min(shift, buffer.Count - 1);
		var current = 0;
		foreach (var item in buffer)
		{
			if (current == index)
				return item;
			current++;
		}

		return value;
	}

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

	private sealed class SignalTracker
	{
		private enum SignalStates
		{
			Neutral,
			BarA,
			BarB
		}

		private readonly bool _isLong;
		private SignalStates _state = SignalStates.Neutral;
		private decimal _barAReference;
		private decimal _entryPrice;

		public SignalTracker(bool isLong)
		{
			_isLong = isLong;
		}

		public void Reset()
		{
			_state = SignalStates.Neutral;
			_barAReference = 0m;
			_entryPrice = 0m;
		}

		public void Update(ICandleMessage candle, decimal vma, decimal barAOffset, decimal barBOffset, decimal tradeOffset)
		{
			var close = candle.ClosePrice;
			var high = candle.HighPrice;
			var low = candle.LowPrice;

			if (_isLong)
			{
				if (close <= vma - barAOffset)
				{
					Reset();
					return;
				}

				switch (_state)
				{
					case SignalStates.Neutral:
						if (close >= vma + barAOffset)
						{
							_state = SignalStates.BarA;
							_barAReference = close;
						}
						break;
					case SignalStates.BarA:
						if (close <= vma - barAOffset)
						{
							Reset();
							return;
						}

						if (close >= _barAReference + barBOffset)
						{
							_state = SignalStates.BarB;
							_entryPrice = high + tradeOffset;
						}
						break;
					case SignalStates.BarB:
						if (close <= vma - barAOffset)
							Reset();
						break;
				}
			}
			else
			{
				if (close >= vma + barAOffset)
				{
					Reset();
					return;
				}

				switch (_state)
				{
					case SignalStates.Neutral:
						if (close <= vma - barAOffset)
						{
							_state = SignalStates.BarA;
							_barAReference = close;
						}
						break;
					case SignalStates.BarA:
						if (close >= vma + barAOffset)
						{
							Reset();
							return;
						}

						if (close <= _barAReference - barBOffset)
						{
							_state = SignalStates.BarB;
							_entryPrice = low - tradeOffset;
						}
						break;
					case SignalStates.BarB:
						if (close >= vma + barAOffset)
							Reset();
						break;
				}
			}
		}

		public bool TryEnter(ICandleMessage candle, decimal entryBand)
		{
			if (_state != SignalStates.BarB)
				return false;

			var close = candle.ClosePrice;

			if (_isLong)
			{
				var upper = _entryPrice + entryBand;
				if (close >= _entryPrice && close <= upper)
				{
					Reset();
					return true;
				}
			}
			else
			{
				var lower = _entryPrice - entryBand;
				if (close <= _entryPrice && close >= lower)
				{
					Reset();
					return true;
				}
			}

			return false;
		}
	}

	private sealed class VariableMovingAverage : DecimalLengthIndicator
	{
		private readonly Queue<decimal> _closes = new();
		private decimal? _previousAma;

		public int FastPeriod { get; set; } = 5;
		public int SlowPeriod { get; set; } = 20;
		public decimal SmoothingPower { get; set; } = 1m;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var value = input.GetValue<decimal>();
			_closes.Enqueue(value);

			var required = Math.Max(2, Length + 1);
			while (_closes.Count > required)
				_closes.Dequeue();

			if (_previousAma == null)
				_previousAma = value;

			var closeCount = _closes.Count;
			if (closeCount < 2)
				return new DecimalIndicatorValue(this, value, input.Time);

			var effectiveLength = Math.Min(Length, closeCount - 1);
			var closes = _closes.ToArray();
			var newestIndex = closes.Length - 1;
			var baseIndex = newestIndex - effectiveLength;
			if (baseIndex < 0)
				baseIndex = 0;

			var newest = closes[newestIndex];
			var oldest = closes[baseIndex];
			var signal = Math.Abs(newest - oldest);

			decimal noise = 0.000000001m;
			for (var i = baseIndex; i < newestIndex; i++)
				noise += Math.Abs(closes[i + 1] - closes[i]);

			var efficiency = noise != 0m ? signal / noise : 0m;
			var slowSc = 2m / (SlowPeriod + 1m);
			var fastSc = 2m / (FastPeriod + 1m);
			var smoothing = slowSc + efficiency * (fastSc - slowSc);
			var smoothingFactor = smoothing > 0m ? (decimal)Math.Pow((double)smoothing, (double)SmoothingPower) : 0m;

			var amaPrev = _previousAma ?? oldest;
			var ama = amaPrev + smoothingFactor * (value - amaPrev);
			_previousAma = ama;
			IsFormed = closeCount >= required;

			return new DecimalIndicatorValue(this, ama, input.Time);
		}

		public override void Reset()
		{
			base.Reset();
			_closes.Clear();
			_previousAma = null;
		}
	}
}