GitHub で見る

Expert ZZLWA ストラテジー

概要

このストラテジーは、元のMetaTrader 5エキスパートアドバイザーExpertZZLWAのStockSharp高レベルポートです。EAは3つの異なる動作モードとオプションのマーチンゲールポジションサイジングを提供していました。このポートは元の専門家の構造を維持しながら、StockSharpのローソク足とインジケーターに適応させています:

  1. オリジナルモード – オープンポジションがない限り、完成したバーごとにロングとショートトレードを交互に切り替えます。
  2. ZigZag Additionモード – ローリングの最高値/最安値を通じて新しいスウィングハイとロウを追跡することで、カスタムインジケーター「ZigZag LW Addition」の動作を再現します。
  3. Moving Average Testモード – MQLコードの平滑化MA(150)対単純MA(10)のクロスオーバーロジックを反映します。

すべてのモードは、価格ポイントで表現された設定可能な保護ストップロスとテイクプロフィットのオフセットを使用します。このストラテジーはオプションのマーチンゲールサイジングをサポートしており、実現損失後に新しいトレードが乗数で増加され、最大ボリュームで制限されます。

トレードロジック

オリジナルモード

  • 完成したローソク足のみで動作します。
  • ポジションが開いていない場合、ストラテジーは新しいバーごとにロングとショートの成行注文を交互に出します。
  • ストップロスとテイクプロフィットは内蔵のStartProtectionヘルパーを通じて登録されます。
  • トレードが閉じると(ストップまたはターゲットで)、次のバーでは逆方向がアクティブになります。

ZigZag Additionモード

  • 選択したローソク足シリーズをサブスクライブし、ローリングのHighestおよびLowestインジケーターを維持します。
  • 前のスウィング方向が上向きでない間にローソク足の高値が現在の最高値に触れるとスウィングハイを検出します。これにより「ZigZag LW Addition」の買い/売りバッファシグナルが再現されます。
  • ローソク足の安値がローリングの最安値に逆の方法で触れるとスウィングローを検出します。
  • ローソク足の終値直後に信号された方向で成行注文を生成します。

Moving Average Testモード

  • 長さ150の平滑移動平均と長さ10の単純移動平均を構築します(MQL実装と一致)。
  • 前のバーから現在のバーにかけて平滑MAが単純MAを上抜けるとロングシグナルを出します。
  • 平滑MAが単純MAを下抜けるとショートシグナルを出します。
  • シグナルは閉じたローソク足でのみ処理されます。

マーチンゲール処理

  • 各自己トレードが受信されるたびに、ストラテジーはネットポジションと平均エントリー価格を追跡します。
  • ポジションが完全に閉じられると、最後のトレードの実現利益が記録されます。
  • トレードが損失で閉じ、マーチンゲールが有効な場合、次の注文ボリュームは最後のボリューム * MartingaleMultiplierMaximumVolumeで制限)になります。
  • トレードが利益で閉じるか、マーチンゲールが無効な場合、ストラテジーはベースボリュームに戻ります。

パラメーター

パラメーター デフォルト 説明
StopLossPoints 600 価格ポイントでの保護ストップまでの距離。
TakeProfitPoints 700 価格ポイントでのテイクプロフィットまでの距離。
BaseVolume 0.01 マーチンゲールが適用されない場合のデフォルト注文サイズ。
UseMartingale false trueに設定するとマーチンゲールサイジングを有効化。
MartingaleMultiplier 2 損失後に最後のトレードボリュームに適用される乗数。
MaximumVolume 10 マーチンゲールサイジングの最大許容ボリューム。
Mode Original 動作モード:OriginalZigZagAddition、またはMovingAverageTest
ZigZagTerm LongTerm ZigZagモードの感度プリセット(ShortTerm、MediumTerm、LongTerm)。
SlowMaPeriod 150 MAテストモードで使用する平滑MAのピリオド。
FastMaPeriod 10 MAテストモードで使用する単純MAのピリオド。
CandleType 15分の時間軸 処理のためにサブスクライブするローソク足タイプ。

