在 GitHub 上查看

ZigZag EvgeTrofi 策略

ZigZag EvgeTrofi 策略是将经典的 MetaTrader 专家顾问移植到 StockSharp 高级 API 的版本。它追踪 ZigZag 逻辑识别的最近一个摆动点,并在信号仍然有效的时间窗口内迅速执行交易。

策略思路

  • 原始顾问会检查 ZigZag 缓冲区中最新的非零点,以判断最后一个摆动是高点还是低点。
  • 默认情况下,高点会触发做多。启用 SignalReverse 后,方向会被反转。
  • 只有在新摆动仍然“新鲜”时才允许开仓,参数 Urgency 限制了从摆动出现后可以进场的最大 K 线数量。
  • 在发送新的订单之前,会先平掉反方向的持仓;在有效窗口内,策略可以在同一方向上逐步加仓。

该移植保持了原策略的反趋势特性:新高触发多单,新低触发空单。

运作流程

  1. HighestLowest 指标模拟 ZigZag 的深度参数,在最近 Depth 根 K 线内跟踪极值。
  2. 当价格突破这些极值且幅度超过 Deviation(按价格步长折算)时,就确认新的摆动点。
  3. 策略记录自摆动出现以来经过的 K 线数量,超过 Urgency 后信号失效。
  4. 只要仍在时间窗口内,每根收盘的 K 线都会按 VolumePerTrade 的数量开仓;先平掉反向仓位,保证顺利翻仓。

参数

参数 默认值 说明
Depth 17 查找摆动高/低点的窗口长度,对应 ZigZag 的深度。
Deviation 7 确认新摆动所需的最小价格变动,以价格步长为单位。
Backstep 5 在允许切换到反向摆动之前必须经过的最小 K 线数。
Urgency 2 摆动出现后允许进场的最大 K 线数。
SignalReverse false 翻转高/低点对应的做多和做空信号。
CandleType 5 分钟 K 线 用于分析的时间框架,可根据需求调整。
VolumePerTrade 0.10 每次进场的下单数量,对应原策略的 Lot。

交易提示

  • 策略本身不包含止损或止盈,需要结合账户或其他模块进行风险控制。
  • Urgency 窗口内每根 K 线都可能加仓,趋势行情中仓位会迅速扩大。
  • 对于波动较大的标的,建议提高 Depth 以减少噪音;若希望更灵敏,则可以降低该值。
  • 启用 SignalReverse 后,策略变为突破跟随:高点做空,低点做多。

文件

  • CS/ZigZagEvgeTrofiStrategy.cs – 策略的 C# 实现。
  • 暂未提供 Python 版本。
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>
/// ZigZag pivot strategy based on the original ZigZagEvgeTrofi expert advisor.
/// Reacts to the most recent zigzag swing and enters within a limited number of bars.
/// </summary>
public class ZigZagEvgeTrofiStrategy : Strategy
{
	private enum PivotTypes
	{
		None,
		High,
		Low
	}

	private readonly StrategyParam<int> _depth;
	private readonly StrategyParam<decimal> _deviation;
	private readonly StrategyParam<int> _backstep;
	private readonly StrategyParam<int> _urgency;
	private readonly StrategyParam<bool> _signalReverse;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _volume;

	private Highest _highest;
	private Lowest _lowest;
	private PivotTypes _pivotType;
	private decimal _pivotPrice;
	private int _barsSincePivot;
	private decimal _priceStep;

	/// <summary>
	/// ZigZag depth parameter controlling the swing detection window.
	/// </summary>
	public int Depth
	{
		get => _depth.Value;
		set => _depth.Value = value;
	}

	/// <summary>
	/// Minimum deviation in price steps required to confirm a new pivot.
	/// </summary>
	public decimal Deviation
	{
		get => _deviation.Value;
		set => _deviation.Value = value;
	}

	/// <summary>
	/// Minimum number of bars between opposite pivot updates.
	/// </summary>
	public int Backstep
	{
		get => _backstep.Value;
		set => _backstep.Value = value;
	}

	/// <summary>
	/// Maximum number of bars after a pivot when entries are allowed.
	/// </summary>
	public int Urgency
	{
		get => _urgency.Value;
		set => _urgency.Value = value;
	}

