Открыть на GitHub

Стратегия ZigAndZag Scalpel

Описание

ZigAndZagScalpelStrategy — это порт комплекта MetaTrader 4 "ZigAndZag" (папка 8304) на StockSharp. В оригинал входят индикатор и советник. Используются два ZigZag:

  • KeelOver — долгий диапазон, формирующий тренд.
  • Slalom — короткий диапазон, задающий точки входа.

Когда длинный ZigZag разворачивается вверх, стратегия отслеживает ближайший минимум Slalom и ждёт, когда цена поднимется на заданное количество пунктов выше поворотной точки. После пробоя открывается покупка. Зеркальное условие открывает продажу: тренд KeelOver вниз, Slalom печатает новый максимум и цена падает ниже него. Позиция может быть закрыта при появлении противоположного Slalom-пивота, что повторяет удаление стрелок лимит-ордеров в индикаторе.

Сохранён суточный лимитер сделок из советника. За торговый день разрешено не более заданного числа входов, счётчик автоматически сбрасывается в полночь (временная зона площадки), как и флаг newday в MQL.

Как работает стратегия

  1. Подписываемся на основной поток свечей CandleType.
  2. Запускаем два ZigZagIndicator:
    • Глубина = KeelOverLength для определения тренда.
    • Глубина = SlalomLength для точек входа.
  3. Последний пивот KeelOver задаёт направление тренда: минимум — рост, максимум — падение.
  4. При появлении нового пивота Slalom активируется ожидание пробоя в его сторону.
  5. Считается взвешенная цена (5×Close + 2×Open + High + Low) / 9. Если цена уходит дальше, чем BreakoutDistancePoints (переведённые в цену), и тренд подтверждает движение, выставляется рыночный ордер.
  6. Текущая позиция закрывается при смене глобального тренда или при противоположном пивоте Slalom, если CloseOnOppositePivot = true.
  7. Суточный счётчик сделок обновляется при каждой смене календарного дня.

Параметры DeviationPoints и Backstep общие для обоих ZigZag, поэтому структура пивотов совпадает с буферами индикатора MetaTrader.

Параметры

Имя Значение по умолчанию Описание
CandleType 15m Таймфрейм свечей для обоих ZigZag.
KeelOverLength 55 Длина ZigZag, формирующего тренд (параметр KeelOver).
SlalomLength 17 Длина ZigZag, формирующего вход (параметр Slalom).
DeviationPoints 5 Минимальное движение в пунктах для фиксации нового пивота.
Backstep 3 Минимальное число баров между соседними пивотами.
BreakoutDistancePoints 2 Отступ от пивота в пунктах перед открытием сделки.
MaxTradesPerDay 1 Максимум сделок в сутки. Аналог флага newday.
CloseOnOppositePivot true Закрывать позицию при противоположном пивоте Slalom.

Все параметры в пунктах умножаются на Security.PriceStep. Если шаг цены не задан, используется 1 для упрощённого тестирования.

Практические замечания

  • Сделки открываются рыночными ордерами (BuyMarket / SellMarket). Добавьте собственные стопы и сопровождение при необходимости.
  • Оба ZigZag используют одинаковые свечи. Убедитесь, что поставщик данных поддерживает выбранный CandleType.
  • Значение MaxTradesPerDay = 1 полностью копирует поведение оригинала. Увеличьте лимит для многократных входов в течение дня.
  • Установите CloseOnOppositePivot = false, если хотите держать позицию до смены глобального тренда и игнорировать мелкие колебания.

Отличия от советника MT4

  • MetaTrader выставлял отложенные лимит-ордера. В StockSharp используется немедленный вход по рынку, чтобы остаться в рамках High Level API.
  • Управление рисками (стопы, часть объёма) не переносилось. Используйте стандартные компоненты StockSharp для управления капиталом.
  • Буферы индикатора заменены прямой логикой стратегии и отрисовкой через DrawIndicator и DrawOwnTrades.

Возможные расширения

  • Добавьте параметры стоп-лосса/тейк-профита на основе ATR или последних пивотов ZigZag.
  • Для визуального сравнения выставьте BreakoutDistancePoints = 0, чтобы видеть чистую лестницу пивотов.
  • Совместите стратегию с фильтром торговых сессий (IsFormedAndOnlineAndAllowTrading).
namespace StockSharp.Samples.Strategies;

using System;

using Ecng.Common;

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