注意事項

  • ストップ/テイクのオフセットはインスツルメントのPriceStepで乗算され、MetaTraderの_Point動作と一致します。
  • ストラテジーはStockSharpの高レベルAPI(SubscribeCandles + インジケーターバインディング)のみを使用します。
  • ZigZag感度プリセットは、Highest/Lowestの長さ12(短期)、24(中期)、48(長期)にマッピングされます。異なるスウィング幅が必要な場合は調整してください。
  • マーチンゲールトラッカーは自己トレード通知に依存します;ストラテジーがフィルが正確に報告される環境で実行されていることを確認してください。

MQLとの変換の違い

  • MQLバージョンはコンパイルされたZigZag LW Additionインジケーターと対話していました。StockSharpではローリングの高値/安値を使用してバッファを近似し、外部バイナリなしで同様のシグナルを提供します。
  • 注文の配置は手動の注文チケットの代わりにBuyMarket / SellMarketと管理された保護ヘルパーに依存します。
  • 元のエキスパートの歴史的なロット計算はターミナルのディールヒストリーを使用していました。このポートはリアルタイムで自己トレードを分析し、最後の閉じたトレードのボリュームと利益を保存することでこの動作を再現します。
  • MQLのスリップとマジックナンバーの入力は、StockSharpがこのコンテキストの成行注文に必要としないため省略されています。
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>
/// Port of the ExpertZZLWA MetaTrader strategy with three operation modes.
/// </summary>
public class ExpertZzlwaStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<decimal> _martingaleMultiplier;
	private readonly StrategyParam<decimal> _maximumVolume;
	private readonly StrategyParam<StrategyModes> _mode;
	private readonly StrategyParam<TermLevels> _termLevel;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest;
	private Lowest _lowest;
	private SmoothedMovingAverage _slowMa;
	private SimpleMovingAverage _fastMa;

	private bool _pendingBuySignal;
	private bool _pendingSellSignal;
	private bool _originalBuyReady;
	private bool _originalSellReady;
	private int _zigZagDirection;
	private decimal _prevSlow;
	private decimal _prevFast;

	private decimal _trackedPosition;
	private decimal _averageEntryPrice;
	private decimal _lastClosedVolume;
	private bool _lastTradeLoss;

	/// <summary>
	/// Operation modes reproduced from the original expert.
	/// </summary>
	public enum StrategyModes
	{
		Original,
		ZigZagAddition,
		MovingAverageTest,
	}

	/// <summary>
	/// ZigZag sensitivity presets available in addition mode.
	/// </summary>
	public enum TermLevels
	{
		ShortTerm,
		MediumTerm,
		LongTerm,
	}

	/// <summary>
	/// Protective stop size in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Profit target size in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Base order volume used by the strategy.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Enable martingale style position sizing.
	/// </summary>
	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	/// <summary>
	/// Multiplier applied after a losing trade when martingale is active.
	/// </summary>
	public decimal MartingaleMultiplier
	{
		get => _martingaleMultiplier.Value;
		set => _martingaleMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum allowed order volume.
	/// </summary>
	public decimal MaximumVolume
	{
		get => _maximumVolume.Value;
		set => _maximumVolume.Value = value;
	}

	/// <summary>
	/// Selected trading mode.
	/// </summary>
	public StrategyModes Mode
	{
		get => _mode.Value;
		set => _mode.Value = value;
	}

	/// <summary>
	/// ZigZag term preset for addition mode.
	/// </summary>
	public TermLevels ZigZagTerm
	{
		get => _termLevel.Value;
		set => _termLevel.Value = value;
	}

	/// <summary>
	/// Period of the slow smoothed moving average used in MA test mode.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the fast simple moving average used in MA test mode.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="ExpertZzlwaStrategy"/> class.
	/// </summary>
	public ExpertZzlwaStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 600)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss (points)", "Protective stop in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 700)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit (points)", "Profit target in points", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Base Volume", "Default order volume", "Trading");

		_useMartingale = Param(nameof(UseMartingale), false)
		.SetDisplay("Use Martingale", "Enable martingale sizing", "Trading");

		_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Martingale Multiplier", "Multiplier applied after a loss", "Trading");

		_maximumVolume = Param(nameof(MaximumVolume), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Maximum Volume", "Upper cap for order size", "Trading");

		_mode = Param(nameof(Mode), StrategyModes.MovingAverageTest)
		.SetDisplay("Mode", "Operating mode", "General");

		_termLevel = Param(nameof(ZigZagTerm), TermLevels.LongTerm)
		.SetDisplay("ZigZag Term", "Sensitivity preset for ZigZag", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 150)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA Period", "Smoothed MA length", "Indicators");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA Period", "Simple MA length", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time frame to analyse", "General");
	}

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

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

		_highest = null;
		_lowest = null;
		_slowMa = null;
		_fastMa = null;
		_pendingBuySignal = false;
		_pendingSellSignal = false;
		_originalBuyReady = true;
		_originalSellReady = true;
		_zigZagDirection = 0;
		_prevSlow = 0m;
		_prevFast = 0m;
		_trackedPosition = 0m;
		_averageEntryPrice = 0m;
		_lastClosedVolume = BaseVolume;
		_lastTradeLoss = false;
	}

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

		StartProtection(
		stopLoss: new Unit(StopLossPoints * GetPriceStep(), UnitTypes.Absolute),
		takeProfit: new Unit(TakeProfitPoints * GetPriceStep(), UnitTypes.Absolute));

		_originalBuyReady = true;
		_originalSellReady = true;
		_pendingBuySignal = false;
		_pendingSellSignal = false;
		_trackedPosition = 0m;
		_averageEntryPrice = 0m;
		_lastClosedVolume = BaseVolume;
		_lastTradeLoss = false;

		var subscription = SubscribeCandles(CandleType);

		switch (Mode)
		{
			case StrategyModes.Original:
				subscription.Bind(ProcessOriginalCandle).Start();
				break;

			case StrategyModes.ZigZagAddition:
				_highest = new Highest { Length = GetZigZagDepth(ZigZagTerm) };
				_lowest = new Lowest { Length = GetZigZagDepth(ZigZagTerm) };
				subscription.Bind(_highest, _lowest, ProcessAdditionCandle).Start();
				break;

			case StrategyModes.MovingAverageTest:
				_slowMa = new SmoothedMovingAverage { Length = SlowMaPeriod };
				_fastMa = new SimpleMovingAverage { Length = FastMaPeriod };
				subscription.Bind(_slowMa, _fastMa, ProcessMovingAverageCandle).Start();
				break;

			default:
				throw new NotSupportedException($"Unsupported mode {Mode}.");
			}

			var area = CreateChartArea();
			if (area != null)
			{
				DrawCandles(area, subscription);

				switch (Mode)
				{
					case StrategyModes.ZigZagAddition:
						DrawIndicator(area, _highest);
						DrawIndicator(area, _lowest);
						break;
					case StrategyModes.MovingAverageTest:
						DrawIndicator(area, _slowMa);
						DrawIndicator(area, _fastMa);
						break;
				}

				DrawOwnTrades(area);
			}
		}

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

			if (Position == 0)
			{
				if (_originalBuyReady)
				{
					ExecuteTrade(Sides.Buy);
					_originalBuyReady = false;
					_originalSellReady = true;
				}
				else if (_originalSellReady)
				{
					ExecuteTrade(Sides.Sell);
					_originalSellReady = false;
					_originalBuyReady = true;
				}
			}
		}

		private void ProcessAdditionCandle(ICandleMessage candle, decimal highest, decimal lowest)
		{
			if (candle.State != CandleStates.Finished)
			return;

			if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

			// Detect fresh ZigZag pivots similar to the original indicator buffers.
			if (candle.HighPrice >= highest && _zigZagDirection != 1)
			{
				_pendingSellSignal = true;
				_pendingBuySignal = false;
				_zigZagDirection = 1;
			}
			else if (candle.LowPrice <= lowest && _zigZagDirection != -1)
			{
				_pendingBuySignal = true;
				_pendingSellSignal = false;
				_zigZagDirection = -1;
			}

			DispatchSignals();
		}

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

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

			// Reproduce cross checks from the MQL version.
			var crossDown = _prevSlow > _prevFast && slow < fast;
			var crossUp = _prevSlow < _prevFast && slow > fast;

			_prevSlow = slow;
			_prevFast = fast;

			if (crossUp)
			{
				_pendingBuySignal = true;
				_pendingSellSignal = false;
			}
			else if (crossDown)
			{
				_pendingSellSignal = true;
				_pendingBuySignal = false;
			}

			DispatchSignals();
		}

		private void DispatchSignals()
		{
			if (_pendingBuySignal)
			{
				ExecuteTrade(Sides.Buy);
				_pendingBuySignal = false;
				_pendingSellSignal = false;
			}
			else if (_pendingSellSignal)
			{
				ExecuteTrade(Sides.Sell);
				_pendingSellSignal = false;
				_pendingBuySignal = false;
			}
		}

		private void ExecuteTrade(Sides side)
		{
			var volume = GetOrderVolume();
			if (volume <= 0)
			return;

			if (side == Sides.Buy)
			BuyMarket(volume);
			else
			SellMarket(volume);
		}

		private decimal GetOrderVolume()
		{
			if (!UseMartingale)
			return BaseVolume;

			if (!_lastTradeLoss)
			return BaseVolume;

			var nextVolume = _lastClosedVolume * MartingaleMultiplier;
			return nextVolume > MaximumVolume ? BaseVolume : nextVolume;
		}

		private int GetZigZagDepth(TermLevels level)
		{
			return level switch
			{
				TermLevels.ShortTerm => 12,
				TermLevels.MediumTerm => 24,
				_ => 48,
			};
		}

		private decimal GetPriceStep()
		{
			return Security?.PriceStep ?? 1m;
		}

		/// <inheritdoc />
		protected override void OnOwnTradeReceived(MyTrade trade)
		{
			if (trade?.Order == null)
			return;

			var side = trade.Order.Side;
			var volume = trade.Trade.Volume;
			var price = trade.Trade.Price;

			var previousPosition = _trackedPosition;

			if (side == Sides.Buy)
			{
				if (previousPosition >= 0)
				{
					// Building or creating a long position.
					var newPosition = previousPosition + volume;
					_averageEntryPrice = newPosition == 0m
					? 0m
					: (_averageEntryPrice * previousPosition + price * volume) / newPosition;
					_trackedPosition = newPosition;
				}
				else
				{
					// Closing part or all of a short position.
					var closingVolume = Math.Min(volume, Math.Abs(previousPosition));
					var profit = (_averageEntryPrice - price) * closingVolume;
					var remaining = previousPosition + volume;

					if (remaining >= 0m)
					{
						RegisterClosedTrade(closingVolume, profit);
						if (remaining > 0m)
						{
							// Flip into a new long position with leftover quantity.
							_trackedPosition = remaining;
							_averageEntryPrice = price;
						}
						else
						{
							_trackedPosition = 0m;
							_averageEntryPrice = 0m;
						}
					}
					else
					{
						_trackedPosition = remaining;
						// Average price of the remaining short stays unchanged.
					}
				}
			}
			else
			{
				if (previousPosition <= 0)
				{
					// Building or creating a short position.
					var newPosition = previousPosition - volume;
					var absPrev = Math.Abs(previousPosition);
					var absNew = Math.Abs(newPosition);
					_averageEntryPrice = absNew == 0m
					? 0m
					: (_averageEntryPrice * absPrev + price * volume) / absNew;
					_trackedPosition = newPosition;
				}
				else
				{
					// Closing part or all of a long position.
					var closingVolume = Math.Min(volume, previousPosition);
					var profit = (price - _averageEntryPrice) * closingVolume;
					var remaining = previousPosition - volume;

					if (remaining <= 0m)
					{
						RegisterClosedTrade(closingVolume, profit);
						if (remaining < 0m)
						{
							_trackedPosition = remaining;
							_averageEntryPrice = price;
						}
						else
						{
							_trackedPosition = 0m;
							_averageEntryPrice = 0m;
						}
					}
					else
					{
						_trackedPosition = remaining;
						// Average entry price is preserved for the reduced long position.
					}
				}
			}
		}

		private void RegisterClosedTrade(decimal volume, decimal profit)
		{
			_lastClosedVolume = volume;
			_lastTradeLoss = profit < 0m;
		}
	}