GitHub で見る

ジグアンドザグトレーダー戦略

概要

ZigAndZag Trader Strategy は、MetaTrader エキスパート ZigAndZag_trader.mq4 の StockSharp 移植です。このシステムは、ZigZag からインスピレーションを得た 2 つのスイング検出器を階層化しています。

  1. 長期ジグザグ (TrendDepth によって構成) は、大きな変動の高値と安値をマークすることで主要なトレンドを追跡します。
  2. 短期ジグザグ (ExitDepth によって構成) は、そのトレンド内の最新のスイング ピボットを特定し、加重価格 ((5×Close + 2×Open + High + Low) / 9) を監視します。

ロボットは、価格が支配的なトレンドの方向に最新のスイングピボットから離れるときにのみ取引を開始し、加重価格がトレンドに反してそのピボットを突破したときにポジションを閉じます。これは、カスタム ZigAndZag インジケーターのバッファ 4 ~ 6 を読み取る元の MetaTrader エキスパートの動作を再現します。

取引ロジック

  • トレンド検出 – 長期ジグザグが新たな安値を確認すると、トレンドは 上昇 とみなされます。新しい高値がダウンに反転します。
  • スイング追跡 – 各短期ピボットは内部状態をリセットし、そのスイングの加重価格を保存します。
  • エントリー条件
    • 上昇トレンド + 最後のピボットは安値です。加重価格が保存されたピボットを少なくとも 1 ピボット上回ったときに購入します。
    • 下降トレンド + 最後のピボットは高値です。加重価格が保存されたピボットを少なくとも 1 ピボット下回ったときに売ります。
  • 終了条件 – トレンドがアクティブなスイングと一致しない間に価格が保存されたピボットを通って戻る場合、すべてのオープンポジションはクローズされます。
  • 注文調整 – 絶対ポジション サイズの合計は MaxOrders × Volume によって制限されます。その上限に達すると、追加のシグナルは無視されます。

パラメーター

パラメータ デフォルト 説明
CandleType 1 Minute 両方の ZigZag 評価に使用されるローソク足タイプ。
Lots 0.1 ロット単位で要求された取引サイズ。最終的な音量は、楽器の音量ステップに合わせられます。
TrendDepth 3 トレンドを定義する長期ジグザグのルックバック (ローソク足)。
ExitDepth 3 スイングのエントリーとエグジットを生み出す短期ジグザグのルックバック(ローソク足)。
MaxOrders 1 同時注文/ポジション単位の最大数。
StopLossPips 0 ピップ単位の保護ストップロス距離 (0 はストップを無効にします)。
TakeProfitPips 0 利益確定距離 (ピップ単位) (0 はターゲットを無効にします)。

リスク管理

StartProtection は自動的に有効になります。ストップロスまたはテイクプロフィットディスタンスがゼロより大きい値に設定されている場合、指定されたピップ距離と商品ティックサイズを使用して、固定保護注文がすべての成行注文に付加されます。

可視化

この戦略は、デフォルトのチャート領域にローソク足と実行された取引を描画します。エントリーロジックとエグジットロジックは内部 ZigZag トラッカーを使用するため、カスタム インジケーターはプロットされません。