/// <summary>
/// ZigAndZagScalpel translation that trades on breakouts from short-term pivots confirmed by a long-term ZigZag trend.
/// </summary>
public class ZigAndZagScalpelStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maxTradesPerDay;
	private readonly StrategyParam<bool> _closeOnOppositePivot;

	private decimal _previousMajorPivot;
	private decimal _lastMajorPivot;
	private decimal _previousMinorPivot;
	private decimal _lastMinorPivot;
	private DateTime _currentDay = DateTime.MinValue;
	private int _tradesToday;
	private bool _trendUp;
	private PivotTypes _lastMinorPivotType = PivotTypes.None;
	private bool _minorPivotUsed;

	/// <summary>
	/// Initializes a new instance of the <see cref="ZigAndZagScalpelStrategy"/> class.
	/// </summary>
	public ZigAndZagScalpelStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for all calculations", "General");

		_maxTradesPerDay = Param(nameof(MaxTradesPerDay), 1)
			.SetDisplay("Max Trades Per Day", "Daily limit matching the original expert advisor", "Trading");

		_closeOnOppositePivot = Param(nameof(CloseOnOppositePivot), true)
			.SetDisplay("Close On Opposite Pivot", "Exit when the entry ZigZag prints the opposite swing", "Risk");
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Maximum number of trades allowed per trading day.
	/// </summary>
	public int MaxTradesPerDay
	{
		get => _maxTradesPerDay.Value;
		set => _maxTradesPerDay.Value = value;
	}

	/// <summary>
	/// Determines whether open positions should be closed on the opposite entry pivot.
	/// </summary>
	public bool CloseOnOppositePivot
	{
		get => _closeOnOppositePivot.Value;
		set => _closeOnOppositePivot.Value = value;
	}

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

		_previousMajorPivot = 0m;
		_lastMajorPivot = 0m;
		_previousMinorPivot = 0m;
		_lastMinorPivot = 0m;
		_currentDay = DateTime.MinValue;
		_tradesToday = 0;
		_trendUp = false;
		_lastMinorPivotType = PivotTypes.None;
		_minorPivotUsed = false;
	}

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

		var majorZigZag = new ZigZag { Deviation = 0.02m };
		var minorZigZag = new ZigZag { Deviation = 0.005m };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindWithEmpty(majorZigZag, minorZigZag, ProcessCandle)
			.Start();

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

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

		UpdateDailyCounter(candle.OpenTime);

		if (majorValue is not null)
			UpdateMajorTrend(majorValue.Value);

		if (minorValue is not null)
			UpdateMinorPivot(minorValue.Value);

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		ManageExistingPosition();

		if (Position != 0)
			return;

		if (_minorPivotUsed)
			return;

		if (_lastMinorPivotType == PivotTypes.None)
			return;

		if (_tradesToday >= MaxTradesPerDay)
			return;

		var navel = CalculateNavel(candle);

		if (_lastMinorPivotType == PivotTypes.Low && _trendUp)
		{
			if (navel > _lastMinorPivot)
			{
				BuyMarket();
				_minorPivotUsed = true;
				_tradesToday++;
			}
		}
		else if (_lastMinorPivotType == PivotTypes.High && !_trendUp)
		{
			if (navel < _lastMinorPivot)
			{
				SellMarket();
				_minorPivotUsed = true;
				_tradesToday++;
			}
		}
	}

	private void UpdateDailyCounter(DateTime time)
	{
		var date = time.Date;
		if (date == _currentDay)
			return;

		_currentDay = date;
		_tradesToday = 0;
	}

	private void UpdateMajorTrend(decimal majorValue)
	{
		if (_lastMajorPivot == 0m)
		{
			_lastMajorPivot = majorValue;
			_previousMajorPivot = majorValue;
			return;
		}

		if (majorValue == _lastMajorPivot)
			return;

		_previousMajorPivot = _lastMajorPivot;
		_lastMajorPivot = majorValue;
		_trendUp = _lastMajorPivot < _previousMajorPivot;
	}

	private void UpdateMinorPivot(decimal minorValue)
	{
		if (_lastMinorPivot == 0m)
		{
			_lastMinorPivot = minorValue;
			_previousMinorPivot = minorValue;
			_lastMinorPivotType = PivotTypes.Low;
			_minorPivotUsed = false;
			return;
		}

		if (minorValue == _lastMinorPivot)
			return;

		_previousMinorPivot = _lastMinorPivot;
		_lastMinorPivot = minorValue;
		_lastMinorPivotType = _lastMinorPivot < _previousMinorPivot ? PivotTypes.Low : PivotTypes.High;
		_minorPivotUsed = false;
	}

	private void ManageExistingPosition()
	{
		if (Position > 0)
		{
			if (!_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.High))
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			if (_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.Low))
				BuyMarket(Position.Abs());
		}
	}

	private static decimal CalculateNavel(ICandleMessage candle)
	{
		return (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;
	}

	private enum PivotTypes
	{
		None,
		Low,
		High
	}
}