Ver no GitHub

Estratégia VarMovAvg

Visão geral

A estratégia VarMovAvg é um sistema de parada e reversão convertido do MetaTrader 4 consultor especialista VarMovAvg_v0011. Ele usa uma média móvel variável adaptativa (VMA) para medir a direção da tendência e aguarda um padrão de retrocesso de duas etapas (chamado de Barra A e Barra B no EA original) antes de reverter a posição. Enquanto uma posição está ativa, um trailing stop baseado na média móvel protege os lucros e inverte a negociação quando a sequência oposta da Barra A/Barra B é concluída.

Lógica de negociação

  1. VMA adaptativo – O indicador personalizado VariableMovingAverage replica a fórmula MT4:
    • O índice de eficiência compara o fechamento atual com o fechamento de AmaPeriod barras atrás e o divide pelo movimento absoluto acumulado do preço.
    • O coeficiente de suavização interpola entre os períodos rápido e lento e é elevado ao parâmetro SmoothingPower exatamente como o valor original G.
  2. Detecção de sinal (Barra A/Barra B) – Duas máquinas de estado independentes rastreiam configurações longas e curtas:
    • Barra A: O preço se move SignalPipsBarA (em pips) além do VMA na direção de negociação potencial.
    • Barra B: O preço estende mais SignalPipsBarB pips na mesma direção, travando o preço extremo.
    • Entrada: Quando o fechamento retorna à banda de entrada definida por SignalPipsTrade ± EntryPipsDiff, a estratégia entra (ou reverte) usando ordens de mercado.
  3. Trailing Stop e Reversão – Enquanto uma posição está aberta, uma média móvel calculada em máximas (para vendas) ou mínimas (para longas) é deslocada por StopMaShift barras e preenchida por StopPipsDiff.
    • Se a vela ultrapassar o nível de stop, a posição será fechada.
    • Se a sequência oposta da Barra A/Barra B for acionada enquanto uma posição existir, a estratégia emite uma única ordem de mercado dimensionada como |Position| + Volume para mudar de direção imediatamente, correspondendo ao comportamento EA.

Parâmetros

Parâmetro Descrição Fonte MT4
AmaPeriod Janela de lookback usada pelo VMA. prm.vma.periodAMA
FastPeriod Fator de suavização rápido dentro do VMA. prm.vma.nfast
SlowPeriod Fator de suavização lento dentro do VMA. prm.vma.nslow
SmoothingPower Expoente G aplicado ao coeficiente adaptativo. prm.vma.G
SignalPipsBarA Distância do VMA necessária para aceitar a Barra A. prm.sig.pipsBarA
SignalPipsBarB Distância adicional necessária para aceitar a Barra B. prm.sig.pipsBarB
SignalPipsTrade Deslocamento do extremo da Barra B até a linha de entrada. prm.sig.pipsTrade
EntryPipsDiff Tolerância aceita em torno da linha de entrada. prm.entry.diff
StopPipsDiff Offset aplicado à média móvel do trailing stop. prm.stop.diff
StopMaPeriod Período da média móvel stop. prm.mastop.period
StopMaShift Mudança (barras) da média móvel stop. prm.mastop.shift
StopMaMethod Método de média móvel (MODE_SMA, EMA, SMMA, LWMA). prm.mastop.method
CandleType Prazo de trabalho. Prazo do gráfico

Conversão de pip – Todas as distâncias de pip são multiplicadas por Security.PriceStep quando estão disponíveis. Caso o instrumento não possua uma etapa configurada, os valores brutos são interpretados em unidades de preço, replicando o fallback EA.

Notas de uso

  • A estratégia depende de SubscribeCandles e funciona inteiramente em velas acabadas; a lógica da banda de entrada reflete as verificações tick-by-tick do EA usando preços de fechamento.
  • As ordens de proteção são modeladas por meio de saídas de mercado quando a vela cruza o nível de stop, o que corresponde ao comportamento EA porque as ordens de stop foram recalculadas a cada tick.
  • A mudança da média móvel é implementada por meio de um buffer FIFO, garantindo que StopMaShift = 0 use o valor mais recente e que as mudanças positivas analisem o número solicitado de barras.
  • Após cada negociação (entrada, reversão ou stop hit), ambos os rastreadores de sinal são redefinidos para o estado neutro para evitar ordens duplicadas, emulando a lógica de redefinição STATUS_TRADE em MetaTrader.

Início rápido

  1. Adicione a estratégia a um ambiente StockSharp e atribua um instrumento com um PriceStep e tamanho de tick válidos.
  2. Configure o período por meio de CandleType (o especialista original foi testado em gráficos intradiários como M5).
  3. Ajuste as distâncias do pip e os parâmetros finais para corresponder à precisão da cotação do corretor.
  4. Inicie a estratégia; ele alternará entre posições longas e curtas sempre que as condições da Barra A/Barra B forem atendidas.

Diferenças do original EA

  • A versão StockSharp funciona em velas fechadas em vez de execução tick-by-tick. A banda de tolerância de entrada mantém o tempo de disparo próximo ao comportamento do MT4.
  • O tratamento de stop-loss é implementado verificando os extremos das velas em vez de colocar/modificar ordens MT4, porque as estratégias StockSharp normalmente gerenciam as saídas de forma programática.
  • O indicador VariableMovingAverage é implementado diretamente em C# e expõe o poder de suavização, eliminando o parâmetro dK não utilizado que existia na fonte MQL.
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;
		}
	}
}