GitHub で見る

VarMovAvg 戦略

概要

VarMovAvg 戦略は、MetaTrader 4 エキスパート アドバイザー VarMovAvg_v0011 から変換されたストップ アンド リバース システムです。適応型変数移動平均 (VMA) を使用してトレンドの方向を測定し、ポジションを反転する前に 2 段階の引き戻しパターン (オリジナルの EA ではバー A およびバー B と呼ばれます) を待ちます。ポジションがアクティブである間、移動平均ベースのトレーリング ストップは利益を保護し、反対側のバー A/バー B シーケンスが完了すると取引を反転します。

取引ロジック

  1. アダプティブ VMA – カスタム VariableMovingAverage インジケーターは MT4 の式を複製します。
    • 効率率は、現在の終値と AmaPeriod バー前の終値を比較し、それを累積された絶対的な価格変動で割ります。
    • 平滑化係数は高速期間と低速期間の間を補間し、元の G 値と同様に SmoothingPower パラメータまで引き上げられます。
  2. 信号検出 (バー A / バー B) – 2 つの独立したステート マシンがロングおよびショート セットアップを追跡します。
    • バー A: 価格は潜在的な取引方向に VMA を超えて SignalPipsBarA (ピップ単位) で移動します。
    • バー B: 価格が同じ方向にさらに SignalPipsBarB ピップを拡張し、極値を固定します。
    • エントリー: 終値が SignalPipsTrade ± EntryPipsDiff で定義されたエントリー バンドに戻ると、戦略は成行注文を使用してエントリー (または反転) します。
  3. トレーリングストップと反転 – ポジションがオープンしている間、高値 (ショートの場合) または安値 (ロングの場合) で計算された移動平均は、StopMaShift バー分シフトされ、StopPipsDiff 分パディングされます。
    • ローソク足がストップレベルを突き抜けた場合、ポジションはクローズされます。
    • ポジションが存在する間に反対のバー A/バー B シーケンスがトリガーされた場合、ストラテジーは、|Position| + Volume のサイズの単一成行注文を発行して、方向を即座に反転し、EA の動作と一致します。

パラメーター

パラメータ 説明 MT4ソース
AmaPeriod VMA によって使用されるルックバック ウィンドウ。 prm.vma.periodAMA
FastPeriod VMA 内の高速平滑化係数。 prm.vma.nfast
SlowPeriod VMA 内の平滑化係数が遅い。 prm.vma.nslow
SmoothingPower 適応係数に適用される指数 G prm.vma.G
SignalPipsBarA バー A を受け入れるために必要な VMA からの距離。 prm.sig.pipsBarA
SignalPipsBarB バー B を受け入れるには追加の距離が必要です。 prm.sig.pipsBarB
SignalPipsTrade バー B の端からエントリーラインまでのオフセット。 prm.sig.pipsTrade
EntryPipsDiff エントリーラインの周囲で許容される許容誤差。 prm.entry.diff
StopPipsDiff トレーリングストップ移動平均に適用されるオフセット。 prm.stop.diff
StopMaPeriod 逆指値移動平均の期間。 prm.mastop.period
StopMaShift ストップ移動平均のシフト (バー)。 prm.mastop.shift
StopMaMethod 移動平均法 (MODE_SMAEMASMMALWMA)。 prm.mastop.method
CandleType 作業時間枠。 チャートの時間枠

ピップ変換 – 利用可能な場合、すべてのピップ距離に Security.PriceStep が乗算されます。商品に設定されたステップがない場合、生の値は価格単位で解釈され、EA フォールバックが複製されます。

使用上の注意

  • この戦略は SubscribeCandles に依存しており、完全に完成したキャンドルで実行されます。エントリーバンドのロジックは、終値を使用した EA のティックごとのチェックを反映しています。
  • プロテクション注文は、ローソク足がストップレベルを横切ったときの市場退場を通じてモデル化されます。これは、ストップ注文がティックごとに再計算されるため、EA の動作と一致します。
  • 移動平均シフトは FIFO バッファを介して実装され、StopMaShift = 0 が最新の値を使用し、正のシフトが要求されたバー数を遡ることを保証します。
  • すべての取引(エントリー、反転、またはストップヒット)の後、両方のシグナルトラッカーは注文の重複を避けるためにニュートラル状態にリセットされ、MetaTrader の STATUS_TRADE リセットロジックをエミュレートします。

クイックスタート

  1. ストラテジーを StockSharp 環境に追加し、有効な PriceStep とティック サイズを持つ商品を割り当てます。
  2. CandleType を通じて時間枠を設定します (元のエキスパートは M5 などの日中チャートでテストされました)。
  3. ブローカーの相場精度に合わせてピップ距離とトレーリングパラメーターを調整します。
  4. 戦略を開始します。バーA/バーBの条件が満たされるたびに、ロングポジションとショートポジションが交互に切り替わります。

オリジナルの EA との違い

  • StockSharp バージョンは、ティックごとの実行ではなく、閉じたローソク足で動作します。エントリー許容範囲により、トリガーのタイミングが MT4 の動作に近く保たれます。
  • StockSharp 戦略は通常、プログラムで出口を管理するため、MT4 注文の発注/変更ではなく、ローソク足の極値をチェックすることでストップロスの処理が実装されます。
  • VariableMovingAverage インジケーターは C# で直接実装されており、平滑化機能を公開して、MQL ソースに存在していた未使用の dK パラメーターを排除します。
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;
		}
	}
}