在 GitHub 上查看

ZigZag EvgeTrofi 1 策略

概述

ZigZag EvgeTrofi 1 复刻了最初的 MetaTrader 智能交易系统,该系统会根据最新的 ZigZag 转折点开仓。策略逐根处理已经收盘的 K 线,利用经典的深度、偏离和回撤参数确定最新的 ZigZag 枢轴,并在信号仍然新鲜时入场。形成的 ZigZag 高点触发做多,形成的 ZigZag 低点触发做空,与原始 EA 的方向完全一致。

交易逻辑

  • 订阅设定的蜡烛类型,并将数据送入长度等于 ZigZag 深度的 Highest/Lowest 指标组合。这组指标无需自定义缓冲即可模拟原生 ZigZag 的转折识别。
  • 每当蜡烛收盘时,检测其最高价是否触及跟踪的最高值或最低价是否触及跟踪的最低值。只有在满足所需的价格偏离并且遵守最小反转间隔(Backstep 参数)时才允许切换到新的枢轴。
  • 记录枢轴后会统计已经过去的 K 线数量。Urgency 参数定义枢轴在多少根 K 线之内仍被视为有效,超过该限制的信号会被忽略,从而避免迟到的入场。
  • 当枢轴位于高点时准备做多,位于低点时准备做空。如果当前已有同方向仓位,信号会被标记为已处理,不会重复下单。
  • 如果账户持有反向仓位,策略会先发出等量的市价单平掉该仓位,然后立即以下单量执行新的市价单,建立目标方向的仓位。
  • 所有操作都要求指标已形成、蜡烛已收盘且交易量为正。策略在下单前调用 IsFormedAndOnlineAndAllowTrading() 检查连接和交易许可,确保只有在交易环境健康时才会发送订单。

参数

名称 说明 默认值
Depth ZigZag 深度,决定转折点的搜索窗口。 17
Deviation 确认同方向枢轴所需的最小价格偏移(以点为单位),内部会换算成合约最小变动。 7
Backstep 切换到相反枢轴前必须经过的最小 K 线数量。 5
Urgency 枢轴形成后仍允许开仓的最大 K 线数量。 2
Candle Type 用于计算的蜡烛类型(时间周期或其它聚合方式)。 5 分钟周期
Volume 每次入场时提交的市价单手数。 0.1

实现要点

  • 通过高层 SubscribeCandles().Bind() API 连接 Highest/Lowest 指标,因此策略只在完整蜡烛上运算,并且无需手工缓存数据。
  • 偏离参数会利用合约的最小价格跳动转换成绝对价格差。如果标的没有提供价格步长,则退回到 1,保证跨市场的一致性。
  • 使用布尔标志阻止同一个枢轴重复下单,完全符合原始 MetaTrader 智能交易系统只对每个波动执行一次的行为。
  • 当绘图功能可用时,策略会自动展示蜡烛和成交,方便复核枢轴位置和进出场情况。
  • 仓位管理保持对称:在开新仓前会用相同数量的市价单平掉反向仓位,从而像原版 EA 一样始终保持单向敞口。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// ZigZag swing strategy that reproduces the ZigZagEvgeTrofi 1 expert advisor.
/// Enters when a fresh ZigZag turning point appears within a limited number of bars.
/// </summary>
public class ZigZagEvgeTrofi1Strategy : 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<DataType> _candleType;
	private readonly StrategyParam<decimal> _volume;

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

	/// <summary>
	/// ZigZag depth parameter identical to the original expert advisor.
	/// </summary>
	public int Depth
	{
		get => _depth.Value;
		set => _depth.Value = value;
	}

	/// <summary>
	/// Minimum deviation expressed in points that confirms a new swing.
	/// </summary>
	public decimal Deviation
	{
		get => _deviation.Value;
		set => _deviation.Value = value;
	}

	/// <summary>
	/// Minimum bars required before switching to an opposite pivot.
	/// </summary>
	public int Backstep
	{
		get => _backstep.Value;
		set => _backstep.Value = value;
	}

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

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

	/// <summary>
	/// Volume applied to each market order.
	/// </summary>
	public decimal VolumePerTrade
	{
		get => _volume.Value;
		set => _volume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ZigZagEvgeTrofi1Strategy"/> class.
	/// </summary>
	public ZigZagEvgeTrofi1Strategy()
	{
		_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)
			.SetNotNegative()
			.SetDisplay("Backstep", "Bars to wait before switching pivots", "ZigZag")
			
			.SetOptimize(0, 10, 1);

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

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

		_volume = Param(nameof(VolumePerTrade), 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;
		_signalHandled = true;
		_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)
	{
		// React only on completed candles to mirror the original bar-by-bar logic.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure both indicators collected enough data before generating signals.
		if (_highest == null || _lowest == null || !_highest.IsFormed || !_lowest.IsFormed)
			return;

		// Track how many bars have passed since the latest identified pivot.
		if (_pivotType != PivotTypes.None && _barsSincePivot < int.MaxValue)
			_barsSincePivot++;

		var deviationPrice = GetDeviationPrice();
		var canSwitch = _pivotType == PivotTypes.None || _barsSincePivot >= Backstep;

		// Detect a new swing high whenever price matches 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 new swing low whenever price touches 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);
		}

		// Stop if no pivot is available after the checks above.
		if (_pivotType == PivotTypes.None)
			return;

		// Skip if the pivot is already considered stale by the urgency filter.
		if (_barsSincePivot > Urgency)
			return;

		// Avoid firing multiple orders for the same pivot.
		if (_signalHandled)
			return;

		// Confirm that trading permissions and connections are valid.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var volume = VolumePerTrade;
		if (volume <= 0m)
		{
			_signalHandled = true;
			return;
		}

		var isBuySignal = _pivotType == PivotTypes.High;

		// Do nothing if a position in the same direction is already open.
		if (isBuySignal)
		{
			if (Position > 0m)
			{
				_signalHandled = true;
				return;
			}
		}
		else
		{
			if (Position < 0m)
			{
				_signalHandled = true;
				return;
			}
		}

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

		_signalHandled = true;
	}

	// Update the internal pivot state when a new turning point is registered.
	private void SetPivot(PivotTypes type, decimal price)
	{
		_pivotType = type;
		_pivotPrice = price;
		_barsSincePivot = 0;
		_signalHandled = false;
	}

	// Convert the deviation parameter expressed in points to an absolute price move.
	private decimal GetDeviationPrice()
	{
		var step = _priceStep > 0m ? _priceStep : 1m;
		var deviation = Deviation;
		if (deviation <= 0m)
			return step;

		var value = deviation * step;
		return value >= step ? value : step;
	}

	// Determine the effective price step for transforming point-based inputs.
	private decimal GetEffectivePriceStep()
	{
		if (Security != null)
		{
			if (Security.PriceStep.HasValue && Security.PriceStep.Value > 0m)
				return Security.PriceStep.Value;
		}

		return 1m;
	}
}