在 GitHub 上查看

Fractal ZigZag 策略

该策略直接移植自 MetaTrader 4 顾问 Fractal ZigZag Expert.mq4。算法重新构建比尔·威廉姆斯的分形序列,并将最近一次确认为市 场当前的主导波段:最新分形如果是低点,则开多;最新分形如果是高点,则开空。所有原始参数——分形深度、止盈、初始止损和移动止 损距离——完整保留,同时将委托逻辑改写为 StockSharp 的高级 API。

默认推荐在 H1 周期上运行,与原版 EA 的设置一致。不过可以通过 CandleType 参数切换到任何受支持的时间框架。所有距离以价格点 (品种最小价格变动)为单位,与 MetaTrader 中的 Point 常量保持一致。

交易规则

  • 信号判定
    • 每根完成的K线都会加入长度为 2 * Level + 1 的滑动窗口。
    • 若窗口中央的K线拥有最高的最高价,则确认向上分形;若拥有最低的最低价,则确认向下分形。
    • 只有最近一次被确认的分形会影响方向:低点将内部趋势值设为 2(看多),高点设为 1(看空)。
  • 入场
    • 当内部趋势为 2 且当前无持仓时,按照 Lots 参数下市价买单。
    • 当内部趋势为 1 且无持仓时,按 Lots 下市价卖单。
    • 如果仓位平仓后趋势未改变,策略会在同一方向重新入场。
  • 离场与风控
    • 每笔交易都会根据参数设置初始止损和固定止盈(单位为点)。把某个距离设为 0 即表示禁用该保护。
    • 可选的追踪止损同样以点数表示,在价格向有利方向移动到指定距离后启动,随后保持与收盘价的固定间隔,且不会穿越初始止损。
    • 通过监控K线的最高价/最低价来模拟盘中触发,最大限度还原原版 MQL4 逻辑。

默认参数

参数 默认值 说明
Level 2 确认分形所需的左右两侧K线数量。
TakeProfitPoints 25 止盈距离,单位为价格点。
InitialStopPoints 20 初始止损距离,单位为价格点。
TrailingStopPoints 10 跟踪止损距离(设为 0 关闭)。
Lots 1 每次市价单的下单量。
CandleType H1 用于计算的K线周期。

说明

  • 策略在启动时调用一次 StartProtection(),以便在必要时使用 StockSharp 的应急平仓机制。
  • 按照仓库要求,代码中的注释统一采用英文,而各 README 文件使用对应语言进行说明。
  • 实现中未使用指标缓冲区,仅保留检测分形所需的最小滑动窗口,从而忠实复现原始算法。
using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Fractal ZigZag: Confirms Bill Williams fractals then trades
/// in the direction of the last confirmed extremum.
/// Bullish after low fractal, bearish after high fractal.
/// </summary>
public class FractalZigZagStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _level;
	private readonly StrategyParam<int> _atrLength;

	private readonly List<(decimal high, decimal low, DateTimeOffset time)> _window = new();
	private int _trend; // 1=bearish (last was high), 2=bullish (last was low)
	private int _prevTrend;
	private decimal _entryPrice;

	public FractalZigZagStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_level = Param(nameof(Level), 2)
			.SetDisplay("Fractal Depth", "Candles on each side to confirm fractal.", "Signals");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for stops.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int Level
	{
		get => _level.Value;
		set => _level.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_window.Clear();
		_trend = 0;
		_prevTrend = 0;
		_entryPrice = 0;
	}

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

		var atr = new AverageTrueRange { Length = AtrLength };

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

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

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

		// Update fractal window
		var depth = Math.Max(1, Level);
		var windowSize = depth * 2 + 1;

		_window.Add((candle.HighPrice, candle.LowPrice, candle.OpenTime));
		while (_window.Count > windowSize)
			_window.RemoveAt(0);

		// Evaluate fractals
		if (_window.Count >= windowSize)
		{
			var centerIndex = _window.Count - 1 - depth;
			var center = _window[centerIndex];
			var isHigh = true;
			var isLow = true;

			for (var i = 0; i < _window.Count; i++)
			{
				if (i == centerIndex)
					continue;

				if (_window[i].high >= center.high)
					isHigh = false;
				if (_window[i].low <= center.low)
					isLow = false;

				if (!isHigh && !isLow)
					break;
			}

			if (isHigh)
				_trend = 1; // bearish: last fractal was a high
			if (isLow)
				_trend = 2; // bullish: last fractal was a low
		}

		if (atrVal <= 0 || _trend == 0)
		{
			_prevTrend = _trend;
			return;
		}

		var close = candle.ClosePrice;

		// Exit management
		if (Position > 0)
		{
			if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 3m || _trend == 1)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 3m || _trend == 2)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		// Entry on trend change
		if (Position == 0 && _prevTrend != 0 && _trend != _prevTrend)
		{
			if (_trend == 2)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (_trend == 1)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevTrend = _trend;
	}
}