Стратегия ZigAndZag Trader
Обзор
ZigAndZag Trader — порт MetaTrader-советника ZigAndZag_trader.mq4 на StockSharp. Стратегия комбинирует два ZigZag-подобных детектора свингов:
- Долгосрочный ZigZag (
TrendDepth) фиксирует глобальные экстремумы и определяет направление тренда. - Краткосрочный ZigZag (
ExitDepth) отслеживает последний свинг внутри тренда и запоминает «взвешенную» цену(5×Close + 2×Open + High + Low) / 9.
Сделки открываются, когда цена уходит от последнего свинга в сторону текущего тренда, и закрываются при возврате этой взвешенной цены через тот же уровень против тренда. Так полностью воспроизводится исходный алгоритм, который считывал 4–6 буферы пользовательского индикатора ZigAndZag в MQL4.
Логика торговли
- Определение тренда — новый минимум долгосрочного ZigZag переводит стратегию в режим uptrend, новый максимум — в downtrend.
- Фиксация свингов — каждый новый свинг короткого ZigZag сбрасывает состояние и обновляет опорную взвешенную цену.
- Входы
- Тренд вверх + последний свинг — минимум: покупка при превышении взвешенной ценой опорного уровня минимум на один пункт.
- Тренд вниз + последний свинг — максимум: продажа при падении взвешенной цены ниже опорного уровня минимум на один пункт.
- Выход — если взвешенная цена пробивает опорный уровень против текущего тренда, все позиции закрываются.
- Ограничение объёма — суммарная позиция не может превышать
MaxOrders × Volume; лишние сигналы игнорируются.
Параметры
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
CandleType |
1 Minute |
Тип свечей для расчёта обоих ZigZag. |
Lots |
0.1 |
Запрашиваемый размер позиции в лотах (приводится к шагу объёма инструмента). |
TrendDepth |
3 |
Глубина (в свечах) долгосрочного ZigZag для определения тренда. |
ExitDepth |
3 |
Глубина краткосрочного ZigZag, генерирующего входы и выходы. |
MaxOrders |
1 |
Максимальное количество одновременно открытых ордеров/единиц позиции. |
StopLossPips |
0 |
Размер стоп-лосса в пунктах (0 — без стопа). |
TakeProfitPips |
0 |
Размер тейк-профита в пунктах (0 — без цели). |
Управление рисками
StartProtection активируется автоматически. Если заданы ненулевые StopLossPips или TakeProfitPips, то к каждой рыночной сделке привязываются защитные ордера на соответствующей дистанции (с учётом шага цены инструмента).
Визуализация
На графике отображаются свечи и собственные сделки. Отдельные индикаторы не рисуются — логика использует внутренние ZigZag-трекеры.
Дополнительно
- Формула взвешенной цены полностью совпадает с оригинальным индикатором и не требует прямого доступа к его буферам.
- Порог срабатывания равен одному пункту, что имитирует проверку на превышение спрэда в исходном коде.
- Все комментарии и сообщения в коде переведены на английский язык согласно требованиям репозитория.
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>
/// Replica of the MetaTrader ZigAndZag trader that follows a long-term ZigZag trend and short-term swings.
/// </summary>
public class ZigAndZagTraderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _lots;
private readonly StrategyParam<int> _trendDepth;
private readonly StrategyParam<int> _exitDepth;
private readonly StrategyParam<int> _maxOrders;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private Lowest _longTermLow = null!;
private Highest _longTermHigh = null!;
private Lowest _shortTermLow = null!;
private Highest _shortTermHigh = null!;
private decimal _pipSize;
private decimal _volumeStep;
private decimal _breakoutThreshold;
private decimal? _lastTrendLow;
private decimal? _lastTrendHigh;
private decimal? _lastShortLow;
private decimal? _lastShortHigh;
private decimal? _lastSlalomZig;
private decimal? _lastSlalomZag;
private bool _trendUp;
private bool _prevTrendUp;
private bool _buyArmed;
private bool _sellArmed;
private bool _limitArmed;
private PivotTypes _lastPivot;
private int _cooldown;
private const int CooldownBars = 500;
/// <summary>
/// Trading candles.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Requested trade volume in lots.
/// </summary>
public decimal Lots
{
get => _lots.Value;
set => _lots.Value = value;
}
/// <summary>
/// Depth of the long-term ZigZag that defines the prevailing trend.
/// </summary>
public int TrendDepth
{
get => _trendDepth.Value;
set => _trendDepth.Value = value;
}
/// <summary>
/// Depth of the short-term ZigZag that produces swing entries and exits.
/// </summary>
public int ExitDepth
{
get => _exitDepth.Value;
set => _exitDepth.Value = value;
}
/// <summary>
/// Maximum number of simultaneously open orders.
/// </summary>
public int MaxOrders
{
get => _maxOrders.Value;
set => _maxOrders.Value = value;
}
/// <summary>
/// Stop loss distance in pips (0 disables the stop).
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips (0 disables the target).
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public ZigAndZagTraderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Candles used for swing detection", "General");
_lots = Param(nameof(Lots), 0.1m)
.SetDisplay("Lots", "Requested trade size in lots", "Trading")
.SetGreaterThanZero();
_trendDepth = Param(nameof(TrendDepth), 3)
.SetDisplay("Trend Depth", "Lookback for the long-term ZigZag", "ZigZag")
.SetGreaterThanZero()
;
_exitDepth = Param(nameof(ExitDepth), 3)
.SetDisplay("Exit Depth", "Lookback for the short-term swing ZigZag", "ZigZag")
.SetGreaterThanZero()
;
_maxOrders = Param(nameof(MaxOrders), 1)
.SetDisplay("Max Orders", "Maximum simultaneous positions", "Trading")
.SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 0m)
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
.SetNotNegative();
_takeProfitPips = Param(nameof(TakeProfitPips), 0m)
.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
.SetNotNegative();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longTermLow = null;
_longTermHigh = null;
_shortTermLow = null;
_shortTermHigh = null;
_lastTrendLow = null;
_lastTrendHigh = null;
_lastShortLow = null;
_lastShortHigh = null;
_lastSlalomZig = null;
_lastSlalomZag = null;
_trendUp = false;
_prevTrendUp = false;
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
_lastPivot = PivotTypes.None;
_pipSize = 0;
_volumeStep = 0;
_breakoutThreshold = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = Security?.PriceStep ?? 0.0001m;
_volumeStep = Security?.VolumeStep ?? 1m;
if (_volumeStep <= 0m)
_volumeStep = 1m;
_breakoutThreshold = _pipSize;
var rawVolume = Lots > 0m ? Lots : _volumeStep;
if (rawVolume < _volumeStep)
rawVolume = _volumeStep;
var steps = Math.Max(1L, (long)Math.Ceiling((double)(rawVolume / _volumeStep)));
Volume = steps * _volumeStep;
StartProtection(
takeProfit: TakeProfitPips > 0m ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : null,
stopLoss: StopLossPips > 0m ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : null,
useMarketOrders: true);
_longTermLow = new Lowest { Length = TrendDepth };
_longTermHigh = new Highest { Length = TrendDepth };
_shortTermLow = new Lowest { Length = ExitDepth };
_shortTermHigh = new Highest { Length = ExitDepth };
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;
if (_cooldown > 0)
_cooldown--;
var t = candle.CloseTime;
var longLow = _longTermLow.Process(candle.LowPrice, t, true).ToDecimal();
var longHigh = _longTermHigh.Process(candle.HighPrice, t, true).ToDecimal();
var shortLow = _shortTermLow.Process(candle.LowPrice, t, true).ToDecimal();
var shortHigh = _shortTermHigh.Process(candle.HighPrice, t, true).ToDecimal();
var longFormed = _longTermLow?.IsFormed == true && _longTermHigh?.IsFormed == true;
var shortFormed = _shortTermLow?.IsFormed == true && _shortTermHigh?.IsFormed == true;
var navel = (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;
if (longFormed)
{
if (candle.LowPrice == longLow && (_lastTrendLow == null || longLow != _lastTrendLow))
{
_trendUp = true;
_lastTrendLow = longLow;
}
if (candle.HighPrice == longHigh && (_lastTrendHigh == null || longHigh != _lastTrendHigh))
{
_trendUp = false;
_lastTrendHigh = longHigh;
}
}
if (_trendUp != _prevTrendUp)
{
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
_prevTrendUp = _trendUp;
}
if (shortFormed)
{
if (candle.LowPrice == shortLow && (_lastShortLow == null || shortLow != _lastShortLow))
{
_lastPivot = PivotTypes.Low;
_lastShortLow = shortLow;
_lastSlalomZig = navel;
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
}
if (candle.HighPrice == shortHigh && (_lastShortHigh == null || shortHigh != _lastShortHigh))
{
_lastPivot = PivotTypes.High;
_lastShortHigh = shortHigh;
_lastSlalomZag = navel;
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
}
}
if (!longFormed || !shortFormed)
return;
var buySignal = false;
var sellSignal = false;
var closeSignal = false;
switch (_lastPivot)
{
case PivotTypes.Low when _lastSlalomZig != null:
{
if (_trendUp)
{
var shouldBuy = navel - _lastSlalomZig.Value >= _breakoutThreshold;
if (shouldBuy && !_buyArmed)
{
_buyArmed = true;
buySignal = true;
}
else if (!shouldBuy && _buyArmed && navel <= _lastSlalomZig.Value)
{
_buyArmed = false;
}
if (_limitArmed && navel <= _lastSlalomZig.Value)
_limitArmed = false;
}
else
{
var shouldClose = navel > _lastSlalomZig.Value;
if (shouldClose && !_limitArmed)
{
_limitArmed = true;
closeSignal = true;
}
else if (!shouldClose && _limitArmed)
{
_limitArmed = false;
}
_buyArmed = false;
}
break;
}
case PivotTypes.High when _lastSlalomZag != null:
{
if (!_trendUp)
{
var shouldSell = _lastSlalomZag.Value - navel >= _breakoutThreshold;
if (shouldSell && !_sellArmed)
{
_sellArmed = true;
sellSignal = true;
}
else if (!shouldSell && _sellArmed && navel >= _lastSlalomZag.Value)
{
_sellArmed = false;
}
if (_limitArmed && navel >= _lastSlalomZag.Value)
_limitArmed = false;
}
else
{
var shouldClose = _lastSlalomZag.Value > navel;
if (shouldClose && !_limitArmed)
{
_limitArmed = true;
closeSignal = true;
}
else if (!shouldClose && _limitArmed)
{
_limitArmed = false;
}
_sellArmed = false;
}
break;
}
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
ExecuteSignals(buySignal, sellSignal, closeSignal);
}
private void ExecuteSignals(bool buySignal, bool sellSignal, bool closeSignal)
{
if (_cooldown > 0)
return;
var volume = Volume;
if (volume <= 0m || MaxOrders <= 0)
return;
var maxVolume = MaxOrders * volume;
if (buySignal)
{
var currentLong = Position > 0m ? Position : 0m;
var available = maxVolume - currentLong;
if (available > 0m)
{
var tradeVolume = Math.Min(volume, available);
BuyMarket(tradeVolume);
_cooldown = CooldownBars;
return;
}
}
if (sellSignal)
{
var currentShort = Position < 0m ? -Position : 0m;
var available = maxVolume - currentShort;
if (available > 0m)
{
var tradeVolume = Math.Min(volume, available);
SellMarket(tradeVolume);
_cooldown = CooldownBars;
return;
}
}
if (closeSignal && Position != 0m)
{
if (Position > 0)
SellMarket(Position);
else
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
private enum PivotTypes
{
None,
Low,
High
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import Lowest, Highest
from indicator_extensions import *
class zig_and_zag_trader_strategy(Strategy):
PIVOT_NONE = 0
PIVOT_LOW = 1
PIVOT_HIGH = 2
def __init__(self):
super(zig_and_zag_trader_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Candles used for swing detection", "General")
self._lots = self.Param("Lots", 0.1).SetDisplay("Lots", "Requested trade size", "Trading")
self._trend_depth = self.Param("TrendDepth", 3).SetDisplay("Trend Depth", "Lookback for long-term ZigZag", "ZigZag")
self._exit_depth = self.Param("ExitDepth", 3).SetDisplay("Exit Depth", "Lookback for short-term ZigZag", "ZigZag")
self._max_orders = self.Param("MaxOrders", 1).SetDisplay("Max Orders", "Maximum simultaneous positions", "Trading")
self._stop_loss_pips = self.Param("StopLossPips", 0.0).SetDisplay("Stop Loss pips", "Protective stop distance", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 0.0).SetDisplay("Take Profit pips", "Profit target distance", "Risk")
self._pip_size = 0.0001
self._volume_step_val = 1.0
self._breakout_threshold = 0.0
self._last_trend_low = None
self._last_trend_high = None
self._last_short_low = None
self._last_short_high = None
self._last_slalom_zig = None
self._last_slalom_zag = None
self._trend_up = True
self._prev_trend_up = True
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
self._last_pivot = self.PIVOT_NONE
self._long_term_low = None
self._long_term_high = None
self._short_term_low = None
self._short_term_high = None
@property
def CandleType(self): return self._candle_type.Value
@property
def Lots(self): return self._lots.Value
@property
def TrendDepth(self): return self._trend_depth.Value
@property
def ExitDepth(self): return self._exit_depth.Value
@property
def MaxOrders(self): return self._max_orders.Value
@property
def StopLossPips(self): return self._stop_loss_pips.Value
@property
def TakeProfitPips(self): return self._take_profit_pips.Value
def OnStarted2(self, time):
super(zig_and_zag_trader_strategy, self).OnStarted2(time)
ps = self.Security.PriceStep if self.Security is not None else None
self._pip_size = float(ps) if ps is not None and float(ps) > 0 else 0.0001
vs = self.Security.VolumeStep if self.Security is not None else None
self._volume_step_val = float(vs) if vs is not None and float(vs) > 0 else 1.0
self._breakout_threshold = self._pip_size
import math
raw_vol = float(self.Lots) if float(self.Lots) > 0 else self._volume_step_val
if raw_vol < self._volume_step_val:
raw_vol = self._volume_step_val
steps = max(1, int(math.ceil(raw_vol / self._volume_step_val)))
self.Volume = steps * self._volume_step_val
tp_p = float(self.TakeProfitPips)
sl_p = float(self.StopLossPips)
tp_unit = Unit(tp_p * self._pip_size, UnitTypes.Absolute) if tp_p > 0 else None
sl_unit = Unit(sl_p * self._pip_size, UnitTypes.Absolute) if sl_p > 0 else None
self.StartProtection(takeProfit=tp_unit, stopLoss=sl_unit, useMarketOrders=True)
self._long_term_low = Lowest()
self._long_term_low.Length = self.TrendDepth
self._long_term_high = Highest()
self._long_term_high.Length = self.TrendDepth
self._short_term_low = Lowest()
self._short_term_low.Length = self.ExitDepth
self._short_term_high = Highest()
self._short_term_high.Length = self.ExitDepth
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
t = candle.CloseTime
long_low = float(process_float(self._long_term_low, candle.LowPrice, t, True))
long_high = float(process_float(self._long_term_high, candle.HighPrice, t, True))
short_low = float(process_float(self._short_term_low, candle.LowPrice, t, True))
short_high = float(process_float(self._short_term_high, candle.HighPrice, t, True))
long_formed = self._long_term_low.IsFormed and self._long_term_high.IsFormed
short_formed = self._short_term_low.IsFormed and self._short_term_high.IsFormed
cl = float(candle.ClosePrice)
op = float(candle.OpenPrice)
hi = float(candle.HighPrice)
lo = float(candle.LowPrice)
navel = (5.0 * cl + 2.0 * op + hi + lo) / 9.0
if long_formed:
if lo == long_low and (self._last_trend_low is None or long_low != self._last_trend_low):
self._trend_up = True
self._last_trend_low = long_low
if hi == long_high and (self._last_trend_high is None or long_high != self._last_trend_high):
self._trend_up = False
self._last_trend_high = long_high
if self._trend_up != self._prev_trend_up:
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
self._prev_trend_up = self._trend_up
if short_formed:
if lo == short_low and (self._last_short_low is None or short_low != self._last_short_low):
self._last_pivot = self.PIVOT_LOW
self._last_short_low = short_low
self._last_slalom_zig = navel
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
if hi == short_high and (self._last_short_high is None or short_high != self._last_short_high):
self._last_pivot = self.PIVOT_HIGH
self._last_short_high = short_high
self._last_slalom_zag = navel
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
if not long_formed or not short_formed:
return
buy_signal = False
sell_signal = False
close_signal = False
if self._last_pivot == self.PIVOT_LOW and self._last_slalom_zig is not None:
if self._trend_up:
sb = navel - self._last_slalom_zig >= self._breakout_threshold
if sb and not self._buy_armed:
self._buy_armed = True
buy_signal = True
elif not sb and self._buy_armed and navel <= self._last_slalom_zig:
self._buy_armed = False
if self._limit_armed and navel <= self._last_slalom_zig:
self._limit_armed = False
else:
sc = navel > self._last_slalom_zig
if sc and not self._limit_armed:
self._limit_armed = True
close_signal = True
elif not sc and self._limit_armed:
self._limit_armed = False
self._buy_armed = False
elif self._last_pivot == self.PIVOT_HIGH and self._last_slalom_zag is not None:
if not self._trend_up:
ss = self._last_slalom_zag - navel >= self._breakout_threshold
if ss and not self._sell_armed:
self._sell_armed = True
sell_signal = True
elif not ss and self._sell_armed and navel >= self._last_slalom_zag:
self._sell_armed = False
if self._limit_armed and navel >= self._last_slalom_zag:
self._limit_armed = False
else:
sc = self._last_slalom_zag > navel
if sc and not self._limit_armed:
self._limit_armed = True
close_signal = True
elif not sc and self._limit_armed:
self._limit_armed = False
self._sell_armed = False
self._execute_signals(buy_signal, sell_signal, close_signal)
def _execute_signals(self, buy_signal, sell_signal, close_signal):
volume = self.Volume
if volume <= 0 or int(self.MaxOrders) <= 0:
return
max_vol = int(self.MaxOrders) * float(volume)
if buy_signal:
cur_long = float(self.Position) if self.Position > 0 else 0.0
avail = max_vol - cur_long
if avail > 0:
self.BuyMarket(min(float(volume), avail))
if sell_signal:
cur_short = -float(self.Position) if self.Position < 0 else 0.0
avail = max_vol - cur_short
if avail > 0:
self.SellMarket(min(float(volume), avail))
if close_signal and self.Position != 0:
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(abs(self.Position))
def OnReseted(self):
super(zig_and_zag_trader_strategy, self).OnReseted()
self._long_term_low = None
self._long_term_high = None
self._short_term_low = None
self._short_term_high = None
self._last_trend_low = None
self._last_trend_high = None
self._last_short_low = None
self._last_short_high = None
self._last_slalom_zig = None
self._last_slalom_zag = None
self._trend_up = True
self._prev_trend_up = True
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
self._last_pivot = self.PIVOT_NONE
def CreateClone(self):
return zig_and_zag_trader_strategy()