注意事項

  • 加重価格式は MetaTrader インジケーターと同じであり、インジケーター バッファーへの直接アクセスを回避します。
  • ブレイクアウトしきい値は 1 銘柄ピップに等しく、現在のスプレッドを超える動きを必要とした元のコードを反映しています。
  • ポートでは、プロジェクト ガイドラインの要求に従って、すべてのコメントとログが英語で保存されます。
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Replica of the MetaTrader ZigAndZag trader that follows a long-term ZigZag trend and short-term swings.
/// </summary>
public class ZigAndZagTraderStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _lots;
	private readonly StrategyParam<int> _trendDepth;
	private readonly StrategyParam<int> _exitDepth;
	private readonly StrategyParam<int> _maxOrders;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;

	private Lowest _longTermLow = null!;
	private Highest _longTermHigh = null!;
	private Lowest _shortTermLow = null!;
	private Highest _shortTermHigh = null!;

	private decimal _pipSize;
	private decimal _volumeStep;
	private decimal _breakoutThreshold;

	private decimal? _lastTrendLow;
	private decimal? _lastTrendHigh;
	private decimal? _lastShortLow;
	private decimal? _lastShortHigh;
	private decimal? _lastSlalomZig;
	private decimal? _lastSlalomZag;

	private bool _trendUp;
	private bool _prevTrendUp;
	private bool _buyArmed;
	private bool _sellArmed;
	private bool _limitArmed;

	private PivotTypes _lastPivot;

	private int _cooldown;
	private const int CooldownBars = 500;

	/// <summary>
	/// Trading candles.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Requested trade volume in lots.
	/// </summary>
	public decimal Lots
	{
		get => _lots.Value;
		set => _lots.Value = value;
	}

	/// <summary>
	/// Depth of the long-term ZigZag that defines the prevailing trend.
	/// </summary>
	public int TrendDepth
	{
		get => _trendDepth.Value;
		set => _trendDepth.Value = value;
	}

	/// <summary>
	/// Depth of the short-term ZigZag that produces swing entries and exits.
	/// </summary>
	public int ExitDepth
	{
		get => _exitDepth.Value;
		set => _exitDepth.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open orders.
	/// </summary>
	public int MaxOrders
	{
		get => _maxOrders.Value;
		set => _maxOrders.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips (0 disables the stop).
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips (0 disables the target).
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public ZigAndZagTraderStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Candles used for swing detection", "General");

		_lots = Param(nameof(Lots), 0.1m)
			.SetDisplay("Lots", "Requested trade size in lots", "Trading")
			.SetGreaterThanZero();

		_trendDepth = Param(nameof(TrendDepth), 3)
			.SetDisplay("Trend Depth", "Lookback for the long-term ZigZag", "ZigZag")
			.SetGreaterThanZero()
			;

		_exitDepth = Param(nameof(ExitDepth), 3)
			.SetDisplay("Exit Depth", "Lookback for the short-term swing ZigZag", "ZigZag")
			.SetGreaterThanZero()
			;

		_maxOrders = Param(nameof(MaxOrders), 1)
			.SetDisplay("Max Orders", "Maximum simultaneous positions", "Trading")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 0m)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
			.SetNotNegative();

		_takeProfitPips = Param(nameof(TakeProfitPips), 0m)
			.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
			.SetNotNegative();
	}

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

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

		_longTermLow = null;
		_longTermHigh = null;
		_shortTermLow = null;
		_shortTermHigh = null;

		_lastTrendLow = null;
		_lastTrendHigh = null;
		_lastShortLow = null;
		_lastShortHigh = null;
		_lastSlalomZig = null;
		_lastSlalomZag = null;

		_trendUp = false;
		_prevTrendUp = false;
		_buyArmed = false;
		_sellArmed = false;
		_limitArmed = false;
		_lastPivot = PivotTypes.None;
		_pipSize = 0;
		_volumeStep = 0;
		_breakoutThreshold = 0;
		_cooldown = 0;
	}

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

		_pipSize = Security?.PriceStep ?? 0.0001m;
		_volumeStep = Security?.VolumeStep ?? 1m;
		if (_volumeStep <= 0m)
			_volumeStep = 1m;

		_breakoutThreshold = _pipSize;

		var rawVolume = Lots > 0m ? Lots : _volumeStep;
		if (rawVolume < _volumeStep)
			rawVolume = _volumeStep;

		var steps = Math.Max(1L, (long)Math.Ceiling((double)(rawVolume / _volumeStep)));
		Volume = steps * _volumeStep;

		StartProtection(
			takeProfit: TakeProfitPips > 0m ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : null,
			stopLoss: StopLossPips > 0m ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : null,
			useMarketOrders: true);

		_longTermLow = new Lowest { Length = TrendDepth };
		_longTermHigh = new Highest { Length = TrendDepth };
		_shortTermLow = new Lowest { Length = ExitDepth };
		_shortTermHigh = new Highest { Length = ExitDepth };

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

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

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

		if (_cooldown > 0)
			_cooldown--;

		var t = candle.CloseTime;
		var longLow = _longTermLow.Process(candle.LowPrice, t, true).ToDecimal();
		var longHigh = _longTermHigh.Process(candle.HighPrice, t, true).ToDecimal();
		var shortLow = _shortTermLow.Process(candle.LowPrice, t, true).ToDecimal();
		var shortHigh = _shortTermHigh.Process(candle.HighPrice, t, true).ToDecimal();

		var longFormed = _longTermLow?.IsFormed == true && _longTermHigh?.IsFormed == true;
		var shortFormed = _shortTermLow?.IsFormed == true && _shortTermHigh?.IsFormed == true;

		var navel = (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;

		if (longFormed)
		{
			if (candle.LowPrice == longLow && (_lastTrendLow == null || longLow != _lastTrendLow))
			{
				_trendUp = true;
				_lastTrendLow = longLow;
			}

			if (candle.HighPrice == longHigh && (_lastTrendHigh == null || longHigh != _lastTrendHigh))
			{
				_trendUp = false;
				_lastTrendHigh = longHigh;
			}
		}

		if (_trendUp != _prevTrendUp)
		{
			_buyArmed = false;
			_sellArmed = false;
			_limitArmed = false;
			_prevTrendUp = _trendUp;
		}

		if (shortFormed)
		{
			if (candle.LowPrice == shortLow && (_lastShortLow == null || shortLow != _lastShortLow))
			{
				_lastPivot = PivotTypes.Low;
				_lastShortLow = shortLow;
				_lastSlalomZig = navel;
				_buyArmed = false;
				_sellArmed = false;
				_limitArmed = false;
			}

			if (candle.HighPrice == shortHigh && (_lastShortHigh == null || shortHigh != _lastShortHigh))
			{
				_lastPivot = PivotTypes.High;
				_lastShortHigh = shortHigh;
				_lastSlalomZag = navel;
				_buyArmed = false;
				_sellArmed = false;
				_limitArmed = false;
			}
		}

		if (!longFormed || !shortFormed)
			return;

		var buySignal = false;
		var sellSignal = false;
		var closeSignal = false;

		switch (_lastPivot)
		{
			case PivotTypes.Low when _lastSlalomZig != null:
			{
				if (_trendUp)
				{
					var shouldBuy = navel - _lastSlalomZig.Value >= _breakoutThreshold;
					if (shouldBuy && !_buyArmed)
					{
						_buyArmed = true;
						buySignal = true;
					}
					else if (!shouldBuy && _buyArmed && navel <= _lastSlalomZig.Value)
					{
						_buyArmed = false;
					}

					if (_limitArmed && navel <= _lastSlalomZig.Value)
						_limitArmed = false;
				}
				else
				{
					var shouldClose = navel > _lastSlalomZig.Value;
					if (shouldClose && !_limitArmed)
					{
						_limitArmed = true;
						closeSignal = true;
					}
					else if (!shouldClose && _limitArmed)
					{
						_limitArmed = false;
					}

					_buyArmed = false;
				}

				break;
			}
			case PivotTypes.High when _lastSlalomZag != null:
			{
				if (!_trendUp)
				{
					var shouldSell = _lastSlalomZag.Value - navel >= _breakoutThreshold;
					if (shouldSell && !_sellArmed)
					{
						_sellArmed = true;
						sellSignal = true;
					}
					else if (!shouldSell && _sellArmed && navel >= _lastSlalomZag.Value)
					{
						_sellArmed = false;
					}

					if (_limitArmed && navel >= _lastSlalomZag.Value)
						_limitArmed = false;
				}
				else
				{
					var shouldClose = _lastSlalomZag.Value > navel;
					if (shouldClose && !_limitArmed)
					{
						_limitArmed = true;
						closeSignal = true;
					}
					else if (!shouldClose && _limitArmed)
					{
						_limitArmed = false;
					}

					_sellArmed = false;
				}

				break;
			}
		}

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		ExecuteSignals(buySignal, sellSignal, closeSignal);
	}

	private void ExecuteSignals(bool buySignal, bool sellSignal, bool closeSignal)
	{
		if (_cooldown > 0)
			return;

		var volume = Volume;
		if (volume <= 0m || MaxOrders <= 0)
			return;

		var maxVolume = MaxOrders * volume;

		if (buySignal)
		{
			var currentLong = Position > 0m ? Position : 0m;
			var available = maxVolume - currentLong;
			if (available > 0m)
			{
				var tradeVolume = Math.Min(volume, available);
				BuyMarket(tradeVolume);
				_cooldown = CooldownBars;
				return;
			}
		}

		if (sellSignal)
		{
			var currentShort = Position < 0m ? -Position : 0m;
			var available = maxVolume - currentShort;
			if (available > 0m)
			{
				var tradeVolume = Math.Min(volume, available);
				SellMarket(tradeVolume);
				_cooldown = CooldownBars;
				return;
			}
		}

		if (closeSignal && Position != 0m)
		{
			if (Position > 0)
				SellMarket(Position);
			else
				BuyMarket(Math.Abs(Position));
			_cooldown = CooldownBars;
		}
	}

	private enum PivotTypes
	{
		None,
		Low,
		High
	}
}