Открыть на GitHub

Стратегия Double ZigZag Alignment

Стратегия является портом советника MQL5 «Double ZigZag». Она воспроизводит логику двойного ZigZag за счёт двух детекторов разворотов с разными глубинами. Сделка формируется только тогда, когда оба детектора подтверждают три последовательных экстремума, а последняя волна достаточно сильна относительно предыдущих.

Концепция

  • Быстрый детектор свингов аппроксимирует настройки ZigZag(13, 5, 3) с помощью скользящих максимумов и минимумов.
  • Медленный детектор использует увеличенное окно (по умолчанию в 8 раз больше), чтобы отфильтровывать менее значимые развороты.
  • Если оба детектора меняют направление на одной свече, фиксируется «совпадающий» экстремум вместе с количеством быстрых свингов, случившихся с момента прошлого совпадения. Эти счётчики полностью соответствуют переменным up и dw из исходного эксперта.

Условия входа в лонг

  1. Последний совпадающий экстремум — максимум, предыдущий — минимум, третий — снова максимум.
  2. Число быстрых свингов после последнего совпадения больше, чем StrengthMultiplier (2.1 по умолчанию), умноженный на счётчик предыдущего сегмента (up > dw * k).
  3. Новый максимум пробивает средний минимум сильнее, чем более старый максимум: (previousHigh - swingLow) * BreakoutMultiplier < (newestHigh - swingLow).
  4. При выполнении условий стратегия покупает объём, равный Volume плюс размер открытого шорта, чтобы итоговая позиция стала длинной.

Условия входа в шорт

  1. Последний совпадающий экстремум — минимум, предыдущий — максимум, третий — снова минимум.
  2. Счётчик последнего сегмента меньше, чем предыдущий, умноженный на StrengthMultiplier (up * k < dw).
  3. Текущий минимум пробивает средний максимум сильнее, чем прошлый минимум, с тем же коэффициентом BreakoutMultiplier.
  4. Стратегия продаёт объём, достаточный для закрытия лонга и открытия короткой позиции.

Управление позицией

  • Сигналы взаимоисключающие: новое направление автоматически закрывает позицию противоположного знака.
  • Стоп-лоссы и тейк-профиты не используются — выход происходит только по встречному сигналу.
  • Анализ ведётся по типу свечей CandleType (по умолчанию таймфрейм 1 минута).

Параметры по умолчанию

  • FastLength = 13
  • SlowLength = 104
  • StrengthMultiplier = 2.1
  • BreakoutMultiplier = 2.1
  • CandleType = таймфрейм TimeSpan.FromMinutes(1)

Теги

  • Категория: Следование тренду / Паттерны
  • Направление: Лонг и шорт
  • Индикаторы: ZigZag (аппроксимация), Highest/Lowest
  • Стопы: Нет
  • Таймфрейм: Внутридневной по умолчанию
  • Сложность: Средняя (нужно синхронно отслеживать свинги)
  • Сезонность: Нет
  • Нейросети: Нет
  • Дивергенция: Нет
  • Риск: Средний из-за отсутствия защитных стопов
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Double ZigZag alignment strategy. Uses fast and slow swing detectors
/// based on highest/lowest price lookbacks to find aligned pivot points and trade breakouts.
/// </summary>
public class DoubleZigZagStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();

	private int _fastDirection;
	private int _slowDirection;
	private decimal _lastFastPivotHigh;
	private decimal _lastFastPivotLow;

	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

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

	public DoubleZigZagStrategy()
	{
		_fastLength = Param(nameof(FastLength), 8)
			.SetDisplay("Fast Length", "Lookback for the fast swing detector", "Indicators");
		_slowLength = Param(nameof(SlowLength), 30)
			.SetDisplay("Slow Length", "Lookback for the slow confirmation swing", "Indicators");
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to analyze", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_highs.Clear();
		_lows.Clear();
		_fastDirection = 0;
		_slowDirection = 0;
		_lastFastPivotHigh = 0;
		_lastFastPivotLow = 0;
	}

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

		_highs.Clear();
		_lows.Clear();
		_fastDirection = 0;
		_slowDirection = 0;
		_lastFastPivotHigh = 0;
		_lastFastPivotLow = 0;

		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;

		_highs.Add(candle.HighPrice);
		_lows.Add(candle.LowPrice);

		var maxBuf = Math.Max(FastLength, SlowLength) + 1;
		if (_highs.Count > maxBuf)
		{
			_highs.RemoveAt(0);
			_lows.RemoveAt(0);
		}

		if (_highs.Count < SlowLength)
			return;

		// Calculate fast and slow highest/lowest
		var fastHighest = GetMax(_highs, FastLength);
		var fastLowest = GetMin(_lows, FastLength);
		var slowHighest = GetMax(_highs, SlowLength);
		var slowLowest = GetMin(_lows, SlowLength);

		var prevFastDir = _fastDirection;
		var prevSlowDir = _slowDirection;

		// Detect fast swing pivot
		if (_fastDirection <= 0 && candle.HighPrice >= fastHighest)
		{
			_fastDirection = 1;
			_lastFastPivotHigh = candle.HighPrice;
		}
		else if (_fastDirection >= 0 && candle.LowPrice <= fastLowest)
		{
			_fastDirection = -1;
			_lastFastPivotLow = candle.LowPrice;
		}

		// Detect slow swing pivot
		if (_slowDirection <= 0 && candle.HighPrice >= slowHighest)
			_slowDirection = 1;
		else if (_slowDirection >= 0 && candle.LowPrice <= slowLowest)
			_slowDirection = -1;

		// When both fast and slow pivot in the same direction, generate signal
		var fastFlippedUp = prevFastDir <= 0 && _fastDirection > 0;
		var fastFlippedDown = prevFastDir >= 0 && _fastDirection < 0;
		var slowFlippedUp = prevSlowDir <= 0 && _slowDirection > 0;
		var slowFlippedDown = prevSlowDir >= 0 && _slowDirection < 0;

		if (fastFlippedUp && (slowFlippedUp || _slowDirection > 0))
		{
			if (Position <= 0)
				BuyMarket();
		}
		else if (fastFlippedDown && (slowFlippedDown || _slowDirection < 0))
		{
			if (Position >= 0)
				SellMarket();
		}
	}

	private static decimal GetMax(List<decimal> data, int length)
	{
		var max = decimal.MinValue;
		var start = Math.Max(0, data.Count - length);
		for (var i = start; i < data.Count; i++)
		{
			if (data[i] > max)
				max = data[i];
		}
		return max;
	}

	private static decimal GetMin(List<decimal> data, int length)
	{
		var min = decimal.MaxValue;
		var start = Math.Max(0, data.Count - length);
		for (var i = start; i < data.Count; i++)
		{
			if (data[i] < min)
				min = data[i];
		}
		return min;
	}
}