Auf GitHub ansehen

VarMovAvg-Strategie

Überblick

Die VarMovAvg-Strategie ist ein Stop-and-Reverse-System, das vom MetaTrader 4 Expert Advisor VarMovAvg_v0011 abgeleitet wurde. Es verwendet einen adaptiven variablen gleitenden Durchschnitt (VMA), um die Trendrichtung zu messen, und wartet auf ein zweistufiges Rückzugsmuster (im Original EA als Balken A und Balken B bezeichnet), bevor die Position umgekehrt wird. Während eine Position aktiv ist, schützt ein auf dem gleitenden Durchschnitt basierender Trailing-Stop die Gewinne und dreht den Handel um, wenn die entgegengesetzte Bar-A-/Bar-B-Sequenz abgeschlossen ist.

Handelslogik

  1. Adaptive VMA – Der benutzerdefinierte VariableMovingAverage-Indikator repliziert die MT4-Formel:
    • Das Effizienzverhältnis vergleicht den aktuellen Schlusskurs mit dem Schlusskurs vor AmaPeriod Balken und dividiert ihn durch die kumulierte absolute Preisbewegung.
    • Der Glättungskoeffizient interpoliert zwischen den schnellen und langsamen Perioden und wird genau wie der ursprüngliche G-Wert auf den Parameter SmoothingPower angehoben.
  2. Signalerkennung (Balken A / Balken B) – Zwei unabhängige Zustandsmaschinen verfolgen lange und kurze Setups:
    • Balken A: Der Preis bewegt sich um SignalPipsBarA (in Pips) über den VMA hinaus in die potenzielle Handelsrichtung.
    • Balken B: Der Preis verlängert sich um weitere SignalPipsBarB Pips in die gleiche Richtung und fixiert so den extremen Preis.
    • Einstieg: Wenn der Schlusskurs in das durch SignalPipsTrade ± EntryPipsDiff definierte Einstiegsband zurückkehrt, erfolgt der Einstieg (oder die Umkehrung) der Strategie mithilfe von Marktaufträgen.
  3. Trailing Stop und Umkehr – Während eine Position offen ist, wird ein gleitender Durchschnitt, der auf Höchst- (für Shorts) oder Tiefstständen (für Long-Positionen) berechnet wird, um StopMaShift Balken verschoben und um StopPipsDiff aufgefüllt.
    • Wenn die Kerze das Stop-Level durchbricht, wird die Position geschlossen.
    • Wenn die entgegengesetzte Balken-A-/Balken-B-Sequenz ausgelöst wird, während eine Position besteht, gibt die Strategie eine einzelne Marktorder mit der Größe |Position| + Volume aus, um die Richtung sofort umzukehren, was dem EA-Verhalten entspricht.

Parameter

Parameter Beschreibung MT4-Quelle
AmaPeriod Von der VMA verwendetes Lookback-Fenster. prm.vma.periodAMA
FastPeriod Schneller Glättungsfaktor innerhalb des VMA. prm.vma.nfast
SlowPeriod Langsamer Glättungsfaktor innerhalb des VMA. prm.vma.nslow
SmoothingPower Exponent G, angewendet auf den adaptiven Koeffizienten. prm.vma.G
SignalPipsBarA Entfernung von der VMA, die erforderlich ist, um Balken A zu akzeptieren. prm.sig.pipsBarA
SignalPipsBarB Zusätzlicher Abstand erforderlich, um Bar B zu akzeptieren. prm.sig.pipsBarB
SignalPipsTrade Versatz vom äußersten Balken B zur Eintrittslinie. prm.sig.pipsTrade
EntryPipsDiff Akzeptierte Toleranz um die Eintrittslinie. prm.entry.diff
StopPipsDiff Auf den gleitenden Durchschnitt des Trailing-Stops angewendeter Offset. prm.stop.diff
StopMaPeriod Zeitraum des Stop-Moving-Average. prm.mastop.period
StopMaShift Verschiebung (Balken) des Stop-Moving-Average. prm.mastop.shift
StopMaMethod Methode des gleitenden Durchschnitts (MODE_SMA, EMA, SMMA, LWMA). prm.mastop.method
CandleType Arbeitszeitrahmen. Zeitrahmen des Diagramms

Pip-Umrechnung – Alle Pip-Abstände werden mit Security.PriceStep multipliziert, sofern verfügbar. Wenn das Instrument keinen konfigurierten Schritt hat, werden die Rohwerte in Preiseinheiten interpretiert, wodurch der EA-Fallback repliziert wird.

Nutzungshinweise

  • Die Strategie basiert auf SubscribeCandles und läuft vollständig auf fertigen Kerzen; Die Einstiegsbandlogik spiegelt die Tick-für-Tick-Prüfungen des EA anhand der Schlusskurse wider.
  • Schutzaufträge werden durch Marktaustritte modelliert, wenn die Kerze das Stop-Level überschreitet, was dem EA-Verhalten entspricht, da Stop-Orders bei jedem Tick neu berechnet wurden.
  • Die Verschiebung des gleitenden Durchschnitts wird über einen FIFO-Puffer implementiert, um sicherzustellen, dass StopMaShift = 0 den neuesten Wert verwendet und positive Verschiebungen die angeforderte Anzahl von Balken zurückblicken.
  • Nach jedem Trade (Einstieg, Umkehr oder Stop-Treffer) werden beide Signal-Tracker in den neutralen Zustand zurückgesetzt, um doppelte Orders zu vermeiden, indem sie die STATUS_TRADE-Reset-Logik in MetaTrader emulieren.

Schnellstart

  1. Fügen Sie die Strategie einer StockSharp-Umgebung hinzu und weisen Sie ein Instrument mit einer gültigen PriceStep- und Tick-Größe zu.
  2. Konfigurieren Sie den Zeitrahmen bis CandleType (der ursprüngliche Experte wurde auf Intraday-Charts wie M5 getestet).
  3. Passen Sie die Pip-Abstände und Trailing-Parameter an die Kursgenauigkeit des Brokers an.
  4. Starten Sie die Strategie; Es wechselt zwischen Long- und Short-Positionen, wenn die Bedingungen von Bar A/Bar B erfüllt sind.

Unterschiede zum Original EA

  • Die StockSharp-Version funktioniert bei geschlossenen Kerzen statt bei der Tick-für-Tick-Ausführung. Das Eingabetoleranzband hält das Trigger-Timing nahe am MT4-Verhalten.
  • Die Stop-Loss-Handhabung wird durch die Überprüfung der Kerzenextreme implementiert, anstatt MT4-Aufträge zu erteilen/zu ändern, da StockSharp-Strategien Exits typischerweise programmgesteuert verwalten.
  • Der Indikator VariableMovingAverage wird direkt in C# implementiert und stellt die Glättungsleistung bereit, wodurch der ungenutzte Parameter dK eliminiert wird, der in der Quelle MQL vorhanden war.
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;
		}
	}
}