	/// <summary>
	/// Reverses the direction of the generated signals.
	/// </summary>
	public bool SignalReverse
	{
		get => _signalReverse.Value;
		set => _signalReverse.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Trading volume submitted on every entry.
	/// </summary>
	public decimal VolumePerTrade
	{
		get => _volume.Value;
		set => _volume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ZigZagEvgeTrofiStrategy"/> class.
	/// </summary>
	public ZigZagEvgeTrofiStrategy()
	{
		_depth = Param(nameof(Depth), 17)
			.SetGreaterThanZero()
			.SetDisplay("Depth", "ZigZag depth parameter", "ZigZag")
			
			.SetOptimize(5, 40, 1);

		_deviation = Param(nameof(Deviation), 7m)
			.SetGreaterThanZero()
			.SetDisplay("Deviation", "Minimum price movement in points", "ZigZag")
			
			.SetOptimize(1m, 20m, 1m);

		_backstep = Param(nameof(Backstep), 5)
			.SetGreaterThanZero()
			.SetDisplay("Backstep", "Bars to lock a pivot before switching", "ZigZag")
			
			.SetOptimize(1, 15, 1);

		_urgency = Param(nameof(Urgency), 2)
			.SetNotNegative()
			.SetDisplay("Urgency", "Maximum bars to use the latest signal", "Trading")
			
			.SetOptimize(0, 5, 1);

		_signalReverse = Param(nameof(SignalReverse), false)
			.SetDisplay("Signal Reverse", "Flip long and short entries", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for analysis", "General");

		_volume = Param(nameof(VolumePerTrade), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume per trade", "Trading");
	}

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

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

		_highest = null;
		_lowest = null;
		_pivotType = PivotTypes.None;
		_pivotPrice = 0m;
		_barsSincePivot = int.MaxValue;
		_priceStep = 0m;
	}

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

		_priceStep = GetEffectivePriceStep();
		_highest = new Highest { Length = Depth };
		_lowest = new Lowest { Length = Depth };

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

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

	private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
	{
		// Skip unfinished candles to ensure decisions are made on closed bars only.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait until both indicators are fully formed before reacting.
		if (_highest == null || _lowest == null || !_highest.IsFormed || !_lowest.IsFormed)
			return;

		// Increment the bar counter that measures freshness of the latest pivot.
		if (_pivotType != PivotTypes.None && _barsSincePivot < int.MaxValue)
			_barsSincePivot++;

		var deviationPrice = Math.Max(GetDeviationInPrice(), _priceStep);
		var canSwitch = _pivotType == PivotTypes.None || _barsSincePivot >= Backstep;

		// Detect a fresh swing high if price pushes above the tracked maximum.
		if (candle.HighPrice >= highestValue && highestValue > 0m)
		{
			var difference = candle.HighPrice - _pivotPrice;
			if ((_pivotType != PivotTypes.High && canSwitch) || (_pivotType == PivotTypes.High && difference >= deviationPrice))
				SetPivot(PivotTypes.High, candle.HighPrice);
		}
		// Detect a fresh swing low when price dips under the tracked minimum.
		else if (candle.LowPrice <= lowestValue && lowestValue > 0m)
		{
			var difference = _pivotPrice - candle.LowPrice;
			if ((_pivotType != PivotTypes.Low && canSwitch) || (_pivotType == PivotTypes.Low && difference >= deviationPrice))
				SetPivot(PivotTypes.Low, candle.LowPrice);
		}

		if (_pivotType == PivotTypes.None)
			return;

		var isBuySignal = _pivotType == PivotTypes.High ? !SignalReverse : SignalReverse;

		// Close opposite exposure before entering in the new direction.
		if (isBuySignal)
		{
			if (Position < 0)
			{
				var closeVolume = Math.Abs(Position);
				if (closeVolume > 0m)
					BuyMarket(closeVolume);
			}
		}
		else
		{
			if (Position > 0)
			{
				var closeVolume = Math.Abs(Position);
				if (closeVolume > 0m)
					SellMarket(closeVolume);
			}
		}

		// Enter the market while the pivot is still considered fresh.
		if (_barsSincePivot > Urgency)
			return;

		var volume = VolumePerTrade;
		if (volume <= 0m)
			return;

		if (isBuySignal)
			BuyMarket(volume);
		else
			SellMarket(volume);
	}

	// Update the stored pivot information when a new swing is confirmed.
	private void SetPivot(PivotTypes type, decimal price)
	{
		_pivotType = type;
		_pivotPrice = price;
		_barsSincePivot = 0;
	}

	// Convert the deviation input expressed in points to a price value.
	private decimal GetDeviationInPrice()
	{
		return Deviation * _priceStep;
	}

	// Determine the effective price step for translating point-based parameters.
	private decimal GetEffectivePriceStep()
	{
		if (Security?.PriceStep is > 0m)
			return Security.PriceStep.Value;

		return 1m;
	}
}