在 GitHub 上查看

ZigAndZag Trader 策略

概述

ZigAndZag Trader 是 MetaTrader 专家 ZigAndZag_trader.mq4 在 StockSharp 平台上的移植版本。策略同时运行两个 ZigZag 风格的摆动检测器:

  1. 长期 ZigZag(参数 TrendDepth)标记主要高低点,用来识别趋势方向。
  2. 短期 ZigZag(参数 ExitDepth)跟踪当前趋势内部最近的摆动,并记录加权价格 (5×收盘 + 2×开盘 + 最高 + 最低) / 9

当价格按照趋势方向突破最近摆动点时开仓;当加权价格逆趋势穿越该摆动点时立即平仓。此流程完全复刻了原始专家读取自定义指标 ZigAndZag 第 4–6 号缓冲区的行为。

交易逻辑

  • 趋势识别:长期 ZigZag 出现新的摆动低点 → 趋势设为向上;出现新的摆动高点 → 趋势设为向下。
  • 摆动管理:短期 ZigZag 确认新摆动时重置内部状态并更新参考加权价格。
  • 入场条件
    • 上升趋势 + 最近摆动为低点:当加权价格向上突破该低点至少 1 个点时买入。
    • 下降趋势 + 最近摆动为高点:当加权价格向下突破该高点至少 1 个点时卖出。
  • 离场条件:若加权价格逆趋势穿越参考摆动点,立即关闭全部仓位。
  • 仓位限制:净头寸绝对值上限为 MaxOrders × Volume,超出后新的信号会被忽略。

参数

参数 默认值 说明
CandleType 1 Minute 用于两种 ZigZag 计算的 K 线类型。
Lots 0.1 目标下单手数,最终会对齐到交易品种的最小成交单位。
TrendDepth 3 长期 ZigZag 的回看长度(以 K 线数计)。
ExitDepth 3 短期 ZigZag 的回看长度,决定入场与平仓信号。
MaxOrders 1 允许同时持有的最大仓位数量。
StopLossPips 0 止损距离(点),0 表示不开启。
TakeProfitPips 0 止盈距离(点),0 表示不开启。

风险控制

策略会自动调用 StartProtection。当 StopLossPipsTakeProfitPips 设置为正值时,每笔市价单都会附带固定距离的保护性止损/止盈,并按照品种的最小价格步长换算为价格差。

可视化

策略在默认图表区域绘制 K 线以及自身的成交记录。由于 ZigZag 逻辑完全在内部实现,因此不额外绘制指标曲线。

补充说明

  • 加权价格计算与原始 MQL 指标保持一致,无需直接访问指标缓冲区。
  • 突破阈值设定为 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
	}
}